Skip to content

feat: add hapi resume command#647

Merged
tiann merged 11 commits into
tiann:mainfrom
lekoOwO:hapi-resume-implementation
May 19, 2026
Merged

feat: add hapi resume command#647
tiann merged 11 commits into
tiann:mainfrom
lekoOwO:hapi-resume-implementation

Conversation

@lekoOwO
Copy link
Copy Markdown
Contributor

@lekoOwO lekoOwO commented May 19, 2026

Summary

  • Add shared local resume protocol types and hub CLI endpoints for resumable sessions, resume targets, and local handoff.
  • Add hapi resume [id] to list current-machine resumable sessions or resume one locally.
  • Add existing-session bootstrap and local handoff RPC wiring across supported agent entrypoints.

Testing

  • bun typecheck
  • bun run test

Closes #612

@lekoOwO lekoOwO marked this pull request as ready for review May 19, 2026 19:05
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] Inactive local sessions are rejected as already local — controlledByUser is sticky session state and is not cleared on session end, so a completed local session can be active: false with controlledByUser: true. This added check rejects those otherwise resumable inactive sessions before launch. Evidence cli/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 in plan collaboration mode resumes locally as default, changing the next turn behavior. Evidence cli/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] handoff is only added to the socket reason schema, not the shared sync-event schema — handoff cleanup emits a session-ended event with the same reason value, but SyncEventSchema still accepts only completed | terminated | error. Any consumer validating SSE/realtime events with the shared schema will reject handoff events. Evidence shared/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): bun is not installed in this runner, so bun typecheck failed before executing.

HAPI Bot

}

if (target.active) {
await api.handoffSessionToLocal(target.sessionId)
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 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)
}

Comment thread cli/src/commands/resume.ts Outdated
startedBy: base.startedBy,
permissionMode: base.permissionMode as CodexPermissionMode | undefined,
model: target.model ?? undefined,
modelReasoningEffort: (target.modelReasoningEffort ?? undefined) as ReasoningEffort | undefined
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] 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';

Comment thread shared/src/socket.ts Outdated

export type TerminalErrorPayload = z.infer<typeof TerminalErrorPayloadSchema>
export const SessionEndReasonSchema = z.enum(['completed', 'terminated', 'error'])
export const SessionEndReasonSchema = z.enum(['completed', 'terminated', 'error', 'handoff'])
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] 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()
})

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.

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

@lekoOwO
Copy link
Copy Markdown
Contributor Author

lekoOwO commented May 19, 2026

Addressed the three review findings in c0dda24:

  • Inactive sessions with sticky controlledByUser now resume normally; only active local sessions are rejected.
  • Codex local resume now carries collaborationMode through to runCodex and the loop startup state.
  • Shared SyncEventSchema now accepts session-ended events with reason: "handoff".

Verification:

  • Added regression coverage for the resume command, Codex resume collaboration mode, and shared handoff sync-event schema.
  • bun typecheck && bun run test
  • PR checks passing.

@tiann tiann merged commit 197f327 into tiann:main May 19, 2026
2 checks passed
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.

feat: 支持通过 tmux 启动远程 session,以便本地终端可以 attach

2 participants