Skip to content

[codex] add Codex usage indicator#537

Open
dsus4wang wants to merge 3 commits into
tiann:mainfrom
dsus4wang:codex/codex-usage-indicator
Open

[codex] add Codex usage indicator#537
dsus4wang wants to merge 3 commits into
tiann:mainfrom
dsus4wang:codex/codex-usage-indicator

Conversation

@dsus4wang

Copy link
Copy Markdown
Contributor

Summary

Adds live Codex usage state and a composer usage ring for Codex sessions.

Changes

  • Captures Codex token_count events from app-server notifications and transcript tailing.
  • Stores structured usage in session.metadata.codexUsage and sends metadata patches over SSE.
  • Shows a compact usage ring beside the send button, with popover details for context window, rate limits, and token breakdown.

Validation

  • bun run test:cli -- src/codex/codexRemoteLauncher.test.ts src/codex/utils/codexUsage.test.ts
  • cd shared && bun test src/codexUsageSchema.test.ts
  • cd web && bun run test -- src/components/AssistantChat/codexUsageDisplay.test.ts
  • bun typecheck

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] Codex session listing bypasses the runner workspace scope — the new machine RPC returns every transcript under CODEX_HOME, including title/path, without using the existing workspace-root guard that protects machine directory listing and spawn. On a scoped runner, this lets the web UI enumerate Codex sessions outside the allowed workspace. Evidence cli/src/modules/common/handlers/codexSessions.ts:15.
    Suggested fix:

    const allowed = options?.isPathAllowed
    const sessions = []
    for (const session of result.sessions) {
        if (!allowed || await allowed(session.path)) {
            sessions.push(session)
        }
    }
    return { success: true, sessions, nextCursor: result.nextCursor }
  • [Major] importHistory is dropped before the runner builds Codex args — the hub now sends importHistory to spawn-happy-session, but the machine RPC handler does not destructure or forward it to spawnSession, so buildCliArgs() never sees it and never adds --hapi-import-history. Resuming a Codex session from the new selector starts the Codex thread but does not import the transcript into HAPI. Evidence hub/src/sync/rpcGateway.ts:158.
    Suggested fix:

    const { directory, sessionId, resumeSessionId, machineId, approvedNewDirectoryCreation, agent, model, effort, modelReasoningEffort, yolo, permissionMode, token, sessionType, worktreeName, importHistory } = params || {}
    
    const result = await spawnSession({
        directory,
        sessionId,
        resumeSessionId,
        importHistory,
        // existing fields...
    })
  • [Minor] Local --hapi-import-history can replay the same transcript twice — runCodex() imports the transcript before launching, then the local scanner is started with replayExistingEvents: session.importHistory and sends those same existing user/agent messages again. Evidence cli/src/codex/codexLocalLauncher.ts:73.
    Suggested fix:

    const createdScanner = await createCodexSessionScanner({
        transcriptPath,
        replayExistingEvents: false,
        onSessionId: (sessionId) => {
            session.onSessionFound(sessionId)
        },
        // existing onEvent...
    })

Questions

  • None.

Summary
Review mode: initial
3 findings: one workspace-scope privacy regression, one broken import-history propagation path, and one local duplicate-history edge case.

Testing

  • Not run (automation); static git diff --check passed.

HAPI Bot

logger.debug('List Codex sessions request');

try {
const result = await listCodexSessions(data ?? {});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] This handler returns every Codex transcript under CODEX_HOME with title/path, but it is registered as a machine RPC and does not receive the runner's workspaceRoot guard. A scoped runner already blocks machine directory listing/spawn outside the root; this endpoint now bypasses that boundary and exposes sessions from unrelated directories.

Suggested fix:

const allowed = options?.isPathAllowed
const sessions = []
for (const session of result.sessions) {
    if (!allowed || await allowed(session.path)) {
        sessions.push(session)
    }
}
return { success: true, sessions, nextCursor: result.nextCursor }

machineId,
'spawn-happy-session',
{ type: 'spawn-in-directory', directory, agent, model, modelReasoningEffort, yolo, sessionType, worktreeName, resumeSessionId, effort, permissionMode }
{ type: 'spawn-in-directory', directory, agent, model, modelReasoningEffort, yolo, sessionType, worktreeName, resumeSessionId, importHistory, effort, permissionMode }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] importHistory is sent here, but the receiving machine RPC handler currently destructures params without importHistory and does not include it in the spawnSession() options. As a result, runner-spawned Codex resumes never pass --hapi-import-history, so the new selector resumes the Codex thread without importing prior transcript messages into HAPI.

Suggested fix:

const { directory, sessionId, resumeSessionId, machineId, approvedNewDirectoryCreation, agent, model, effort, modelReasoningEffort, yolo, permissionMode, token, sessionType, worktreeName, importHistory } = params || {}

const result = await spawnSession({
    directory,
    sessionId,
    resumeSessionId,
    importHistory,
    machineId,
    approvedNewDirectoryCreation,
    agent,
    model,
    effort,
    modelReasoningEffort,
    yolo,
    permissionMode,
    token,
    sessionType,
    worktreeName
})

}
const createdScanner = await createCodexSessionScanner({
transcriptPath,
replayExistingEvents: session.importHistory,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MINOR] runCodex() already imports the transcript before launch when --hapi-import-history is set. In local mode this scanner then replays the existing transcript and sends the same user/agent messages again, producing duplicate history in HAPI for hapi codex resume <id> --hapi-import-history.

Suggested fix:

const createdScanner = await createCodexSessionScanner({
    transcriptPath,
    replayExistingEvents: false,
    onSessionId: (sessionId) => {
        session.onSessionFound(sessionId)
    },
    // existing onEvent...
})

@heavygee

heavygee commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Hi @dsus4wang — thanks for this PR; the design (compact usage ring beside the send button + session.metadata.codexUsage + SSE patches + popover with context window / rate limits / token breakdown) is the right shape, and I'd like to see it land.

State today (no judgement, just summarising):

  • Opened 2026-04-27, no commits since (~6 weeks)
  • mergeable: CONFLICTING against current upstream/main
  • No maintainer review yet

I'm putting together an umbrella for cross-flavor agent budget/quota gauges (#846) that intentionally treats this PR as the natural seed — Codex is the most-baked piece, and I'd like to align Claude (via statusline-JSON rate_limits) and Cursor (Admin API for Enterprise) on the same session.metadata.agentUsage shape your PR establishes.

To help unblock this, since maintainerCanModify: true is set, I'd be happy to:

  1. Rebase the branch onto current upstream/main
  2. Resolve the conflicts
  3. Push the result back to this PR directly via the maintainer-edit privilege

That keeps the PR number, your authorship on the original commits, and the existing thread intact. I'd add Co-authored-by: lines to only the rebase / conflict-resolution commits.

Are you still planning to iterate on this yourself? If yes, I'll stand down and just track #846 around it. If you're blocked / waiting for review and would welcome the rebase, no need to do anything — I'll proceed if I don't hear back from you within ~7 days. Either way, your name stays on the original work.


Disclosure: this comment was drafted by Claude Sonnet 4.6 (model claude-sonnet-4-5) acting as a peer agent in the heavygee/hapi fork's tooling. Per the AI-assisted contributions policy in CONTRIBUTING.md.

@heavygee

heavygee commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Hi @dsus4wang - I rebased this PR onto current main (179 commits ahead since April) and opened it as #847 with your three original commits preserved intact (authorship on 8f5f164e, 3f4f33dc, 2f32998b is yours). The PR body in #847 leads with attribution to your original work here.

The rebase also extended the indicator to handle Codex Pro accounts that bill from credits when the subscription windows are exhausted (codex emits rate_limits.primary=null + secondary=null + credits.has_credits=false for those, which your original couldn't have caught against a Plus-tier test account). And refactored the ring into a flavor-agnostic shape so Claude / Cursor / Gemini adapters can drop in without rewriting the renderer (umbrella issue #846).

Happy for you to take #847 over as primary if you want to keep ownership, or to land it as-is with attribution preserved. Either is fine - I just didn't want this stuck for another 179 commits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants