mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix(mobile): carry resumeId through the phone overview's past rows
#386 made Codex conversations resumable from Past Sessions, and resumeMobileOverviewSession() correctly passes row.resumeId on to resumeHistorySession(). The phone's own row projection never copied the field off the unified-list item though, so row.resumeId was always undefined there and a tapped Codex row started a FRESH session on a thread that was already on disk. The desktop path worked; only the phone was blind. The test fails without the projection line, and pins the other half too: a claude row must not grow a resumeId, since the field is what distinguishes "resume this conversation" from "start a new one". Docs: CLAUDE.md and architecture-invariants both still described the unified list as merging Claude transcript files. It has been three stores since this PR (Claude's ~/.claude/projects, omp's ~/.omp/agent/sessions, codex's ~/.codex/sessions), the alias field keeps its Claude-era name without being Claude-only, and the scanner-only rule behind resumeId was written down nowhere. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -231,7 +231,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph
|
||||
|
||||
**PR bot** (`scripts/pr-bot/`, maintainer tooling, NOT part of the server; `docs/pr-bot.md`): a daemon (`codeman-pr-bot` user unit) that lists open PRs with `gh`, reviews each head commit once in a Codeman claude session named `prbot-<n>` running in a private `git clone --shared` under `~/.codeman/pr-bot/worktrees/` (a clone, NOT a linked worktree: Claude Code reads a linked worktree's project settings from the MAIN checkout, so its `opus[1m]` pin silently overrode the bot's `modelOverride`, measured), and reports verdict + ranked findings + recommendation to Telegram with buttons. ⚠️ It reviews on its own but **never writes to GitHub on its own**: merge / close / post-comment / approve-CI happen only from a Telegram command or button from the configured chat, and merge/close/post take a second confirmation tap (`runConfirmed` in `bot.ts` is the one write site). ⚠️ The shared checkout is never checked out or reset by it (it only fetches into `refs/pr-bot/<n>`, which also anchors the clone's objects against gc), a clone's `node_modules` is a SYMLINK into the main checkout unless the PR changes the lockfile (then the link is unlinked before `npm ci`), and `src/web/public/vendor` is copied per file, never linked, because postinstall regenerates it in place. Readiness/end-of-turn follow the codeman skill's rules (composer first, trust dialog read off the screen, `stop,blocked,exit` never `idle`). The Telegram token + chat id come from the existing notifier bot's `~/codeman-cases/telegram/.env`. Type-checked via `config/tsconfig.pr-bot.json` (part of `npm run typecheck`), linted/formatted with `src/`; tests `test/pr-bot-report.test.ts`, `test/pr-bot-state.test.ts`, `test/pr-bot-commands.test.ts` (the confirm-before-write flows against stubbed `gh`/Telegram).
|
||||
|
||||
**Unified session list**: `GET /api/sessions/unified` merges live sessions, persisted state, lifecycle-log history, and Claude transcript files into one deduped list (pure core in `src/services/unified-session-service.ts`). Transcript rows fold into their owning session via a `claudeSessionId → Codeman id` alias map, so resumed and `/clear`-respawned sessions do not appear twice. No terminal buffers in the response, unlike `/api/sessions`. Backs the Cmd+K Session Manager, plus pinning and cross-device tab order (`PUT /api/session-order`; pure merge helpers in `src/session-order.ts`, pushing device wins and server-only ids are never dropped). → [architecture-invariants#unified-session-list-and-session-manager](docs/architecture-invariants.md#unified-session-list-and-session-manager)
|
||||
**Unified session list**: `GET /api/sessions/unified` merges live sessions, persisted state, lifecycle-log history, and transcript files into one deduped list (pure core in `src/services/unified-session-service.ts`). ⚠️ **Transcript history is THREE stores, not one**, because each CLI keeps its conversations in its own: Claude's `~/.claude/projects`, omp's `~/.omp/agent/sessions` and codex's `~/.codex/sessions` (#386). Rows fold into their owning session via the `claudeSessionId → Codeman id` alias map, so resumed and `/clear`-respawned sessions do not appear twice; that field is named for Claude and carries whatever id the CLI names its conversation with, which for every non-Claude row diverges from the Codeman id by construction. ⚠️ **`resumeId` is set by a SCANNER row only, never by a live session**, and that is what makes it safe to resume on: a row carrying one is a conversation already on disk, so `resumeHistorySession()` sends `codexConfig.resumeSessionId` and a row without one is a genuinely fresh session. Every surface that re-projects these rows has to carry the field through, the phone overview included, or a tap on that surface silently starts a second conversation. No terminal buffers in the response, unlike `/api/sessions`. Backs the Cmd+K Session Manager, plus pinning and cross-device tab order (`PUT /api/session-order`; pure merge helpers in `src/session-order.ts`, pushing device wins and server-only ids are never dropped). → [architecture-invariants#unified-session-list-and-session-manager](docs/architecture-invariants.md#unified-session-list-and-session-manager)
|
||||
|
||||
**Owner tab layouts** (COD-359, `tab-layout*.ts` + `GET`/`PUT /api/tab-layout`): named tab GROUPS over the flat tab strip, scoped per owner (`SINGLE_USER_LAYOUT_OWNER` = `@single` when multi-user is off), persisted under the `tabLayouts` key in state.json. A layout is `{version, groups[], ungrouped[], updatedAt}` whose refs point at either a session or a saved webview (`TabRefKind`), capped at 32 groups / 512 refs. ⚠️ **BACKEND ONLY as of 1.24.1**: nothing in `src/web/public/` calls these routes yet, so a UI built on top is new frontend work, not a rewiring job. ⚠️ **`TabLayoutService` is the single mutation boundary** and every lifecycle caller (session created/removed, webview created/deleted, a legacy order PUT) describes ONE completed server action and gets AT MOST ONE versioned write; writing layout state from a route or a manager directly is what the service exists to prevent. ⚠️ The layout does not replace `PUT /api/session-order`, it PROJECTS onto it: `tab-layout-legacy-order.ts` is the pure translation both ways (`putLegacyOrder()` recomposes a global order from the owner's groups), so changing one side without the other silently desyncs the tab strip from the stored layout. ⚠️ **Reconciliation is gated on a SUCCESSFUL restore** (`markRestorationComplete` / `markRestorationFailed` / `markRestorationSkipped`, plus `assertDeletionReady()`): pruning refs against a session list that failed to load would delete live tabs, so a failed restore must leave the layout untouched. `PUT` takes exactly `{baseVersion, layout}` (any other key shape is a validation error), answers a stale `baseVersion` with the current layout rather than clobbering, and is capped at 128 KiB. Broadcasts `tab:layoutChanged`, owner-routed via `deriveTabLayoutSseHint`.
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ Tests: `test/docker-hosts.test.ts`, `test/docker-exec-options.test.ts`, `test/do
|
||||
|
||||
### Unified session list and Session Manager
|
||||
|
||||
**Unified session list** (COD-160/#139): `GET /api/sessions/unified?limit=&q=` merges live sessions, persisted state, lifecycle-log history, and Claude transcript files into one deduped list (pure core in `src/services/unified-session-service.ts`). Transcript rows are keyed by conversation UUID and folded into their owning session via a `claudeSessionId → Codeman id` alias map (resumed//clear-respawned sessions must not appear twice); lifecycle name/mode resolution is first-seen-wins (the log returns entries NEWEST-first). No terminal buffers in the response (unlike `/api/sessions`). Consumed by the Cmd+K Session Manager (#146). Session Manager polish (COD-162/#157, 1.6.0): **pinning** via `POST /api/sessions/:id/pin` (`session:pinned` SSE; killing a pinned session demotes it to a lightweight stopped record that stays visible/resumable, and cleanup skips pinned records); **cross-device tab order** via `PUT /api/session-order` (`session:orderChanged` SSE, persisted in `state.json`; pure `normalizeSessionOrder`/`mergeSessionOrder` in `src/session-order.ts`: pushing device wins, server-only ids fall to the end, never dropped); resume from the manager keeps the original session name (COD-143); `firstPrompt` is backfilled for sessions whose id != transcript UUID and the most recent prompt (`lastPrompt`) is shown + searched (COD-140/145).
|
||||
**Unified session list** (COD-160/#139): `GET /api/sessions/unified?limit=&q=` merges live sessions, persisted state, lifecycle-log history, and transcript files into one deduped list (pure core in `src/services/unified-session-service.ts`). ⚠️ **Transcript history is three stores** (#386): Claude's `~/.claude/projects`, omp's `~/.omp/agent/sessions` and codex's `~/.codex/sessions`. Rows are keyed by whatever id that CLI names the conversation with and folded into their owning session via the `claudeSessionId → Codeman id` alias map (resumed//clear-respawned sessions must not appear twice; the field keeps its Claude-era name and is not Claude-only). A codex row additionally carries `resumeId`, the rollout's own thread id, set by the scanner and never by a live session, which is what lets a row be resumed through `codexConfig.resumeSessionId` while a row without one stays a fresh session; the alias chain therefore includes `config.codexConfig?.resumeSessionId`, and a fresh codex pane is matched by `session_meta.originator` (`codeman_<sessionId>` for every pane Codeman spawns); lifecycle name/mode resolution is first-seen-wins (the log returns entries NEWEST-first). No terminal buffers in the response (unlike `/api/sessions`). Consumed by the Cmd+K Session Manager (#146). Session Manager polish (COD-162/#157, 1.6.0): **pinning** via `POST /api/sessions/:id/pin` (`session:pinned` SSE; killing a pinned session demotes it to a lightweight stopped record that stays visible/resumable, and cleanup skips pinned records); **cross-device tab order** via `PUT /api/session-order` (`session:orderChanged` SSE, persisted in `state.json`; pure `normalizeSessionOrder`/`mergeSessionOrder` in `src/session-order.ts`: pushing device wins, server-only ids fall to the end, never dropped); resume from the manager keeps the original session name (COD-143); `firstPrompt` is backfilled for sessions whose id != transcript UUID and the most recent prompt (`lastPrompt`) is shown + searched (COD-140/145).
|
||||
|
||||
### Session lineage lines (tab → tab it spawned)
|
||||
|
||||
|
||||
@@ -228,6 +228,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
name: item.name || '',
|
||||
title: title || item.name || dir.split('/').pop() || item.sessionId.slice(0, 8),
|
||||
mode: item.mode || 'claude',
|
||||
// Only the scanner sets this, and only for a codex rollout. Dropping it here
|
||||
// is not cosmetic: resumeMobileOverviewSession() passes row.resumeId on to
|
||||
// resumeHistorySession(), so without it a tapped Codex row starts a FRESH
|
||||
// session on a thread that is already on disk.
|
||||
resumeId: item.resumeId || undefined,
|
||||
caseName: matched ? matched.name : '',
|
||||
dir: this._shortenHomePath ? this._shortenHomePath(dir) : dir,
|
||||
at: item.lastActivityAt || item.createdAt || 0,
|
||||
|
||||
@@ -224,6 +224,37 @@ describe('mobile overview model', () => {
|
||||
expect(model.past[0].title).toBe('w4-claudeman');
|
||||
});
|
||||
|
||||
it('carries resumeId through to the past row so a Codex tap resumes its thread', () => {
|
||||
const app = loadOverviewApp();
|
||||
const model = app.buildMobileOverviewModel({
|
||||
sessions: [],
|
||||
cases: CASES,
|
||||
history: [
|
||||
{
|
||||
sessionId: 'rollout-1',
|
||||
workingDir: '/home/arkon/codeman-cases/beta',
|
||||
firstPrompt: 'port the parser',
|
||||
mode: 'codex',
|
||||
resumeId: 'codex-thread-id',
|
||||
lastActivityAt: 300,
|
||||
},
|
||||
// A claude row carries none, and must not grow one.
|
||||
{
|
||||
sessionId: 'claude-1',
|
||||
workingDir: '/home/arkon/default/claudeman',
|
||||
claudeSessionId: 'claude-uuid-a',
|
||||
lastActivityAt: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// resumeMobileOverviewSession() reads row.resumeId off exactly this projection and
|
||||
// hands it to resumeHistorySession(); an undefined here is a fresh codex session on
|
||||
// a thread that already exists, which is the phone-only half of the resume feature.
|
||||
expect(model.past[0]).toMatchObject({ mode: 'codex', resumeId: 'codex-thread-id' });
|
||||
expect(model.past[1].resumeId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not title a past row with the transcript reader placeholder', () => {
|
||||
const app = loadOverviewApp();
|
||||
const model = app.buildMobileOverviewModel({
|
||||
|
||||
Reference in New Issue
Block a user