@mnemom/mnemom 0.17.33 → 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 (236) 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-share-render.d.ts +1 -1
  10. package/dist/commands/agent-share-render.js +1 -1
  11. package/dist/commands/agent.d.ts +26 -0
  12. package/dist/commands/agent.js +228 -53
  13. package/dist/commands/durable-doctor.d.ts +50 -0
  14. package/dist/commands/durable-doctor.js +186 -0
  15. package/dist/commands/durable-lock.d.ts +254 -0
  16. package/dist/commands/durable-lock.js +595 -0
  17. package/dist/commands/durable-sessions.d.ts +87 -0
  18. package/dist/commands/durable-sessions.js +303 -0
  19. package/dist/commands/durable-verbs.d.ts +116 -0
  20. package/dist/commands/durable-verbs.js +393 -0
  21. package/dist/commands/protection.js +1 -1
  22. package/dist/commands/scan.d.ts +97 -0
  23. package/dist/commands/scan.js +238 -0
  24. package/dist/durable/flock.d.ts +26 -0
  25. package/dist/durable/flock.js +80 -0
  26. package/dist/durable/launch-spec.d.ts +7 -1
  27. package/dist/durable/launch-spec.js +10 -3
  28. package/dist/durable/seat-proxy-entry.d.ts +11 -0
  29. package/dist/durable/seat-proxy-entry.js +25 -0
  30. package/dist/durable/seat-proxy.d.ts +107 -0
  31. package/dist/durable/seat-proxy.js +599 -0
  32. package/dist/durable/seat-socket.d.ts +46 -0
  33. package/dist/durable/seat-socket.js +179 -0
  34. package/dist/durable/seat-spec-validator.d.ts +28 -0
  35. package/dist/durable/seat-spec-validator.js +59 -0
  36. package/dist/durable/test-mode.mjs +81 -0
  37. package/dist/durable/turn-chain.d.ts +55 -0
  38. package/dist/durable/turn-chain.js +178 -0
  39. package/dist/host/auto-continue.d.ts +67 -0
  40. package/dist/host/auto-continue.js +118 -0
  41. package/dist/host/bootid.d.ts +13 -0
  42. package/dist/host/bootid.js +41 -0
  43. package/dist/host/client.d.ts +123 -0
  44. package/dist/host/client.js +171 -0
  45. package/dist/host/commands.d.ts +2 -0
  46. package/dist/host/commands.js +122 -0
  47. package/dist/host/control.d.ts +33 -0
  48. package/dist/host/control.js +144 -0
  49. package/dist/host/daemon.d.ts +217 -0
  50. package/dist/host/daemon.js +606 -0
  51. package/dist/host/gate.d.ts +46 -0
  52. package/dist/host/gate.js +82 -0
  53. package/dist/host/grant.d.ts +24 -0
  54. package/dist/host/grant.js +24 -0
  55. package/dist/host/instance.d.ts +35 -0
  56. package/dist/host/instance.js +62 -0
  57. package/dist/host/integrity.d.ts +35 -0
  58. package/dist/host/integrity.js +115 -0
  59. package/dist/host/launchd.d.ts +16 -0
  60. package/dist/host/launchd.js +45 -0
  61. package/dist/host/lock.d.ts +20 -0
  62. package/dist/host/lock.js +63 -0
  63. package/dist/host/owner.d.ts +55 -0
  64. package/dist/host/owner.js +121 -0
  65. package/dist/host/plist.d.ts +16 -0
  66. package/dist/host/plist.js +85 -0
  67. package/dist/host/procs.d.ts +27 -0
  68. package/dist/host/procs.js +105 -0
  69. package/dist/host/run.d.ts +66 -0
  70. package/dist/host/run.js +314 -0
  71. package/dist/host/runtime.d.ts +8 -0
  72. package/dist/host/runtime.js +22 -0
  73. package/dist/host/s1-names.d.ts +22 -0
  74. package/dist/host/s1-names.js +21 -0
  75. package/dist/host/seat-credential.d.ts +27 -0
  76. package/dist/host/seat-credential.js +41 -0
  77. package/dist/host/seat-launch-main.d.ts +1 -0
  78. package/dist/host/seat-launch-main.js +59 -0
  79. package/dist/host/seat-launch.d.ts +116 -0
  80. package/dist/host/seat-launch.js +302 -0
  81. package/dist/host/seat.d.ts +48 -0
  82. package/dist/host/seat.js +149 -0
  83. package/dist/host/state.d.ts +33 -0
  84. package/dist/host/state.js +117 -0
  85. package/dist/host/status.d.ts +101 -0
  86. package/dist/host/status.js +307 -0
  87. package/dist/host/tmux.d.ts +56 -0
  88. package/dist/host/tmux.js +182 -0
  89. package/dist/host/transcript.d.ts +30 -0
  90. package/dist/host/transcript.js +130 -0
  91. package/dist/host/verbs.d.ts +50 -0
  92. package/dist/host/verbs.js +271 -0
  93. package/dist/index.d.ts +8 -6
  94. package/dist/index.js +232 -35
  95. package/dist/lib/agent.d.ts +8 -26
  96. package/dist/lib/agent.js +11 -26
  97. package/dist/lib/api.d.ts +13 -0
  98. package/dist/lib/api.js +8 -0
  99. package/dist/lib/claude-plugin.d.ts +11 -2
  100. package/dist/lib/claude-plugin.js +10 -1
  101. package/dist/lib/durable/api.d.ts +136 -0
  102. package/dist/lib/durable/api.js +220 -0
  103. package/dist/lib/durable/codes.d.ts +8 -0
  104. package/dist/lib/durable/codes.js +2 -0
  105. package/dist/lib/durable/first-turn.d.ts +50 -0
  106. package/dist/lib/durable/first-turn.js +102 -0
  107. package/dist/lib/durable/gate.d.ts +48 -0
  108. package/dist/lib/durable/gate.js +122 -0
  109. package/dist/lib/durable/handover.d.ts +101 -0
  110. package/dist/lib/durable/handover.js +175 -0
  111. package/dist/lib/durable/launch.d.ts +182 -0
  112. package/dist/lib/durable/launch.js +443 -0
  113. package/dist/lib/durable/launching.d.ts +15 -0
  114. package/dist/lib/durable/launching.js +70 -0
  115. package/dist/lib/durable/live.d.ts +194 -0
  116. package/dist/lib/durable/live.js +635 -0
  117. package/dist/lib/durable/refusal.d.ts +57 -0
  118. package/dist/lib/durable/refusal.js +78 -0
  119. package/dist/lib/durable/view.d.ts +83 -0
  120. package/dist/lib/durable/view.js +244 -0
  121. package/dist/lib/experimental.d.ts +0 -9
  122. package/dist/lib/experimental.js +18 -17
  123. package/dist/lib/launcher-config.d.ts +7 -0
  124. package/dist/lib/launcher-config.js +21 -0
  125. package/dist/lib/scan/caps.d.ts +35 -0
  126. package/dist/lib/scan/caps.js +67 -0
  127. package/dist/lib/scan/credential.d.ts +37 -0
  128. package/dist/lib/scan/credential.js +104 -0
  129. package/dist/lib/scan/failure.d.ts +10 -0
  130. package/dist/lib/scan/failure.js +43 -0
  131. package/dist/lib/scan/pr-source.d.ts +57 -0
  132. package/dist/lib/scan/pr-source.js +130 -0
  133. package/dist/lib/scan/record.d.ts +49 -0
  134. package/dist/lib/scan/record.js +85 -0
  135. package/dist/lib/scan/report-email.d.ts +92 -0
  136. package/dist/lib/scan/report-email.js +144 -0
  137. package/dist/lib/scan/report.d.ts +13 -0
  138. package/dist/lib/scan/report.js +30 -0
  139. package/dist/lib/scan/scan.d.ts +64 -0
  140. package/dist/lib/scan/scan.js +182 -0
  141. package/dist/lib/scan/share.d.ts +38 -0
  142. package/dist/lib/scan/share.js +47 -0
  143. package/dist/lib/scan/spend/ai-tool-detect.d.ts +6 -0
  144. package/dist/lib/scan/spend/ai-tool-detect.js +18 -0
  145. package/dist/lib/scan/spend/anthropic-usage.d.ts +57 -0
  146. package/dist/lib/scan/spend/anthropic-usage.js +244 -0
  147. package/dist/lib/scan/spend/cost-corpus.v1.d.ts +56 -0
  148. package/dist/lib/scan/spend/cost-corpus.v1.js +64 -0
  149. package/dist/lib/scan/spend/prices.d.ts +33 -0
  150. package/dist/lib/scan/spend/prices.js +57 -0
  151. package/dist/lib/scan/spend/spend-estimate.d.ts +30 -0
  152. package/dist/lib/scan/spend/spend-estimate.js +70 -0
  153. package/dist/lib/scan/spend/spend-tiers.d.ts +25 -0
  154. package/dist/lib/scan/spend/spend-tiers.js +42 -0
  155. package/dist/lib/scan/spend/token-rate.d.ts +30 -0
  156. package/dist/lib/scan/spend/token-rate.js +54 -0
  157. package/dist/lib/session-key.d.ts +41 -0
  158. package/dist/lib/session-key.js +124 -0
  159. package/dist/lib/subscription-login.d.ts +15 -0
  160. package/dist/lib/subscription-login.js +28 -1
  161. package/dist/rc-key-401.mjs +75 -0
  162. package/dist/rc-proxy.mjs +221 -25
  163. package/dist/vendor/durable-contract/index.js +240 -18
  164. package/dist/vendor/shared/durable-contract/admission.d.ts +240 -0
  165. package/dist/vendor/shared/durable-contract/admission.js +533 -0
  166. package/dist/vendor/shared/durable-contract/arm.d.ts +51 -0
  167. package/dist/vendor/shared/durable-contract/arm.js +133 -0
  168. package/dist/vendor/shared/durable-contract/canonical.d.ts +29 -0
  169. package/dist/vendor/shared/durable-contract/canonical.js +98 -0
  170. package/dist/vendor/shared/durable-contract/cell-id.d.ts +22 -0
  171. package/dist/vendor/shared/durable-contract/cell-id.js +38 -0
  172. package/dist/vendor/shared/durable-contract/cli-output.d.ts +342 -0
  173. package/dist/vendor/shared/durable-contract/cli-output.js +201 -0
  174. package/dist/vendor/shared/durable-contract/cli-surface.d.ts +119 -0
  175. package/dist/vendor/shared/durable-contract/cli-surface.js +270 -0
  176. package/dist/vendor/shared/durable-contract/codes.d.ts +537 -0
  177. package/dist/vendor/shared/durable-contract/codes.js +475 -0
  178. package/dist/vendor/shared/durable-contract/constants.d.ts +153 -0
  179. package/dist/vendor/shared/durable-contract/constants.js +179 -0
  180. package/dist/vendor/shared/durable-contract/doors.d.ts +119 -0
  181. package/dist/vendor/shared/durable-contract/doors.js +249 -0
  182. package/dist/vendor/shared/durable-contract/envelope.d.ts +103 -0
  183. package/dist/vendor/shared/durable-contract/envelope.js +317 -0
  184. package/dist/vendor/shared/durable-contract/events.d.ts +147 -0
  185. package/dist/vendor/shared/durable-contract/events.js +630 -0
  186. package/dist/vendor/shared/durable-contract/executor-state.d.ts +41 -0
  187. package/dist/vendor/shared/durable-contract/executor-state.js +32 -0
  188. package/dist/vendor/shared/durable-contract/fixlines.d.ts +318 -0
  189. package/dist/vendor/shared/durable-contract/fixlines.js +483 -0
  190. package/dist/vendor/shared/durable-contract/freeze-record.d.ts +787 -0
  191. package/dist/vendor/shared/durable-contract/freeze-record.js +361 -0
  192. package/dist/vendor/shared/durable-contract/index.d.ts +36 -0
  193. package/dist/vendor/shared/durable-contract/index.js +37 -0
  194. package/dist/vendor/shared/durable-contract/journal-approvals.d.ts +29 -0
  195. package/dist/vendor/shared/durable-contract/journal-approvals.js +60 -0
  196. package/dist/vendor/shared/durable-contract/keys.d.ts +24 -0
  197. package/dist/vendor/shared/durable-contract/keys.js +66 -0
  198. package/dist/vendor/shared/durable-contract/lock.d.ts +128 -0
  199. package/dist/vendor/shared/durable-contract/lock.js +151 -0
  200. package/dist/vendor/shared/durable-contract/names.d.ts +25 -0
  201. package/dist/vendor/shared/durable-contract/names.js +78 -0
  202. package/dist/vendor/shared/durable-contract/owner-verbs.d.ts +175 -0
  203. package/dist/vendor/shared/durable-contract/owner-verbs.js +403 -0
  204. package/dist/vendor/shared/durable-contract/paths.d.ts +23 -0
  205. package/dist/vendor/shared/durable-contract/paths.js +35 -0
  206. package/dist/vendor/shared/durable-contract/pure-fn.d.ts +48 -0
  207. package/dist/vendor/shared/durable-contract/pure-fn.js +89 -0
  208. package/dist/vendor/shared/durable-contract/routes.d.ts +205 -0
  209. package/dist/vendor/shared/durable-contract/routes.js +315 -0
  210. package/dist/vendor/shared/durable-contract/schema.d.ts +126 -0
  211. package/dist/vendor/shared/durable-contract/schema.js +441 -0
  212. package/dist/vendor/shared/durable-contract/seal.d.ts +54 -0
  213. package/dist/vendor/shared/durable-contract/seal.js +99 -0
  214. package/dist/vendor/shared/durable-contract/secret-scope.d.ts +28 -0
  215. package/dist/vendor/shared/durable-contract/secret-scope.js +76 -0
  216. package/dist/vendor/shared/durable-contract/session-id.d.ts +21 -0
  217. package/dist/vendor/shared/durable-contract/session-id.js +58 -0
  218. package/dist/vendor/shared/durable-contract/spec.d.ts +162 -0
  219. package/dist/vendor/shared/durable-contract/spec.js +286 -0
  220. package/dist/vendor/shared/durable-contract/states.d.ts +96 -0
  221. package/dist/vendor/shared/durable-contract/states.js +218 -0
  222. package/dist/vendor/shared/durable-contract/telemetry.d.ts +94 -0
  223. package/dist/vendor/shared/durable-contract/telemetry.js +180 -0
  224. package/dist/vendor/shared/durable-contract/test-mode.d.ts +62 -0
  225. package/dist/vendor/shared/durable-contract/test-mode.js +140 -0
  226. package/dist/vendor/shared/durable-contract/turn.d.ts +57 -0
  227. package/dist/vendor/shared/durable-contract/turn.js +81 -0
  228. package/dist/vendor/shared/durable-contract/vectors.d.ts +191 -0
  229. package/dist/vendor/shared/durable-contract/vectors.js +208 -0
  230. package/package.json +4 -3
  231. package/plugins/integrity-manifest.json +35 -0
  232. package/plugins/mnemom/hooks/hooks.json +15 -1
  233. package/plugins/mnemom/hooks/register.tsx +60 -2
  234. package/plugins/mnemom/hooks/seat-hello.mjs +116 -0
  235. package/dist/commands/agent-implicit-notice.d.ts +0 -31
  236. package/dist/commands/agent-implicit-notice.js +0 -73
@@ -0,0 +1,314 @@
1
+ /**
2
+ * `mnemom agent host run` (hidden; what the LaunchAgent execs). Startup order (C12-R01, R25, R26,
3
+ * R39): read-only gate and state checks, then the lock (the first write), then the boot-id
4
+ * warning (before any network call), then the nonce binding and the state write, then the loop.
5
+ */
6
+ import * as fs from "node:fs";
7
+ import * as os from "node:os";
8
+ import * as path from "node:path";
9
+ import { createHash } from "node:crypto";
10
+ import { execFileSync } from "node:child_process";
11
+ import { setTimeout as sleep } from "node:timers/promises";
12
+ import { resolveInstance, hostPaths } from "./instance.js";
13
+ import { durableOnInConfig, GATED_TEXT } from "./gate.js";
14
+ import { readExecutorState, bindNonce, writeExecutorState, stateFileCurrent, } from "./state.js";
15
+ import { acquireHostLock, lockStillHeld } from "./lock.js";
16
+ import { resolveBootId, defaultBootIdDeps, BOOT_ID_IGNORED_WARNING } from "./bootid.js";
17
+ import { executorApi } from "./client.js";
18
+ import { HostDaemon } from "./daemon.js";
19
+ import { Tmux, tmuxRunner } from "./tmux.js";
20
+ import { defaultProcDeps, killTree, alive, seatPids } from "./procs.js";
21
+ import { ccProjectsDir, refreshGuard, findTranscript, ensureTranscript, removeGuard, guardedSessionIds, } from "./transcript.js";
22
+ import { checkIntegrity, packageRootOf } from "./integrity.js";
23
+ import { buildSeatLaunch, seatDoor } from "./seat.js";
24
+ import { socketDir, ensureSocketDir, controlSocketPath, releaseControlSocket, startControlServer, } from "./control.js";
25
+ import { writeRuntime } from "./runtime.js";
26
+ import { autoContinue, interruptedTurnId, turnContinuedJournal, } from "./auto-continue.js";
27
+ import { hostTestPauseAt } from "../durable/test-mode.mjs";
28
+ import { resolvePluginDir } from "../lib/claude-plugin.js";
29
+ /** A tag that names this HOME on a shared tmux server (not secret: a hash of the path). */
30
+ export const homeTag = (home) => createHash("sha256").update(path.resolve(home)).digest("hex").slice(0, 16);
31
+ /** The pinned claude: MNEMOM_AGENT_CLI when absolute, else `claude` on PATH, else null. */
32
+ export function resolveClaudeBin(env) {
33
+ const v = (env.MNEMOM_AGENT_CLI ?? "").trim();
34
+ if (v.startsWith("/"))
35
+ return v;
36
+ const name = v || "claude";
37
+ for (const dir of (env.PATH ?? "").split(":")) {
38
+ if (!dir)
39
+ continue;
40
+ const p = path.join(dir, name);
41
+ try {
42
+ fs.accessSync(p, fs.constants.X_OK);
43
+ return p;
44
+ }
45
+ catch {
46
+ /* next */
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+ /** The argv that runs this CLI again: node + the real entry script. */
52
+ export function cliExec() {
53
+ const entry = process.argv[1] ? fs.realpathSync(process.argv[1]) : "";
54
+ return [process.execPath, entry];
55
+ }
56
+ /** The read-only part of startup: what `run` does before it takes the lock. */
57
+ export function startupCheck(io) {
58
+ const instance = resolveInstance(io.env);
59
+ const paths = hostPaths(io.home, instance);
60
+ const on = durableOnInConfig(paths.configFile);
61
+ const st = readExecutorState(paths.stateFile);
62
+ if (st.kind === "absent") {
63
+ if (!on)
64
+ return { kind: "exit", code: 2, message: GATED_TEXT };
65
+ return {
66
+ kind: "exit",
67
+ code: 1,
68
+ message: "this machine is not enrolled: `mnemom agent host install`",
69
+ };
70
+ }
71
+ if (st.kind === "corrupt")
72
+ return {
73
+ kind: "exit",
74
+ code: 1,
75
+ message: `the host state file is unreadable (${st.reason}): \`mnemom agent host install\``,
76
+ };
77
+ return { kind: "go", paths, instance };
78
+ }
79
+ /**
80
+ * The boot-bound nonce (C12-R06), bound on first use: the first call compares the recorded boot id
81
+ * with the OS one, writes a rotated nonce when they differ (or the record is unusable), and caches
82
+ * the result; later calls return it with no I/O.
83
+ */
84
+ export function lazyNonce(stateFile, loaded, bootId, log) {
85
+ let value = null;
86
+ return () => {
87
+ if (value !== null)
88
+ return value;
89
+ const bound = bindNonce(loaded, bootId);
90
+ if (!stateFileCurrent(stateFile, bound.state))
91
+ writeExecutorState(stateFile, bound.state);
92
+ if (bound.rotated)
93
+ log("nonce rotated (boot id changed, or no usable boot id in the state file)");
94
+ value = bound.state.nonce;
95
+ return value;
96
+ };
97
+ }
98
+ export async function hostRun(io) {
99
+ const s = startupCheck(io);
100
+ const log = (line) => io.err(`${new Date().toISOString()} ${line}`);
101
+ // The gated refusal (exit 2) prints the gated text only. Every other start resolves the boot id
102
+ // first, so an ignored DURABLE_BOOT_ID_FILE is warned about before anything else (C12-R26).
103
+ if (s.kind === "exit" && s.code === 2) {
104
+ io.err(s.message);
105
+ return s.code;
106
+ }
107
+ const boot = resolveBootId({ ...defaultBootIdDeps(), env: io.env });
108
+ if (boot.warn)
109
+ log(BOOT_ID_IGNORED_WARNING);
110
+ if (s.kind === "exit") {
111
+ io.err(s.message);
112
+ return s.code;
113
+ }
114
+ const { paths, instance } = s;
115
+ // C12-R01: the lock is the first write.
116
+ const lock = acquireHostLock(paths.lockFile);
117
+ if (!lock.ok) {
118
+ io.err(`mnemom agent host: ${lock.message}`);
119
+ return 1;
120
+ }
121
+ let lockLost = () => undefined;
122
+ const loaded = readExecutorState(paths.stateFile);
123
+ if (loaded.kind !== "ok") {
124
+ io.err("the host state file changed under the daemon");
125
+ return 1;
126
+ }
127
+ // The nonce is bound (and the state file rewritten) only when first needed, i.e. before the first
128
+ // API call; a daemon that starts and stays idle (durable off in config) leaves the file alone.
129
+ const nonce = lazyNonce(paths.stateFile, loaded.state, boot.bootId, log);
130
+ log(`host daemon ${instance.label} pid ${process.pid} executor ${loaded.state.executor_id.slice(0, 8)}`);
131
+ const env = io.env;
132
+ const tmux = new Tmux(tmuxRunner(instance.tmuxSocket, env));
133
+ const procs = defaultProcDeps();
134
+ const tag = homeTag(io.home);
135
+ const projects = ccProjectsDir(env, io.home);
136
+ const sockDir = socketDir(paths.runDir, io.home);
137
+ ensureSocketDir(sockDir);
138
+ const control = controlSocketPath(sockDir);
139
+ const pluginDir = resolvePluginDir();
140
+ const claudeBin = resolveClaudeBin(env) ?? "claude";
141
+ const door = seatDoor(env, (env.MNEMOM_GATEWAY_URL ?? "").trim() || (await import("../lib/config.js")).getGatewayUrl());
142
+ const apiUrl = (await import("../lib/config.js")).getApiUrl();
143
+ let serverUp = false;
144
+ let guardMode = "link";
145
+ const lastKnown = new Map();
146
+ // One file per continued turn (<session>.<turn uuid>); kept across respawns and daemon restarts.
147
+ const continuedDir = path.join(path.dirname(paths.stateFile), "continued");
148
+ const pauseAt = hostTestPauseAt({ ...env });
149
+ const deps = {
150
+ api: executorApi({
151
+ apiUrl,
152
+ key: loaded.state.executor_key,
153
+ fetch: globalThis.fetch,
154
+ }),
155
+ get nonce() {
156
+ return nonce();
157
+ },
158
+ home: io.home,
159
+ now: () => Date.now(),
160
+ random: Math.random,
161
+ log,
162
+ gateOn: () => durableOnInConfig(paths.configFile),
163
+ integrity: async () => pluginDir ? checkIntegrity(packageRootOf(pluginDir)) : { ok: false, cause: "no_plugin_dir" },
164
+ panes: async () => (await tmux.panes()).filter((p) => p.home === tag),
165
+ claudePid: (panePid) => seatPids(procs.ps(), panePid).claude,
166
+ alive: (pid) => alive(procs, pid),
167
+ killSeat: async (session, panePid) => {
168
+ if (panePid)
169
+ killTree(procs, [panePid]);
170
+ await tmux.killSession(session);
171
+ },
172
+ spawnSeat: async (req) => {
173
+ if (!serverUp) {
174
+ await tmux.ensureServer();
175
+ serverUp = true;
176
+ }
177
+ const launch = buildSeatLaunch({
178
+ env,
179
+ cliExec: cliExec(),
180
+ claudeBin,
181
+ pluginDir,
182
+ item: req.item,
183
+ epoch: req.epoch,
184
+ spawnId: req.spawnId,
185
+ newSession: req.newSession,
186
+ specDir: paths.specDir,
187
+ controlSocket: control,
188
+ door,
189
+ });
190
+ const r = await tmux.newSeat({
191
+ name: req.tmuxSession,
192
+ cwd: req.item.cwd ?? io.home,
193
+ command: launch.command,
194
+ env: launch.env,
195
+ tags: { sid: req.item.id, epoch: req.epoch, spawn: req.spawnId, home: tag },
196
+ });
197
+ return { panePid: r.panePid, specPath: launch.specPath };
198
+ },
199
+ guard: (item) => {
200
+ if (!item.cc_session_id)
201
+ return null;
202
+ const orig = findTranscript(projects, item.cwd, item.cc_session_id);
203
+ if (orig)
204
+ lastKnown.set(item.id, orig);
205
+ const g = refreshGuard(io.home, item.id, orig);
206
+ if (g.mode)
207
+ guardMode = g.mode;
208
+ return orig;
209
+ },
210
+ ensureTranscript: (item) => ensureTranscript(io.home, item.id, projects, item.cwd, item.cc_session_id ?? "", lastKnown.get(item.id) ?? null),
211
+ removeGuard: (id) => removeGuard(io.home, id),
212
+ guardedIds: () => guardedSessionIds(io.home),
213
+ pauseAt,
214
+ pause: async (point) => {
215
+ log(`test pause at ${point}`);
216
+ for (;;)
217
+ await sleep(3600_000);
218
+ },
219
+ interruptedAt: (p) => interruptedAtFile(p, (f) => fs.readFileSync(f, "utf8")),
220
+ autoContinue: (seat, turnId) => autoContinue(seat, turnId, {
221
+ ...fileContinuedMarkers(continuedDir, fs),
222
+ journal: turnContinuedJournal(deps.api, () => deps.nonce),
223
+ capture: (t) => tmux.capturePane(t),
224
+ send: (t, text) => tmux.sendLine(t, text),
225
+ sleep,
226
+ now: () => Date.now(),
227
+ log,
228
+ }),
229
+ publish: (snap) => publishIfLockHeld(() => lockStillHeld(lock.fd, paths.lockFile), () => writeRuntimeOrLog(() => writeRuntime(paths.runtimeFile, {
230
+ ...snap,
231
+ instance: instance.label,
232
+ transcript_guard: guardMode,
233
+ control_socket: control,
234
+ }), log), () => lockLost()),
235
+ };
236
+ const daemon = new HostDaemon(deps);
237
+ const server = startControlServer(control, daemon, log);
238
+ let stopping = false;
239
+ let released = "closed";
240
+ const stop = (sig) => {
241
+ if (stopping)
242
+ return;
243
+ stopping = true;
244
+ log(`${sig}: stopping (seats keep running)`);
245
+ daemon.stop();
246
+ released = releaseControlSocket(server, control);
247
+ };
248
+ lockLost = () => stop(`${paths.lockFile} was replaced`);
249
+ process.on("SIGTERM", () => stop("SIGTERM"));
250
+ process.on("SIGINT", () => stop("SIGINT"));
251
+ await daemon.run((ms) => sleep(ms));
252
+ fs.closeSync(lock.fd);
253
+ exitIfSocketLeft(released, (code) => process.exit(code));
254
+ return 0;
255
+ }
256
+ /**
257
+ * The runtime file is written only while this daemon still locks the current host.lock. A replaced
258
+ * lock file means another daemon may own the state dir: stop instead of writing over its runtime.
259
+ */
260
+ export function publishIfLockHeld(held, write, lost) {
261
+ if (!held()) {
262
+ lost();
263
+ return;
264
+ }
265
+ write();
266
+ }
267
+ /** For tests and status: the OS boot id (macOS sysctl). */
268
+ export function sysctlBootId() {
269
+ return execFileSync("/usr/sbin/sysctl", ["-n", "kern.bootsessionuuid"], {
270
+ encoding: "utf8",
271
+ }).trim();
272
+ }
273
+ export const defaultHome = () => os.homedir();
274
+ /** The interrupted turn of the transcript at `p` (auto-continue.ts), or null when it cannot be read. */
275
+ export function interruptedAtFile(p, read) {
276
+ try {
277
+ return interruptedTurnId(read(p));
278
+ }
279
+ catch {
280
+ return null;
281
+ }
282
+ }
283
+ /**
284
+ * The auto-continue markers: one file per session and turn in `dir` (0700). The write is exclusive
285
+ * ("wx"), so marking a turn already marked throws and that turn is never continued twice.
286
+ */
287
+ export function fileContinuedMarkers(dir, f) {
288
+ return {
289
+ continued: (id, turn) => f.existsSync(path.join(dir, `${id}.${turn}`)),
290
+ markContinued: (id, turn) => {
291
+ f.mkdirSync(dir, { recursive: true, mode: 0o700 });
292
+ f.writeFileSync(path.join(dir, `${id}.${turn}`), `${new Date().toISOString()}\n`, {
293
+ flag: "wx",
294
+ });
295
+ },
296
+ };
297
+ }
298
+ /** Write the runtime file; a failed write is logged and the daemon goes on. */
299
+ export function writeRuntimeOrLog(write, log) {
300
+ try {
301
+ write();
302
+ }
303
+ catch (e) {
304
+ log(`runtime file: ${e.message}`);
305
+ }
306
+ }
307
+ /**
308
+ * The daemon's last step. Another daemon owns the control socket path now ("left"): leave with
309
+ * exit, since a normal exit would unlink that daemon's socket.
310
+ */
311
+ export function exitIfSocketLeft(released, exit) {
312
+ if (released === "left")
313
+ exit(0);
314
+ }
@@ -0,0 +1,8 @@
1
+ import type { DaemonSnapshot } from "./daemon.js";
2
+ export interface RuntimeFile extends DaemonSnapshot {
3
+ readonly instance: string;
4
+ readonly transcript_guard: "link" | "copy";
5
+ readonly control_socket: string;
6
+ }
7
+ export declare function writeRuntime(file: string, r: RuntimeFile): void;
8
+ export declare function readRuntime(file: string): RuntimeFile | null;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The daemon's runtime file (`host status` reads it): what the daemon knows that the OS does not
3
+ * (per-session claim-unavailable cause and count, the seat spawn ids, the integrity verdict, idle).
4
+ * No secret: no key, no nonce, no lease key. Written 0600 by tmp+rename.
5
+ */
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ export function writeRuntime(file, r) {
9
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
10
+ const tmp = `${file}.${process.pid}.tmp`;
11
+ fs.writeFileSync(tmp, `${JSON.stringify(r)}\n`, { mode: 0o600 });
12
+ fs.renameSync(tmp, file);
13
+ }
14
+ export function readRuntime(file) {
15
+ try {
16
+ const r = JSON.parse(fs.readFileSync(file, "utf8"));
17
+ return r && typeof r === "object" && typeof r.pid === "number" ? r : null;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * S1 names the host daemon uses, taken from the vendored contract so cli/src/host never spells an
3
+ * event kind or a durable_* code as a literal (C10-R4; the static check C10-STATIC-001).
4
+ */
5
+ import { type DurableCode } from "../vendor/shared/durable-contract/codes.js";
6
+ /** Every S1 durable code, keyed by itself: `CODE.durable_hello_late` is checked against S1 at compile time. */
7
+ export declare const CODE: {
8
+ readonly [K in DurableCode]: K;
9
+ };
10
+ /**
11
+ * The executor.spawned kind: S1 EXECUTOR_WRITABLE_KINDS[0]. It was M1's only executor-writable kind
12
+ * (L01); the Doors amendment (ruling 34.06) appended session.waiting after it, which the daemon does
13
+ * not write.
14
+ */
15
+ export declare const SPAWNED_KIND: "executor.spawned";
16
+ /** The executor.turn_continued kind: S1 EXECUTOR_WRITABLE_KINDS[2] (lead ruling 2026-10-28). */
17
+ export declare const TURN_CONTINUED_KIND: "executor.turn_continued";
18
+ /**
19
+ * The control socket's answer when no registered seat has the relayed spawn id. It is local to the
20
+ * daemon and its seat proxy, never a wire code, so it is not a durable_* code (S1 owns those).
21
+ */
22
+ export declare const SEAT_UNKNOWN = "seat_unknown";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * S1 names the host daemon uses, taken from the vendored contract so cli/src/host never spells an
3
+ * event kind or a durable_* code as a literal (C10-R4; the static check C10-STATIC-001).
4
+ */
5
+ import { DURABLE_CODES } from "../vendor/shared/durable-contract/codes.js";
6
+ import { EXECUTOR_WRITABLE_KINDS } from "../vendor/shared/durable-contract/events.js";
7
+ /** Every S1 durable code, keyed by itself: `CODE.durable_hello_late` is checked against S1 at compile time. */
8
+ export const CODE = Object.freeze(Object.fromEntries(Object.keys(DURABLE_CODES).map((k) => [k, k])));
9
+ /**
10
+ * The executor.spawned kind: S1 EXECUTOR_WRITABLE_KINDS[0]. It was M1's only executor-writable kind
11
+ * (L01); the Doors amendment (ruling 34.06) appended session.waiting after it, which the daemon does
12
+ * not write.
13
+ */
14
+ export const SPAWNED_KIND = EXECUTOR_WRITABLE_KINDS[0];
15
+ /** The executor.turn_continued kind: S1 EXECUTOR_WRITABLE_KINDS[2] (lead ruling 2026-10-28). */
16
+ export const TURN_CONTINUED_KIND = EXECUTOR_WRITABLE_KINDS[2];
17
+ /**
18
+ * The control socket's answer when no registered seat has the relayed spawn id. It is local to the
19
+ * daemon and its seat proxy, never a wire code, so it is not a durable_* code (S1 owns those).
20
+ */
21
+ export const SEAT_UNKNOWN = "seat_unknown";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A durable seat's provider credential, resolved exactly as `mnemom agent` resolves it (dogfood B6):
3
+ * the auth mode from MNEMOM_AGENT_AUTH, else launcher.toml `auth` (folded in under env, as the CLI
4
+ * does), else api-key; on the api-key arm the Anthropic key from agent.ts's own resolver
5
+ * (MNEMOM_AGENT_ANTHROPIC_KEY / ANTHROPIC_API_KEY env, then ~/.mnemom/launcher.json, honouring
6
+ * launcher.toml `key_source`), never prompting. A subscription seat carries no key: claude sends
7
+ * its own claude.ai login. The daemon runs under launchd without the user's shell env, so the
8
+ * launcher files are usually the only source.
9
+ *
10
+ * Pure over injected deps; defaultSeatCredentialDeps wires the real config reader and resolver.
11
+ */
12
+ import { resolveAnthropicKey } from "../commands/agent.js";
13
+ import { type LauncherConfig } from "../lib/launcher-config.js";
14
+ export type SeatCredential = {
15
+ readonly ok: true;
16
+ readonly anthropicKey?: string;
17
+ } | {
18
+ readonly ok: false;
19
+ readonly message: string;
20
+ };
21
+ export interface SeatCredentialDeps {
22
+ readonly env: Readonly<Record<string, string | undefined>>;
23
+ loadConfig(): LauncherConfig;
24
+ resolveKey: typeof resolveAnthropicKey;
25
+ }
26
+ export declare function resolveSeatCredential(d: SeatCredentialDeps): Promise<SeatCredential>;
27
+ export declare const defaultSeatCredentialDeps: (env: Readonly<Record<string, string | undefined>>) => SeatCredentialDeps;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * A durable seat's provider credential, resolved exactly as `mnemom agent` resolves it (dogfood B6):
3
+ * the auth mode from MNEMOM_AGENT_AUTH, else launcher.toml `auth` (folded in under env, as the CLI
4
+ * does), else api-key; on the api-key arm the Anthropic key from agent.ts's own resolver
5
+ * (MNEMOM_AGENT_ANTHROPIC_KEY / ANTHROPIC_API_KEY env, then ~/.mnemom/launcher.json, honouring
6
+ * launcher.toml `key_source`), never prompting. A subscription seat carries no key: claude sends
7
+ * its own claude.ai login. The daemon runs under launchd without the user's shell env, so the
8
+ * launcher files are usually the only source.
9
+ *
10
+ * Pure over injected deps; defaultSeatCredentialDeps wires the real config reader and resolver.
11
+ */
12
+ import { resolveAnthropicKey } from "../commands/agent.js";
13
+ import { applyConfigToEnv, loadLauncherConfig, } from "../lib/launcher-config.js";
14
+ import { resolveAgentAuth } from "../lib/subscription-login.js";
15
+ const messageOf = (e) => (e instanceof Error ? e.message : String(e));
16
+ export async function resolveSeatCredential(d) {
17
+ let cfg;
18
+ try {
19
+ cfg = d.loadConfig();
20
+ }
21
+ catch (e) {
22
+ return { ok: false, message: messageOf(e) };
23
+ }
24
+ const env = { ...d.env };
25
+ applyConfigToEnv(cfg, env);
26
+ if (resolveAgentAuth(undefined, env) === "subscription")
27
+ return { ok: true };
28
+ try {
29
+ const anthropicKey = await d.resolveKey(env, { allowPrompt: false, keySource: cfg.keySource });
30
+ return { ok: true, anthropicKey };
31
+ }
32
+ catch (e) {
33
+ // The same refusal `mnemom agent` prints ("no Anthropic key found — refusing to launch un-routed").
34
+ return { ok: false, message: messageOf(e) };
35
+ }
36
+ }
37
+ export const defaultSeatCredentialDeps = (env) => ({
38
+ env,
39
+ loadConfig: () => loadLauncherConfig(),
40
+ resolveKey: resolveAnthropicKey,
41
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Thin driver for the seat launcher (seat-launch.ts): what a durable seat's tmux pane execs. Not a
3
+ * CLI word; the host daemon starts it by path. All behaviour lives in runSeatLaunch.
4
+ */
5
+ import { spawn } from "node:child_process";
6
+ import * as fs from "node:fs";
7
+ import * as net from "node:net";
8
+ import * as os from "node:os";
9
+ import * as path from "node:path";
10
+ import { DURABLE_DIR } from "../vendor/shared/durable-contract/paths.js";
11
+ import { defaultSeatCredentialDeps, resolveSeatCredential } from "./seat-credential.js";
12
+ import { openProxyLog, parseSeatLaunchArgs, runSeatLaunch, seatLaunchDeps, seatProxyLogPath, } from "./seat-launch.js";
13
+ function request(socketPath, frame) {
14
+ return new Promise((resolve, reject) => {
15
+ const sock = net.createConnection({ path: socketPath });
16
+ let buf = "";
17
+ const timer = setTimeout(() => {
18
+ sock.destroy();
19
+ reject(new Error("control socket timeout"));
20
+ }, 10_000);
21
+ sock.on("connect", () => sock.write(`${JSON.stringify(frame)}\n`));
22
+ sock.on("data", (d) => {
23
+ buf += d.toString("utf8");
24
+ const i = buf.indexOf("\n");
25
+ if (i < 0)
26
+ return;
27
+ clearTimeout(timer);
28
+ sock.end();
29
+ try {
30
+ resolve(JSON.parse(buf.slice(0, i)));
31
+ }
32
+ catch (e) {
33
+ reject(e);
34
+ }
35
+ });
36
+ sock.on("error", (e) => {
37
+ clearTimeout(timer);
38
+ reject(e);
39
+ });
40
+ });
41
+ }
42
+ const parsed = parseSeatLaunchArgs(process.argv.slice(2));
43
+ if (!parsed.ok) {
44
+ process.stderr.write(`mnemom seat: ${parsed.error}\n`);
45
+ process.exit(2);
46
+ }
47
+ else {
48
+ const proxyLog = seatProxyLogPath(path.join(os.homedir(), DURABLE_DIR, "logs"), parsed.args.session, parsed.args.spawnId);
49
+ void runSeatLaunch(parsed.args, seatLaunchDeps({
50
+ env: process.env,
51
+ execPath: process.execPath,
52
+ credential: () => resolveSeatCredential(defaultSeatCredentialDeps(process.env)),
53
+ request,
54
+ spawn: (cmd, args, opts) => spawn(cmd, args, opts),
55
+ stderr: (line) => void process.stderr.write(line),
56
+ proxyLog,
57
+ proxyLogFd: openProxyLog(proxyLog, fs),
58
+ })).then((code) => process.exit(code));
59
+ }
@@ -0,0 +1,116 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ import type { SeatCredential } from "./seat-credential.js";
3
+ export interface SeatLaunchArgs {
4
+ readonly control: string;
5
+ readonly session: string;
6
+ readonly epoch: number;
7
+ readonly spawnId: string;
8
+ readonly conversation: string;
9
+ readonly agentId: string;
10
+ readonly door: string;
11
+ readonly cwd: string;
12
+ readonly cliEntry: string;
13
+ readonly claude: readonly string[];
14
+ }
15
+ /** How long the proxy has to print READY (it reads the key, takes the conversation lock, listens). */
16
+ export declare const READY_TIMEOUT_MS = 15000;
17
+ export declare function parseSeatLaunchArgs(argv: readonly string[]): {
18
+ ok: true;
19
+ args: SeatLaunchArgs;
20
+ } | {
21
+ ok: false;
22
+ error: string;
23
+ };
24
+ /** C3's seat-proxy argv (after the node binary): the seat ids, and the daemon socket as the hello relay. */
25
+ export declare function proxyArgv(a: SeatLaunchArgs): string[];
26
+ /**
27
+ * The proxy reads its seat ids from argv; it gets no seat fact and no Anthropic credential from env
28
+ * (the provider credential reaches it only per request, from claude, and passes through).
29
+ */
30
+ export declare const proxyEnv: (env: Readonly<Record<string, string | undefined>>) => Record<string, string>;
31
+ /**
32
+ * claude's env: `mnemom agent`'s own env for the resolved credential (launchChildEnv over
33
+ * buildCustomHeaders), pointed at the loopback proxy, plus the seat socket for the plugin's hello.
34
+ * The headers' x-mnemom-* lines are replaced by the proxy from the lease; their x-api-key line (the
35
+ * api-key arm) is what reaches the door. The plugin's hello hook runs `node`, so the directory of
36
+ * the node running this launcher goes LAST on PATH: a node already on PATH still wins, and a PATH
37
+ * without one (a version-manager shim that has since gone) still finds this one. Claude Code's
38
+ * gateway hint headers come with launchChildEnv, as for `mnemom agent`: without x-claude-code-request-class and
39
+ * x-claude-code-prompt-id the proxy can only chain a turn to the newest prompt already in the
40
+ * transcript, which for a seat's first typed turn is the launch prompt (dogfood B8).
41
+ */
42
+ export declare function claudeEnv(env: Readonly<Record<string, string | undefined>>, port: number, socketPath: string, seat: {
43
+ readonly agentId: string;
44
+ readonly conversation: string;
45
+ readonly anthropicKey?: string;
46
+ }, nodeDir?: string): Record<string, string>;
47
+ export declare function parseReady(line: string): {
48
+ port: number;
49
+ socketPath: string;
50
+ } | null;
51
+ export interface LaunchChild {
52
+ readonly pid?: number;
53
+ readonly stdin: Writable | null;
54
+ readonly stdout: Readable | null;
55
+ kill(signal: NodeJS.Signals): boolean;
56
+ on(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
57
+ }
58
+ export interface SeatLaunchDeps {
59
+ readonly env: Readonly<Record<string, string | undefined>>;
60
+ /** The node binary that runs the CLI again (the proxy). */
61
+ readonly execPath: string;
62
+ /** The owner's provider credential, resolved as `mnemom agent` does (seat-credential.ts). */
63
+ credential(): Promise<SeatCredential>;
64
+ /** One newline-JSON request on a unix socket, one reply line parsed. */
65
+ request(socketPath: string, frame: Record<string, unknown>): Promise<Record<string, unknown>>;
66
+ /**
67
+ * role "proxy": stdin and stdout piped, stderr to the seat's proxy log (seatProxyStdio), never the
68
+ * pane. role "claude": stdio inherited.
69
+ */
70
+ spawn(cmd: string, args: readonly string[], opts: {
71
+ env: Record<string, string>;
72
+ role: "proxy" | "claude";
73
+ }): LaunchChild;
74
+ stderr(line: string): void;
75
+ /** Where the proxy's stderr goes (seatProxyLogPath), named when the proxy fails; absent: nowhere. */
76
+ readonly proxyLog?: string;
77
+ readonly readyTimeoutMs?: number;
78
+ }
79
+ /**
80
+ * The seat proxy's log: its per-request lines (`seat-proxy /v1/messages -> 200 …`) and its errors.
81
+ * The pane belongs to claude; a proxy line written to the tty is drawn over Claude Code's input box
82
+ * and reads like a half-typed prompt (dogfood cosmetic finding).
83
+ */
84
+ export declare function seatProxyLogPath(logDir: string, session: string, spawnId: string): string;
85
+ /** The proxy's stdio: stdin and stdout piped (key in, READY out), stderr to the log fd or dropped. */
86
+ export declare function seatProxyStdio(logFd: number | null): ["pipe", "pipe", number | "ignore"];
87
+ /** The fs calls openProxyLog makes. */
88
+ export interface ProxyLogFs {
89
+ mkdirSync(dir: string, o: {
90
+ recursive: true;
91
+ mode: number;
92
+ }): unknown;
93
+ openSync(file: string, flags: string, mode: number): number;
94
+ }
95
+ /** Open the seat proxy's log (append, 0600, in the 0700 durable log dir); null when it cannot. */
96
+ export declare function openProxyLog(file: string, f: ProxyLogFs): number | null;
97
+ /** A node-style spawn: what seat-launch-main.ts passes from node:child_process. */
98
+ export type NodeSpawn = (cmd: string, args: string[], opts: {
99
+ env: Record<string, string>;
100
+ stdio: "inherit" | ReturnType<typeof seatProxyStdio>;
101
+ }) => unknown;
102
+ /**
103
+ * runSeatLaunch's deps over the process's own: the proxy's stderr goes to its log fd
104
+ * (seatProxyStdio), claude inherits the pane, and the log is named only when it opened.
105
+ */
106
+ export declare function seatLaunchDeps(io: {
107
+ env: SeatLaunchDeps["env"];
108
+ execPath: string;
109
+ credential: SeatLaunchDeps["credential"];
110
+ request: SeatLaunchDeps["request"];
111
+ spawn: NodeSpawn;
112
+ stderr: SeatLaunchDeps["stderr"];
113
+ proxyLog: string;
114
+ proxyLogFd: number | null;
115
+ }): SeatLaunchDeps;
116
+ export declare function runSeatLaunch(a: SeatLaunchArgs, d: SeatLaunchDeps): Promise<number>;