Skip to content

feat(codex): add message rewind and fork controls#775

Open
NightWatcher314 wants to merge 5 commits into
tiann:mainfrom
NightWatcher314:feat/codex-history-ops
Open

feat(codex): add message rewind and fork controls#775
NightWatcher314 wants to merge 5 commits into
tiann:mainfrom
NightWatcher314:feat/codex-history-ops

Conversation

@NightWatcher314
Copy link
Copy Markdown
Contributor

Summary

  • add Codex app-server thread read/fork/rollback and turn steer RPC plumbing
  • wire web actions for user-message edit, rewind/resend, fork/resend, and active-turn steer
  • keep HAPI local message IDs mapped to Codex client user message IDs so rewinds/forks target the correct turn
  • copy visible history before the fork point into the forked HAPI session

Notes

  • Codex thread rollback/fork updates conversation context only; it does not revert local file changes. The UI/API warning reflects that.
  • This PR intentionally does not include @file mention support; that is split into feat(codex): support file mentions #774.

Tests

  • bun typecheck
  • cd cli && bun test src/codex/utils/appServerConfig.test.ts src/utils/MessageQueue2.test.ts
  • cd cli && bun run test src/codex/codexRemoteLauncher.test.ts src/codex/runCodex.test.ts
  • cd hub && bun test src/web/routes/sessions.test.ts src/store/messages.test.ts

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Findings

  • [Major] Preserve the steer message localId in HAPI storage — clientUserMessageId is generated and sent to Codex, but session.sendUserMessage(text) emits the visible user bubble without the same top-level socket localId. The hub persists local_id only from the top-level socket payload (hub/src/socket/handlers/cli/sessionHandlers.ts:84, hub/src/socket/handlers/cli/sessionHandlers.ts:108), and the UI enables the new Rewind/Fork controls only when a user message has metadata.custom.localId (web/src/components/AssistantChat/messages/UserMessage.tsx:60). As a result, user steer messages become visible but cannot be targeted by the PR's new localId-based controls, and later rollback/fork lookups for that steer turn lose the mapping. Evidence cli/src/codex/codexRemoteLauncher.ts:2921
    Suggested fix:
    // ApiSessionClient.sendUserMessage should forward the id as the socket localId,
    // then pass the steer id through from the Codex RPC handler.
    session.sendUserMessage(text, undefined, clientUserMessageId);

Questions

  • None

Summary

  • Review mode: initial
  • One issue found: steer-created user messages lose their HAPI localId, which breaks the new rewind/fork affordances for those messages.

Testing

  • Not run (automation)

HAPI Bot

Comment thread cli/src/codex/codexRemoteLauncher.ts Outdated
}, {
signal: this.abortController.signal
});
session.sendUserMessage(text);
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] Preserve the steer message localId in HAPI storage

clientUserMessageId is generated and sent to Codex, but session.sendUserMessage(text) emits the visible user bubble without the same top-level socket localId. The hub persists local_id only from the top-level socket payload (hub/src/socket/handlers/cli/sessionHandlers.ts:84, hub/src/socket/handlers/cli/sessionHandlers.ts:108), and the UI enables the new Rewind/Fork controls only when a user message has metadata.custom.localId (web/src/components/AssistantChat/messages/UserMessage.tsx:60). As a result, steer messages become visible but cannot be targeted by the new localId-based controls, and later rollback/fork lookups for that steer turn lose the mapping.

Suggested fix:

// ApiSessionClient.sendUserMessage should forward the id as the socket localId,
// then pass the steer id through from the Codex RPC handler.
session.sendUserMessage(text, undefined, clientUserMessageId);

@NightWatcher314
Copy link
Copy Markdown
Contributor Author

Thanks, fixed in 65bd7e1: steer-created user messages now emit the same top-level HAPI localId that is sent as Codex clientUserMessageId, and the launcher test asserts the visible user bubble keeps that id.

Verified:

  • bun typecheck
  • cd cli && bun run test src/codex/codexRemoteLauncher.test.ts src/codex/runCodex.test.ts
  • cd hub && bun test src/web/routes/sessions.test.ts src/store/messages.test.ts

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Findings

  • [Major] Validate the rollback target before aborting the active turn — codexRollbackToMessage interrupts and resets the current turn before resolveRollbackPlan() verifies that the requested HAPI localId exists in the current Codex thread. If the lookup fails (for example a local message row exists but the app-server thread cannot map it), the API returns an error while the in-flight Codex turn has already been cancelled and HAPI messages are left unchanged. Evidence cli/src/codex/codexRemoteLauncher.ts:2832
    Suggested fix:
    const plan = await resolveRollbackPlan(threadId, localId);
    await interruptActiveTurn();
    resetCurrentTurnState();
    
    await appServerClient.rollbackThread({
        threadId,
        numTurns: plan.numTurns
    }, {
        signal: this.abortController.signal
    });

Questions

  • None

Summary

  • Review mode: follow-up after new commits
  • The prior steer localId issue is addressed in the follow-up commit. One remaining issue: failed rewind validation can still cancel a running Codex turn before proving the requested target is valid.

Testing

  • Not run (automation)

HAPI Bot

const localId = readLocalIdParam(payload);
return await runCodexOperation(async () => {
const threadId = await ensureThreadForRpc();
await interruptActiveTurn();
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] Validate the rollback target before aborting the active turn

codexRollbackToMessage interrupts and resets the current turn before resolveRollbackPlan() verifies that the requested HAPI localId exists in the current Codex thread. If the lookup fails (for example a local message row exists but the app-server thread cannot map it), the API returns an error while the in-flight Codex turn has already been cancelled and HAPI messages are left unchanged.

Suggested fix:

const plan = await resolveRollbackPlan(threadId, localId);
await interruptActiveTurn();
resetCurrentTurnState();

await appServerClient.rollbackThread({
    threadId,
    numTurns: plan.numTurns
}, {
    signal: this.abortController.signal
});

@NightWatcher314
Copy link
Copy Markdown
Contributor Author

Thanks, fixed in 71f830c: codexRollbackToMessage now resolves and validates the target turn before interrupting/resetting the active turn. Added a regression test that failed target lookup does not interrupt the in-flight Codex turn.

Verified:

  • bun typecheck
  • cd cli && bun run test src/codex/codexRemoteLauncher.test.ts src/codex/runCodex.test.ts
  • cd hub && bun test src/web/routes/sessions.test.ts src/store/messages.test.ts

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Findings

  • [Major] Mark steer echo messages as invoked — codexSteerCurrentTurn now echoes the steer text back into HAPI with session.sendUserMessage(..., clientUserMessageId), but that path inserts a DB row with invoked_at = NULL whenever a localId is present (hub/src/store/messages.ts:69). Normal web prompts are later fixed by the queue messages-consumed ack, but steer bypasses the queue after turn/steer, so no ack is emitted and the UI keeps the already-sent steer message in the queued/pending state across refreshes. Evidence cli/src/codex/codexRemoteLauncher.ts:2921
    Suggested fix:
    session.sendUserMessage(text, undefined, clientUserMessageId);
    session.client.emitMessagesConsumed([clientUserMessageId]);

Questions

  • None

Summary

  • Review mode: follow-up after new commits
  • The prior rollback validation issue is fixed in the new commit. One remaining issue: steer messages are persisted as uninvoked queued rows because the new local-id echo does not emit the corresponding consumption ack.

Testing

  • Not run (automation)

HAPI Bot

}, {
signal: this.abortController.signal
});
session.sendUserMessage(text, undefined, clientUserMessageId);
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] Mark steer echo messages as invoked

codexSteerCurrentTurn now echoes the steer text back into HAPI with session.sendUserMessage(..., clientUserMessageId), but that path inserts a DB row with invoked_at = NULL whenever a localId is present (hub/src/store/messages.ts:69). Normal web prompts are later fixed by the queue messages-consumed ack, but steer bypasses the queue after turn/steer, so no ack is emitted and the UI keeps the already-sent steer message in the queued/pending state across refreshes.

Suggested fix:

session.sendUserMessage(text, undefined, clientUserMessageId);
session.client.emitMessagesConsumed([clientUserMessageId]);

@NightWatcher314
Copy link
Copy Markdown
Contributor Author

Thanks, fixed in e1f473e: steer echoes now immediately emit messages-consumed for the same localId after the HAPI user bubble is written, so the steer message is visible but not left in queued/pending state. The launcher test now asserts both the localId-preserving echo and the consumed ack.

Verified:

  • bun typecheck
  • cd cli && bun run test src/codex/codexRemoteLauncher.test.ts src/codex/runCodex.test.ts
  • cd hub && bun test src/web/routes/sessions.test.ts src/store/messages.test.ts

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Findings

  • No high-confidence issues found.

Questions

  • None

Summary

  • Review mode: follow-up after new commits
  • No blocking issues found in the latest added/modified lines. The previous steer-message acknowledgement issue is addressed at the current head. Residual risk: full project tests were not run in this automated review pass.

Testing

  • Not run (automation)

HAPI Bot

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Findings

  • No high-confidence issues found.

Questions

  • None

Summary

  • Review mode: follow-up after new commits
  • No blocking issues found in the latest added/modified lines. Full PR diff reviewed, with the prior bot review and compare diff used as context only. Residual risk: I did not independently run the test suite in this review pass.

Testing

  • Not run (automation)

HAPI Bot

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.

1 participant