@aragon-agent/cli 0.6.24 → 0.6.26
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 +1924 -1786
- package/README.md +2730 -2562
- package/dist/agent/controller.d.ts +49 -3
- package/dist/agent/controller.js +78 -3
- package/dist/agent/conversation-reset.d.ts +91 -0
- package/dist/agent/conversation-reset.js +66 -0
- package/dist/agent/model-probe-limits.d.ts +39 -0
- package/dist/agent/model-probe-limits.js +25 -0
- package/dist/agent/model-probe.d.ts +104 -0
- package/dist/agent/model-probe.js +391 -0
- package/dist/agent/reducer.d.ts +22 -0
- package/dist/agent/reducer.js +49 -14
- 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 +62 -40
- package/dist/commands/clear-command.d.ts +27 -0
- package/dist/commands/clear-command.js +107 -0
- package/dist/commands/registry.d.ts +48 -1
- 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/config/model-profiles.d.ts +8 -0
- package/dist/config/model-profiles.js +10 -0
- package/dist/exec/index.js +10 -1
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/terminal-plan.d.ts +155 -0
- package/dist/proc/terminal-plan.js +360 -0
- package/dist/session/branch-invocation.d.ts +83 -0
- package/dist/session/branch-invocation.js +109 -0
- package/dist/session/branch-launch.d.ts +70 -0
- package/dist/session/branch-launch.js +248 -0
- package/dist/session/checkpoint.d.ts +257 -0
- package/dist/session/checkpoint.js +512 -0
- package/dist/session/clear-archive.d.ts +51 -0
- package/dist/session/clear-archive.js +160 -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/todo/types.d.ts +5 -3
- package/dist/ui/App.d.ts +54 -2
- package/dist/ui/App.js +154 -28
- 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/entries/CompactionCard.d.ts +5 -4
- package/dist/ui/entries/CompactionCard.js +5 -4
- 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 +17 -5
- package/dist/ui/overlays/ModelDiscoveryList.d.ts +23 -0
- package/dist/ui/overlays/ModelDiscoveryList.js +161 -0
- package/dist/ui/overlays/ModelProfileEditor.d.ts +18 -1
- package/dist/ui/overlays/ModelProfileEditor.js +117 -68
- package/dist/ui/overlays/SettingsScreen.d.ts +8 -0
- package/dist/ui/overlays/SettingsScreen.js +34 -2
- package/dist/ui/overlays/model-probe-view.d.ts +128 -0
- package/dist/ui/overlays/model-probe-view.js +225 -0
- package/dist/ui/overlays/model-profile-state.d.ts +9 -1
- package/dist/ui/overlays/model-profile-state.js +10 -0
- package/dist/ui/overlays/use-model-probe.d.ts +30 -0
- package/dist/ui/overlays/use-model-probe.js +109 -0
- package/dist/ui/session-bridge.d.ts +26 -0
- package/dist/ui/session-bridge.js +32 -0
- package/package.json +2 -2
|
@@ -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;
|
|
@@ -0,0 +1,315 @@
|
|
|
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 { existsSync } from 'node:fs';
|
|
22
|
+
import { getSessionsDir } from '../config/app-paths.js';
|
|
23
|
+
import { branchSessionId, createSessionId, derivedChildId, isGeneratedSessionId, reserveSessionId, } from './identity.js';
|
|
24
|
+
import { SESSION_ID_LIMITS } from './limits.js';
|
|
25
|
+
import { loadSession } from './persist.js';
|
|
26
|
+
import { basenameId, isValidSessionId, looksLikeSessionPath, resolveSessionRef, sessionPathFor, } from './store.js';
|
|
27
|
+
/**
|
|
28
|
+
* Adopt a saved conversation. THE ONLY IMPLEMENTATION of the three calls
|
|
29
|
+
* `/resume` makes; the slash command and both startup paths call it.
|
|
30
|
+
*
|
|
31
|
+
* `skipModel` exists because an explicit `--model` / `--provider` on this run
|
|
32
|
+
* outranks the model recorded in the file: a user who says which model they want
|
|
33
|
+
* has said it more recently than the file did.
|
|
34
|
+
*
|
|
35
|
+
* `clearAllQueues()` and the `restoreEntries` dispatch are NOT here and belong at
|
|
36
|
+
* the call sites: the first is a no-op on the startup path (there are no queues
|
|
37
|
+
* yet) and indispensable inside a live TUI, and the second is a VIEW action with
|
|
38
|
+
* no controller to attach it to - the startup path seeds the reducer instead.
|
|
39
|
+
*/
|
|
40
|
+
export function applyRestoredSession(port, saved, opts = {}) {
|
|
41
|
+
port.replaceMessages(saved.messages, saved.compactionIdentity);
|
|
42
|
+
// Absence clears the previous conversation's list, including old files.
|
|
43
|
+
port.restoreTodos(saved.todos ?? []);
|
|
44
|
+
if (!opts.skipModel && saved.model?.providerId && saved.model?.modelId) {
|
|
45
|
+
port.setModel(saved.model.providerId, saved.model.modelId, saved.model.baseUrl);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Work out which session this process is, and what it starts from.
|
|
50
|
+
*
|
|
51
|
+
* FAILURE IS EXIT 2 WITH ONE COPYABLE SENTENCE, and it happens BEFORE any write
|
|
52
|
+
* (the only write is the id claim, and it is last). Sources: a `--save` value
|
|
53
|
+
* that is not an id (D-6), a `--save` id that is already taken (D-6), a `--save`
|
|
54
|
+
* naming the id being resumed (D-7), and a `--resume` that is missing or
|
|
55
|
+
* unreadable (D-8).
|
|
56
|
+
*/
|
|
57
|
+
export function resolveStartupSession(input) {
|
|
58
|
+
const rawSaveAs = input.saveAs?.trim();
|
|
59
|
+
if (rawSaveAs !== undefined && rawSaveAs.length > 0 && !isValidSessionId(rawSaveAs)) {
|
|
60
|
+
return invalidSaveId(rawSaveAs);
|
|
61
|
+
}
|
|
62
|
+
const saveAs = rawSaveAs !== undefined && rawSaveAs.length > 0 ? rawSaveAs : undefined;
|
|
63
|
+
const resume = input.resume?.trim();
|
|
64
|
+
if (resume !== undefined && resume.length > 0)
|
|
65
|
+
return resolveBranch(input, resume, saveAs);
|
|
66
|
+
return resolveFresh(saveAs);
|
|
67
|
+
}
|
|
68
|
+
/** A brand new session: optionally named by `--save`, otherwise auto-minted. */
|
|
69
|
+
function resolveFresh(saveAs) {
|
|
70
|
+
if (saveAs !== undefined) {
|
|
71
|
+
const fate = tryReserve(saveAs);
|
|
72
|
+
if (fate === 'taken')
|
|
73
|
+
return alreadyExists(saveAs);
|
|
74
|
+
if (fate === 'unwritable')
|
|
75
|
+
return unwritable(saveAs);
|
|
76
|
+
return {
|
|
77
|
+
ok: true,
|
|
78
|
+
identity: { id: saveAs, kind: 'named', pinned: true },
|
|
79
|
+
initialEntries: [],
|
|
80
|
+
notice: `Session ${saveAs} (pinned). Resume it from any terminal: aragon --resume ${saveAs}`,
|
|
81
|
+
apply: () => { },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// A collision is not a failure for an auto-minted id: another process took the
|
|
85
|
+
// name in the last few milliseconds, so minting again is the whole fix.
|
|
86
|
+
for (let attempt = 0; attempt < SESSION_ID_LIMITS.reserveAttempts; attempt += 1) {
|
|
87
|
+
const id = createSessionId();
|
|
88
|
+
const fate = tryReserve(id);
|
|
89
|
+
// Retrying cannot repair a directory that refuses writes, and burning the
|
|
90
|
+
// remaining attempts only delays the same answer.
|
|
91
|
+
if (fate === 'unwritable')
|
|
92
|
+
return unwritable(id);
|
|
93
|
+
if (fate === 'ok') {
|
|
94
|
+
return { ok: true, identity: { id, kind: 'fresh' }, initialEntries: [], apply: () => { } };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
exitCode: 2,
|
|
100
|
+
message: `Could not allocate a session id after ${SESSION_ID_LIMITS.reserveAttempts} attempts. ` +
|
|
101
|
+
`Try: aragon sessions prune --origin tui --yes (sessions live in ${getSessionsDir()}).`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** `--resume`: read the parent, derive a new id, claim it (D-5 / D-8). */
|
|
105
|
+
function resolveBranch(input, reference, saveAs) {
|
|
106
|
+
const path = resolveSessionRef(reference, input.cwd);
|
|
107
|
+
if (path === null)
|
|
108
|
+
return notFound(reference);
|
|
109
|
+
let saved;
|
|
110
|
+
try {
|
|
111
|
+
saved = loadSession(path);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
// A file that exists but cannot be read is NOT the same absence: this third
|
|
115
|
+
// message exists because a raw stack trace here is a user staring at a
|
|
116
|
+
// puzzle, and the only way out of it is deleting the file (D-8 / R-3).
|
|
117
|
+
const id = basenameId(path);
|
|
118
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
119
|
+
const next = isValidSessionId(id)
|
|
120
|
+
? `Delete it with: aragon sessions rm ${id} --yes`
|
|
121
|
+
: 'Delete the file, or resume another one.';
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
exitCode: 2,
|
|
125
|
+
message: `Session "${id}" exists but could not be read (${reason}). ${next}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// D-7 compares IDS, not the text the user typed: `--resume ./x/a4k2m9.json
|
|
129
|
+
// --save a4k2m9` is the same session (D-5 derives the parent from the file
|
|
130
|
+
// NAME), so the path spelling must be refused identically.
|
|
131
|
+
const loadedId = basenameId(path);
|
|
132
|
+
if (saveAs !== undefined && saveAs === loadedId) {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
exitCode: 2,
|
|
136
|
+
message: `--save ${saveAs} names the session you are resuming. ` +
|
|
137
|
+
'Pick a different id to branch into.',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// LINEAGE ONLY EXISTS FOR A GENERATED NAME (D-5 / §5.1). A `/save` export, a
|
|
141
|
+
// `/clear` archive and a user-chosen name all land in this directory under
|
|
142
|
+
// names this CLI did not mint; recording one of those as `meta.branchOf` would
|
|
143
|
+
// write a parent that is not a session, and announcing it would claim a
|
|
144
|
+
// lineage that does not exist. `derivedChildId` already draws that line for the
|
|
145
|
+
// NEW id; the recorded parent must be drawn on the same line.
|
|
146
|
+
const parentId = isGeneratedSessionId(loadedId) ? loadedId : undefined;
|
|
147
|
+
const derived = saveAs ?? derivedChildId(path);
|
|
148
|
+
const claimed = claimBranchId(derived, {
|
|
149
|
+
named: saveAs !== undefined,
|
|
150
|
+
// A retry must keep the branch SHAPE: losing the `-xxxx` suffix on a
|
|
151
|
+
// collision would drop the one thing on disk that says where this came from.
|
|
152
|
+
mint: () => (parentId ? branchSessionId(parentId) : createSessionId()),
|
|
153
|
+
});
|
|
154
|
+
if (!claimed.ok)
|
|
155
|
+
return claimed.resolution;
|
|
156
|
+
const identity = {
|
|
157
|
+
id: claimed.id,
|
|
158
|
+
// Not a "branch" when the file it came from has no id of ours to name, and
|
|
159
|
+
// not a "branch" that the `-p` notice should announce.
|
|
160
|
+
kind: parentId !== undefined ? 'branch' : saveAs !== undefined ? 'named' : 'fresh',
|
|
161
|
+
...(parentId !== undefined ? { parentId } : {}),
|
|
162
|
+
// Written only when TRUE, like `SessionMeta.pinned`: the field means "the
|
|
163
|
+
// user asked for this name", and `pinned: false` would be a second spelling
|
|
164
|
+
// of the same absence.
|
|
165
|
+
...(saveAs !== undefined ? { pinned: true } : {}),
|
|
166
|
+
};
|
|
167
|
+
const notice = parentId !== undefined
|
|
168
|
+
? `Resumed ${parentId} as a new branch: session ${claimed.id}. ` +
|
|
169
|
+
'The session you branched from is unchanged.'
|
|
170
|
+
: saveAs !== undefined
|
|
171
|
+
? `Session ${claimed.id} (pinned). ` +
|
|
172
|
+
`Resume it from any terminal: aragon --resume ${claimed.id}`
|
|
173
|
+
: undefined;
|
|
174
|
+
return {
|
|
175
|
+
ok: true,
|
|
176
|
+
identity,
|
|
177
|
+
initialEntries: saved.entries,
|
|
178
|
+
...(notice === undefined ? {} : { notice }),
|
|
179
|
+
saved,
|
|
180
|
+
apply: (port) => applyRestoredSession(port, saved, { skipModel: input.explicitModel === true }),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Claim the branch id, retrying only when it was auto-derived.
|
|
185
|
+
*
|
|
186
|
+
* A `--save` name that is taken is the user's decision to lose, so it is refused
|
|
187
|
+
* (D-6); an auto-derived suffix colliding means nothing to anyone, so it is
|
|
188
|
+
* re-minted - WITH THE SAME ROOT (`mint`), because the suffix is the disposable
|
|
189
|
+
* half of a branch id and the root is the part that carries the lineage.
|
|
190
|
+
*/
|
|
191
|
+
function claimBranchId(derived, opts) {
|
|
192
|
+
if (derived !== null) {
|
|
193
|
+
const fate = tryReserve(derived);
|
|
194
|
+
if (fate === 'ok')
|
|
195
|
+
return { ok: true, id: derived };
|
|
196
|
+
if (fate === 'unwritable')
|
|
197
|
+
return { ok: false, resolution: unwritable(derived) };
|
|
198
|
+
if (opts.named)
|
|
199
|
+
return { ok: false, resolution: alreadyExists(derived) };
|
|
200
|
+
}
|
|
201
|
+
for (let attempt = 0; attempt < SESSION_ID_LIMITS.reserveAttempts; attempt += 1) {
|
|
202
|
+
const id = opts.mint();
|
|
203
|
+
const fate = tryReserve(id);
|
|
204
|
+
if (fate === 'ok')
|
|
205
|
+
return { ok: true, id };
|
|
206
|
+
if (fate === 'unwritable')
|
|
207
|
+
return { ok: false, resolution: unwritable(id) };
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
resolution: {
|
|
212
|
+
ok: false,
|
|
213
|
+
exitCode: 2,
|
|
214
|
+
message: `Could not allocate a branch id after ${SESSION_ID_LIMITS.reserveAttempts} attempts.`,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* `reserveSessionId` answers "did I get this name"; the CALLER needs to know
|
|
220
|
+
* "who refused, and what should I tell the user".
|
|
221
|
+
*
|
|
222
|
+
* A read-only (or full) session directory fails the reservation for every id
|
|
223
|
+
* exactly like a collision does, and reporting that as `Session "myfix" already
|
|
224
|
+
* exists` sends the user to `--resume myfix`, which then reports that nothing is
|
|
225
|
+
* there. One `existsSync` between the two is enough to tell "somebody holds this
|
|
226
|
+
* name" from "nothing can be written here at all".
|
|
227
|
+
*/
|
|
228
|
+
function tryReserve(id) {
|
|
229
|
+
if (reserveSessionId(id))
|
|
230
|
+
return 'ok';
|
|
231
|
+
return existsSync(sessionPathFor(id)) ? 'taken' : 'unwritable';
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Move to a derived id after a `/resume` inside the TUI (D-15 / R-9).
|
|
235
|
+
*
|
|
236
|
+
* THE ROTATION IS NOT COSMETIC. `/resume` adopts another conversation, so the
|
|
237
|
+
* next checkpoint would otherwise write THAT conversation into THIS id's file -
|
|
238
|
+
* silently destroying the disk record the user was building. The id therefore
|
|
239
|
+
* changes with the content, exactly as it does on a startup branch, and the
|
|
240
|
+
* caller resets the spend in the same breath (D-10).
|
|
241
|
+
*
|
|
242
|
+
* The parent id comes from the FILE NAME, never from `meta.id` (D-5): a file's
|
|
243
|
+
* contents are input.
|
|
244
|
+
*/
|
|
245
|
+
export function rotateToChildOf(loadedPath) {
|
|
246
|
+
// Same lineage rule as a startup branch: a name this CLI did not mint has no
|
|
247
|
+
// id to record, so it records none (D-5 / §5.1).
|
|
248
|
+
const loadedId = basenameId(loadedPath);
|
|
249
|
+
const parentId = isGeneratedSessionId(loadedId) ? loadedId : undefined;
|
|
250
|
+
const claimed = claimBranchId(derivedChildId(loadedPath), {
|
|
251
|
+
named: false,
|
|
252
|
+
mint: () => (parentId ? branchSessionId(parentId) : createSessionId()),
|
|
253
|
+
});
|
|
254
|
+
if (!claimed.ok)
|
|
255
|
+
return undefined;
|
|
256
|
+
return {
|
|
257
|
+
id: claimed.id,
|
|
258
|
+
kind: parentId !== undefined ? 'branch' : 'fresh',
|
|
259
|
+
...(parentId !== undefined ? { parentId } : {}),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function invalidSaveId(value) {
|
|
263
|
+
return {
|
|
264
|
+
ok: false,
|
|
265
|
+
exitCode: 2,
|
|
266
|
+
message: `Invalid session id "${value}". --save takes an id ` +
|
|
267
|
+
'(letters, digits, ".", "_", "-"), not a path. ' +
|
|
268
|
+
'Use /save inside the TUI to export to a file.',
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function alreadyExists(id) {
|
|
272
|
+
return {
|
|
273
|
+
ok: false,
|
|
274
|
+
exitCode: 2,
|
|
275
|
+
message: `Session "${id}" already exists. ` +
|
|
276
|
+
`Use --resume ${id} to continue it as a new branch, or pick a new id.`,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The id was free but the file could not be created: a read-only or full
|
|
281
|
+
* directory. Naming the directory is the only actionable half of this sentence -
|
|
282
|
+
* "already exists" (the answer a bare `false` used to produce) would send the
|
|
283
|
+
* user to `--resume <id>`, which reports that nothing is there.
|
|
284
|
+
*/
|
|
285
|
+
function unwritable(id) {
|
|
286
|
+
return {
|
|
287
|
+
ok: false,
|
|
288
|
+
exitCode: 2,
|
|
289
|
+
message: `Could not create session "${id}" in ${getSessionsDir()} ` +
|
|
290
|
+
'(the directory is not writable).',
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* `--resume` found nothing. TWO SPELLINGS, TWO SENTENCES, because the next step
|
|
295
|
+
* differs: an id is looked up in a directory, a path is looked at directly.
|
|
296
|
+
*/
|
|
297
|
+
function notFound(reference) {
|
|
298
|
+
// THE SAME PREDICATE THE LOOKUP USED (D-5 / R-8), not a second copy of it: two
|
|
299
|
+
// copies would eventually disagree about one spelling and send the user to the
|
|
300
|
+
// wrong next step (`sessions list --all` for a path, or `sessions path` for an
|
|
301
|
+
// id).
|
|
302
|
+
if (looksLikeSessionPath(reference)) {
|
|
303
|
+
return {
|
|
304
|
+
ok: false,
|
|
305
|
+
exitCode: 2,
|
|
306
|
+
message: `No session file at "${reference}". Try: aragon sessions path`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
ok: false,
|
|
311
|
+
exitCode: 2,
|
|
312
|
+
message: `No session "${reference}". Try: aragon sessions list --all ` +
|
|
313
|
+
`(sessions live in ${getSessionsDir()}).`,
|
|
314
|
+
};
|
|
315
|
+
}
|
package/dist/session/store.d.ts
CHANGED
|
@@ -41,6 +41,16 @@ export declare function isValidSessionId(id: string): boolean;
|
|
|
41
41
|
* feature exists for.
|
|
42
42
|
*/
|
|
43
43
|
export declare const SESSION_LOCK_TTL_MS = 600000;
|
|
44
|
+
/**
|
|
45
|
+
* Who wrote a session file (tui-session-id-resume §3.7 / D-20).
|
|
46
|
+
*
|
|
47
|
+
* A SECOND, ORTHOGONAL AXIS from `managed`. `managed` answers "may a maintenance
|
|
48
|
+
* command see this row at all" (it means "the file carries `meta.id`"); `origin`
|
|
49
|
+
* answers "which writer produced it". TUI checkpoints are managed AND
|
|
50
|
+
* `origin: 'tui'`, which is exactly why `--all` must no longer be able to reach
|
|
51
|
+
* them: widening axis A must not widen axis B.
|
|
52
|
+
*/
|
|
53
|
+
export type SessionOrigin = 'exec' | 'tui';
|
|
44
54
|
export interface SessionListEntry {
|
|
45
55
|
id: string;
|
|
46
56
|
path: string;
|
|
@@ -50,8 +60,14 @@ export interface SessionListEntry {
|
|
|
50
60
|
turns: number;
|
|
51
61
|
provider: string;
|
|
52
62
|
model: string;
|
|
53
|
-
/** `true` when the file carries `meta.id`, i.e. `aragon exec` wrote it. */
|
|
63
|
+
/** `true` when the file carries `meta.id`, i.e. `aragon exec` or the TUI wrote it. */
|
|
54
64
|
managed: boolean;
|
|
65
|
+
/** Who wrote it; absent/unknown metadata reads as `'exec'` (older files). */
|
|
66
|
+
origin?: SessionOrigin;
|
|
67
|
+
/** `--save <id>` or `/session pin`: `sessions prune` never deletes it. */
|
|
68
|
+
pinned?: boolean;
|
|
69
|
+
/** The writer considered itself live at its last write. */
|
|
70
|
+
live?: boolean;
|
|
55
71
|
}
|
|
56
72
|
export declare function sessionsDir(): string;
|
|
57
73
|
export declare function sessionPathFor(id: string): string;
|
|
@@ -66,6 +82,19 @@ export declare function sessionPathFor(id: string): string;
|
|
|
66
82
|
* prevent, arriving through the flag next to it. `meta.id` is no safer: it comes
|
|
67
83
|
* out of a file, and files are inputs.
|
|
68
84
|
*
|
|
85
|
+
* A TUI CHECKPOINT NEVER NAMES ITSELF (D-20 / R-1). This is the single line that
|
|
86
|
+
* keeps `aragon exec --resume <tui-id>` from OVERWRITING the parent checkpoint:
|
|
87
|
+
* before this check, a TUI file carrying `meta.id` made `exec` take that id,
|
|
88
|
+
* write its own result into `a4k2m9.json` and move the `--continue` pointer -
|
|
89
|
+
* silent, unrecoverable loss of the one conversation the user was still having,
|
|
90
|
+
* arriving through a directory the design had declared out of scope. The rule
|
|
91
|
+
* belongs HERE rather than in `exec/**` because every exec write-back path
|
|
92
|
+
* already funnels through this function, so one check covers all of them and
|
|
93
|
+
* `exec/**` stays untouched.
|
|
94
|
+
*
|
|
95
|
+
* The fallthrough is the "stranger file" branch that already existed below:
|
|
96
|
+
* mint a fresh id rather than build a path out of the leftovers.
|
|
97
|
+
*
|
|
69
98
|
* The order is the one that keeps every legitimate resume landing where the user
|
|
70
99
|
* expects: an exec-written session names itself, a TUI `/save` is named by its
|
|
71
100
|
* file, and anything else gets a fresh id rather than a guess - reported back in
|
|
@@ -73,6 +102,15 @@ export declare function sessionPathFor(id: string): string;
|
|
|
73
102
|
* wondering which session it just wrote.
|
|
74
103
|
*/
|
|
75
104
|
export declare function resumedSessionId(path: string, saved: SavedSession): string;
|
|
105
|
+
/**
|
|
106
|
+
* Whether a `--resume` value is meant as a PATH rather than an id.
|
|
107
|
+
*
|
|
108
|
+
* EXPORTED SO THE FAILURE MESSAGE CANNOT DISAGREE WITH THE LOOKUP: the startup
|
|
109
|
+
* path prints one sentence for "no such id" and another for "no such file", and
|
|
110
|
+
* a second copy of this predicate is a copy that can drift (`isAbsolute` was
|
|
111
|
+
* missing from exactly such a copy).
|
|
112
|
+
*/
|
|
113
|
+
export declare function looksLikeSessionPath(value: string): boolean;
|
|
76
114
|
/** Read one file, or `null` for anything unreadable. NEVER THROWS. */
|
|
77
115
|
export declare function readSessionFile(path: string): SavedSession | null;
|
|
78
116
|
/**
|
|
@@ -100,6 +138,14 @@ export interface WriteSessionInput {
|
|
|
100
138
|
*/
|
|
101
139
|
export declare function writeSession(input: WriteSessionInput): string;
|
|
102
140
|
export declare function removeSession(id: string): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* The file name without its directory or `.json` suffix.
|
|
143
|
+
*
|
|
144
|
+
* Exported because session IDENTITY derives the parent of a branch from this
|
|
145
|
+
* value and NOT from `meta.id` (D-5): the contents of a file are input, so a
|
|
146
|
+
* hand-written `meta.id` must not be able to name where this process writes.
|
|
147
|
+
*/
|
|
148
|
+
export declare function basenameId(path: string): string;
|
|
103
149
|
/**
|
|
104
150
|
* Every session in the directory, newest first.
|
|
105
151
|
*
|
|
@@ -114,7 +160,20 @@ export interface PruneOptions {
|
|
|
114
160
|
olderThanDays?: number;
|
|
115
161
|
dryRun?: boolean;
|
|
116
162
|
all?: boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Which WRITER's files this sweep may touch: `'exec'` (default), `'tui'`, or
|
|
165
|
+
* `'all'` (tui-session-id-resume §3.7 / R-7).
|
|
166
|
+
*
|
|
167
|
+
* A SECOND AXIS, NOT A THIRD CHOICE. `all` above widens "which rows exist"
|
|
168
|
+
* (files with no metadata too); this narrows "which writers are in scope".
|
|
169
|
+
* Keeping them separate is what makes `--all` mean, forever, exactly what its
|
|
170
|
+
* help text says - and never "also delete every TUI checkpoint", which is what
|
|
171
|
+
* a single merged axis produced when TUI files started carrying `meta`.
|
|
172
|
+
*/
|
|
173
|
+
origin?: SessionOrigin | 'all';
|
|
117
174
|
now?: () => number;
|
|
175
|
+
/** Injected for tests; defaults to `process.kill(pid, 0)` liveness probing. */
|
|
176
|
+
isProcessAlive?: (pid: number) => boolean;
|
|
118
177
|
}
|
|
119
178
|
/** `--dry-run` reports and deletes nothing; nothing is ever pruned automatically. */
|
|
120
179
|
export declare function pruneSessions(opts?: PruneOptions): SessionListEntry[];
|