mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
master and this branch both rewrote the two `_claudeSessionId` resets inside
`start()`, so `src/session.ts` conflicted at both of them.
master's commit ccfda623 puts `restoredConversation` at the head of each
fallback chain. A restored mux attach means the CLI never stopped, so a
`/clear` before the Codeman restart may already have moved it to a
conversation the launch id knows nothing about. The persisted chain's tail is
that conversation, and the CLI's own hook reported it first-hand.
This branch adds `this._codexConfig?.resumeSessionId` to the same two chains,
so a resumed codex session keeps its thread-id alias across every mux reattach
and boot recovery.
Both fixes belong. Each chain now reads restoredConversation, then
_resumeSessionId, then omp's alias, then codex's alias, then the launch id.
The comments from both sides are kept.
test/session-claude-conversation-chain.test.ts pins the shape of those two
assignments by matching the source text, and its pattern named omp's alias as
the last term before `this.id`. Codex's alias now sits between the two, so the
pattern widens to pin the ends of the chain and let the middle grow. A `[^;]`
run cannot cross a statement boundary, so each match is still one assignment.
Checked on the merged tree: typecheck, lint, prettier and the frontend syntax
check all pass, and the CI suite runs 6721 tests green across 349 files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
121 lines
5.9 KiB
TypeScript
121 lines
5.9 KiB
TypeScript
/**
|
|
* @fileoverview Session.claudeSessionChain — the record of which Claude
|
|
* conversations a pane has actually been on.
|
|
*
|
|
* Which conversation the response viewer reads is `Session.claudeSessionId`,
|
|
* and `start()` reassigns it to the launch id at THREE separate points. That is
|
|
* correct for a fresh pane and a lie for a re-attached one: a mux session that
|
|
* survived a Codeman restart never stopped, so the CLI may have `/clear`ed hours
|
|
* ago and moved to a conversation the launch id knows nothing about. The chain
|
|
* is what carries that across the restart, and its tail must therefore outrank
|
|
* the launch id on the restored path only.
|
|
*
|
|
* Two properties are pinned here because both were broken in ways nothing else
|
|
* caught:
|
|
*
|
|
* 1. **Only a first-hand adoption extends the chain.** The id has to come from
|
|
* the CLI's own hook payload, delivered under the pane's `$CODEMAN_SESSION_ID`.
|
|
* A history-correlated guess writing into this record would make the
|
|
* "showed a stranger's conversation" bug permanent instead of transient.
|
|
* 2. **A restored conversation survives every reset point.** The mux branch and
|
|
* the unconditional "third reset point" after it both reassign the field, so
|
|
* patching only the first leaves the restore silently undone.
|
|
*
|
|
* Port: N/A
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { Session } from '../src/session.js';
|
|
|
|
describe('Session claude conversation chain', () => {
|
|
it('extends the chain only for a first-hand adoption', () => {
|
|
const session = new Session({ workingDir: '/tmp', mode: 'claude' });
|
|
|
|
// A correlated guess: adopted for display, but never recorded.
|
|
session.adoptClaudeSessionId('guessed-conversation');
|
|
expect(session.claudeSessionId).toBe('guessed-conversation');
|
|
expect(session.claudeSessionChain).toEqual([]);
|
|
expect(session.claudeSessionIdIsFirstHand).toBe(false);
|
|
|
|
// The CLI's own hook: recorded.
|
|
session.adoptClaudeSessionId('hook-conversation', { firstHand: true });
|
|
expect(session.claudeSessionChain).toEqual(['hook-conversation']);
|
|
expect(session.claudeSessionIdIsFirstHand).toBe(true);
|
|
});
|
|
|
|
it('records a /clear successor once, however many prompts report it', () => {
|
|
const session = new Session({ workingDir: '/tmp', mode: 'claude' });
|
|
|
|
session.adoptClaudeSessionId('conv-1', { firstHand: true });
|
|
session.adoptClaudeSessionId('conv-1', { firstHand: true }); // every prompt reports the same id
|
|
session.adoptClaudeSessionId('conv-2', { firstHand: true }); // the user ran /clear
|
|
|
|
expect(session.claudeSessionChain).toEqual(['conv-1', 'conv-2']);
|
|
expect(session.claudeSessionId).toBe('conv-2');
|
|
});
|
|
|
|
it('moves a resumed conversation to the tail instead of duplicating it', () => {
|
|
const session = new Session({ workingDir: '/tmp', mode: 'claude' });
|
|
|
|
session.adoptClaudeSessionId('conv-1', { firstHand: true });
|
|
session.adoptClaudeSessionId('conv-2', { firstHand: true });
|
|
session.adoptClaudeSessionId('conv-1', { firstHand: true }); // /resume back
|
|
|
|
expect(session.claudeSessionChain).toEqual(['conv-2', 'conv-1']);
|
|
});
|
|
|
|
it('round-trips the chain through toState and re-pins the conversation on restore', () => {
|
|
const original = new Session({ workingDir: '/tmp', mode: 'claude' });
|
|
original.adoptClaudeSessionId('conv-1', { firstHand: true });
|
|
original.adoptClaudeSessionId('conv-2', { firstHand: true });
|
|
|
|
const state = original.toState() as { claudeSessionChain?: string[] };
|
|
expect(state.claudeSessionChain).toEqual(['conv-1', 'conv-2']);
|
|
|
|
// Boot recovery rebuilds the pane from that state. The launch id would point
|
|
// the viewer at the pre-/clear conversation; the chain's tail corrects it.
|
|
const restored = new Session({
|
|
workingDir: '/tmp',
|
|
mode: 'claude',
|
|
id: original.id,
|
|
claudeSessionChain: state.claudeSessionChain,
|
|
});
|
|
expect(restored.claudeSessionId).toBe('conv-2');
|
|
// ⚠️ NOT restored: a persisted claim is not a fact. The pane re-earns the
|
|
// guess-free path from its next hook.
|
|
expect(restored.claudeSessionIdIsFirstHand).toBe(false);
|
|
});
|
|
|
|
it('omits the chain from toState when the pane never moved conversation', () => {
|
|
const session = new Session({ workingDir: '/tmp', mode: 'claude' });
|
|
expect((session.toState() as { claudeSessionChain?: string[] }).claudeSessionChain).toBeUndefined();
|
|
});
|
|
|
|
it('applies the restored conversation at EVERY reset point in start()', () => {
|
|
// ⚠️ Structural pin, not a behavioural one: exercising start() needs a real
|
|
// PTY and mux. start() reassigns _claudeSessionId at three points, and the
|
|
// last one runs unconditionally AFTER the mux branch — so patching only the
|
|
// mux branch leaves the restore silently undone, which is what shipped
|
|
// before this existed. Every assignment built from the launch-id fallback
|
|
// must therefore carry `restoredConversation` first.
|
|
const source = readFileSync(resolve(import.meta.dirname, '../src/session.ts'), 'utf8');
|
|
// The tail of the chain grows as each CLI gains a resume alias of its own
|
|
// (omp, then codex), so the pattern pins the two ends and lets the middle
|
|
// widen. A `[^;]` run cannot cross a statement boundary, so each match is
|
|
// still one assignment.
|
|
const fallbackAssignments = source.match(/_claudeSessionId =[^;]*?_resumeSessionId[^;]*?this\.id;/g);
|
|
expect(fallbackAssignments).not.toBeNull();
|
|
expect(fallbackAssignments!.length).toBeGreaterThanOrEqual(2);
|
|
for (const assignment of fallbackAssignments!) {
|
|
expect(assignment).toContain('restoredConversation ||');
|
|
}
|
|
});
|
|
|
|
it('leaves a fresh pane on its launch id', () => {
|
|
const session = new Session({ workingDir: '/tmp', mode: 'claude' });
|
|
expect(session.claudeSessionId).toBe(session.id);
|
|
expect(session.claudeSessionChain).toEqual([]);
|
|
});
|
|
});
|