@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.
Files changed (195) hide show
  1. package/dist/base-url-proxy.mjs +18 -4
  2. package/dist/commands/agent-config.js +5 -1
  3. package/dist/commands/agent-doctor.d.ts +9 -0
  4. package/dist/commands/agent-doctor.js +4 -0
  5. package/dist/commands/agent-session.d.ts +1 -2
  6. package/dist/commands/agent-session.js +1 -2
  7. package/dist/commands/agent-setup.d.ts +6 -0
  8. package/dist/commands/agent-setup.js +12 -1
  9. package/dist/commands/agent.d.ts +11 -0
  10. package/dist/commands/agent.js +145 -46
  11. package/dist/commands/durable-doctor.d.ts +50 -0
  12. package/dist/commands/durable-doctor.js +186 -0
  13. package/dist/commands/durable-lock.d.ts +254 -0
  14. package/dist/commands/durable-lock.js +595 -0
  15. package/dist/commands/durable-sessions.d.ts +87 -0
  16. package/dist/commands/durable-sessions.js +303 -0
  17. package/dist/commands/durable-verbs.d.ts +116 -0
  18. package/dist/commands/durable-verbs.js +393 -0
  19. package/dist/durable/flock.d.ts +26 -0
  20. package/dist/durable/flock.js +80 -0
  21. package/dist/durable/launch-spec.d.ts +7 -1
  22. package/dist/durable/launch-spec.js +10 -3
  23. package/dist/durable/seat-proxy-entry.d.ts +11 -0
  24. package/dist/durable/seat-proxy-entry.js +25 -0
  25. package/dist/durable/seat-proxy.d.ts +107 -0
  26. package/dist/durable/seat-proxy.js +599 -0
  27. package/dist/durable/seat-socket.d.ts +46 -0
  28. package/dist/durable/seat-socket.js +179 -0
  29. package/dist/durable/seat-spec-validator.d.ts +28 -0
  30. package/dist/durable/seat-spec-validator.js +59 -0
  31. package/dist/durable/test-mode.mjs +81 -0
  32. package/dist/durable/turn-chain.d.ts +55 -0
  33. package/dist/durable/turn-chain.js +178 -0
  34. package/dist/host/auto-continue.d.ts +67 -0
  35. package/dist/host/auto-continue.js +118 -0
  36. package/dist/host/bootid.d.ts +13 -0
  37. package/dist/host/bootid.js +41 -0
  38. package/dist/host/client.d.ts +123 -0
  39. package/dist/host/client.js +171 -0
  40. package/dist/host/commands.d.ts +2 -0
  41. package/dist/host/commands.js +122 -0
  42. package/dist/host/control.d.ts +33 -0
  43. package/dist/host/control.js +144 -0
  44. package/dist/host/daemon.d.ts +217 -0
  45. package/dist/host/daemon.js +606 -0
  46. package/dist/host/gate.d.ts +46 -0
  47. package/dist/host/gate.js +82 -0
  48. package/dist/host/grant.d.ts +24 -0
  49. package/dist/host/grant.js +24 -0
  50. package/dist/host/instance.d.ts +35 -0
  51. package/dist/host/instance.js +62 -0
  52. package/dist/host/integrity.d.ts +35 -0
  53. package/dist/host/integrity.js +115 -0
  54. package/dist/host/launchd.d.ts +16 -0
  55. package/dist/host/launchd.js +45 -0
  56. package/dist/host/lock.d.ts +20 -0
  57. package/dist/host/lock.js +63 -0
  58. package/dist/host/owner.d.ts +55 -0
  59. package/dist/host/owner.js +121 -0
  60. package/dist/host/plist.d.ts +16 -0
  61. package/dist/host/plist.js +85 -0
  62. package/dist/host/procs.d.ts +27 -0
  63. package/dist/host/procs.js +105 -0
  64. package/dist/host/run.d.ts +66 -0
  65. package/dist/host/run.js +314 -0
  66. package/dist/host/runtime.d.ts +8 -0
  67. package/dist/host/runtime.js +22 -0
  68. package/dist/host/s1-names.d.ts +22 -0
  69. package/dist/host/s1-names.js +21 -0
  70. package/dist/host/seat-credential.d.ts +27 -0
  71. package/dist/host/seat-credential.js +41 -0
  72. package/dist/host/seat-launch-main.d.ts +1 -0
  73. package/dist/host/seat-launch-main.js +59 -0
  74. package/dist/host/seat-launch.d.ts +116 -0
  75. package/dist/host/seat-launch.js +302 -0
  76. package/dist/host/seat.d.ts +48 -0
  77. package/dist/host/seat.js +149 -0
  78. package/dist/host/state.d.ts +33 -0
  79. package/dist/host/state.js +117 -0
  80. package/dist/host/status.d.ts +101 -0
  81. package/dist/host/status.js +307 -0
  82. package/dist/host/tmux.d.ts +56 -0
  83. package/dist/host/tmux.js +182 -0
  84. package/dist/host/transcript.d.ts +30 -0
  85. package/dist/host/transcript.js +130 -0
  86. package/dist/host/verbs.d.ts +50 -0
  87. package/dist/host/verbs.js +271 -0
  88. package/dist/index.d.ts +8 -6
  89. package/dist/index.js +197 -35
  90. package/dist/lib/agent.d.ts +8 -26
  91. package/dist/lib/agent.js +11 -26
  92. package/dist/lib/claude-plugin.d.ts +11 -2
  93. package/dist/lib/claude-plugin.js +10 -1
  94. package/dist/lib/durable/api.d.ts +136 -0
  95. package/dist/lib/durable/api.js +220 -0
  96. package/dist/lib/durable/codes.d.ts +8 -0
  97. package/dist/lib/durable/codes.js +2 -0
  98. package/dist/lib/durable/first-turn.d.ts +50 -0
  99. package/dist/lib/durable/first-turn.js +102 -0
  100. package/dist/lib/durable/gate.d.ts +48 -0
  101. package/dist/lib/durable/gate.js +122 -0
  102. package/dist/lib/durable/handover.d.ts +101 -0
  103. package/dist/lib/durable/handover.js +175 -0
  104. package/dist/lib/durable/launch.d.ts +182 -0
  105. package/dist/lib/durable/launch.js +443 -0
  106. package/dist/lib/durable/launching.d.ts +15 -0
  107. package/dist/lib/durable/launching.js +70 -0
  108. package/dist/lib/durable/live.d.ts +194 -0
  109. package/dist/lib/durable/live.js +635 -0
  110. package/dist/lib/durable/refusal.d.ts +57 -0
  111. package/dist/lib/durable/refusal.js +78 -0
  112. package/dist/lib/durable/view.d.ts +83 -0
  113. package/dist/lib/durable/view.js +244 -0
  114. package/dist/lib/experimental.d.ts +0 -9
  115. package/dist/lib/experimental.js +18 -17
  116. package/dist/lib/launcher-config.d.ts +7 -0
  117. package/dist/lib/launcher-config.js +21 -0
  118. package/dist/lib/scan/report-email.d.ts +1 -1
  119. package/dist/lib/scan/report-email.js +9 -1
  120. package/dist/lib/subscription-login.d.ts +15 -0
  121. package/dist/lib/subscription-login.js +28 -1
  122. package/dist/vendor/durable-contract/index.js +240 -18
  123. package/dist/vendor/shared/durable-contract/admission.d.ts +240 -0
  124. package/dist/vendor/shared/durable-contract/admission.js +533 -0
  125. package/dist/vendor/shared/durable-contract/arm.d.ts +51 -0
  126. package/dist/vendor/shared/durable-contract/arm.js +133 -0
  127. package/dist/vendor/shared/durable-contract/canonical.d.ts +29 -0
  128. package/dist/vendor/shared/durable-contract/canonical.js +98 -0
  129. package/dist/vendor/shared/durable-contract/cell-id.d.ts +22 -0
  130. package/dist/vendor/shared/durable-contract/cell-id.js +38 -0
  131. package/dist/vendor/shared/durable-contract/cli-output.d.ts +342 -0
  132. package/dist/vendor/shared/durable-contract/cli-output.js +201 -0
  133. package/dist/vendor/shared/durable-contract/cli-surface.d.ts +119 -0
  134. package/dist/vendor/shared/durable-contract/cli-surface.js +270 -0
  135. package/dist/vendor/shared/durable-contract/codes.d.ts +537 -0
  136. package/dist/vendor/shared/durable-contract/codes.js +475 -0
  137. package/dist/vendor/shared/durable-contract/constants.d.ts +153 -0
  138. package/dist/vendor/shared/durable-contract/constants.js +179 -0
  139. package/dist/vendor/shared/durable-contract/doors.d.ts +119 -0
  140. package/dist/vendor/shared/durable-contract/doors.js +249 -0
  141. package/dist/vendor/shared/durable-contract/envelope.d.ts +103 -0
  142. package/dist/vendor/shared/durable-contract/envelope.js +317 -0
  143. package/dist/vendor/shared/durable-contract/events.d.ts +147 -0
  144. package/dist/vendor/shared/durable-contract/events.js +630 -0
  145. package/dist/vendor/shared/durable-contract/executor-state.d.ts +41 -0
  146. package/dist/vendor/shared/durable-contract/executor-state.js +32 -0
  147. package/dist/vendor/shared/durable-contract/fixlines.d.ts +318 -0
  148. package/dist/vendor/shared/durable-contract/fixlines.js +483 -0
  149. package/dist/vendor/shared/durable-contract/freeze-record.d.ts +787 -0
  150. package/dist/vendor/shared/durable-contract/freeze-record.js +361 -0
  151. package/dist/vendor/shared/durable-contract/index.d.ts +36 -0
  152. package/dist/vendor/shared/durable-contract/index.js +37 -0
  153. package/dist/vendor/shared/durable-contract/journal-approvals.d.ts +29 -0
  154. package/dist/vendor/shared/durable-contract/journal-approvals.js +60 -0
  155. package/dist/vendor/shared/durable-contract/keys.d.ts +24 -0
  156. package/dist/vendor/shared/durable-contract/keys.js +66 -0
  157. package/dist/vendor/shared/durable-contract/lock.d.ts +128 -0
  158. package/dist/vendor/shared/durable-contract/lock.js +151 -0
  159. package/dist/vendor/shared/durable-contract/names.d.ts +25 -0
  160. package/dist/vendor/shared/durable-contract/names.js +78 -0
  161. package/dist/vendor/shared/durable-contract/owner-verbs.d.ts +175 -0
  162. package/dist/vendor/shared/durable-contract/owner-verbs.js +403 -0
  163. package/dist/vendor/shared/durable-contract/paths.d.ts +23 -0
  164. package/dist/vendor/shared/durable-contract/paths.js +35 -0
  165. package/dist/vendor/shared/durable-contract/pure-fn.d.ts +48 -0
  166. package/dist/vendor/shared/durable-contract/pure-fn.js +89 -0
  167. package/dist/vendor/shared/durable-contract/routes.d.ts +205 -0
  168. package/dist/vendor/shared/durable-contract/routes.js +315 -0
  169. package/dist/vendor/shared/durable-contract/schema.d.ts +126 -0
  170. package/dist/vendor/shared/durable-contract/schema.js +441 -0
  171. package/dist/vendor/shared/durable-contract/seal.d.ts +54 -0
  172. package/dist/vendor/shared/durable-contract/seal.js +99 -0
  173. package/dist/vendor/shared/durable-contract/secret-scope.d.ts +28 -0
  174. package/dist/vendor/shared/durable-contract/secret-scope.js +76 -0
  175. package/dist/vendor/shared/durable-contract/session-id.d.ts +21 -0
  176. package/dist/vendor/shared/durable-contract/session-id.js +58 -0
  177. package/dist/vendor/shared/durable-contract/spec.d.ts +162 -0
  178. package/dist/vendor/shared/durable-contract/spec.js +286 -0
  179. package/dist/vendor/shared/durable-contract/states.d.ts +96 -0
  180. package/dist/vendor/shared/durable-contract/states.js +218 -0
  181. package/dist/vendor/shared/durable-contract/telemetry.d.ts +94 -0
  182. package/dist/vendor/shared/durable-contract/telemetry.js +180 -0
  183. package/dist/vendor/shared/durable-contract/test-mode.d.ts +62 -0
  184. package/dist/vendor/shared/durable-contract/test-mode.js +140 -0
  185. package/dist/vendor/shared/durable-contract/turn.d.ts +57 -0
  186. package/dist/vendor/shared/durable-contract/turn.js +81 -0
  187. package/dist/vendor/shared/durable-contract/vectors.d.ts +191 -0
  188. package/dist/vendor/shared/durable-contract/vectors.js +208 -0
  189. package/package.json +3 -2
  190. package/plugins/integrity-manifest.json +35 -0
  191. package/plugins/mnemom/hooks/hooks.json +15 -1
  192. package/plugins/mnemom/hooks/register.tsx +60 -2
  193. package/plugins/mnemom/hooks/seat-hello.mjs +116 -0
  194. package/dist/commands/agent-implicit-notice.d.ts +0 -31
  195. package/dist/commands/agent-implicit-notice.js +0 -73
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The CLI `durable` gate (BACKBONE Flags/CLI; C17-R10; amendments 26, 35).
3
+ *
4
+ * While `durable` is off every durable verb word exits 2 with `gatedMessage(…, 'durable')`, sends
5
+ * no HTTP, writes nothing under HOME and starts no process — `--help` and `--json` included, which
6
+ * is why the check runs on raw argv before commander parses it (commander answers `--help` before
7
+ * any preAction hook). The words are the declared allowlisted difference: a session named `lock`
8
+ * or `containment` is not launched by name while durable is off.
9
+ *
10
+ * `mnemom agent host run` is gated like every other word until C2 registers its handler (C2 then
11
+ * reads the gate from config.json only, C12-R25); ungated, it would fall through to the launcher as
12
+ * a session named "host". The pre-existing `mnemom agent sessions` / `show` are not gated.
13
+ */
14
+ import { gatedMessage } from "../experimental.js";
15
+ export const DURABLE_ID = "durable";
16
+ /** `mnemom agent <word>` words that exist only with durable on. */
17
+ export const DURABLE_VERB_WORDS = [
18
+ "lock",
19
+ "pause",
20
+ "resume",
21
+ "done",
22
+ "kill",
23
+ "amend",
24
+ "host",
25
+ "containment",
26
+ ];
27
+ /** `mnemom agent sessions <sub>` sub-verbs that exist only with durable on. */
28
+ export const DURABLE_SESSIONS_SUBVERBS = ["unlock", "adopt"];
29
+ export function durableOn(enabled) {
30
+ return enabled.includes(DURABLE_ID);
31
+ }
32
+ const NO_SHAPES = { required: new Set(), optional: new Set() };
33
+ /** The value-taking options of `commands` (commander `Command#options`), merged. */
34
+ export function optionShapesOf(commands) {
35
+ const required = new Set();
36
+ const optional = new Set();
37
+ for (const c of commands) {
38
+ for (const raw of c.options) {
39
+ const o = raw;
40
+ if (o.negate)
41
+ continue;
42
+ const into = o.required ? required : o.optional ? optional : null;
43
+ if (into === null)
44
+ continue;
45
+ if (o.long)
46
+ into.add(o.long);
47
+ if (o.short)
48
+ into.add(o.short);
49
+ }
50
+ }
51
+ return { required, optional };
52
+ }
53
+ /**
54
+ * The index of the first operand at or after `i`, stepping over options (and the values the value-
55
+ * taking ones consume). -1 at `--` (everything after it is passthrough, never a verb).
56
+ */
57
+ function firstOperand(argv, i, shapes) {
58
+ while (i < argv.length) {
59
+ const a = argv[i];
60
+ if (a === "--")
61
+ return -1;
62
+ if (!a.startsWith("-") || a === "-")
63
+ return i;
64
+ if (a.includes("=")) {
65
+ i += 1;
66
+ continue;
67
+ }
68
+ if (shapes.required.has(a)) {
69
+ i += 2;
70
+ continue;
71
+ }
72
+ if (shapes.optional.has(a)) {
73
+ const next = argv[i + 1];
74
+ i += next !== undefined && !next.startsWith("-") ? 2 : 1;
75
+ continue;
76
+ }
77
+ i += 1;
78
+ }
79
+ return i;
80
+ }
81
+ /**
82
+ * The gated use `argv` (process.argv.slice(2)) makes, as the `what` of the gate message, or null
83
+ * when argv names no durable verb. The verb is the first operand after `agent`, with options (and
84
+ * their values, per `shapes`) stepped over exactly as commander would before it dispatches the
85
+ * subcommand, so `agent --new lock x` is gated like `agent lock x`. A word after `--` or as a later
86
+ * positional is not a verb use.
87
+ */
88
+ export function durableVerbUse(argv, shapes = NO_SHAPES) {
89
+ const at = firstOperand(argv, 0, shapes);
90
+ if (at < 0 || argv[at] !== "agent")
91
+ return null;
92
+ const w = firstOperand(argv, at + 1, shapes);
93
+ const word = w < 0 ? undefined : argv[w];
94
+ if (word === undefined)
95
+ return null;
96
+ if (DURABLE_VERB_WORDS.includes(word)) {
97
+ const sub = word === "host" || word === "containment" ? argv[w + 1] : undefined;
98
+ const tail = sub && !sub.startsWith("-") ? ` ${sub}` : "";
99
+ return `\`mnemom agent ${word}${tail}\``;
100
+ }
101
+ if (word === "sessions") {
102
+ const s = firstOperand(argv, w + 1, shapes);
103
+ const sub = s < 0 ? undefined : argv[s];
104
+ if (DURABLE_SESSIONS_SUBVERBS.includes(sub ?? ""))
105
+ return `\`mnemom agent sessions ${sub}\``;
106
+ }
107
+ return null;
108
+ }
109
+ /**
110
+ * The guard inside every durable verb's action: the pointer (exit 2) while durable is off, else
111
+ * null. The argv gate above runs first; this one holds for any argv shape that reaches commander.
112
+ */
113
+ export function durableActionGate(what, enabled) {
114
+ return durableOn(enabled) ? null : gatedMessage(what, DURABLE_ID);
115
+ }
116
+ /** The gate decision for argv: null when allowed, else the stderr message (exit 2). */
117
+ export function durableGate(argv, enabled, shapes = NO_SHAPES) {
118
+ if (durableOn(enabled))
119
+ return null;
120
+ const what = durableVerbUse(argv, shapes);
121
+ return what === null ? null : gatedMessage(what, DURABLE_ID);
122
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The running-session handover (C02-R26 steps 2-5 and 7; BACKBONE C1 and amendment 31). Lock of a
3
+ * session with a live tracked claude on this machine, and adopt, follow one order:
4
+ *
5
+ * (1) pre-commit checks run first (the caller: cwd rule, executor id, spec); a failure never
6
+ * reaches here, so the original is untouched;
7
+ * (2) freeze: SIGSTOP to the original claude pid only, not its descendants (under `/mnemom lock`
8
+ * the lock CLI is a descendant and keeps running);
9
+ * (3) commit: on any failure (non-2xx, a no-op, or a dropped response with no committed row on
10
+ * re-read) SIGCONT, and the original continues with no row change;
11
+ * (5) end: on the seat's hello, or at ATTACH_WAIT_S without one, SIGKILL to the frozen original's
12
+ * tree, excluding this process and its own descendants. After the commit the original is never
13
+ * resumed, so the conversation never has two running writers.
14
+ *
15
+ * Step 7 is the parent launcher's side: its child ended and the session is now locked, so it restores
16
+ * the terminal, prints the lock line and attaches to the seat.
17
+ *
18
+ * No I/O here: the live wiring (lib/durable/live.ts) injects the process table, signals and reads.
19
+ */
20
+ import { type ApiAnswer, type RegistryRow } from "./api.js";
21
+ import { DurableRefusal } from "./refusal.js";
22
+ export interface HandoverDeps {
23
+ /** Step-2 discovery: the live tracked claude running this CC session here, or null. */
24
+ findOriginal(ccSessionId: string): number | null;
25
+ signal(pid: number, sig: "SIGSTOP" | "SIGCONT"): void;
26
+ /** The seat's hello for this session has landed (the row is locked and running). */
27
+ helloSeen(row: RegistryRow): Promise<boolean>;
28
+ /** Re-read after a dropped response: did the lock commit? */
29
+ committed(row: RegistryRow): Promise<boolean>;
30
+ /** SIGKILL the original's process tree (see processTree). */
31
+ killTree(pid: number): void;
32
+ sleep(ms: number): Promise<void>;
33
+ now(): number;
34
+ attachWaitS: number;
35
+ /**
36
+ * This machine's launch ledger, consulted only when the registry row has no cc_session_id (the
37
+ * owner view does not carry it yet). Only a launch on this machine can have a live original to
38
+ * hand over, so its ledger can name the CC session; anything but exactly one id fails closed.
39
+ */
40
+ ledgerCcSessionId?(conversationId: string): LedgerLookup;
41
+ /** Why a handover was skipped (stderr; stdout keeps its one line or JSON document). */
42
+ log?(line: string): void;
43
+ }
44
+ /** The ledger's answer for one conversation: one CC session id, or why there is none to trust. */
45
+ export type LedgerLookup = {
46
+ readonly ok: true;
47
+ readonly ccSessionId: string;
48
+ } | {
49
+ readonly ok: false;
50
+ readonly why: "ledger_unreadable" | "no_match" | "several_matches";
51
+ };
52
+ /**
53
+ * The CC session id to hand over: the row's own when it has one, else this machine's ledger entry
54
+ * for its conversation. Null (with the reason logged) when the ledger cannot name exactly one, so no
55
+ * process is ever frozen or killed on a guess.
56
+ */
57
+ export declare function ccSessionIdOf(row: RegistryRow, d: HandoverDeps | undefined): string | null;
58
+ /**
59
+ * The ledger lookup over its records: the distinct CC session ids recorded for `conversationId`
60
+ * (a conversation launched again under the same CC session repeats one id; that is still one match).
61
+ */
62
+ export declare function ledgerCcSessionIdFrom(records: readonly {
63
+ conversation_id: string;
64
+ cc_session_id?: string | null;
65
+ }[] | null, conversationId: string): LedgerLookup;
66
+ /**
67
+ * Send the lock commit for `row` under the handover order. Without handover deps, or with no live
68
+ * original, this is the idle source: the commit alone, zero signals.
69
+ */
70
+ export declare function commitWithHandover(row: RegistryRow, d: HandoverDeps | undefined, commit: () => Promise<ApiAnswer>): Promise<ApiAnswer>;
71
+ /**
72
+ * The pids to SIGKILL for `root` from a `ps -axo pid=,ppid=` listing: the root and every
73
+ * descendant, minus `self` and self's own descendants.
74
+ */
75
+ export declare function processTree(psOutput: string, root: number, self: number): number[];
76
+ /** The row in a GET /v1/agent/sessions/{cid} answer (bare or under `session`), or null. */
77
+ export declare function sessionRowOf(a: ApiAnswer): RegistryRow | null;
78
+ /** The seat's hello has landed: the row is locked and effectively running. */
79
+ export declare function helloLanded(row: RegistryRow | null): boolean;
80
+ /** The lock committed: the row is locked. */
81
+ export declare function lockCommitted(row: RegistryRow | null): boolean;
82
+ /**
83
+ * Whether the launcher's child exit may be a handover: a durable launch whose claude was SIGKILLed
84
+ * (step 5 kills the frozen tree). Any other exit is handled exactly as today.
85
+ */
86
+ export declare function mayBeHandover(durable: boolean, signal: NodeJS.Signals | null): boolean;
87
+ export interface ParentDeps {
88
+ /** The session row now, or null when it cannot be read. */
89
+ reread(row: RegistryRow): Promise<RegistryRow | null>;
90
+ /** Put the terminal back in the mode saved before claude started. */
91
+ restoreTty(): void;
92
+ printLockLine(row: RegistryRow, name: string): void;
93
+ /** attachToSeat: the attach exit, or a thrown seat_wait_timeout refusal. */
94
+ attach(row: RegistryRow, name: string): Promise<number>;
95
+ printRefusal(r: DurableRefusal): number;
96
+ }
97
+ /**
98
+ * Step 7: the launcher's tracked child ended. When the session is now locked (a handover took it),
99
+ * attach to its seat and return the exit status; otherwise null (exit with the child's status).
100
+ */
101
+ export declare function parentAfterChildEnded(row: RegistryRow, name: string, d: ParentDeps): Promise<number | null>;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * The running-session handover (C02-R26 steps 2-5 and 7; BACKBONE C1 and amendment 31). Lock of a
3
+ * session with a live tracked claude on this machine, and adopt, follow one order:
4
+ *
5
+ * (1) pre-commit checks run first (the caller: cwd rule, executor id, spec); a failure never
6
+ * reaches here, so the original is untouched;
7
+ * (2) freeze: SIGSTOP to the original claude pid only, not its descendants (under `/mnemom lock`
8
+ * the lock CLI is a descendant and keeps running);
9
+ * (3) commit: on any failure (non-2xx, a no-op, or a dropped response with no committed row on
10
+ * re-read) SIGCONT, and the original continues with no row change;
11
+ * (5) end: on the seat's hello, or at ATTACH_WAIT_S without one, SIGKILL to the frozen original's
12
+ * tree, excluding this process and its own descendants. After the commit the original is never
13
+ * resumed, so the conversation never has two running writers.
14
+ *
15
+ * Step 7 is the parent launcher's side: its child ended and the session is now locked, so it restores
16
+ * the terminal, prints the lock line and attaches to the seat.
17
+ *
18
+ * No I/O here: the live wiring (lib/durable/live.ts) injects the process table, signals and reads.
19
+ */
20
+ import { RegistryUnavailable } from "./api.js";
21
+ import { DurableRefusal } from "./refusal.js";
22
+ import { effectiveOf, isEnded, isLocked } from "./view.js";
23
+ import { SEAT_POLL_MS } from "./launch.js";
24
+ const changed = (a) => a.status >= 200 &&
25
+ a.status < 300 &&
26
+ !(a.body !== null &&
27
+ typeof a.body === "object" &&
28
+ a.body.changed === false);
29
+ async function waitHello(row, d) {
30
+ const deadline = d.now() + d.attachWaitS * 1000;
31
+ for (;;) {
32
+ const seen = await d.helloSeen(row).catch(() => false);
33
+ if (seen || d.now() >= deadline)
34
+ return;
35
+ await d.sleep(SEAT_POLL_MS);
36
+ }
37
+ }
38
+ /**
39
+ * The CC session id to hand over: the row's own when it has one, else this machine's ledger entry
40
+ * for its conversation. Null (with the reason logged) when the ledger cannot name exactly one, so no
41
+ * process is ever frozen or killed on a guess.
42
+ */
43
+ export function ccSessionIdOf(row, d) {
44
+ if (typeof row.cc_session_id === "string" && row.cc_session_id)
45
+ return row.cc_session_id;
46
+ if (!d?.ledgerCcSessionId || typeof row.conversation_id !== "string")
47
+ return null;
48
+ const l = d.ledgerCcSessionId(row.conversation_id);
49
+ if (l.ok)
50
+ return l.ccSessionId;
51
+ d.log?.(`lock handover skipped for ${row.conversation_id}: ${l.why}`);
52
+ return null;
53
+ }
54
+ /**
55
+ * The ledger lookup over its records: the distinct CC session ids recorded for `conversationId`
56
+ * (a conversation launched again under the same CC session repeats one id; that is still one match).
57
+ */
58
+ export function ledgerCcSessionIdFrom(records, conversationId) {
59
+ if (records === null)
60
+ return { ok: false, why: "ledger_unreadable" };
61
+ const ids = new Set();
62
+ for (const r of records)
63
+ if (r.conversation_id === conversationId &&
64
+ typeof r.cc_session_id === "string" &&
65
+ r.cc_session_id)
66
+ ids.add(r.cc_session_id);
67
+ if (ids.size === 0)
68
+ return { ok: false, why: "no_match" };
69
+ if (ids.size > 1)
70
+ return { ok: false, why: "several_matches" };
71
+ return { ok: true, ccSessionId: [...ids][0] };
72
+ }
73
+ /**
74
+ * Send the lock commit for `row` under the handover order. Without handover deps, or with no live
75
+ * original, this is the idle source: the commit alone, zero signals.
76
+ */
77
+ export async function commitWithHandover(row, d, commit) {
78
+ const cc = ccSessionIdOf(row, d);
79
+ const original = d && cc !== null ? d.findOriginal(cc) : null;
80
+ if (!d || original === null)
81
+ return commit();
82
+ d.signal(original, "SIGSTOP");
83
+ let a;
84
+ try {
85
+ a = await commit();
86
+ }
87
+ catch (e) {
88
+ const done = e instanceof RegistryUnavailable && (await d.committed(row).catch(() => false));
89
+ if (!done) {
90
+ d.signal(original, "SIGCONT");
91
+ throw e;
92
+ }
93
+ a = { status: 200, body: {} };
94
+ }
95
+ if (!changed(a)) {
96
+ d.signal(original, "SIGCONT");
97
+ return a;
98
+ }
99
+ await waitHello(row, d);
100
+ d.killTree(original);
101
+ return a;
102
+ }
103
+ /**
104
+ * The pids to SIGKILL for `root` from a `ps -axo pid=,ppid=` listing: the root and every
105
+ * descendant, minus `self` and self's own descendants.
106
+ */
107
+ export function processTree(psOutput, root, self) {
108
+ const children = new Map();
109
+ for (const line of psOutput.split("\n")) {
110
+ const m = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
111
+ if (!m)
112
+ continue;
113
+ const pid = Number(m[1]);
114
+ const ppid = Number(m[2]);
115
+ children.set(ppid, [...(children.get(ppid) ?? []), pid]);
116
+ }
117
+ const out = [];
118
+ const stack = [root];
119
+ while (stack.length > 0) {
120
+ const pid = stack.pop();
121
+ if (pid === self)
122
+ continue;
123
+ out.push(pid);
124
+ stack.push(...(children.get(pid) ?? []));
125
+ }
126
+ return out;
127
+ }
128
+ /** The row in a GET /v1/agent/sessions/{cid} answer (bare or under `session`), or null. */
129
+ export function sessionRowOf(a) {
130
+ if (a.status !== 200 || a.body === null || typeof a.body !== "object")
131
+ return null;
132
+ const b = a.body;
133
+ const r = b.session !== null && typeof b.session === "object" ? b.session : b;
134
+ return typeof r.conversation_id === "string" ? r : null;
135
+ }
136
+ /** The seat's hello has landed: the row is locked and effectively running. */
137
+ export function helloLanded(row) {
138
+ return row !== null && isLocked(row) && effectiveOf(row) === "running";
139
+ }
140
+ /** The lock committed: the row is locked. */
141
+ export function lockCommitted(row) {
142
+ return row !== null && isLocked(row);
143
+ }
144
+ /**
145
+ * Whether the launcher's child exit may be a handover: a durable launch whose claude was SIGKILLed
146
+ * (step 5 kills the frozen tree). Any other exit is handled exactly as today.
147
+ */
148
+ export function mayBeHandover(durable, signal) {
149
+ return durable && signal === "SIGKILL";
150
+ }
151
+ /**
152
+ * Step 7: the launcher's tracked child ended. When the session is now locked (a handover took it),
153
+ * attach to its seat and return the exit status; otherwise null (exit with the child's status).
154
+ */
155
+ export async function parentAfterChildEnded(row, name, d) {
156
+ let now;
157
+ try {
158
+ now = await d.reread(row);
159
+ }
160
+ catch {
161
+ return null;
162
+ }
163
+ if (now === null || !isLocked(now) || isEnded(now))
164
+ return null;
165
+ d.restoreTty();
166
+ d.printLockLine(now, name);
167
+ try {
168
+ return await d.attach(now, name);
169
+ }
170
+ catch (e) {
171
+ if (e instanceof DurableRefusal)
172
+ return d.printRefusal(e);
173
+ throw e;
174
+ }
175
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * The durable launcher (BACKBONE C1; C01-R01, R02, R04, R14, R15, R16, R17, R18, R19, R21, R22;
3
+ * C02-R20, R23, R24). Pure decisions plus one orchestrator over injected deps; the launcher in
4
+ * commands/agent.ts calls `planDurableLaunch` once it knows the org and the served agent, and
5
+ * `finishDurableLaunch` once the conversation id is fixed (registration, then lock-or-spawn).
6
+ *
7
+ * Only runs with the `durable` gate on. With it off nothing here is reached (C17).
8
+ */
9
+ import { type LauncherKey, type LockRefusal } from "../../vendor/shared/durable-contract/fixlines.js";
10
+ import { type ApiAnswer, type RegistryRow } from "./api.js";
11
+ import { DurableRefusal } from "./refusal.js";
12
+ import { type HomeFacts } from "./view.js";
13
+ /** The env var a launched session carries (lib/agent.ts SESSION_NAME_ENV). */
14
+ export declare const NESTED_ENV = "MNEMOM_AGENT_SESSION_NAME";
15
+ export interface PreflightInput {
16
+ name: string | undefined;
17
+ env: NodeJS.ProcessEnv;
18
+ server: boolean;
19
+ launcherKey: LauncherKey;
20
+ }
21
+ /**
22
+ * Refusals the CLI decides before any registry call (C01-R15, R19; C02-R20). Null = go on.
23
+ * - a nested launch (MNEMOM_AGENT_SESSION_NAME set) -> nested_launch_refused
24
+ * - --server while the launch may be durable (key not off) -> refused, naming --server
25
+ * - a name outside the S1 grammar -> invalid_session_name
26
+ */
27
+ export declare function durablePreflight(i: PreflightInput): DurableRefusal | null;
28
+ export declare function flagValue(argv: readonly string[], flags: readonly string[]): string | null | undefined;
29
+ /**
30
+ * The passthrough a continue keeps, or a refusal. `--continue` and `--resume <same>` are plain
31
+ * continue (dropped; the launcher adds `--resume <cc>` itself); `--resume <other>`,
32
+ * `--session-id` and `--fork-session` refuse with session_resume_conflict; a `--goal` refuses with
33
+ * session_goal_immutable.
34
+ */
35
+ export declare function continuePassthrough(passthrough: readonly string[], ccSessionId: string | null, goalGiven: boolean): {
36
+ ok: true;
37
+ passthrough: string[];
38
+ } | {
39
+ ok: false;
40
+ refusal: DurableRefusal;
41
+ };
42
+ export type DurableLaunchPlan =
43
+ /** Registry off for this caller (route absent / not entitled): launch exactly as today. */
44
+ {
45
+ kind: "legacy";
46
+ } | {
47
+ kind: "spawn";
48
+ conversationId: string;
49
+ /** Continue: the registered CC session to resume; new: null. */
50
+ resumeCcSessionId: string | null;
51
+ /** New: the CC session id the launch pins (registered at create). */
52
+ newCcSessionId: string | null;
53
+ passthrough: string[];
54
+ /** A new row is registered before the spawn. */
55
+ register: boolean;
56
+ /** The registry row continued, when continuing. */
57
+ row: RegistryRow | null;
58
+ /** Lock instead of spawning (key always, or ask answered yes); a lock refusal falls back to spawn. */
59
+ lock: boolean;
60
+ /** The agent is durable-only (C02-R24): a lock refusal never falls back to a tracked spawn. */
61
+ durableOnly: boolean;
62
+ } | {
63
+ kind: "attach";
64
+ row: RegistryRow;
65
+ wait: boolean;
66
+ };
67
+ export interface PlanInput {
68
+ name: string;
69
+ newFlag: boolean;
70
+ launcherKey: LauncherKey;
71
+ passthrough: readonly string[];
72
+ goalGiven: boolean;
73
+ explicitAgentId: string | null;
74
+ interactive: boolean;
75
+ home: HomeFacts;
76
+ /** The registry answer already read by `registryRows` (the launcher reads it first). */
77
+ rows?: RegistryRow[] | "legacy";
78
+ /** `--conversation-id`, when given (C01-R21). */
79
+ explicitConversationId?: string | null;
80
+ }
81
+ export interface PlanDeps {
82
+ /** GET /v1/code/entitlement (C01-R26): read before the list; absent or untracked is legacy. */
83
+ entitlement(): Promise<ApiAnswer>;
84
+ listSessions(filter: {
85
+ name: string;
86
+ }): Promise<RegistryRow[]>;
87
+ failTracked(row: RegistryRow): Promise<ApiAnswer>;
88
+ transcriptExists(ccSessionId: string): boolean;
89
+ daemonLoaded(): boolean;
90
+ askYesNo(question: string, defaultYes: boolean): Promise<boolean>;
91
+ newId(): string;
92
+ /** agents.agent_durable_only of the served agent (C02-R24). */
93
+ agentDurableOnly(): Promise<boolean>;
94
+ }
95
+ /** A lock refusal code and its cwd_kind, as durable_only_agent's <precondition> reads it. */
96
+ export declare function lockRefusalOf(r: DurableRefusal | null): LockRefusal | null;
97
+ /**
98
+ * The durable-only refusal (C02-R24; S1 durable_only_agent, byte-equal): before any spawn, naming
99
+ * the lock refusal when one happened. `fixParams` fills <repo>, <toplevel>, <checkout>, <realpath>.
100
+ */
101
+ export declare function durableOnlyRefusal(launcherKey: LauncherKey, name: string, lockRefusal: DurableRefusal | null, fixParams?: Record<string, string>): DurableRefusal;
102
+ /**
103
+ * Whether the entitlement answer gives this caller the registry (C01-R26, R14): a 200 whose
104
+ * `durable.tracked` is exactly true. The field absent (API flag off or a schema mismatch) or
105
+ * `tracked` false (not entitled) is not.
106
+ */
107
+ export declare function durableTracked(a: ApiAnswer): boolean;
108
+ /** The registry reads `registryRows` makes, in order. */
109
+ export type RegistryReads = Pick<PlanDeps, "entitlement" | "listSessions">;
110
+ /**
111
+ * The caller's rows under `name`, or "legacy" when there is no registry for this caller: the
112
+ * entitlement's `durable` field absent or not tracked (C01-R26, R14: zero registry calls), the
113
+ * entitlement refused (403/404: the launch preamble decides access), or the list route absent (404)
114
+ * or refused (403). Unreachable (transport, timeout, 5xx) throws session_registry_unavailable
115
+ * (C01-R18). A 401 is rethrown as a RegistryAnswerError (preReadRegistry defers it). The launcher
116
+ * reads this FIRST, before any other API call, so a down registry refuses within
117
+ * REGISTRY_TIMEOUT_MS.
118
+ */
119
+ export declare function registryRows(d: RegistryReads, name: string): Promise<RegistryRow[] | "legacy">;
120
+ /**
121
+ * The launcher's early read (before sign-in): `registryRows`, except that a 401 (not signed in yet)
122
+ * defers the read to the plan, which runs after sign-in. Any other answer is final here.
123
+ */
124
+ export declare function preReadRegistry(d: RegistryReads, name: string): Promise<RegistryRow[] | "legacy" | undefined>;
125
+ /**
126
+ * C01-R21: `--conversation-id X` on a name live under another cid refuses (session_name_in_use, no
127
+ * spawn). A free name registers on X (the create route refuses another owner's cid without
128
+ * disclosing it), and the live row's own cid continues it.
129
+ */
130
+ export declare function explicitCidRefusal(rows: RegistryRow[], explicitCid: string | null, name: string): DurableRefusal | null;
131
+ export declare function planDurableLaunch(i: PlanInput, deps: PlanDeps): Promise<DurableLaunchPlan>;
132
+ /**
133
+ * What a launcher does after its lock attempt refused: a durable-only agent gets the
134
+ * durable_only_agent refusal and exit 1, never a tracked spawn (C02-R24, final F02); any other
135
+ * agent prints the lock refusal and its fix, then spawns tracked (exit null).
136
+ */
137
+ export declare function afterLockRefusal(refusal: DurableRefusal, durableOnly: boolean, launcherKey: LauncherKey, name: string): {
138
+ print: DurableRefusal;
139
+ exit: number | null;
140
+ };
141
+ /**
142
+ * A lock on a NEW Claude Code session (no transcript yet) waits for the session's first turn: the
143
+ * seat resumes the transcript, so a lock before it exists fails transcript_lost. The launcher
144
+ * spawns claude attended and locks once that turn has completed (lib/durable/first-turn.ts). A
145
+ * continued session already has its transcript and locks instead of spawning, as before.
146
+ */
147
+ export declare function locksAfterFirstTurn(plan: Extract<DurableLaunchPlan, {
148
+ kind: "spawn";
149
+ }>): boolean;
150
+ /** The row the first-turn lock commits: the registered row, naming the CC session this launch pins. */
151
+ export declare function firstTurnRow(row: RegistryRow, plan: Extract<DurableLaunchPlan, {
152
+ kind: "spawn";
153
+ }>): RegistryRow;
154
+ /** The create body the CLI sends (A2 owns the route; see notes-c1.md). */
155
+ export declare function createBody(name: string, ccSessionId: string | null, cwd: string): Record<string, unknown>;
156
+ /** A create answer: the row, or a refusal (409 session_name_in_use and the rest). */
157
+ export declare function createOutcome(a: ApiAnswer, name: string): RegistryRow | DurableRefusal;
158
+ export interface SeatDeps {
159
+ /** The live seat of a session: the tmux socket and target, or null while none. */
160
+ findSeat(row: RegistryRow): Promise<{
161
+ socket: string;
162
+ target: string;
163
+ } | null>;
164
+ attach(seat: {
165
+ socket: string;
166
+ target: string;
167
+ }): number;
168
+ sleep(ms: number): Promise<void>;
169
+ now(): number;
170
+ out(line: string): void;
171
+ err(line: string): void;
172
+ interactive: boolean;
173
+ launcherKey: LauncherKey;
174
+ }
175
+ /** The poll interval of the seat wait. */
176
+ export declare const SEAT_POLL_MS = 1000;
177
+ /**
178
+ * Attach to a locked session's seat. Non-TTY: the one S1 line and exit 0, nothing attached. TTY:
179
+ * wait up to ATTACH_WAIT_S for the seat, attach; on timeout exit non-zero with the seat-wait fix
180
+ * (the session stays locked).
181
+ */
182
+ export declare function attachToSeat(row: RegistryRow, name: string, droppedFlags: readonly string[], deps: SeatDeps): Promise<number>;