@mnemom/mnemom 0.17.34-next.1 → 0.17.34-next.2
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/dist/base-url-proxy.mjs +18 -4
- package/dist/commands/agent-config.js +5 -1
- package/dist/commands/agent-doctor.d.ts +9 -0
- package/dist/commands/agent-doctor.js +4 -0
- package/dist/commands/agent-session.d.ts +1 -2
- package/dist/commands/agent-session.js +1 -2
- package/dist/commands/agent-setup.d.ts +6 -0
- package/dist/commands/agent-setup.js +12 -1
- package/dist/commands/agent.d.ts +11 -0
- package/dist/commands/agent.js +145 -46
- package/dist/commands/durable-doctor.d.ts +50 -0
- package/dist/commands/durable-doctor.js +186 -0
- package/dist/commands/durable-lock.d.ts +254 -0
- package/dist/commands/durable-lock.js +595 -0
- package/dist/commands/durable-sessions.d.ts +87 -0
- package/dist/commands/durable-sessions.js +303 -0
- package/dist/commands/durable-verbs.d.ts +116 -0
- package/dist/commands/durable-verbs.js +393 -0
- package/dist/durable/flock.d.ts +26 -0
- package/dist/durable/flock.js +80 -0
- package/dist/durable/launch-spec.d.ts +7 -1
- package/dist/durable/launch-spec.js +10 -3
- package/dist/durable/seat-proxy-entry.d.ts +11 -0
- package/dist/durable/seat-proxy-entry.js +25 -0
- package/dist/durable/seat-proxy.d.ts +107 -0
- package/dist/durable/seat-proxy.js +599 -0
- package/dist/durable/seat-socket.d.ts +46 -0
- package/dist/durable/seat-socket.js +179 -0
- package/dist/durable/seat-spec-validator.d.ts +28 -0
- package/dist/durable/seat-spec-validator.js +59 -0
- package/dist/durable/test-mode.mjs +81 -0
- package/dist/durable/turn-chain.d.ts +55 -0
- package/dist/durable/turn-chain.js +178 -0
- package/dist/host/auto-continue.d.ts +67 -0
- package/dist/host/auto-continue.js +118 -0
- package/dist/host/bootid.d.ts +13 -0
- package/dist/host/bootid.js +41 -0
- package/dist/host/client.d.ts +123 -0
- package/dist/host/client.js +171 -0
- package/dist/host/commands.d.ts +2 -0
- package/dist/host/commands.js +122 -0
- package/dist/host/control.d.ts +33 -0
- package/dist/host/control.js +144 -0
- package/dist/host/daemon.d.ts +217 -0
- package/dist/host/daemon.js +606 -0
- package/dist/host/gate.d.ts +46 -0
- package/dist/host/gate.js +82 -0
- package/dist/host/grant.d.ts +24 -0
- package/dist/host/grant.js +24 -0
- package/dist/host/instance.d.ts +35 -0
- package/dist/host/instance.js +62 -0
- package/dist/host/integrity.d.ts +35 -0
- package/dist/host/integrity.js +115 -0
- package/dist/host/launchd.d.ts +16 -0
- package/dist/host/launchd.js +45 -0
- package/dist/host/lock.d.ts +20 -0
- package/dist/host/lock.js +63 -0
- package/dist/host/owner.d.ts +55 -0
- package/dist/host/owner.js +121 -0
- package/dist/host/plist.d.ts +16 -0
- package/dist/host/plist.js +85 -0
- package/dist/host/procs.d.ts +27 -0
- package/dist/host/procs.js +105 -0
- package/dist/host/run.d.ts +66 -0
- package/dist/host/run.js +314 -0
- package/dist/host/runtime.d.ts +8 -0
- package/dist/host/runtime.js +22 -0
- package/dist/host/s1-names.d.ts +22 -0
- package/dist/host/s1-names.js +21 -0
- package/dist/host/seat-credential.d.ts +27 -0
- package/dist/host/seat-credential.js +41 -0
- package/dist/host/seat-launch-main.d.ts +1 -0
- package/dist/host/seat-launch-main.js +59 -0
- package/dist/host/seat-launch.d.ts +116 -0
- package/dist/host/seat-launch.js +302 -0
- package/dist/host/seat.d.ts +48 -0
- package/dist/host/seat.js +149 -0
- package/dist/host/state.d.ts +33 -0
- package/dist/host/state.js +117 -0
- package/dist/host/status.d.ts +101 -0
- package/dist/host/status.js +307 -0
- package/dist/host/tmux.d.ts +56 -0
- package/dist/host/tmux.js +182 -0
- package/dist/host/transcript.d.ts +30 -0
- package/dist/host/transcript.js +130 -0
- package/dist/host/verbs.d.ts +50 -0
- package/dist/host/verbs.js +271 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.js +197 -35
- package/dist/lib/agent.d.ts +8 -26
- package/dist/lib/agent.js +11 -26
- package/dist/lib/claude-plugin.d.ts +11 -2
- package/dist/lib/claude-plugin.js +10 -1
- package/dist/lib/durable/api.d.ts +136 -0
- package/dist/lib/durable/api.js +220 -0
- package/dist/lib/durable/codes.d.ts +8 -0
- package/dist/lib/durable/codes.js +2 -0
- package/dist/lib/durable/first-turn.d.ts +50 -0
- package/dist/lib/durable/first-turn.js +102 -0
- package/dist/lib/durable/gate.d.ts +48 -0
- package/dist/lib/durable/gate.js +122 -0
- package/dist/lib/durable/handover.d.ts +101 -0
- package/dist/lib/durable/handover.js +175 -0
- package/dist/lib/durable/launch.d.ts +182 -0
- package/dist/lib/durable/launch.js +443 -0
- package/dist/lib/durable/launching.d.ts +15 -0
- package/dist/lib/durable/launching.js +70 -0
- package/dist/lib/durable/live.d.ts +194 -0
- package/dist/lib/durable/live.js +635 -0
- package/dist/lib/durable/refusal.d.ts +57 -0
- package/dist/lib/durable/refusal.js +78 -0
- package/dist/lib/durable/view.d.ts +83 -0
- package/dist/lib/durable/view.js +244 -0
- package/dist/lib/experimental.d.ts +0 -9
- package/dist/lib/experimental.js +18 -17
- package/dist/lib/launcher-config.d.ts +7 -0
- package/dist/lib/launcher-config.js +21 -0
- package/dist/lib/scan/report-email.d.ts +1 -1
- package/dist/lib/scan/report-email.js +9 -1
- package/dist/lib/subscription-login.d.ts +15 -0
- package/dist/lib/subscription-login.js +28 -1
- package/dist/vendor/durable-contract/index.js +240 -18
- package/dist/vendor/shared/durable-contract/admission.d.ts +240 -0
- package/dist/vendor/shared/durable-contract/admission.js +533 -0
- package/dist/vendor/shared/durable-contract/arm.d.ts +51 -0
- package/dist/vendor/shared/durable-contract/arm.js +133 -0
- package/dist/vendor/shared/durable-contract/canonical.d.ts +29 -0
- package/dist/vendor/shared/durable-contract/canonical.js +98 -0
- package/dist/vendor/shared/durable-contract/cell-id.d.ts +22 -0
- package/dist/vendor/shared/durable-contract/cell-id.js +38 -0
- package/dist/vendor/shared/durable-contract/cli-output.d.ts +342 -0
- package/dist/vendor/shared/durable-contract/cli-output.js +201 -0
- package/dist/vendor/shared/durable-contract/cli-surface.d.ts +119 -0
- package/dist/vendor/shared/durable-contract/cli-surface.js +270 -0
- package/dist/vendor/shared/durable-contract/codes.d.ts +537 -0
- package/dist/vendor/shared/durable-contract/codes.js +475 -0
- package/dist/vendor/shared/durable-contract/constants.d.ts +153 -0
- package/dist/vendor/shared/durable-contract/constants.js +179 -0
- package/dist/vendor/shared/durable-contract/doors.d.ts +119 -0
- package/dist/vendor/shared/durable-contract/doors.js +249 -0
- package/dist/vendor/shared/durable-contract/envelope.d.ts +103 -0
- package/dist/vendor/shared/durable-contract/envelope.js +317 -0
- package/dist/vendor/shared/durable-contract/events.d.ts +147 -0
- package/dist/vendor/shared/durable-contract/events.js +630 -0
- package/dist/vendor/shared/durable-contract/executor-state.d.ts +41 -0
- package/dist/vendor/shared/durable-contract/executor-state.js +32 -0
- package/dist/vendor/shared/durable-contract/fixlines.d.ts +318 -0
- package/dist/vendor/shared/durable-contract/fixlines.js +483 -0
- package/dist/vendor/shared/durable-contract/freeze-record.d.ts +787 -0
- package/dist/vendor/shared/durable-contract/freeze-record.js +361 -0
- package/dist/vendor/shared/durable-contract/index.d.ts +36 -0
- package/dist/vendor/shared/durable-contract/index.js +37 -0
- package/dist/vendor/shared/durable-contract/journal-approvals.d.ts +29 -0
- package/dist/vendor/shared/durable-contract/journal-approvals.js +60 -0
- package/dist/vendor/shared/durable-contract/keys.d.ts +24 -0
- package/dist/vendor/shared/durable-contract/keys.js +66 -0
- package/dist/vendor/shared/durable-contract/lock.d.ts +128 -0
- package/dist/vendor/shared/durable-contract/lock.js +151 -0
- package/dist/vendor/shared/durable-contract/names.d.ts +25 -0
- package/dist/vendor/shared/durable-contract/names.js +78 -0
- package/dist/vendor/shared/durable-contract/owner-verbs.d.ts +175 -0
- package/dist/vendor/shared/durable-contract/owner-verbs.js +403 -0
- package/dist/vendor/shared/durable-contract/paths.d.ts +23 -0
- package/dist/vendor/shared/durable-contract/paths.js +35 -0
- package/dist/vendor/shared/durable-contract/pure-fn.d.ts +48 -0
- package/dist/vendor/shared/durable-contract/pure-fn.js +89 -0
- package/dist/vendor/shared/durable-contract/routes.d.ts +205 -0
- package/dist/vendor/shared/durable-contract/routes.js +315 -0
- package/dist/vendor/shared/durable-contract/schema.d.ts +126 -0
- package/dist/vendor/shared/durable-contract/schema.js +441 -0
- package/dist/vendor/shared/durable-contract/seal.d.ts +54 -0
- package/dist/vendor/shared/durable-contract/seal.js +99 -0
- package/dist/vendor/shared/durable-contract/secret-scope.d.ts +28 -0
- package/dist/vendor/shared/durable-contract/secret-scope.js +76 -0
- package/dist/vendor/shared/durable-contract/session-id.d.ts +21 -0
- package/dist/vendor/shared/durable-contract/session-id.js +58 -0
- package/dist/vendor/shared/durable-contract/spec.d.ts +162 -0
- package/dist/vendor/shared/durable-contract/spec.js +286 -0
- package/dist/vendor/shared/durable-contract/states.d.ts +96 -0
- package/dist/vendor/shared/durable-contract/states.js +218 -0
- package/dist/vendor/shared/durable-contract/telemetry.d.ts +94 -0
- package/dist/vendor/shared/durable-contract/telemetry.js +180 -0
- package/dist/vendor/shared/durable-contract/test-mode.d.ts +62 -0
- package/dist/vendor/shared/durable-contract/test-mode.js +140 -0
- package/dist/vendor/shared/durable-contract/turn.d.ts +57 -0
- package/dist/vendor/shared/durable-contract/turn.js +81 -0
- package/dist/vendor/shared/durable-contract/vectors.d.ts +191 -0
- package/dist/vendor/shared/durable-contract/vectors.js +208 -0
- package/package.json +3 -2
- package/plugins/integrity-manifest.json +35 -0
- package/plugins/mnemom/hooks/hooks.json +15 -1
- package/plugins/mnemom/hooks/register.tsx +60 -2
- package/plugins/mnemom/hooks/seat-hello.mjs +116 -0
- package/dist/commands/agent-implicit-notice.d.ts +0 -31
- package/dist/commands/agent-implicit-notice.js +0 -73
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom agent lock <name> [--own-agent] [--json]` (BACKBONE C1, design PR 6; C02-R01, R05, R17,
|
|
3
|
+
* R20, R26, R27; S1 lock.ts, spec.ts, fixlines.ts).
|
|
4
|
+
*
|
|
5
|
+
* The idle-source lock (no live tracked claude): resolve the name through the registry, apply the
|
|
6
|
+
* R27 cwd rule to the session's registered cwd BEFORE any lock request (a failing cwd sends none),
|
|
7
|
+
* read this machine's executor id, build the launch spec, POST ROUTES.own_lock, and print the one
|
|
8
|
+
* quiet lock line plus the S1 laptop risk sentence. The API runs the S1 LOCK_STEPS in order; the CLI
|
|
9
|
+
* only renders its answer.
|
|
10
|
+
*
|
|
11
|
+
* Boundaries (cross-piece, see notes-c1.md):
|
|
12
|
+
* - The spec comes from C4's `buildDurableLaunchSpec` by the builder input table (`lockSpecOf`):
|
|
13
|
+
* a live original is read exactly (C14-R13), else the launcher's resolved continue flags; what
|
|
14
|
+
* it dropped is named on the lock line (C14-R12).
|
|
15
|
+
* - The executor id is read from S1 EXECUTOR_STATE_FILE (C2 writes it); only `executor_id` is kept.
|
|
16
|
+
* - A live tracked claude for the session on this machine (the verb from another terminal, or
|
|
17
|
+
* `/mnemom lock` inside the session) takes the C02-R26 handover order (lib/durable/handover.ts)
|
|
18
|
+
* around the commit when `handover` is injected; the launcher's own lock (idle source) injects
|
|
19
|
+
* none and sends zero signals. The verb takes no passthrough.
|
|
20
|
+
*/
|
|
21
|
+
import { spawnSync } from "node:child_process";
|
|
22
|
+
import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { executorStateOk } from "../vendor/shared/durable-contract/executor-state.js";
|
|
26
|
+
import { APPROVALS_TEXT_M1, lockRefusalFixKey, RISK_LINE_TEXT, } from "../vendor/shared/durable-contract/fixlines.js";
|
|
27
|
+
import { EXECUTOR_STATE_FILE } from "../vendor/shared/durable-contract/paths.js";
|
|
28
|
+
import { parseLaunchSpec, PLUGIN_TOKEN, SPEC_VERSION, } from "../vendor/shared/durable-contract/spec.js";
|
|
29
|
+
import { errorCodeOf, errorDetailsOf } from "../lib/durable/api.js";
|
|
30
|
+
import { ccSessionIdOf, commitWithHandover } from "../lib/durable/handover.js";
|
|
31
|
+
import { DurableRefusal, fixLine } from "../lib/durable/refusal.js";
|
|
32
|
+
import { effectiveOf, isEnded } from "../lib/durable/view.js";
|
|
33
|
+
import { handleVerbError, refusalFromAnswer, resolveTarget, } from "./durable-verbs.js";
|
|
34
|
+
import { CODE } from "../lib/durable/codes.js";
|
|
35
|
+
import { buildDurableLaunchSpec, launchSpecChangeFixLines, runningSourceInput, specUserArgv, } from "../durable/launch-spec.js";
|
|
36
|
+
/**
|
|
37
|
+
* C02-R27: the registered root is realpath(D) when git's toplevel equals it. not_git knows no
|
|
38
|
+
* checkout, so it carries no <checkout> and prints no fix line (C11-R27, as the C4 rule).
|
|
39
|
+
*/
|
|
40
|
+
export function cwdRule(dir, probe) {
|
|
41
|
+
const real = probe.realpath(dir);
|
|
42
|
+
if (real === null)
|
|
43
|
+
return { ok: false, kind: "not_git", params: {} };
|
|
44
|
+
const top = probe.toplevel(real);
|
|
45
|
+
if (top === null)
|
|
46
|
+
return { ok: false, kind: "not_git", params: {} };
|
|
47
|
+
const topReal = probe.realpath(top) ?? top;
|
|
48
|
+
if (topReal !== real)
|
|
49
|
+
return { ok: false, kind: "subdirectory", params: { toplevel: topReal } };
|
|
50
|
+
return { ok: true, root: real };
|
|
51
|
+
}
|
|
52
|
+
/** The live probe: node realpath and git with hooks/fsmonitor off and every GIT_* var cleared. */
|
|
53
|
+
export const liveCwdProbe = {
|
|
54
|
+
realpath: (dir) => {
|
|
55
|
+
try {
|
|
56
|
+
return realpathSync(dir);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
toplevel: (dir) => {
|
|
63
|
+
const env = {};
|
|
64
|
+
for (const [k, v] of Object.entries(process.env))
|
|
65
|
+
if (!k.startsWith("GIT_"))
|
|
66
|
+
env[k] = v;
|
|
67
|
+
const r = spawnSync("git", [
|
|
68
|
+
"-c",
|
|
69
|
+
"core.hooksPath=/dev/null",
|
|
70
|
+
"-c",
|
|
71
|
+
"core.fsmonitor=false",
|
|
72
|
+
"rev-parse",
|
|
73
|
+
"--show-toplevel",
|
|
74
|
+
], { cwd: dir, env, encoding: "utf8", timeout: 10_000 });
|
|
75
|
+
if (r.status !== 0 || typeof r.stdout !== "string")
|
|
76
|
+
return null;
|
|
77
|
+
const out = r.stdout.trim();
|
|
78
|
+
return out === "" ? null : out;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
// ---- the transcript slug check (symlink_transcript) ---------------------------------------------
|
|
82
|
+
/** Claude Code's project directory name for a cwd: every non-alphanumeric character becomes '-'. */
|
|
83
|
+
export const claudeSlug = (cwd) => cwd.replace(/[^A-Za-z0-9]/g, "-");
|
|
84
|
+
const CC_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
85
|
+
/** The live lookup under `${CLAUDE_CONFIG_DIR:-~/.claude}/projects`. */
|
|
86
|
+
export function liveTranscriptDirs(env) {
|
|
87
|
+
const projects = join((env.CLAUDE_CONFIG_DIR ?? "").trim() || join(homedir(), ".claude"), "projects");
|
|
88
|
+
return (cc) => {
|
|
89
|
+
if (!CC_ID_RE.test(cc))
|
|
90
|
+
return [];
|
|
91
|
+
try {
|
|
92
|
+
return readdirSync(projects).filter((d) => existsSync(join(projects, d, `${cc}.jsonl`)));
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
// No projects dir: Claude Code has written no transcript here, so there is nothing to check.
|
|
96
|
+
// Any other failure (EACCES, EIO, ENOTDIR, ...) means the check cannot be made: fail closed.
|
|
97
|
+
return e.code === "ENOENT" ? [] : null;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* BACKBONE C1 lock-time cwd check: the realpath `root` is committed only if the session's transcript
|
|
103
|
+
* (found by cc_session_id) sits under the project dir slugged from it; a transcript found only under
|
|
104
|
+
* another slug (the symlink path's) is symlink_transcript. No transcript found: nothing to check.
|
|
105
|
+
*/
|
|
106
|
+
export function transcriptSlugRule(root, ccSessionId, dirs) {
|
|
107
|
+
if (!dirs || ccSessionId === null)
|
|
108
|
+
return { ok: true, root };
|
|
109
|
+
const found = dirs(ccSessionId);
|
|
110
|
+
if (found === null)
|
|
111
|
+
return { ok: false, kind: "transcript_unverifiable" };
|
|
112
|
+
if (found.length === 0 || found.includes(claudeSlug(root)))
|
|
113
|
+
return { ok: true, root };
|
|
114
|
+
return { ok: false, kind: "symlink_transcript", params: { realpath: root } };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* claude's order for a root (the cwd rule already made it a realpath'd toplevel): the canonical key
|
|
118
|
+
* (the main checkout for a linked worktree) first, then the bounded walk, which at a toplevel is the
|
|
119
|
+
* root itself. A parent above the root never counts; the keys are realpaths, never symlink paths.
|
|
120
|
+
*/
|
|
121
|
+
export function rootTrusted(root, probe) {
|
|
122
|
+
const projects = probe.projects();
|
|
123
|
+
if (projects === null)
|
|
124
|
+
return false;
|
|
125
|
+
const trusted = (k) => rec(projects[k]).hasTrustDialogAccepted === true;
|
|
126
|
+
const main = probe.mainRoot(root);
|
|
127
|
+
return (main !== null && trusted(main)) || trusted(root);
|
|
128
|
+
}
|
|
129
|
+
/** A path as one POSIX shell word: bare when it is plainly safe, else single-quoted. */
|
|
130
|
+
export const shellWord = (p) => /^[A-Za-z0-9_./@%+=:,-]+$/.test(p) ? p : `'${p.replace(/'/g, "'\\''")}'`;
|
|
131
|
+
/** The live probe: the config file Claude Code reads, and git with hooks/fsmonitor off and GIT_* cleared. */
|
|
132
|
+
export function liveTrustProbe(env) {
|
|
133
|
+
const dir = (env.CLAUDE_CONFIG_DIR ?? "").trim();
|
|
134
|
+
const file = dir ? join(dir, ".claude.json") : join(homedir(), ".claude.json");
|
|
135
|
+
return {
|
|
136
|
+
projects: () => {
|
|
137
|
+
try {
|
|
138
|
+
const p = rec(JSON.parse(readFileSync(file, "utf8"))).projects;
|
|
139
|
+
return rec(p);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
mainRoot: (d) => {
|
|
146
|
+
const genv = {};
|
|
147
|
+
for (const [k, v] of Object.entries(env))
|
|
148
|
+
if (!k.startsWith("GIT_"))
|
|
149
|
+
genv[k] = v;
|
|
150
|
+
const r = spawnSync("git", [
|
|
151
|
+
"-c",
|
|
152
|
+
"core.hooksPath=/dev/null",
|
|
153
|
+
"-c",
|
|
154
|
+
"core.fsmonitor=false",
|
|
155
|
+
"rev-parse",
|
|
156
|
+
"--path-format=absolute",
|
|
157
|
+
"--git-common-dir",
|
|
158
|
+
], { cwd: d, env: genv, encoding: "utf8", timeout: 10_000 });
|
|
159
|
+
const out = r.status === 0 && typeof r.stdout === "string" ? r.stdout.trim() : "";
|
|
160
|
+
return out === "" ? null : liveCwdProbe.realpath(dirname(out));
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// ---- executor id ------------------------------------------------------------------------------
|
|
165
|
+
/** This machine's executor id from S1 EXECUTOR_STATE_FILE, or null (not enrolled here). */
|
|
166
|
+
export function readLocalExecutorId(home, read = (p) => readFileSync(p, "utf8")) {
|
|
167
|
+
let raw;
|
|
168
|
+
try {
|
|
169
|
+
raw = JSON.parse(read(join(home, EXECUTOR_STATE_FILE)));
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return executorStateOk(raw) ? raw.executor_id : null;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The idle-source spec in the S1 closed schema; fields the schema refuses are dropped, not sent.
|
|
178
|
+
* Null when even the required core does not parse (e.g. a root the schema refuses).
|
|
179
|
+
*/
|
|
180
|
+
export function idleLaunchSpec(i) {
|
|
181
|
+
const spec = {
|
|
182
|
+
v: SPEC_VERSION,
|
|
183
|
+
cwd: i.root,
|
|
184
|
+
plugin: PLUGIN_TOKEN,
|
|
185
|
+
trim_mcp: i.trimMcp === true,
|
|
186
|
+
};
|
|
187
|
+
if (i.model)
|
|
188
|
+
spec.model = i.model;
|
|
189
|
+
if (i.effort)
|
|
190
|
+
spec.effort = i.effort;
|
|
191
|
+
const parsed = parseLaunchSpec(spec);
|
|
192
|
+
if (parsed !== null)
|
|
193
|
+
return parsed;
|
|
194
|
+
// A model or effort outside the schema: keep the required core only.
|
|
195
|
+
return parseLaunchSpec({
|
|
196
|
+
v: SPEC_VERSION,
|
|
197
|
+
cwd: i.root,
|
|
198
|
+
plugin: PLUGIN_TOKEN,
|
|
199
|
+
trim_mcp: i.trimMcp === true,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The lock spec's model/effort: the locked conversation's own launch (its newest ledger record)
|
|
204
|
+
* wins over the launcher defaults, so a session launched with `--model` keeps that model in its
|
|
205
|
+
* seat instead of resuming on launcher.toml's (or Claude Code's default) model.
|
|
206
|
+
*/
|
|
207
|
+
export function lockSpecDefaults(conversationId, records, launcher) {
|
|
208
|
+
const own = [...records].reverse().find((r) => r.conversation_id === conversationId);
|
|
209
|
+
const out = { ...launcher };
|
|
210
|
+
if (own?.model)
|
|
211
|
+
out.model = own.model;
|
|
212
|
+
if (own?.effort)
|
|
213
|
+
out.effort = own.effort;
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
const fmtUsd = (n) => `$${Number.isInteger(n) ? n : n.toFixed(2)}`;
|
|
217
|
+
export function lockLine(f) {
|
|
218
|
+
const parts = [
|
|
219
|
+
`${f.name} is durable`,
|
|
220
|
+
`rules: ${f.rules ?? "default"}`,
|
|
221
|
+
`approvals → ${APPROVALS_TEXT_M1}`,
|
|
222
|
+
`budget ${f.budgetUsd === null ? "-" : fmtUsd(f.budgetUsd)}`,
|
|
223
|
+
];
|
|
224
|
+
if (f.ownAgent)
|
|
225
|
+
parts.push(`moved to its own agent ${f.agent}`);
|
|
226
|
+
if (f.durableOnly)
|
|
227
|
+
parts.push(`agent ${f.agent} durable-only: its plain keys stop`);
|
|
228
|
+
if (f.sharedWith >= 1)
|
|
229
|
+
parts.push(`shares it with ${f.sharedWith} sessions (pausing pauses all)`);
|
|
230
|
+
parts.push(`mnemom agent show ${f.name}`);
|
|
231
|
+
// R17: one line. Every M1 lock homes on a host executor, so the current S1 laptop risk sentence
|
|
232
|
+
// always rides on it, before the dropped-flags fragment that ends the line.
|
|
233
|
+
parts.push(RISK_LINE_TEXT);
|
|
234
|
+
const line = parts.join(" · ");
|
|
235
|
+
return f.droppedFlags.length === 0
|
|
236
|
+
? line
|
|
237
|
+
: `${line}; flags dropped: ${f.droppedFlags.join(", ")}`;
|
|
238
|
+
}
|
|
239
|
+
function rec(v) {
|
|
240
|
+
return v !== null && typeof v === "object" && !Array.isArray(v)
|
|
241
|
+
? v
|
|
242
|
+
: {};
|
|
243
|
+
}
|
|
244
|
+
const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : null);
|
|
245
|
+
/** Lock facts from the lock answer, falling back to the row (A2 owns the body; read defensively). */
|
|
246
|
+
export function lockFactsOf(body, row, name, built = []) {
|
|
247
|
+
const b = rec(body);
|
|
248
|
+
const s = rec(b.session ?? b);
|
|
249
|
+
const charter = rec(b.charter ?? s.charter);
|
|
250
|
+
const limits = rec(charter.limits ?? charter);
|
|
251
|
+
const shared = b.shared_sessions ?? b.locked_sessions ?? b.shares_with;
|
|
252
|
+
// --own-agent: the api answers the new agent as {agent: {id, name}} (mnemom-test #881).
|
|
253
|
+
const moved = rec(b.agent).name;
|
|
254
|
+
return {
|
|
255
|
+
name,
|
|
256
|
+
rules: typeof charter.profile === "string"
|
|
257
|
+
? charter.profile
|
|
258
|
+
: typeof b.rules === "string"
|
|
259
|
+
? b.rules
|
|
260
|
+
: null,
|
|
261
|
+
budgetUsd: num(limits.budget_usd) ?? num(b.budget_usd) ?? num(row.guard_state?.budget_usd ?? null),
|
|
262
|
+
agent: (typeof moved === "string" ? moved : null) ??
|
|
263
|
+
(typeof b.agent_name === "string" ? b.agent_name : null) ??
|
|
264
|
+
row.agent_name ??
|
|
265
|
+
row.agent_id,
|
|
266
|
+
ownAgent: typeof moved === "string",
|
|
267
|
+
durableOnly: b.durable_only === true || b.agent_durable_only === true,
|
|
268
|
+
sharedWith: Array.isArray(shared) ? shared.length : (num(shared) ?? 0),
|
|
269
|
+
// The builder's names first (C14-R12), then any the answer adds, each once.
|
|
270
|
+
droppedFlags: [
|
|
271
|
+
...new Set([
|
|
272
|
+
...built,
|
|
273
|
+
...(Array.isArray(b.flags_dropped)
|
|
274
|
+
? b.flags_dropped.filter((x) => typeof x === "string")
|
|
275
|
+
: []),
|
|
276
|
+
]),
|
|
277
|
+
],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Would the launcher's continue have been Remote Control? An explicit shape counts only when it is
|
|
282
|
+
* RC on an entitled account (a non-entitled one is degraded to terminal by the launcher, nothing
|
|
283
|
+
* is dropped); an unpinned one only by the auto-upgrade (entitled and capable). The capability
|
|
284
|
+
* probe runs only when it decides.
|
|
285
|
+
*/
|
|
286
|
+
export function rcWouldBeNamed(i) {
|
|
287
|
+
if (!i.rcAllowed)
|
|
288
|
+
return false;
|
|
289
|
+
if (i.shapeExplicit)
|
|
290
|
+
return i.shape === "remote-control";
|
|
291
|
+
return i.shape === "terminal" && i.rcCapable();
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* The spec by the builder input table (BACKBONE C4, final F01): a live tracked claude for the
|
|
295
|
+
* session on this machine is the running source, read exactly; otherwise the idle source, the
|
|
296
|
+
* launcher's resolved continue flags. `unreadable` when the original is live but its argv cannot be
|
|
297
|
+
* read (re-resolving the launcher config for a running session is C02-M53); null when the spec does
|
|
298
|
+
* not fit the closed schema.
|
|
299
|
+
*/
|
|
300
|
+
export async function lockSpecOf(row, root, deps) {
|
|
301
|
+
const cc = typeof row.cc_session_id === "string" ? row.cc_session_id : null;
|
|
302
|
+
const pid = cc !== null && deps.handover ? deps.handover.findOriginal(cc) : null;
|
|
303
|
+
try {
|
|
304
|
+
if (pid !== null) {
|
|
305
|
+
const o = deps.readOriginal ? deps.readOriginal(pid) : null;
|
|
306
|
+
if (o === null)
|
|
307
|
+
return "unreadable";
|
|
308
|
+
return await buildDurableLaunchSpec(runningSourceInput(o.argv.slice(1), root, {
|
|
309
|
+
passthroughHint: o.passthroughHint,
|
|
310
|
+
remoteControl: o.remoteControl,
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
const d = deps.specDefaults(row);
|
|
314
|
+
const argv = [
|
|
315
|
+
...(d.model ? ["--model", d.model] : []),
|
|
316
|
+
...(d.effort ? ["--effort", d.effort] : []),
|
|
317
|
+
...(d.trimMcp === true ? ["--trim-mcp"] : []),
|
|
318
|
+
];
|
|
319
|
+
return await buildDurableLaunchSpec({
|
|
320
|
+
source: "idle",
|
|
321
|
+
argv,
|
|
322
|
+
// The launcher's own flags: one outside the schema is dropped from the spec, not named.
|
|
323
|
+
userArgv: [],
|
|
324
|
+
remoteControl: deps.rcNamed ? await deps.rcNamed() : false,
|
|
325
|
+
cwd: root,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
catch (e) {
|
|
329
|
+
if (e instanceof RangeError)
|
|
330
|
+
return null;
|
|
331
|
+
throw e;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/** The refusal for a session that has not run its first turn, registered or not yet. */
|
|
335
|
+
export function firstTurnPending(name) {
|
|
336
|
+
return new DurableRefusal({
|
|
337
|
+
code: "first_turn_pending",
|
|
338
|
+
message: `${name} hasn't started its first turn yet: start a first turn, then lock; nothing was locked`,
|
|
339
|
+
fix: null,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* A lock refusal's fix line from the S1 lock catalogue, its params (<repo>, <toplevel>, <checkout>,
|
|
344
|
+
* <realpath>) from the API details. A param the answer does not carry leaves no fix line: an
|
|
345
|
+
* unfilled placeholder is never printed.
|
|
346
|
+
*/
|
|
347
|
+
export function lockRefusalFix(code, details, name, launcherKey, nameHeld) {
|
|
348
|
+
if (!(code in LOCK_CODES))
|
|
349
|
+
return null;
|
|
350
|
+
const refusals = Array.isArray(details.refusals) ? details.refusals : [];
|
|
351
|
+
const params = { name };
|
|
352
|
+
const repo = rec(refusals[0]).repo;
|
|
353
|
+
if (typeof repo === "string")
|
|
354
|
+
params.repo = repo;
|
|
355
|
+
for (const k of ["toplevel", "checkout", "realpath"]) {
|
|
356
|
+
const v = details[k];
|
|
357
|
+
if (typeof v === "string" && v !== "")
|
|
358
|
+
params[k] = v;
|
|
359
|
+
}
|
|
360
|
+
const key = lockRefusalFixKey({
|
|
361
|
+
code: code,
|
|
362
|
+
cwd_kind: details.cwd_kind ?? undefined,
|
|
363
|
+
});
|
|
364
|
+
if (key === null)
|
|
365
|
+
return null;
|
|
366
|
+
try {
|
|
367
|
+
return fixLine(key, launcherKey, nameHeld, params);
|
|
368
|
+
}
|
|
369
|
+
catch (e) {
|
|
370
|
+
if (e instanceof RangeError)
|
|
371
|
+
return null;
|
|
372
|
+
throw e;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const LOCK_CODES = {
|
|
376
|
+
durable_cwd_not_root: true,
|
|
377
|
+
durable_lock_not_entitled: true,
|
|
378
|
+
durable_lock_mfa_required: true,
|
|
379
|
+
durable_lock_branch_unprotected: true,
|
|
380
|
+
durable_lock_agent_shared: true,
|
|
381
|
+
durable_no_executor: true,
|
|
382
|
+
durable_precondition_unavailable: true,
|
|
383
|
+
};
|
|
384
|
+
const CWD_REFUSAL_TEXT = {
|
|
385
|
+
subdirectory: "the session's directory is not the repo root",
|
|
386
|
+
not_git: "the session's directory is not a git checkout",
|
|
387
|
+
symlink_transcript: "the session's transcript sits under a symlinked path, not the repo's realpath",
|
|
388
|
+
};
|
|
389
|
+
/**
|
|
390
|
+
* Lock one resolved row (the idle source): the R27 cwd rule, this machine's executor id, the spec,
|
|
391
|
+
* POST own_lock. Never prints; the lock verb and the launcher (C02-R23 c, e) render the outcome.
|
|
392
|
+
*/
|
|
393
|
+
export async function lockResolvedRow(row, name, opts, deps) {
|
|
394
|
+
// The CC session id from the row, else the launch ledger (a --new session's row can lack it).
|
|
395
|
+
const cc = ccSessionIdOf(row, deps.handover);
|
|
396
|
+
// A running tracked session with no CC session id from either: its live original (if any) can't
|
|
397
|
+
// be found, so the idle path would leave it running beside the seat. Fail closed, before any
|
|
398
|
+
// signal or request.
|
|
399
|
+
if (row.durable_mode === "tracked" && effectiveOf(row) === "running" && cc === null) {
|
|
400
|
+
return {
|
|
401
|
+
ok: false,
|
|
402
|
+
refusal: new DurableRefusal({
|
|
403
|
+
code: "session_registry_unavailable",
|
|
404
|
+
message: `${name}: the session registry did not return this session's Claude Code session id, so its running claude can't be handed over; nothing was signalled and nothing was locked`,
|
|
405
|
+
fix: null,
|
|
406
|
+
}),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
const launcherKey = deps.launcherKey();
|
|
410
|
+
const held = !isEnded(row);
|
|
411
|
+
const ruled = cwdRule(row.cwd ?? deps.cwd(), deps.probe);
|
|
412
|
+
const cwd = ruled.ok ? transcriptSlugRule(ruled.root, cc, deps.transcriptDirs) : ruled;
|
|
413
|
+
if (!cwd.ok && cwd.kind === "transcript_unverifiable") {
|
|
414
|
+
return {
|
|
415
|
+
ok: false,
|
|
416
|
+
refusal: new DurableRefusal({
|
|
417
|
+
code: "transcript_unverifiable",
|
|
418
|
+
message: `${name}: can't verify where the session's transcript sits (the Claude Code projects directory is unreadable); nothing was locked`,
|
|
419
|
+
fix: null,
|
|
420
|
+
}),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
if (!cwd.ok) {
|
|
424
|
+
const key = cwd.kind === "subdirectory"
|
|
425
|
+
? "cwd_subdirectory"
|
|
426
|
+
: cwd.kind === "not_git"
|
|
427
|
+
? "cwd_not_git"
|
|
428
|
+
: "cwd_symlink_transcript";
|
|
429
|
+
return {
|
|
430
|
+
ok: false,
|
|
431
|
+
refusal: new DurableRefusal({
|
|
432
|
+
code: CODE.durable_cwd_not_root,
|
|
433
|
+
message: `${name}: ${CWD_REFUSAL_TEXT[cwd.kind]} (cwd_kind: ${cwd.kind}); nothing was locked`,
|
|
434
|
+
fix: cwd.kind === "not_git" ? null : fixLine(key, launcherKey, held, { name, ...cwd.params }),
|
|
435
|
+
details: { cwd_kind: cwd.kind },
|
|
436
|
+
fixParams: cwd.params,
|
|
437
|
+
}),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
if (deps.trust && !rootTrusted(cwd.root, deps.trust)) {
|
|
441
|
+
return {
|
|
442
|
+
ok: false,
|
|
443
|
+
refusal: new DurableRefusal({
|
|
444
|
+
code: "cwd_untrusted",
|
|
445
|
+
message: `${name}: Claude Code has not been trusted in ${cwd.root}, so the durable seat would stop at its trust dialog; nothing was locked`,
|
|
446
|
+
fix: `run \`cd ${shellWord(cwd.root)} && claude\` once, accept the trust dialog, then \`mnemom agent lock ${name}\``,
|
|
447
|
+
details: { cwd: cwd.root },
|
|
448
|
+
}),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
// A running session that has not run its first turn has no transcript for the seat to resume
|
|
452
|
+
// (it would fail transcript_lost): refuse, and leave the attended session as it is.
|
|
453
|
+
if (deps.handover && deps.transcriptDirs) {
|
|
454
|
+
if (cc !== null && deps.transcriptDirs(cc)?.length === 0)
|
|
455
|
+
return { ok: false, refusal: firstTurnPending(name) };
|
|
456
|
+
}
|
|
457
|
+
const executorId = deps.readExecutorId(deps.home);
|
|
458
|
+
if (executorId === null) {
|
|
459
|
+
return {
|
|
460
|
+
ok: false,
|
|
461
|
+
refusal: new DurableRefusal({
|
|
462
|
+
code: CODE.durable_no_executor,
|
|
463
|
+
message: `${name}: this machine has no host daemon enrolled; nothing was locked`,
|
|
464
|
+
fix: fixLine("no_executor", launcherKey, held, { name }),
|
|
465
|
+
}),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
const built = await lockSpecOf(row, cwd.root, deps);
|
|
469
|
+
if (built === "unreadable") {
|
|
470
|
+
return {
|
|
471
|
+
ok: false,
|
|
472
|
+
refusal: new DurableRefusal({
|
|
473
|
+
code: "original_unreadable",
|
|
474
|
+
message: `${name}: can't read the running session's command line on this machine; nothing was locked`,
|
|
475
|
+
fix: null,
|
|
476
|
+
}),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
if (built === null) {
|
|
480
|
+
return {
|
|
481
|
+
ok: false,
|
|
482
|
+
refusal: new DurableRefusal({
|
|
483
|
+
code: "launch_spec_invalid",
|
|
484
|
+
message: `${name}: the session's launch spec does not fit the durable schema; nothing was locked`,
|
|
485
|
+
fix: null,
|
|
486
|
+
}),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
const body = { spec: built.spec, executor_id: executorId };
|
|
490
|
+
if (opts.ownAgent)
|
|
491
|
+
body.own_agent = true;
|
|
492
|
+
const a = await commitWithHandover(row, deps.handover, () => deps.api.lock(row.conversation_id, row.agent_id, body));
|
|
493
|
+
if (a.status >= 200 && a.status < 300)
|
|
494
|
+
return {
|
|
495
|
+
ok: true,
|
|
496
|
+
body: a.body,
|
|
497
|
+
changed: rec(a.body).changed !== false,
|
|
498
|
+
dropped: built.dropped,
|
|
499
|
+
};
|
|
500
|
+
const code = errorCodeOf(a.body);
|
|
501
|
+
const details = errorDetailsOf(a.body);
|
|
502
|
+
if (code === CODE.durable_unsupported && details.feature === "launch_spec_change")
|
|
503
|
+
return { ok: false, refusal: specChangeRefusal(name, built, launcherKey, a.body) };
|
|
504
|
+
if (code === CODE.durable_no_executor) {
|
|
505
|
+
const mismatch = await orgMismatchRefusal(row, name, executorId, deps, a.body);
|
|
506
|
+
if (mismatch !== null)
|
|
507
|
+
return { ok: false, refusal: mismatch };
|
|
508
|
+
}
|
|
509
|
+
const r = refusalFromAnswer(a, name, launcherKey, row);
|
|
510
|
+
const firstRepo = rec(Array.isArray(details.refusals) ? details.refusals[0] : null).repo;
|
|
511
|
+
const fixParams = typeof firstRepo === "string" ? { repo: firstRepo } : {};
|
|
512
|
+
const fix = r.fix === null && code !== null
|
|
513
|
+
? lockRefusalFix(code, details, name, launcherKey, held)
|
|
514
|
+
: r.fix;
|
|
515
|
+
return {
|
|
516
|
+
ok: false,
|
|
517
|
+
refusal: new DurableRefusal({
|
|
518
|
+
code: r.code,
|
|
519
|
+
message: r.message,
|
|
520
|
+
fix,
|
|
521
|
+
apiBody: a.body,
|
|
522
|
+
fixParams,
|
|
523
|
+
...(typeof details.cwd_kind === "string" ? { details: { cwd_kind: details.cwd_kind } } : {}),
|
|
524
|
+
}),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* durable_no_executor because this machine's executor is enrolled in another org than the
|
|
529
|
+
* session's: say so, with no fix line (installing a host here would not change the org). Null when
|
|
530
|
+
* the orgs match or either is unknown; the refusal then renders as the API answered.
|
|
531
|
+
*/
|
|
532
|
+
export async function orgMismatchRefusal(row, name, executorId, deps, apiBody) {
|
|
533
|
+
if (!deps.executorOrg || typeof row.org_id !== "string")
|
|
534
|
+
return null;
|
|
535
|
+
const hostOrg = await deps.executorOrg(executorId).catch(() => null);
|
|
536
|
+
if (hostOrg === null || hostOrg === row.org_id)
|
|
537
|
+
return null;
|
|
538
|
+
return new DurableRefusal({
|
|
539
|
+
code: CODE.durable_no_executor,
|
|
540
|
+
message: `${name}: the session is in org ${row.org_id}, but this machine's host is enrolled in org ${hostOrg}; nothing was locked`,
|
|
541
|
+
fix: null,
|
|
542
|
+
apiBody,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* 422 durable_unsupported {feature: launch_spec_change} on a re-lock (BACKBONE l.57, final F09): the
|
|
547
|
+
* S1 `launch_spec_change` entry with <flags> filled from the spec this lock built, so the printed
|
|
548
|
+
* line is a command that works (C11-R27). The locked row holds the name: the done clause applies.
|
|
549
|
+
*/
|
|
550
|
+
export function specChangeRefusal(name, built, launcherKey, apiBody) {
|
|
551
|
+
const flags = specUserArgv(built.spec).join(" ");
|
|
552
|
+
const [line] = launchSpecChangeFixLines({
|
|
553
|
+
name,
|
|
554
|
+
flags,
|
|
555
|
+
launcherKey,
|
|
556
|
+
soleLockedSession: false,
|
|
557
|
+
});
|
|
558
|
+
return new DurableRefusal({
|
|
559
|
+
code: CODE.durable_unsupported,
|
|
560
|
+
message: `${name}: the launch spec differs from the committed one (launch_spec_change); a locked session's flags can't change in M1, nothing was changed`,
|
|
561
|
+
// No flags to pass: `--new` alone, never a dangling space.
|
|
562
|
+
fix: flags === "" ? line.replace(" --new `", " --new`") : line,
|
|
563
|
+
details: { feature: "launch_spec_change" },
|
|
564
|
+
apiBody,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
/** Print a successful lock: the one quiet line and the S1 laptop risk sentence (C02-R17). */
|
|
568
|
+
export function printLocked(o, row, name, out) {
|
|
569
|
+
if (!o.changed) {
|
|
570
|
+
out(`${name} is already durable · mnemom agent show ${name}`);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
out(lockLine(lockFactsOf(o.body, row, name, o.dropped)));
|
|
574
|
+
}
|
|
575
|
+
export async function lockCommand(name, opts, deps) {
|
|
576
|
+
try {
|
|
577
|
+
const row = await resolveTarget(deps.api, name).catch((e) => {
|
|
578
|
+
// Seconds after `agent <name> --new` the launch may not have registered yet: not a typo.
|
|
579
|
+
if (e instanceof DurableRefusal && e.code === "agent_not_found" && deps.launching?.(name))
|
|
580
|
+
throw firstTurnPending(name);
|
|
581
|
+
throw e;
|
|
582
|
+
});
|
|
583
|
+
const o = await lockResolvedRow(row, name, opts, deps);
|
|
584
|
+
if (!o.ok)
|
|
585
|
+
throw o.refusal;
|
|
586
|
+
// --json: stdout carries the one JSON document; the human line goes to stderr.
|
|
587
|
+
printLocked(o, row, name, opts.json ? deps.err : deps.out);
|
|
588
|
+
if (opts.json)
|
|
589
|
+
deps.out(JSON.stringify(o.body ?? {}));
|
|
590
|
+
return 0;
|
|
591
|
+
}
|
|
592
|
+
catch (e) {
|
|
593
|
+
return handleVerbError(e, opts.json === true, deps);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom agent sessions` and `mnemom agent show <name>` with the `durable` gate on (BACKBONE C1;
|
|
3
|
+
* C01-R12, R24, R25, R27, R28; C11-R27): read from the server registry, never from the local
|
|
4
|
+
* ledger. With the gate off these are not reached and the ledger commands run exactly as today.
|
|
5
|
+
*
|
|
6
|
+
* sessions one row per session: name, user-facing state (of the EFFECTIVE state), agent,
|
|
7
|
+
* machine, spend (guard_state.spend_usd to cents for locked rows, `-` for tracked) and
|
|
8
|
+
* last activity; under a row, its fix line and the grant warning when they apply.
|
|
9
|
+
* show the S1 SHOW_KEYS sections in order (the --json keys are the same strings, same
|
|
10
|
+
* order); keys that do not apply to a tracked session print NA_TEXT (null in --json).
|
|
11
|
+
*
|
|
12
|
+
* A registry that is absent for this caller (route 404, not entitled 403) falls back to the ledger
|
|
13
|
+
* command (`fallback`); an unreachable one refuses with session_registry_unavailable.
|
|
14
|
+
*/
|
|
15
|
+
import { SHOW_KEYS, type LauncherKey } from "../vendor/shared/durable-contract/fixlines.js";
|
|
16
|
+
import { type ApiAnswer, type RegistryRow } from "../lib/durable/api.js";
|
|
17
|
+
import { type HomeFacts } from "../lib/durable/view.js";
|
|
18
|
+
/** The slice of DurableApi these read verbs use (a fake in tests). */
|
|
19
|
+
export interface ReadApi {
|
|
20
|
+
listSessions(filter: {
|
|
21
|
+
name?: string;
|
|
22
|
+
agentId?: string;
|
|
23
|
+
}): Promise<RegistryRow[]>;
|
|
24
|
+
getSession(cid: string, agentId: string, include?: "goal"): Promise<ApiAnswer>;
|
|
25
|
+
getAgent(agentId: string): Promise<ApiAnswer>;
|
|
26
|
+
}
|
|
27
|
+
export interface SessionsReadDeps {
|
|
28
|
+
api: ReadApi;
|
|
29
|
+
launcherKey: () => LauncherKey;
|
|
30
|
+
home: () => Promise<HomeFacts>;
|
|
31
|
+
out: (line: string) => void;
|
|
32
|
+
err: (line: string) => void;
|
|
33
|
+
}
|
|
34
|
+
/** The ledger command to run when the registry is absent for this caller. */
|
|
35
|
+
export type Fallback = () => Promise<number>;
|
|
36
|
+
/** Rows from the registry, or "absent" (route 404 / not entitled 403). Down -> a refusal is thrown. */
|
|
37
|
+
export declare function readRows(api: ReadApi, filter: {
|
|
38
|
+
name?: string;
|
|
39
|
+
agentId?: string;
|
|
40
|
+
}): Promise<RegistryRow[] | "absent">;
|
|
41
|
+
/** Newest first by last activity (rows without one last, input order kept among equals). */
|
|
42
|
+
export declare function newestFirst(rows: readonly RegistryRow[]): RegistryRow[];
|
|
43
|
+
export interface SessionJson {
|
|
44
|
+
name: string | null;
|
|
45
|
+
conversation_id: string;
|
|
46
|
+
state: string;
|
|
47
|
+
internal_state: string;
|
|
48
|
+
effective_state: string;
|
|
49
|
+
state_reason: string | null;
|
|
50
|
+
durable_mode: "tracked" | "locked";
|
|
51
|
+
agent: string;
|
|
52
|
+
machine: string | null;
|
|
53
|
+
spend: string | null;
|
|
54
|
+
last_activity: string | null;
|
|
55
|
+
fix: string | null;
|
|
56
|
+
grant_warning: string | null;
|
|
57
|
+
}
|
|
58
|
+
/** One row's --json document and its fix / grant lines (the human table prints the same). */
|
|
59
|
+
export declare function sessionJson(row: RegistryRow, launcherKey: LauncherKey, home: HomeFacts): SessionJson;
|
|
60
|
+
export declare function durableSessionsCommand(opts: {
|
|
61
|
+
all?: boolean;
|
|
62
|
+
json?: boolean;
|
|
63
|
+
}, deps: SessionsReadDeps, fallback: Fallback): Promise<number>;
|
|
64
|
+
export type ShowKey = (typeof SHOW_KEYS)[number];
|
|
65
|
+
export interface ShowInput {
|
|
66
|
+
row: RegistryRow;
|
|
67
|
+
/** GET /{cid}?include=goal body (owner only), or null when unreadable. */
|
|
68
|
+
detail: Record<string, unknown> | null;
|
|
69
|
+
/** The caller's other non-terminal sessions on the same agent; null when the registry could not be read. */
|
|
70
|
+
others: RegistryRow[] | null;
|
|
71
|
+
/**
|
|
72
|
+
* agents.agent_durable_only; null when unread (transport, non-200), "protocol_error" when a 200
|
|
73
|
+
* agent record carries no boolean agent_durable_only (never read as false).
|
|
74
|
+
*/
|
|
75
|
+
durableOnly: boolean | null | "protocol_error";
|
|
76
|
+
launcherKey: LauncherKey;
|
|
77
|
+
home: HomeFacts;
|
|
78
|
+
}
|
|
79
|
+
/** The show document: SHOW_KEYS in order; locked-only keys are null on a tracked session. */
|
|
80
|
+
export declare function showDocument(i: ShowInput): Record<ShowKey, unknown>;
|
|
81
|
+
/** The human lines of a show document, one section per SHOW_KEYS entry, in order. */
|
|
82
|
+
export declare function showLines(name: string, doc: Record<ShowKey, unknown>): string[];
|
|
83
|
+
/** A 200 agent record's agent_durable_only: a boolean, or a protocol error when it is missing. */
|
|
84
|
+
export declare function durableOnlyOf(body: unknown): boolean | "protocol_error";
|
|
85
|
+
export declare function durableShowCommand(ref: string | undefined, opts: {
|
|
86
|
+
json?: boolean;
|
|
87
|
+
}, deps: SessionsReadDeps, fallback: Fallback): Promise<number>;
|