@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.
- package/CHANGELOG.md +104 -0
- package/README.md +194 -13
- package/dist/agent/reducer.d.ts +22 -0
- package/dist/agent/reducer.js +4 -1
- package/dist/cli.js +214 -2
- package/dist/commands/branch-command.d.ts +61 -0
- package/dist/commands/branch-command.js +117 -0
- package/dist/commands/builtins.js +48 -6
- package/dist/commands/registry.d.ts +28 -0
- package/dist/commands/session-command.d.ts +54 -0
- package/dist/commands/session-command.js +109 -0
- package/dist/config/load.d.ts +15 -0
- package/dist/exec/index.js +10 -1
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/terminal-plan.d.ts +173 -0
- package/dist/proc/terminal-plan.js +393 -0
- package/dist/session/branch-invocation.d.ts +127 -0
- package/dist/session/branch-invocation.js +153 -0
- package/dist/session/branch-launch.d.ts +79 -0
- package/dist/session/branch-launch.js +265 -0
- package/dist/session/checkpoint.d.ts +257 -0
- package/dist/session/checkpoint.js +512 -0
- package/dist/session/cli-commands.d.ts +2 -0
- package/dist/session/cli-commands.js +36 -5
- package/dist/session/identity.d.ts +72 -0
- package/dist/session/identity.js +148 -0
- package/dist/session/limits.d.ts +66 -0
- package/dist/session/limits.js +66 -0
- package/dist/session/persist.d.ts +30 -5
- package/dist/session/startup-session.d.ts +107 -0
- package/dist/session/startup-session.js +315 -0
- package/dist/session/store.d.ts +60 -1
- package/dist/session/store.js +95 -6
- package/dist/ui/App.d.ts +54 -2
- package/dist/ui/App.js +115 -21
- package/dist/ui/Header.js +8 -16
- package/dist/ui/StatusBar.d.ts +8 -0
- package/dist/ui/StatusBar.js +12 -3
- package/dist/ui/layout/header-hints.d.ts +68 -0
- package/dist/ui/layout/header-hints.js +138 -0
- package/dist/ui/layout/session-chip.d.ts +52 -0
- package/dist/ui/layout/session-chip.js +64 -0
- package/dist/ui/layout/status-layout.d.ts +10 -0
- package/dist/ui/layout/status-layout.js +8 -1
- package/dist/ui/overlays/HelpOverlay.js +9 -3
- package/dist/ui/session-bridge.d.ts +26 -0
- package/dist/ui/session-bridge.js +32 -0
- 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.
|
|
22
|
-
*
|
|
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
|
|
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`
|
|
62
|
-
*
|
|
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;
|