@aragon-agent/cli 0.6.25 → 0.6.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +194 -13
  3. package/dist/agent/reducer.d.ts +22 -0
  4. package/dist/agent/reducer.js +4 -1
  5. package/dist/cli.js +214 -2
  6. package/dist/commands/branch-command.d.ts +61 -0
  7. package/dist/commands/branch-command.js +117 -0
  8. package/dist/commands/builtins.js +48 -6
  9. package/dist/commands/registry.d.ts +28 -0
  10. package/dist/commands/session-command.d.ts +54 -0
  11. package/dist/commands/session-command.js +109 -0
  12. package/dist/config/load.d.ts +15 -0
  13. package/dist/exec/index.js +10 -1
  14. package/dist/logging/logger.d.ts +1 -1
  15. package/dist/proc/terminal-plan.d.ts +173 -0
  16. package/dist/proc/terminal-plan.js +393 -0
  17. package/dist/session/branch-invocation.d.ts +127 -0
  18. package/dist/session/branch-invocation.js +153 -0
  19. package/dist/session/branch-launch.d.ts +79 -0
  20. package/dist/session/branch-launch.js +265 -0
  21. package/dist/session/checkpoint.d.ts +257 -0
  22. package/dist/session/checkpoint.js +512 -0
  23. package/dist/session/cli-commands.d.ts +2 -0
  24. package/dist/session/cli-commands.js +36 -5
  25. package/dist/session/identity.d.ts +72 -0
  26. package/dist/session/identity.js +148 -0
  27. package/dist/session/limits.d.ts +66 -0
  28. package/dist/session/limits.js +66 -0
  29. package/dist/session/persist.d.ts +30 -5
  30. package/dist/session/startup-session.d.ts +107 -0
  31. package/dist/session/startup-session.js +315 -0
  32. package/dist/session/store.d.ts +60 -1
  33. package/dist/session/store.js +95 -6
  34. package/dist/ui/App.d.ts +54 -2
  35. package/dist/ui/App.js +115 -21
  36. package/dist/ui/Header.js +8 -16
  37. package/dist/ui/StatusBar.d.ts +8 -0
  38. package/dist/ui/StatusBar.js +12 -3
  39. package/dist/ui/layout/header-hints.d.ts +68 -0
  40. package/dist/ui/layout/header-hints.js +138 -0
  41. package/dist/ui/layout/session-chip.d.ts +52 -0
  42. package/dist/ui/layout/session-chip.js +64 -0
  43. package/dist/ui/layout/status-layout.d.ts +10 -0
  44. package/dist/ui/layout/status-layout.js +8 -1
  45. package/dist/ui/overlays/HelpOverlay.js +9 -3
  46. package/dist/ui/session-bridge.d.ts +26 -0
  47. package/dist/ui/session-bridge.js +32 -0
  48. package/package.json +2 -2
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Session identity: minting, branching, deriving, and reserving
3
+ * (tui-session-id-resume §3.5 / D-1 / D-5 / D-23).
4
+ *
5
+ * ASCII ONLY - `src/session/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * ONE AUTHORITY FOR "WHAT IS A SESSION CALLED". `store.ts` already owns the
8
+ * SYNTAX (`SESSION_ID_RE`) and the path (`sessionPathFor`); this module owns the
9
+ * generator and the branch rule, and it produces ids that pass that syntax by
10
+ * construction rather than by a second regex nobody re-checks.
11
+ *
12
+ * BRANCHING NEVER REUSES THE PARENT ID (D-5 / I-3). A `--resume` always derives
13
+ * a NEW id, so two processes can never take turns overwriting one file: the
14
+ * parent keeps writing its own id, the branch writes its own. That is the whole
15
+ * technical content of "a branch must not affect the session it branched from".
16
+ */
17
+ import { closeSync, mkdirSync, openSync, unlinkSync, writeSync } from 'node:fs';
18
+ import { dirname } from 'node:path';
19
+ import { SESSION_ID_LIMITS } from './limits.js';
20
+ import { basenameId, sessionPathFor } from './store.js';
21
+ /**
22
+ * 31 characters: the ten digits and the lowercase alphabet MINUS `0 o 1 l i`.
23
+ *
24
+ * Those five are the pairs a human misreads when copying an id out of the
25
+ * bottom-right corner of a terminal, and copying an id by eye is the one
26
+ * irreducible failure mode this feature has (D-14 exists to remove it, and this
27
+ * alphabet removes most of the rest).
28
+ */
29
+ export const SESSION_ID_ALPHABET = '23456789abcdefghjkmnpqrstuvwxyz';
30
+ /** A generated id: 6 characters, optionally with the 4-character branch suffix. */
31
+ const GENERATED_ID_RE = /^([a-z0-9]{6})(?:-([a-z0-9]{4}))?$/;
32
+ /** One character from the alphabet. `random` is injected for deterministic tests. */
33
+ function pick(random) {
34
+ // `Math.min` because a stubbed generator may legitimately return 1.
35
+ const index = Math.min(SESSION_ID_ALPHABET.length - 1, Math.floor(Math.abs(random()) * SESSION_ID_ALPHABET.length));
36
+ return SESSION_ID_ALPHABET[index];
37
+ }
38
+ function randomChars(count, random) {
39
+ let out = '';
40
+ for (let i = 0; i < count; i += 1)
41
+ out += pick(random);
42
+ return out;
43
+ }
44
+ /** A fresh 6-character id. Satisfies `SESSION_ID_RE` by construction (D-1). */
45
+ export function createSessionId(random = Math.random) {
46
+ return randomChars(SESSION_ID_LIMITS.idLength, random);
47
+ }
48
+ /**
49
+ * The child id for `parentId`: 6-character root plus `-` plus 4 characters.
50
+ *
51
+ * THE ROOT DOES NOT GROW. `a4k2m9` and `a4k2m9-x7f2` both branch to
52
+ * `a4k2m9-<new>`, so a chain of branches keeps a name that still fits the
53
+ * status bar and still points at the conversation it came from.
54
+ */
55
+ export function branchSessionId(parentId, random = Math.random) {
56
+ const root = GENERATED_ID_RE.exec(parentId)?.[1];
57
+ if (!root)
58
+ return createSessionId(random);
59
+ const suffix = randomChars(SESSION_ID_LIMITS.suffixLength, random);
60
+ return `${root}-${suffix}`;
61
+ }
62
+ /** Whether `id` is a shape this CLI mints (as opposed to a hand-chosen name). */
63
+ export function isGeneratedSessionId(id) {
64
+ return GENERATED_ID_RE.test(id);
65
+ }
66
+ /**
67
+ * The next id for a session LOADED FROM `loadedPath`, or `null` when the file
68
+ * name is not a generated id.
69
+ *
70
+ * THE INPUT IS A PATH, NOT AN ID, AND THE PARENT'S IDENTITY COMES FROM THE FILE
71
+ * NAME (D-5 / R-8). `meta.id` lives inside the file, and a file is input - a
72
+ * hand-written `meta.id` must not be able to name where this process writes.
73
+ * `null` means "this was a `/save` export or an archive": the caller mints a
74
+ * fresh id and the new file has no `branchOf`.
75
+ */
76
+ export function derivedChildId(loadedPath, random = Math.random) {
77
+ const base = basenameId(loadedPath);
78
+ return isGeneratedSessionId(base) ? branchSessionId(base, random) : null;
79
+ }
80
+ /**
81
+ * Claim `id` with an atomic `wx` create, writing a READABLE EMPTY SESSION into
82
+ * it (D-23 / R-3).
83
+ *
84
+ * `wx` IS THE MUTUAL EXCLUSION and stays that way: `existsSync` followed by a
85
+ * write has a window in which two processes both see "absent". What changed in
86
+ * v2 is the CONTENT. A 0-byte placeholder is unparseable, so `loadSession()`
87
+ * throws a raw `SyntaxError` on it, `listSessions` silently skips it, and
88
+ * `--save` refuses the name forever - a file the user can neither see nor
89
+ * resume. A legal empty `SavedSession` fixes all three at once: it loads, it
90
+ * lists, and `sessions rm` can delete it.
91
+ *
92
+ * NO `meta` IS WRITTEN, deliberately: "has `meta`" stays equivalent to "at least
93
+ * one checkpoint has been written", which is the predicate `sessions list` /
94
+ * `prune` are built on. `model` is written only when the caller has one - it is
95
+ * optional in the format, and the startup path resolves an identity BEFORE the
96
+ * controller exists, so a fabricated model would be a lie on disk for a field
97
+ * nothing reads back.
98
+ */
99
+ export function reserveSessionId(id, opts = {}) {
100
+ const path = (opts.pathFor ?? sessionPathFor)(id);
101
+ const payload = {
102
+ version: 1,
103
+ savedAt: Date.now(),
104
+ ...(opts.model ? { model: opts.model } : {}),
105
+ messages: [],
106
+ entries: [],
107
+ };
108
+ let fd;
109
+ try {
110
+ // THE DIRECTORY FIRST, and inside the same `try`: on a fresh machine
111
+ // `sessions/` does not exist yet, and a reservation that silently answered
112
+ // "name taken" because of a missing parent would send the caller into a
113
+ // pointless retry loop. A parent that is a FILE fails here, which is the
114
+ // honest answer.
115
+ mkdirSync(dirname(path), { recursive: true });
116
+ fd = openSync(path, 'wx');
117
+ }
118
+ catch {
119
+ // EEXIST is the ordinary answer to "someone already holds this name", and a
120
+ // missing directory is the ordinary answer on a fresh machine. Both mean
121
+ // "this id is not available", which is all the caller can act on.
122
+ return false;
123
+ }
124
+ try {
125
+ writeSync(fd, `${JSON.stringify(payload, null, 2)}\n`);
126
+ }
127
+ catch {
128
+ // A half-written placeholder is exactly the unreadable file D-23 exists to
129
+ // rule out, so the failure is undone rather than left behind.
130
+ closeSync(fd);
131
+ unlinkQuietly(path);
132
+ return false;
133
+ }
134
+ // Closed IMMEDIATELY: an open handle keeps Windows from replacing the file
135
+ // when the first checkpoint lands (the same reason `store.ts` closes its lock
136
+ // handle at once).
137
+ closeSync(fd);
138
+ return true;
139
+ }
140
+ /** Best effort: a cleanup failure must not turn "id unavailable" into a throw. */
141
+ function unlinkQuietly(path) {
142
+ try {
143
+ unlinkSync(path);
144
+ }
145
+ catch {
146
+ // Nothing to clean up, or someone else already did.
147
+ }
148
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Structural bounds for session identity and the session checkpoint
3
+ * (tui-session-id-resume §3.13).
4
+ *
5
+ * ASCII ONLY - `src/session/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * STRUCTURE, NOT POLICY: these are the shapes an id and a checkpoint can
8
+ * physically take, so they live in the subsystem's own `limits.ts` like every
9
+ * other subsystem's do (CLAUDE.md: "`limits.ts` is structural,
10
+ * `config/schema.ts` is policy"). Nothing here is user-tunable (D-19): a
11
+ * "show an id but never write it" switch would manufacture the one state this
12
+ * feature must not have - an id a user can copy and cannot use.
13
+ */
14
+ export declare const SESSION_ID_LIMITS: {
15
+ /** 6 characters of `SESSION_ID_ALPHABET`: 31^6 is about 8.9e8 ids. */
16
+ readonly idLength: 6;
17
+ /** The branch suffix, `-` plus this many characters. */
18
+ readonly suffixLength: 4;
19
+ /** The upper bound `SESSION_ID_RE` already imposes (`store.ts:50`). */
20
+ readonly maxIdLength: 64;
21
+ /** `maxIdLength - 1 - suffixLength` = 64 - 1 - 4. */
22
+ readonly maxRootLength: 59;
23
+ /** `wx` reservation attempts before a fresh id is minted instead. */
24
+ readonly reserveAttempts: 5;
25
+ };
26
+ export declare const SESSION_CHECKPOINT_LIMITS: {
27
+ /** The window several `agent_end` events are merged into (T-11b). */
28
+ readonly debounceMs: 750;
29
+ /** Floor between two writes, so a burst cannot land twice in one turn. */
30
+ readonly minWriteIntervalMs: 250;
31
+ /** `renameSync` retries when Windows refuses to replace an open file (D-4). */
32
+ readonly renameAttempts: 3;
33
+ /** Gap between those retries, waited synchronously (`Atomics.wait`). */
34
+ readonly renameRetryMs: 20;
35
+ /** Consecutive failures before the log record is raised from warn to error. */
36
+ readonly loudFailureStreak: 3;
37
+ };
38
+ /**
39
+ * Structural bounds for branching a session into a NEW terminal window
40
+ * (tui-branch-terminal-shortcut 5.3).
41
+ *
42
+ * WHY THESE LIVE HERE (R-12): this file's header says STRUCTURE, NOT POLICY, and
43
+ * `cooldownMs` is a timing policy. The precedent is already in this file -
44
+ * `SESSION_CHECKPOINT_LIMITS.debounceMs` / `minWriteIntervalMs` are timing
45
+ * policies of exactly the same kind. D-17 keeps every one of these out of
46
+ * `config/schema.ts`, so none of them is user-tunable.
47
+ *
48
+ * EVERY ENTRY NEEDS AN ENFORCEMENT POINT (R-7): a declared limit nobody checks is
49
+ * worse than no limit, because it makes a review believe the case was handled.
50
+ * `cooldownMs` is read in `branch-launch.ts` steps 1-2, `titleMaxCells` by
51
+ * `truncateTitle()` in step 7, `spawnProbeMs` by the `trySpawn` timer,
52
+ * `maxCommandLineCells` in step 4.5 BEFORE any spawn, and `maxRungs` as the
53
+ * bound of the rung loop in step 8.
54
+ */
55
+ export declare const BRANCH_TERMINAL_LIMITS: {
56
+ /** Minimum gap between two successful opens (D-10). FAILURES DO NOT CONSUME IT. */
57
+ readonly cooldownMs: 2000;
58
+ /** Max cells of the new window title (`aragon ` + id; an id can be 64 long). */
59
+ readonly titleMaxCells: 60;
60
+ /** How long to wait for the `'spawn'` event; a timeout counts as success. */
61
+ readonly spawnProbeMs: 1500;
62
+ /** Hard cap on one command line (base64 included); over it we refuse (step 4.5). */
63
+ readonly maxCommandLineCells: 8000;
64
+ /** How many rungs to try (the POSIX candidate table can be long; 3 covers reality). */
65
+ readonly maxRungs: 3;
66
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Structural bounds for session identity and the session checkpoint
3
+ * (tui-session-id-resume §3.13).
4
+ *
5
+ * ASCII ONLY - `src/session/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * STRUCTURE, NOT POLICY: these are the shapes an id and a checkpoint can
8
+ * physically take, so they live in the subsystem's own `limits.ts` like every
9
+ * other subsystem's do (CLAUDE.md: "`limits.ts` is structural,
10
+ * `config/schema.ts` is policy"). Nothing here is user-tunable (D-19): a
11
+ * "show an id but never write it" switch would manufacture the one state this
12
+ * feature must not have - an id a user can copy and cannot use.
13
+ */
14
+ export const SESSION_ID_LIMITS = {
15
+ /** 6 characters of `SESSION_ID_ALPHABET`: 31^6 is about 8.9e8 ids. */
16
+ idLength: 6,
17
+ /** The branch suffix, `-` plus this many characters. */
18
+ suffixLength: 4,
19
+ /** The upper bound `SESSION_ID_RE` already imposes (`store.ts:50`). */
20
+ maxIdLength: 64,
21
+ /** `maxIdLength - 1 - suffixLength` = 64 - 1 - 4. */
22
+ maxRootLength: 59,
23
+ /** `wx` reservation attempts before a fresh id is minted instead. */
24
+ reserveAttempts: 5,
25
+ };
26
+ export const SESSION_CHECKPOINT_LIMITS = {
27
+ /** The window several `agent_end` events are merged into (T-11b). */
28
+ debounceMs: 750,
29
+ /** Floor between two writes, so a burst cannot land twice in one turn. */
30
+ minWriteIntervalMs: 250,
31
+ /** `renameSync` retries when Windows refuses to replace an open file (D-4). */
32
+ renameAttempts: 3,
33
+ /** Gap between those retries, waited synchronously (`Atomics.wait`). */
34
+ renameRetryMs: 20,
35
+ /** Consecutive failures before the log record is raised from warn to error. */
36
+ loudFailureStreak: 3,
37
+ };
38
+ /**
39
+ * Structural bounds for branching a session into a NEW terminal window
40
+ * (tui-branch-terminal-shortcut 5.3).
41
+ *
42
+ * WHY THESE LIVE HERE (R-12): this file's header says STRUCTURE, NOT POLICY, and
43
+ * `cooldownMs` is a timing policy. The precedent is already in this file -
44
+ * `SESSION_CHECKPOINT_LIMITS.debounceMs` / `minWriteIntervalMs` are timing
45
+ * policies of exactly the same kind. D-17 keeps every one of these out of
46
+ * `config/schema.ts`, so none of them is user-tunable.
47
+ *
48
+ * EVERY ENTRY NEEDS AN ENFORCEMENT POINT (R-7): a declared limit nobody checks is
49
+ * worse than no limit, because it makes a review believe the case was handled.
50
+ * `cooldownMs` is read in `branch-launch.ts` steps 1-2, `titleMaxCells` by
51
+ * `truncateTitle()` in step 7, `spawnProbeMs` by the `trySpawn` timer,
52
+ * `maxCommandLineCells` in step 4.5 BEFORE any spawn, and `maxRungs` as the
53
+ * bound of the rung loop in step 8.
54
+ */
55
+ export const BRANCH_TERMINAL_LIMITS = {
56
+ /** Minimum gap between two successful opens (D-10). FAILURES DO NOT CONSUME IT. */
57
+ cooldownMs: 2_000,
58
+ /** Max cells of the new window title (`aragon ` + id; an id can be 64 long). */
59
+ titleMaxCells: 60,
60
+ /** How long to wait for the `'spawn'` event; a timeout counts as success. */
61
+ spawnProbeMs: 1_500,
62
+ /** Hard cap on one command line (base64 included); over it we refuse (step 4.5). */
63
+ maxCommandLineCells: 8_000,
64
+ /** How many rungs to try (the POSIX candidate table can be long; 3 covers reality). */
65
+ maxRungs: 3,
66
+ };
@@ -18,8 +18,17 @@ import { type CompactionIdentity } from '../compaction/memory-identity.js';
18
18
  * every bare-name and default `/save` target in `getSessionsDir()`. So
19
19
  * `aragon sessions list` / `prune` operate ONLY on files carrying `meta.id`,
20
20
  * which is what stops a maintenance command destroying conversations a human
21
- * saved by hand. Anything that starts writing `meta` from the TUI path must
22
- * revisit that scoping first.
21
+ * saved by hand.
22
+ *
23
+ * THE TUI NOW WRITES `meta`, AND THE RE-EXAMINATION THAT WARNING DEMANDED IS
24
+ * DONE (tui-session-id-resume 3.7 / R-7). Two axes were hiding in the one
25
+ * `managed` flag: "may a maintenance command see this row" (still `meta.id`) and
26
+ * "which writer produced it" (`origin`). `prune` now defaults to
27
+ * `origin === 'exec'`, so it behaves EXACTLY as it did before this feature on
28
+ * every file that existed before it; `--all` still only means "also consider
29
+ * files with no metadata at all" and can no longer reach a TUI checkpoint;
30
+ * `pinned` is a permanent opt-out and `live` plus a live owner pid is a
31
+ * temporary one. Four fields, four different jobs - none derivable from another.
23
32
  */
24
33
  export interface SessionMeta {
25
34
  id: string;
@@ -36,6 +45,20 @@ export interface SessionMeta {
36
45
  inputTokens: number;
37
46
  outputTokens: number;
38
47
  };
48
+ /**
49
+ * Who wrote this file. Absent (every pre-feature file) reads as 'exec'.
50
+ *
51
+ * OPTIONAL AND ADDITIVE, so SESSION_VERSION stays 1: validate-session.ts does
52
+ * not shape-check `meta` at all, so an older build reads a newer file and a
53
+ * newer build reads an older one without a migration.
54
+ */
55
+ origin?: 'exec' | 'tui';
56
+ /** `--save <id>` / `/session pin`: `sessions prune` never deletes it. */
57
+ pinned?: true;
58
+ /** The writer was alive at this write; omitted by the exit flush. */
59
+ live?: true;
60
+ /** The id this session branched from (D-5); absent for a fresh or named one. */
61
+ branchOf?: string;
39
62
  }
40
63
  export interface SavedSession {
41
64
  version: number;
@@ -53,13 +76,15 @@ export interface SavedSession {
53
76
  */
54
77
  todos?: TodoItem[];
55
78
  /**
56
- * Written only by `aragon exec` (cli-integration-surface §5.2 / D-10).
79
+ * Written by `aragon exec` (cli-integration-surface §5.2 / D-10) AND by the
80
+ * TUI's session checkpoint (tui-session-id-resume §6.1).
57
81
  *
58
82
  * `SESSION_VERSION` STAYS 1 for the reason `todos?` records above, and this is
59
83
  * the precedent that file's own comments already set: the field is additive
60
84
  * and optional. Boundary validation preserves it without changing its shape.
61
- * A TUI `/save` file simply has no `meta`, which is exactly how
62
- * `sessions list` tells the two apart.
85
+ * A TUI `/save` EXPORT still has no `meta` at all, which is what keeps a
86
+ * hand-made file out of the maintenance commands' scope; a TUI CHECKPOINT has
87
+ * one, and is told apart from an exec file by `meta.origin`.
63
88
  */
64
89
  meta?: SessionMeta;
65
90
  }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Startup-time session resolution: `--resume <id|path>` and `--save <id>`
3
+ * (tui-session-id-resume §3.8 / D-5..D-9 / D-20 / D-23).
4
+ *
5
+ * ASCII ONLY - `src/session/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * ONE RESOLUTION, TWO CALLERS. The TUI (`runInteractive`) and the headless
8
+ * one-shot (`runOneShot`) both come through here, and the in-TUI `/resume`
9
+ * command shares `applyRestoredSession` with them, because two copies of "adopt
10
+ * a saved conversation" is how `/clear` and `/reset` drifted apart once already.
11
+ *
12
+ * THIS MODULE DOES NO I/O BEYOND READING THE FILE AND CLAIMING THE NEW ID. It
13
+ * builds no controller and renders nothing, so every rule below is testable as a
14
+ * pure function: a caller gets either an identity plus an `apply` thunk, or one
15
+ * message and exit code 2.
16
+ *
17
+ * THE PARENT IS NEVER WRITTEN (I-3). The id comes from the file NAME, the new id
18
+ * is always derived, and the only write this module performs is the `wx` claim
19
+ * of that new id - which is a different file by construction.
20
+ */
21
+ import type { Message } from '@aragon-agent/core';
22
+ import type { Entry } from '../agent/reducer.js';
23
+ import type { TodoItem } from '../todo/types.js';
24
+ import type { CompactionIdentity } from '../compaction/memory-identity.js';
25
+ import { type SavedSession } from './persist.js';
26
+ /** Who this process is, as the rest of the CLI sees it. */
27
+ export interface SessionIdentity {
28
+ id: string;
29
+ kind: 'fresh' | 'named' | 'branch';
30
+ /** The id this one branched from; absent unless `kind === 'branch'`. */
31
+ parentId?: string;
32
+ /** `--save` / `/session pin`: `sessions prune` will not delete it. */
33
+ pinned?: boolean;
34
+ }
35
+ export interface StartupSessionInput {
36
+ /** `--resume <id|path>` - both spellings are legal (D-5 / R-8). */
37
+ resume?: string;
38
+ /** `--save <id>` - an ID, never a path (D-6). */
39
+ saveAs?: string;
40
+ cwd: string;
41
+ /** True when the user named `--model` / `--provider` explicitly (D-9). */
42
+ explicitModel?: boolean;
43
+ }
44
+ /**
45
+ * The minimum controller surface the restore needs.
46
+ *
47
+ * THREE METHODS, and `getConfig` is deliberately not one of them: the three
48
+ * calls below need nothing else, and a wider port only makes the startup path
49
+ * look like it knows more about the controller than it does.
50
+ */
51
+ export interface SessionRestorePort {
52
+ replaceMessages(messages: Message[], identity?: CompactionIdentity): void;
53
+ restoreTodos(items: TodoItem[]): void;
54
+ setModel(providerId: string, modelId: string, baseUrl?: string): void;
55
+ }
56
+ export type StartupSessionResolution = {
57
+ ok: true;
58
+ identity: SessionIdentity;
59
+ initialEntries: Entry[];
60
+ notice?: string;
61
+ /** The file this session was branched from, if any. */
62
+ saved?: SavedSession;
63
+ apply(port: SessionRestorePort): void;
64
+ } | {
65
+ ok: false;
66
+ exitCode: 2;
67
+ message: string;
68
+ };
69
+ /**
70
+ * Adopt a saved conversation. THE ONLY IMPLEMENTATION of the three calls
71
+ * `/resume` makes; the slash command and both startup paths call it.
72
+ *
73
+ * `skipModel` exists because an explicit `--model` / `--provider` on this run
74
+ * outranks the model recorded in the file: a user who says which model they want
75
+ * has said it more recently than the file did.
76
+ *
77
+ * `clearAllQueues()` and the `restoreEntries` dispatch are NOT here and belong at
78
+ * the call sites: the first is a no-op on the startup path (there are no queues
79
+ * yet) and indispensable inside a live TUI, and the second is a VIEW action with
80
+ * no controller to attach it to - the startup path seeds the reducer instead.
81
+ */
82
+ export declare function applyRestoredSession(port: SessionRestorePort, saved: SavedSession, opts?: {
83
+ skipModel?: boolean;
84
+ }): void;
85
+ /**
86
+ * Work out which session this process is, and what it starts from.
87
+ *
88
+ * FAILURE IS EXIT 2 WITH ONE COPYABLE SENTENCE, and it happens BEFORE any write
89
+ * (the only write is the id claim, and it is last). Sources: a `--save` value
90
+ * that is not an id (D-6), a `--save` id that is already taken (D-6), a `--save`
91
+ * naming the id being resumed (D-7), and a `--resume` that is missing or
92
+ * unreadable (D-8).
93
+ */
94
+ export declare function resolveStartupSession(input: StartupSessionInput): StartupSessionResolution;
95
+ /**
96
+ * Move to a derived id after a `/resume` inside the TUI (D-15 / R-9).
97
+ *
98
+ * THE ROTATION IS NOT COSMETIC. `/resume` adopts another conversation, so the
99
+ * next checkpoint would otherwise write THAT conversation into THIS id's file -
100
+ * silently destroying the disk record the user was building. The id therefore
101
+ * changes with the content, exactly as it does on a startup branch, and the
102
+ * caller resets the spend in the same breath (D-10).
103
+ *
104
+ * The parent id comes from the FILE NAME, never from `meta.id` (D-5): a file's
105
+ * contents are input.
106
+ */
107
+ export declare function rotateToChildOf(loadedPath: string): SessionIdentity | undefined;