@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,118 @@
1
+ /**
2
+ * The seat's automatic "continue" (lead rulings, 2026-10-28: "auto-continue an interrupted turn once
3
+ * on takeover"): a lock that lands mid-turn leaves the resumed conversation's last turn interrupted,
4
+ * and `claude --resume` then idles at "What should Claude do instead?". On the seat's first accepted
5
+ * hello, if the transcript's last turn (as it was before the seat started) was interrupted and that
6
+ * turn was never continued, the daemon types one "continue" into the pane and logs it. The marker is
7
+ * keyed on the session and the interrupted turn's record uuid, written before the keys are sent and
8
+ * kept across respawns: each interrupted turn is continued at most once, so a later mid-turn lock in
9
+ * the same session is continued too, and a respawn never continues the same turn twice.
10
+ *
11
+ * The continue is also journaled: S1's executor.turn_continued (refs {spawn_id}, lead ruling
12
+ * 2026-10-28) is appended under the seat's epoch after the marker and before the keys. A refused or
13
+ * failed append sends nothing; the marker stays, so no restart retries that turn unjournaled. The
14
+ * marker remains the per-turn guard: the event's refs name the spawn, not the turn.
15
+ *
16
+ * Pure over injected deps; host/run.ts wires tmux, the marker directory and the daemon log.
17
+ */
18
+ import { TURN_CONTINUED_KIND } from "./s1-names.js";
19
+ export const CONTINUE_TEXT = "continue";
20
+ /** How long to wait for claude's input prompt in the pane before giving up. */
21
+ export const PANE_READY_TIMEOUT_MS = 30_000;
22
+ export const PANE_READY_POLL_MS = 500;
23
+ /** The marker claude's TUI shows at its input line. */
24
+ const PROMPT_MARK = "❯";
25
+ const blocks = (r) => Array.isArray(r.message?.content) ? r.message.content : [];
26
+ /**
27
+ * The interrupted turn of a CC transcript: the uuid of its last conversation record when the turn was
28
+ * cut off, else null. The last record decides: the assistant's tool call (no result yet) or any user
29
+ * record (a prompt no reply followed, a tool result the model never answered, or CC's "[Request
30
+ * interrupted by user]") is cut off, as is an assistant record with only thinking; the assistant's own
31
+ * text finished the turn. Meta records and lines that are not JSON are skipped. A cut-off record with
32
+ * no uuid also gives null: without a turn id there is no marker to guard the continue with.
33
+ */
34
+ export function interruptedTurnId(transcript) {
35
+ const msgs = [];
36
+ for (const line of transcript.split("\n")) {
37
+ if (!line.trim())
38
+ continue;
39
+ let r;
40
+ try {
41
+ r = JSON.parse(line);
42
+ }
43
+ catch {
44
+ continue;
45
+ }
46
+ if ((r.type === "user" || r.type === "assistant") && r.message && r.isMeta !== true)
47
+ msgs.push(r);
48
+ }
49
+ const last = msgs[msgs.length - 1];
50
+ if (!last)
51
+ return null;
52
+ if (last.type === "assistant") {
53
+ const b = blocks(last);
54
+ if (!b.some((x) => x.type === "tool_use") && b.some((x) => x.type === "text"))
55
+ return null;
56
+ }
57
+ return typeof last.uuid === "string" && TURN_ID_RE.test(last.uuid) ? last.uuid : null;
58
+ }
59
+ /** A turn id usable in a marker file name. */
60
+ const TURN_ID_RE = /^[A-Za-z0-9-]{1,64}$/;
61
+ /** The turn_continued idempotency key: one continue per spawn of a session under one epoch. */
62
+ export const continueIdempotencyKey = (seat) => `continue:${seat.sessionId}:${seat.epoch}:${seat.spawnId}`;
63
+ /**
64
+ * The journal dep over the executor API: POST executor.turn_continued for the seat under its claim
65
+ * epoch and the boot-bound nonce. A 2xx is journaled (null); anything else answers its code.
66
+ */
67
+ export function turnContinuedJournal(api, nonce) {
68
+ return async (seat) => {
69
+ const r = await api.append(seat.sessionId, {
70
+ kind: TURN_CONTINUED_KIND,
71
+ refs: { spawn_id: seat.spawnId },
72
+ idempotency_key: continueIdempotencyKey(seat),
73
+ expected_epoch: seat.epoch,
74
+ nonce: nonce(),
75
+ });
76
+ if (r.status >= 200 && r.status < 300)
77
+ return null;
78
+ return r.code ?? r.networkError ?? `HTTP ${r.status}`;
79
+ };
80
+ }
81
+ /**
82
+ * Run once per seat after its first accepted hello. `turnId` is interruptedTurnId of the transcript
83
+ * as it was at spawn (null: nothing to continue).
84
+ */
85
+ export async function autoContinue(seat, turnId, d) {
86
+ if (turnId === null)
87
+ return "not_interrupted";
88
+ if (d.continued(seat.sessionId, turnId)) {
89
+ d.log(`auto-continue ${seat.sessionId}: turn ${turnId} already continued; not sending again`);
90
+ return "already_continued";
91
+ }
92
+ const deadline = d.now() + (d.readyTimeoutMs ?? PANE_READY_TIMEOUT_MS);
93
+ for (;;) {
94
+ const text = await d.capture(seat.tmuxSession);
95
+ if (text !== null && text.includes(PROMPT_MARK))
96
+ break;
97
+ if (text === null || d.now() >= deadline) {
98
+ d.log(`auto-continue ${seat.sessionId}: claude's prompt never showed; not sending`);
99
+ return "pane_not_ready";
100
+ }
101
+ await d.sleep(PANE_READY_POLL_MS);
102
+ }
103
+ try {
104
+ d.markContinued(seat.sessionId, turnId);
105
+ }
106
+ catch (e) {
107
+ d.log(`auto-continue ${seat.sessionId}: marker not written (${e.message}); not sending`);
108
+ return "marker_unwritable";
109
+ }
110
+ const refused = await d.journal(seat).catch((e) => e.message);
111
+ if (refused !== null) {
112
+ d.log(`auto-continue ${seat.sessionId}: turn ${turnId} was interrupted but turn_continued was not journaled (${refused}); not sending, and this turn will not be auto-continued`);
113
+ return "journal_refused";
114
+ }
115
+ await d.send(seat.tmuxSession, CONTINUE_TEXT);
116
+ d.log(`auto-continue ${seat.sessionId}: turn ${turnId} was interrupted; sent "${CONTINUE_TEXT}" once`);
117
+ return "sent";
118
+ }
@@ -0,0 +1,13 @@
1
+ export declare const BOOT_ID_IGNORED_WARNING = "mnemom agent host: DURABLE_BOOT_ID_FILE ignored (test mode off or a non-loopback API/gateway URL); using the OS boot id";
2
+ export interface BootIdDeps {
3
+ readonly env: Readonly<Record<string, string | undefined>>;
4
+ readonly sysctl: () => string;
5
+ readonly readFile: (p: string) => string;
6
+ }
7
+ export declare function osBootIdSysctl(): string;
8
+ export declare const defaultBootIdDeps: () => BootIdDeps;
9
+ /** The boot id and whether a T1 override was ignored (the caller warns). */
10
+ export declare function resolveBootId(deps: BootIdDeps): {
11
+ bootId: string;
12
+ warn: boolean;
13
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The OS boot id the nonce is bound to (C12-R06, R26). macOS: sysctl kern.bootsessionuuid.
3
+ * T1 (DURABLE_BOOT_ID_FILE) is read only through the CLI test-mode module; when it is set but
4
+ * not honoured, the caller warns before any network call.
5
+ */
6
+ import * as fs from "node:fs";
7
+ import { execFileSync } from "node:child_process";
8
+ import { bootIdFileOverride } from "../durable/test-mode.mjs";
9
+ export const BOOT_ID_IGNORED_WARNING = "mnemom agent host: DURABLE_BOOT_ID_FILE ignored (test mode off or a non-loopback API/gateway URL); using the OS boot id";
10
+ export function osBootIdSysctl() {
11
+ if (process.platform === "darwin") {
12
+ return execFileSync("/usr/sbin/sysctl", ["-n", "kern.bootsessionuuid"], {
13
+ encoding: "utf8",
14
+ }).trim();
15
+ }
16
+ // Linux (CI only): the kernel's per-boot uuid.
17
+ return fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
18
+ }
19
+ export const defaultBootIdDeps = () => ({
20
+ env: process.env,
21
+ sysctl: osBootIdSysctl,
22
+ readFile: (p) => fs.readFileSync(p, "utf8"),
23
+ });
24
+ /** The boot id and whether a T1 override was ignored (the caller warns). */
25
+ export function resolveBootId(deps) {
26
+ const o = bootIdFileOverride({ ...deps.env });
27
+ if (o.file) {
28
+ let v;
29
+ try {
30
+ v = deps.readFile(o.file).trim();
31
+ }
32
+ catch {
33
+ v = "";
34
+ }
35
+ // An unreadable or empty override is not a boot id: use the OS one.
36
+ if (v !== "")
37
+ return { bootId: v, warn: false };
38
+ return { bootId: deps.sysctl(), warn: false };
39
+ }
40
+ return { bootId: deps.sysctl(), warn: o.ignored };
41
+ }
@@ -0,0 +1,123 @@
1
+ import { type ClaimOutcome, type ClaimUnavailableCause } from "../vendor/shared/durable-contract/codes.js";
2
+ /** The S1 claim outcomes that grant the lease (CLAIM_OUTCOME_STATUS 200). */
3
+ export type ClaimSuccess = Extract<ClaimOutcome, "acquired" | "reclaimed" | "epoch_bump">;
4
+ /** The header that carries the executor key (ruling 33.28(1); S1 EXECUTOR_KEY_HEADER once frozen). */
5
+ export declare const EXECUTOR_KEY_HEADER = "X-Mnemom-Api-Key";
6
+ export interface HttpResult {
7
+ readonly status: number;
8
+ readonly body: unknown;
9
+ /** The durable error code, when the body is `{error: {code}}` (or a bare `{code}`). */
10
+ readonly code: string | null;
11
+ /** Network failure (no HTTP status). */
12
+ readonly networkError?: string;
13
+ }
14
+ export type FetchFn = (url: string, init: {
15
+ method: string;
16
+ headers: Record<string, string>;
17
+ body?: string;
18
+ signal?: AbortSignal;
19
+ }) => Promise<{
20
+ status: number;
21
+ text(): Promise<string>;
22
+ }>;
23
+ export declare function errorCode(body: unknown): string | null;
24
+ export declare function errorDetails(body: unknown): Record<string, unknown>;
25
+ /**
26
+ * One poll item: only `id` is frozen (ruling 24.16); A3 sends {id, agent_id, state, state_reason,
27
+ * lease_epoch, lease_expires_at, held_here, cc_session_id, cwd, spec} (ruling 33.28.2), read
28
+ * defensively. spawn_id, hello and lease_seconds are never read from the poll: the first two are the
29
+ * daemon's own seat state, the last comes from the claim answer.
30
+ */
31
+ export interface SessionItem {
32
+ readonly id: string;
33
+ readonly name: string | null;
34
+ readonly conversation_id: string | null;
35
+ readonly agent_id: string | null;
36
+ readonly state: string | null;
37
+ readonly state_reason: string | null;
38
+ readonly lease_epoch: number | null;
39
+ readonly lease_expires_at: string | null;
40
+ readonly cc_session_id: string | null;
41
+ readonly cwd: string | null;
42
+ readonly spec: Record<string, unknown> | null;
43
+ /** The lease is held by this executor (A3 lease_executor_id = me); null when the poll omits it. */
44
+ readonly held_here: boolean | null;
45
+ }
46
+ export declare function parseSessionItem(raw: unknown): SessionItem | null;
47
+ /** One heartbeat entry: exactly S1 HEARTBEAT_ENTRY_FIELDS (ruling 33.28(2)). */
48
+ export interface HeartbeatEntry {
49
+ readonly session_id: string;
50
+ readonly epoch: number;
51
+ readonly spawn_id: string;
52
+ }
53
+ /** The claim body (ruling 33.28(3)); new_key only on the retry after 409 durable_claim_needs_key. */
54
+ export interface ClaimBody {
55
+ readonly nonce: string;
56
+ readonly tokens?: unknown;
57
+ readonly new_key?: true;
58
+ }
59
+ export interface ExecutorApi {
60
+ /** grantExpired: the lapsed-grant answer `{sessions: [], grant_expired: true}` (ruling 33.28(4)). */
61
+ sessions(): Promise<HttpResult & {
62
+ items: SessionItem[] | null;
63
+ grantExpired: boolean;
64
+ }>;
65
+ attest(sessionId: string, nonce: string): Promise<HttpResult>;
66
+ claim(sessionId: string, body: ClaimBody): Promise<HttpResult>;
67
+ append(sessionId: string, body: Record<string, unknown>): Promise<HttpResult>;
68
+ hello(sessionId: string, body: {
69
+ epoch: number;
70
+ nonce: string;
71
+ spawn_id: string;
72
+ }): Promise<HttpResult>;
73
+ seatExit(sessionId: string, body: {
74
+ epoch: number;
75
+ nonce: string;
76
+ spawn_id: string;
77
+ exit_status: number;
78
+ hello_accepted: boolean;
79
+ }): Promise<HttpResult>;
80
+ fail(sessionId: string, body: {
81
+ epoch: number;
82
+ nonce: string;
83
+ reason: "transcript_lost";
84
+ }): Promise<HttpResult>;
85
+ /** Exactly `{nonce, entries}`: the daemon's nonce once at the top level, none per entry (ruling 33.28.1). */
86
+ heartbeat(body: {
87
+ nonce: string;
88
+ entries: readonly HeartbeatEntry[];
89
+ }): Promise<HttpResult>;
90
+ }
91
+ export declare function executorApi(opts: {
92
+ apiUrl: string;
93
+ key: string;
94
+ fetch: FetchFn;
95
+ timeoutMs?: number;
96
+ }): ExecutorApi;
97
+ /** The claim's success fields (A1 durable_claim_lease plus the minted lease key, A3). */
98
+ export interface ClaimOk {
99
+ readonly outcome: ClaimSuccess;
100
+ readonly epoch: number;
101
+ readonly leaseSeconds: number;
102
+ readonly leaseExpiresAt: number;
103
+ readonly leaseKey: string | null;
104
+ /**
105
+ * S1 amendment 54 (pending): the row was born locked by create-and-lock (B5a), so its CC session
106
+ * has no transcript yet. Read from the claim answer only; the poll item keys stay frozen.
107
+ */
108
+ readonly newSession: boolean;
109
+ }
110
+ export type ClaimDecision = {
111
+ kind: "ok";
112
+ claim: ClaimOk;
113
+ }
114
+ /** cause is null when the server names none, or one outside S1 CLAIM_UNAVAILABLE_CAUSES. */
115
+ | {
116
+ kind: "unavailable";
117
+ cause: ClaimUnavailableCause | null;
118
+ } | {
119
+ kind: "none";
120
+ why: string;
121
+ };
122
+ /** Map a claim response to what the daemon does with it (C12-R14, R31). */
123
+ export declare function classifyClaim(r: HttpResult, now: number): ClaimDecision;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * The executor contract client (S1 ROUTES exec_*; BACKBONE A3). Every call carries the executor
3
+ * key in X-Mnemom-Api-Key and never sends Authorization (ruling 33.28(1)); nothing else on the host
4
+ * ever sees the key (C12-R27). Bodies follow S1 where S1 freezes them (heartbeat entries,
5
+ * executor.spawned refs and idempotency key, seat-exit fields, fail reason) and ruling 33.28 / A3
6
+ * elsewhere; response parsing is tolerant of extra fields.
7
+ */
8
+ import { routePath } from "../vendor/shared/durable-contract/routes.js";
9
+ import { CLAIM_OUTCOME_STATUS, CLAIM_UNAVAILABLE_CAUSES, } from "../vendor/shared/durable-contract/codes.js";
10
+ import { LEASE_SECONDS_DEFAULT } from "../vendor/shared/durable-contract/constants.js";
11
+ import { CODE } from "./s1-names.js";
12
+ const isClaimSuccess = (v) => typeof v === "string" &&
13
+ Object.prototype.hasOwnProperty.call(CLAIM_OUTCOME_STATUS, v) &&
14
+ CLAIM_OUTCOME_STATUS[v] === 200;
15
+ /** The header that carries the executor key (ruling 33.28(1); S1 EXECUTOR_KEY_HEADER once frozen). */
16
+ export const EXECUTOR_KEY_HEADER = "X-Mnemom-Api-Key";
17
+ export function errorCode(body) {
18
+ if (body && typeof body === "object") {
19
+ const b = body;
20
+ if (b.error &&
21
+ typeof b.error === "object" &&
22
+ typeof b.error.code === "string")
23
+ return b.error.code;
24
+ if (typeof b.error === "string")
25
+ return b.error;
26
+ if (typeof b.code === "string")
27
+ return b.code;
28
+ }
29
+ return null;
30
+ }
31
+ export function errorDetails(body) {
32
+ if (body && typeof body === "object") {
33
+ const e = body.error;
34
+ if (e && typeof e === "object") {
35
+ const d = e.details;
36
+ if (d && typeof d === "object")
37
+ return d;
38
+ }
39
+ }
40
+ return {};
41
+ }
42
+ const str = (v) => (typeof v === "string" && v !== "" ? v : null);
43
+ const num = (v) => typeof v === "number" && Number.isFinite(v)
44
+ ? v
45
+ : typeof v === "string" && /^\d+$/.test(v)
46
+ ? Number(v)
47
+ : null;
48
+ export function parseSessionItem(raw) {
49
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
50
+ return null;
51
+ const o = raw;
52
+ const id = str(o.id);
53
+ if (!id || !/^[0-9a-f-]{36}$/i.test(id))
54
+ return null;
55
+ const spec = o.spec && typeof o.spec === "object" && !Array.isArray(o.spec)
56
+ ? o.spec
57
+ : null;
58
+ return {
59
+ id: id.toLowerCase(),
60
+ name: str(o.name),
61
+ conversation_id: str(o.conversation_id),
62
+ agent_id: str(o.agent_id),
63
+ state: str(o.state),
64
+ state_reason: str(o.state_reason),
65
+ lease_epoch: num(o.lease_epoch ?? o.epoch),
66
+ lease_expires_at: str(o.lease_expires_at),
67
+ cc_session_id: str(o.cc_session_id),
68
+ cwd: str(o.cwd) ?? (spec ? str(spec.cwd) : null),
69
+ spec,
70
+ held_here: typeof o.held_here === "boolean" ? o.held_here : null,
71
+ };
72
+ }
73
+ export function executorApi(opts) {
74
+ const base = opts.apiUrl.replace(/\/+$/, "");
75
+ const call = async (route, id, method, body) => {
76
+ const url = `${base}${routePath(route, id)}`;
77
+ const ctl = new AbortController();
78
+ const timer = setTimeout(() => ctl.abort(), opts.timeoutMs ?? 15000);
79
+ try {
80
+ const res = await opts.fetch(url, {
81
+ method,
82
+ headers: {
83
+ [EXECUTOR_KEY_HEADER]: opts.key,
84
+ "content-type": "application/json",
85
+ accept: "application/json",
86
+ },
87
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
88
+ signal: ctl.signal,
89
+ });
90
+ const text = await res.text();
91
+ let parsed = null;
92
+ try {
93
+ parsed = text ? JSON.parse(text) : null;
94
+ }
95
+ catch {
96
+ parsed = text;
97
+ }
98
+ return {
99
+ status: res.status,
100
+ body: parsed,
101
+ code: res.status >= 400 ? errorCode(parsed) : null,
102
+ };
103
+ }
104
+ catch (e) {
105
+ return { status: 0, body: null, code: null, networkError: e.message };
106
+ }
107
+ finally {
108
+ clearTimeout(timer);
109
+ }
110
+ };
111
+ return {
112
+ async sessions() {
113
+ const r = await call("exec_sessions", undefined, "GET");
114
+ let items = null;
115
+ let grantExpired = false;
116
+ if (r.status === 200 &&
117
+ r.body &&
118
+ typeof r.body === "object" &&
119
+ Array.isArray(r.body.sessions)) {
120
+ items = r.body.sessions
121
+ .map(parseSessionItem)
122
+ .filter((x) => x !== null);
123
+ grantExpired = r.body.grant_expired === true;
124
+ }
125
+ return { ...r, items, grantExpired };
126
+ },
127
+ attest: (id, nonce) => call("exec_attest", id, "POST", { nonce }),
128
+ claim: (id, body) => call("exec_claim", id, "POST", body),
129
+ append: (id, body) => call("exec_append", id, "POST", body),
130
+ hello: (id, body) => call("exec_hello", id, "POST", body),
131
+ seatExit: (id, body) => call("exec_seat_exit", id, "POST", body),
132
+ fail: (id, body) => call("exec_fail", id, "POST", body),
133
+ heartbeat: (body) => call("exec_heartbeat", undefined, "POST", body),
134
+ };
135
+ }
136
+ /** Map a claim response to what the daemon does with it (C12-R14, R31). */
137
+ export function classifyClaim(r, now) {
138
+ if (r.status === 503 && (r.code === CODE.durable_claim_unavailable || r.code === null)) {
139
+ const cause = errorDetails(r.body).cause;
140
+ return {
141
+ kind: "unavailable",
142
+ cause: CLAIM_UNAVAILABLE_CAUSES.includes(cause)
143
+ ? cause
144
+ : null,
145
+ };
146
+ }
147
+ if (r.status === 200 && r.body && typeof r.body === "object") {
148
+ const b = r.body;
149
+ const outcome = b.outcome;
150
+ const epoch = num(b.epoch);
151
+ if (isClaimSuccess(outcome) && epoch !== null) {
152
+ const ls = num(b.lease_seconds) ?? LEASE_SECONDS_DEFAULT;
153
+ const exp = typeof b.lease_expires_at === "string" ? Date.parse(b.lease_expires_at) : NaN;
154
+ const key = str(b.lease_key) ?? str(b.key);
155
+ return {
156
+ kind: "ok",
157
+ claim: {
158
+ outcome,
159
+ epoch,
160
+ leaseSeconds: ls,
161
+ leaseExpiresAt: Number.isFinite(exp) ? exp : now + ls * 1000,
162
+ leaseKey: key,
163
+ newSession: b.new_session === true,
164
+ },
165
+ };
166
+ }
167
+ }
168
+ if (r.networkError)
169
+ return { kind: "none", why: `network: ${r.networkError}` };
170
+ return { kind: "none", why: r.code ?? `http ${r.status}` };
171
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerHostCommands(agent: Command, hidden: boolean): Command;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Registers `mnemom agent host …` (BACKBONE C2; S1 M1_CLI_SURFACE builder group C2) on the
3
+ * `agent` command. The whole group is hidden from help while durable is off at load, so the
4
+ * pre-M1 help text is unchanged (C17). Every verb refuses with the gated text, exit 2, before
5
+ * any side effect when durable is off (C12-R25); `run` applies its own carve-out (run.ts).
6
+ */
7
+ import * as os from "node:os";
8
+ import { CLI_VERSION } from "../version.js";
9
+ import { getApiUrl } from "../lib/config.js";
10
+ import { getAccessToken } from "../lib/auth.js";
11
+ import { resolveBootId, defaultBootIdDeps } from "./bootid.js";
12
+ import { realLaunchctl } from "./launchd.js";
13
+ import { ownerApi, parseExecutorList } from "./owner.js";
14
+ import { freshGate, hostInstall, hostEnroll, hostRenew, hostUninstall, } from "./verbs.js";
15
+ import { collectStatus, printStatus, lsofPids, lsofListenPort, lsofUnixPaths } from "./status.js";
16
+ import { hostRun, cliExec, resolveClaudeBin, homeTag } from "./run.js";
17
+ import { Tmux, tmuxRunner } from "./tmux.js";
18
+ import { psTable, alive, defaultProcDeps } from "./procs.js";
19
+ import { resolveInstance } from "./instance.js";
20
+ function verbIo(json) {
21
+ const env = process.env;
22
+ const owner = ownerApi({
23
+ apiUrl: getApiUrl(),
24
+ jwt: getAccessToken,
25
+ fetch: globalThis.fetch,
26
+ });
27
+ return {
28
+ env,
29
+ home: os.homedir(),
30
+ uid: os.userInfo().uid,
31
+ json,
32
+ out: (l) => console.log(l),
33
+ err: (l) => console.error(l),
34
+ owner,
35
+ launchctl: realLaunchctl,
36
+ now: () => Date.now(),
37
+ bootId: () => resolveBootId({ ...defaultBootIdDeps(), env }).bootId,
38
+ programArguments: () => [...cliExec(), "agent", "host", "run"],
39
+ claudeBin: () => resolveClaudeBin(env),
40
+ };
41
+ }
42
+ async function guarded(json, fn) {
43
+ const io = verbIo(json);
44
+ const g = freshGate(io);
45
+ if (g !== null) {
46
+ process.exitCode = g;
47
+ return;
48
+ }
49
+ try {
50
+ process.exitCode = await fn(io);
51
+ }
52
+ catch (e) {
53
+ console.error("Error:", e instanceof Error ? e.message : e);
54
+ process.exitCode = 1;
55
+ }
56
+ }
57
+ export function registerHostCommands(agent, hidden) {
58
+ const host = agent
59
+ .command("host", { hidden })
60
+ .description("Run locked durable sessions on this machine (experimental: `mnemom experimental enable durable`)");
61
+ host
62
+ .command("install")
63
+ .description("Enroll this machine (once) and install the host daemon as a user LaunchAgent")
64
+ .option("--json", "JSON lines output")
65
+ .action(async (o) => guarded(Boolean(o.json), hostInstall));
66
+ host
67
+ .command("enroll")
68
+ .description("Enroll this machine as an executor (device step); `install` does this when needed")
69
+ .action(async () => guarded(false, hostEnroll));
70
+ host
71
+ .command("renew")
72
+ .description("Renew this machine's executor grant (device step)")
73
+ .option("--json", "JSON lines output")
74
+ .action(async (o) => guarded(Boolean(o.json), hostRenew));
75
+ host
76
+ .command("uninstall")
77
+ .description("Unload the host daemon; seats keep running and the enrollment stays")
78
+ .action(async () => guarded(false, hostUninstall));
79
+ host
80
+ .command("status")
81
+ .description("Show the host daemon, its lock and its seats")
82
+ .option("--json", "Output as JSON")
83
+ .action(async (o) => guarded(Boolean(o.json), async (io) => {
84
+ const tmux = new Tmux(tmuxRunner(resolveInstance(io.env).tmuxSocket, io.env));
85
+ const s = await collectStatus({
86
+ env: io.env,
87
+ home: io.home,
88
+ uid: io.uid,
89
+ launchctl: io.launchctl,
90
+ panes: () => tmux.panes(),
91
+ ps: psTable,
92
+ lsofPids,
93
+ lsofListenPort,
94
+ lsofUnixPaths,
95
+ alive: (pid) => alive(defaultProcDeps(), pid),
96
+ executorRow: async (id) => {
97
+ const r = await io.owner.list();
98
+ return r.status === 200
99
+ ? (parseExecutorList(r.body)?.find((x) => x.id === id) ?? null)
100
+ : null;
101
+ },
102
+ homeTag: homeTag(io.home),
103
+ cliVersion: CLI_VERSION,
104
+ now: io.now,
105
+ });
106
+ printStatus(s, Boolean(o.json), io.out, io.err);
107
+ return 0;
108
+ }));
109
+ host
110
+ .command("run", { hidden: true })
111
+ .description("The host daemon (started by launchd)")
112
+ .action(async () => {
113
+ const code = await hostRun({
114
+ env: process.env,
115
+ home: os.homedir(),
116
+ err: (l) => console.error(l),
117
+ });
118
+ // Seats live on their own tmux server; nothing the daemon leaves open may keep it alive.
119
+ process.exit(code);
120
+ });
121
+ return host;
122
+ }
@@ -0,0 +1,33 @@
1
+ import * as net from "node:net";
2
+ /** macOS sun_path is 104 bytes; keep a margin. */
3
+ export declare const MAX_SOCKET_PATH = 100;
4
+ /** A socket dir for this HOME: under the durable run dir when short enough, else a hashed /tmp dir. */
5
+ export declare function socketDir(runDir: string, home: string, uid?: number): string;
6
+ /**
7
+ * Create (or check) the control socket's directory: 0700 and owned by this uid. Under the /tmp
8
+ * fallback the predictable /tmp/mnm-host-<uid> parent gets the same check, so a directory another
9
+ * user created there first is refused rather than used. Throws when either is not ours.
10
+ */
11
+ export declare function ensureSocketDir(dir: string, uid?: number): void;
12
+ export declare const controlSocketPath: (dir: string) => string;
13
+ export interface ControlHandlers {
14
+ takeLeaseKey(sessionId: string, spawnId: string): string | null;
15
+ relayHello(sessionId: string, spawnId: string): Promise<{
16
+ status: number;
17
+ body: unknown;
18
+ }>;
19
+ }
20
+ export declare function handleControl(h: ControlHandlers, line: string): Promise<Record<string, unknown>>;
21
+ export declare function startControlServer(socket: string, h: ControlHandlers, log: (s: string) => void): net.Server;
22
+ /** dev and inode of the socket file at `socket`, or null when there is none. */
23
+ export declare function socketIdentity(socket: string): {
24
+ dev: number;
25
+ ino: number;
26
+ } | null;
27
+ /**
28
+ * Stop serving `socket`. Node unlinks a unix socket's path when its server closes, whoever owns
29
+ * the path by then, so the server is closed (and the path removed) only while the path is still
30
+ * the socket this server created. Otherwise another daemon has re-bound it: the server is only
31
+ * unref'd and the caller must leave with process.exit, which never unlinks.
32
+ */
33
+ export declare function releaseControlSocket(srv: net.Server, socket: string): "closed" | "left";