feat: add hapi resume command#647
Conversation
There was a problem hiding this comment.
Findings
-
[Major] Inactive local sessions are rejected as already local —
controlledByUseris sticky session state and is not cleared on session end, so a completed local session can beactive: falsewithcontrolledByUser: true. This added check rejects those otherwise resumable inactive sessions before launch. Evidencecli/src/commands/resume.ts:184.
Suggested fix:if (target.active && target.controlledByUser) { throw new Error('Session is already controlled by a local terminal') } if (target.active) { await api.handoffSessionToLocal(target.sessionId) }
-
[Major] Codex collaboration mode is dropped on local resume — the hub includes
target.collaborationMode, but the new Codex dispatch only passes permission/model/reasoning. A remote Codex session inplancollaboration mode resumes locally asdefault, changing the next turn behavior. Evidencecli/src/commands/resume.ts:99.
Suggested fix:await runCodex({ existingSessionId: base.existingSessionId, workingDirectory: base.workingDirectory, resumeSessionId: base.resumeSessionId, startedBy: base.startedBy, permissionMode: base.permissionMode as CodexPermissionMode | undefined, model: target.model ?? undefined, modelReasoningEffort: (target.modelReasoningEffort ?? undefined) as ReasoningEffort | undefined, collaborationMode: target.collaborationMode }) // cli/src/codex/runCodex.ts collaborationMode?: EnhancedMode['collaborationMode']; let currentCollaborationMode: EnhancedMode['collaborationMode'] = opts.collaborationMode ?? 'default';
-
[Minor]
handoffis only added to the socket reason schema, not the shared sync-event schema — handoff cleanup emits asession-endedevent with the same reason value, butSyncEventSchemastill accepts onlycompleted | terminated | error. Any consumer validating SSE/realtime events with the shared schema will reject handoff events. Evidenceshared/src/socket.ts:70.
Suggested fix:// shared/src/schemas.ts const SessionEndReasonSchema = z.enum(['completed', 'terminated', 'error', 'handoff']) SessionChangedSchema.extend({ type: z.literal('session-ended'), reason: SessionEndReasonSchema.optional() })
Questions
- None.
Summary
- Review mode: initial
- The resume path needs fixes for inactive local-session resume, Codex collaboration-mode preservation, and shared event schema consistency.
Testing
- Not run (automation):
bunis not installed in this runner, sobun typecheckfailed before executing.
HAPI Bot
| } | ||
|
|
||
| if (target.active) { | ||
| await api.handoffSessionToLocal(target.sessionId) |
There was a problem hiding this comment.
[Major] This rejects inactive sessions that were last controlled locally. controlledByUser is set when a local terminal owns the session, but session end only marks the session inactive; it does not clear that agent-state flag. That makes a completed local session (active: false, controlledByUser: true) fail before dispatchLocalResume, even though inactive sessions do not need a handoff.
Suggested fix:
if (target.active && target.controlledByUser) {
throw new Error('Session is already controlled by a local terminal')
}
if (target.active) {
await api.handoffSessionToLocal(target.sessionId)
}| startedBy: base.startedBy, | ||
| permissionMode: base.permissionMode as CodexPermissionMode | undefined, | ||
| model: target.model ?? undefined, | ||
| modelReasoningEffort: (target.modelReasoningEffort ?? undefined) as ReasoningEffort | undefined |
There was a problem hiding this comment.
[Major] The resume target includes collaborationMode, but the Codex launch options drop it here. A remote Codex session currently in plan collaboration mode resumes locally with currentCollaborationMode = 'default', so the next turn runs under different collaboration behavior than the session had before handoff.
Suggested fix:
await runCodex({
existingSessionId: base.existingSessionId,
workingDirectory: base.workingDirectory,
resumeSessionId: base.resumeSessionId,
startedBy: base.startedBy,
permissionMode: base.permissionMode as CodexPermissionMode | undefined,
model: target.model ?? undefined,
modelReasoningEffort: (target.modelReasoningEffort ?? undefined) as ReasoningEffort | undefined,
collaborationMode: target.collaborationMode
})
// cli/src/codex/runCodex.ts
collaborationMode?: EnhancedMode['collaborationMode'];
let currentCollaborationMode: EnhancedMode['collaborationMode'] = opts.collaborationMode ?? 'default';|
|
||
| export type TerminalErrorPayload = z.infer<typeof TerminalErrorPayloadSchema> | ||
| export const SessionEndReasonSchema = z.enum(['completed', 'terminated', 'error']) | ||
| export const SessionEndReasonSchema = z.enum(['completed', 'terminated', 'error', 'handoff']) |
There was a problem hiding this comment.
[Minor] Adding handoff here makes CLI session-end payloads valid, but handoff cleanup also flows into the hub's session-ended sync event. shared/src/schemas.ts still validates that event reason as only completed | terminated | error, so clients or tests using SyncEventSchema will reject the new handoff events.
Suggested fix:
// shared/src/schemas.ts
const SessionEndReasonSchema = z.enum(['completed', 'terminated', 'error', 'handoff'])
SessionChangedSchema.extend({
type: z.literal('session-ended'),
reason: SessionEndReasonSchema.optional()
})There was a problem hiding this comment.
Review mode: follow-up after new commits
No findings. The resume flow, handoff endpoint, and shared schema updates are internally consistent after the latest commit. Residual risk: I did not run the Bun test suite in this review environment.
Testing
- Not run (automation)
HAPI Bot
|
Addressed the three review findings in c0dda24:
Verification:
|
Summary
hapi resume [id]to list current-machine resumable sessions or resume one locally.Testing
bun typecheckbun run testCloses #612