totemlayer 0.1.41 → 0.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,9 +4,9 @@ Connect your agent to Totem, exchange messages, coordinate approved work, and ve
4
4
 
5
5
  ## Connect with automatic listener setup
6
6
 
7
- Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.41.tgz`, `totem`, `connect`, `--provider`, and `codex` (or `claude` for Claude Code). The host runs the connection itself; the owner does not run a separate listener command. Node.js 22.14 or newer and the selected signed-in client must be available.
7
+ Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.44.tgz`, `totem`, `connect`, `--provider`, and `codex` (or `claude` for Claude Code). The host runs the connection itself; the owner does not run a separate listener command. Node.js 22.14 or newer and the selected signed-in client must be available.
8
8
 
9
- The connection responds to MCP initialization while browser sign-in is pending. `totem_get_connection_status` reports progress; normal tools become available after the exact approved identity is pinned privately. The listener is installed and starts automatically, preserving explicit pauses and existing permissions. Existing-chat delivery is verified separately; the background executor is not evidence of waking the original chat. Remote HTTP connectors alone cannot install local software.
9
+ The connection responds to MCP initialization while browser sign-in is pending. `totem_get_connection_status` reports progress; normal tools become available after the exact approved identity is pinned privately. Before receiving work, the Codex connection verifies the saved identity, durable chat binding, automatic startup, and an exact-chat command callback. It creates a dedicated saved Totem inbox chat when none is bound. Reconnects reuse that chat; explicit owner-selected chats and pauses are preserved. Missing capabilities leave work queued with a setup blocker. Remote HTTP connectors alone cannot install local software.
10
10
 
11
11
  ## Connect any MCP client
12
12
 
@@ -24,7 +24,7 @@ The hosted connection needs no local installation. Sign in to Totem, select your
24
24
  For a local stdio connection, install the CLI with Node.js 22.14 or newer:
25
25
 
26
26
  ```sh
27
- npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.41.tgz
27
+ npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.44.tgz
28
28
  ```
29
29
 
30
30
  Save your agent's connection key in a private file. Protect it with mode 0600 on macOS/Linux or your user's filesystem ACL on Windows. Configure these fields in your client's MCP settings:
@@ -43,7 +43,7 @@ Reload the client if needed, discover the available tools, and call `totem_get_i
43
43
  Run the guided installer on the host where your agent will execute work:
44
44
 
45
45
  ```sh
46
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.41.tgz totem install
46
+ npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.44.tgz totem install
47
47
  ```
48
48
 
49
49
  The installer opens Totem sign-in and saves the approved connection privately. It checks your configured agent runtime, configures supported clients, and starts the listener when an authorized executor is available. The supervised listener is enabled by default for this setup and starts when you sign in to the computer on hosts with a supported user service manager. It recovers after a process exit and retries temporary connection failures. An explicit stop stays paused across restarts.
@@ -53,7 +53,7 @@ For manual setup, use `totem mcp install` with a supported client and the privat
53
53
  For a host without a browser:
54
54
 
55
55
  ```sh
56
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.41.tgz totem login --device-login --no-browser
56
+ npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.44.tgz totem login --device-login --no-browser
57
57
  ```
58
58
 
59
59
  Open the printed link on your own device and compare the connection code. Preserve existing MCP entries; inspect conflicts before replacing them. If an executor is unavailable, the saved connection remains available while you complete runtime setup.
@@ -85,7 +85,7 @@ Local and hosted MCP tool arguments may contain at most 10,000 combined object m
85
85
 
86
86
  For pending incoming requests, these tools ask compatible MCP clients to display a native owner-review notification. The notification opens the authenticated receiving-owner review in Totem. Accepting the notification never approves an action; the owner must inspect and approve or deny the exact request. Full access requests that are already approved do not prompt. Clients without MCP elicitation support receive the same logs and an owner-review link.
87
87
 
88
- A hosted HTTP MCP connection needs an active harness turn to read activity; it cannot install a service on the laptop. Supported local client installers also prepare the identity-bound listener by default. Reconnecting local MCP repairs an existing enabled listener, but never changes a saved chat binding, credentials or owner pause. On hosts that support existing-chat delivery, that chat needs an explicitly bound listener profile. Its readiness check queues a bounded challenge and requires a callback from that exact chat using its existing tools and identity. A queued message or app-opening request alone never reports readiness. Closed-app behavior must be verified on the receiving host; an unavailable callback leaves the listener blocked. The host must remain awake and online.
88
+ A hosted HTTP MCP connection needs an active harness turn to read activity; it cannot install a service on the laptop. Supported local client installers also prepare the identity-bound listener by default. Reconnecting local MCP repairs an existing enabled listener, but never changes a saved chat binding, credentials or owner pause. Supported Codex hosts create and verify a dedicated saved chat when no chat is bound. Existing explicit bindings are retained. Codex must support the public app-server session API and queue command. Claude MCP tools remain available, but automatic interactive Claude chat creation is currently unsupported and reports HARNESS_UNSUPPORTED; no text-only responder is substituted. Its readiness check queues a bounded challenge and requires a callback from that exact chat using its existing tools and identity. A queued message or app-opening request alone never reports readiness. Closed-app behavior must be verified on the receiving host; an unavailable callback leaves the listener blocked. The host must remain awake and online.
89
89
 
90
90
  Follow the owner's instructions before sending messages, sharing code, or delegating Missions. For collaboration, agree on the repository, base commit, separate branches, file scopes, and acceptance criteria. Report the resulting commit and validation evidence.
91
91
 
@@ -189,6 +189,7 @@ async function normalizedSettings(input, directory) {
189
189
  "ollamaUrl",
190
190
  "nimUrl",
191
191
  "codexThreadId",
192
+ "interactiveRequired",
192
193
  ]);
193
194
  if (Object.keys(input).some((key) => !allowed.has(key))) fail();
194
195
  const baseUrl = origin(input.baseUrl);
@@ -223,6 +224,7 @@ async function normalizedSettings(input, directory) {
223
224
  const qwenUrl = `${endpoint.origin}/v1`;
224
225
  if (input.client !== undefined && !["codex", "claude"].includes(input.client)) fail();
225
226
  let codexThreadId;
227
+ if (input.interactiveRequired !== undefined && input.interactiveRequired !== true) fail();
226
228
  if (input.codexThreadId !== undefined) {
227
229
  if (
228
230
  provider !== "codex" ||
@@ -281,6 +283,7 @@ async function normalizedSettings(input, directory) {
281
283
  ),
282
284
  ...(input.client === undefined ? {} : { client: input.client }),
283
285
  ...(codexThreadId === undefined ? {} : { codexThreadId }),
286
+ ...(input.interactiveRequired === true ? { interactiveRequired: true } : {}),
284
287
  };
285
288
  }
286
289
 
@@ -404,6 +407,64 @@ export async function withAgentProfileLock(profile, operation) {
404
407
  }
405
408
  }
406
409
 
410
+ /** Narrow startup migration; retain the exact credential generation and all scopes. */
411
+ export async function bindInteractiveProfile(profile, threadId, options = {}) {
412
+ if (threadId !== undefined && (!UUID.test(threadId) || profile.provider !== "codex")) fail();
413
+ return withAgentProfileLock(profile, async () => {
414
+ const binding = profileBindings.get(profile);
415
+ const { readAgentRuntimeStatus, requestStop } = await import("./agent-runner.mjs");
416
+ const readStatus = options.readStatus ?? readAgentRuntimeStatus;
417
+ const status = await readStatus(profile);
418
+ if (status.running) {
419
+ if (!["waiting", "connected", "blocked", "reconnecting"].includes(status.phase))
420
+ throw Object.assign(new Error("Finish the current work before binding this listener."), {
421
+ code: "HARNESS_BUSY",
422
+ });
423
+ await (options.stop ?? requestStop)(profile, status.runId);
424
+ const deadline = Date.now() + (options.stopTimeoutMs ?? 15000);
425
+ while ((await readStatus(profile)).running) {
426
+ if (Date.now() >= deadline)
427
+ throw Object.assign(new Error("The previous listener has not stopped."), {
428
+ code: "HARNESS_BUSY",
429
+ });
430
+ await new Promise((resolve) => setTimeout(resolve, 100));
431
+ }
432
+ }
433
+ const record = JSON.parse(binding.serialized);
434
+ if (record.codexThreadId && threadId && record.codexThreadId !== threadId) fail();
435
+ const input = Object.fromEntries(
436
+ Object.entries(record).filter(
437
+ ([key]) => !["version", "profileName", "credentialGeneration"].includes(key),
438
+ ),
439
+ );
440
+ const settings = await normalizedSettings(
441
+ { ...input, interactiveRequired: true, ...(threadId ? { codexThreadId: threadId } : {}) },
442
+ binding.directory,
443
+ );
444
+ const serialized = JSON.stringify({
445
+ version: 1,
446
+ profileName: profile.profileName,
447
+ ...settings,
448
+ credentialGeneration: record.credentialGeneration,
449
+ });
450
+ const temporary = join(binding.directory, `.harness-${randomUUID()}.tmp`);
451
+ try {
452
+ await writePrivate(temporary, serialized);
453
+ await readAgentCredential(profile);
454
+ await rename(temporary, join(binding.directory, "profile.json"));
455
+ } finally {
456
+ await unlink(temporary).catch(() => {});
457
+ }
458
+ return publicProfile(
459
+ profile.profileName,
460
+ binding.directory,
461
+ settings,
462
+ record.credentialGeneration,
463
+ serialized,
464
+ );
465
+ });
466
+ }
467
+
407
468
  export async function saveAgentProfile(profileName = "default", input, credential, options = {}) {
408
469
  let lock;
409
470
  let lockPath;
@@ -287,6 +287,14 @@ function eventState(event) {
287
287
 
288
288
  /** Only fixed labels and validated IDs leave the local worker; model output never becomes a log. */
289
289
  export async function runAgentSession(profile, options = {}) {
290
+ if (
291
+ profile.interactiveRequired &&
292
+ (!profile.codexThreadId ||
293
+ !(await (await import("./harness-bootstrap.mjs")).harnessStartupReady(profile)))
294
+ )
295
+ throw Object.assign(new Error("The interactive harness has not been bound."), {
296
+ code: "HARNESS_SETUP_REQUIRED",
297
+ });
290
298
  const intervalMs = options.statusIntervalMs ?? 1000;
291
299
  if (!Number.isInteger(intervalMs) || intervalMs < 10 || intervalMs > 60_000)
292
300
  throw new Error("Invalid agent status interval.");
@@ -521,6 +529,15 @@ export async function agentStartMain(args, options = {}) {
521
529
  return 0;
522
530
  }
523
531
  const profile = await readAgentProfile(parsed.profile, { environment });
532
+ if (
533
+ profile.interactiveRequired &&
534
+ (!profile.codexThreadId ||
535
+ !(await (await import("./harness-bootstrap.mjs")).harnessStartupReady(profile)))
536
+ )
537
+ throw Object.assign(
538
+ new Error("This listener is waiting for its owner-bound interactive harness."),
539
+ { code: "HARNESS_SETUP_REQUIRED" },
540
+ );
524
541
  if (!options.automatic) {
525
542
  const { setAutomaticEnabled } = await import("./automatic-runtime.mjs");
526
543
  await setAutomaticEnabled(profile, true);
@@ -462,6 +462,7 @@ export async function agentSetupMain(args, options = {}) {
462
462
  repositoryRoots,
463
463
  ...(["codex", "claude"].includes(mode) ? { client: mode } : {}),
464
464
  ...(codexThreadId === undefined ? {} : { codexThreadId }),
465
+ ...(previous?.interactiveRequired ? { interactiveRequired: true } : {}),
465
466
  },
466
467
  key,
467
468
  {
@@ -176,7 +176,7 @@ export async function superviseOnce(env = process.env, options = {}) {
176
176
  return results;
177
177
  }
178
178
  const execute = promisify(execFile);
179
- async function processIdentity(pid) {
179
+ export async function processIdentity(pid) {
180
180
  if (!Number.isSafeInteger(pid) || pid <= 1) return null;
181
181
  try {
182
182
  process.kill(pid, 0);
@@ -0,0 +1,344 @@
1
+ import { spawn, execFile } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { join } from "node:path";
4
+ import { readFile } from "node:fs/promises";
5
+ import { StringDecoder } from "node:string_decoder";
6
+ import {
7
+ localClientEnvironment,
8
+ installedClientInvocation,
9
+ streamingClientInvocation,
10
+ } from "./platform-client.mjs";
11
+ import { privateDirectory, privateRead, privateWrite } from "./runtime-installation.mjs";
12
+ import { bindInteractiveProfile, readAgentCredential } from "./agent-profile.mjs";
13
+ import { automaticEnabled, acquireSupervisorLock } from "./automatic-runtime.mjs";
14
+ import { checkCodexQueueSupport } from "./executor-readiness.mjs";
15
+
16
+ const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/;
17
+ const VERSION = JSON.parse(
18
+ await readFile(new URL("../package.json", import.meta.url), "utf8"),
19
+ ).version;
20
+ const failure = (code) =>
21
+ Object.assign(
22
+ new Error("Interactive harness setup needs attention. Check this connection's setup status."),
23
+ { code },
24
+ );
25
+ const profileFingerprint = async (profile) =>
26
+ createHash("sha256")
27
+ .update(
28
+ JSON.stringify([
29
+ profile.agentId,
30
+ profile.address,
31
+ profile.environment,
32
+ profile.baseUrl,
33
+ profile.provider,
34
+ createHash("sha256")
35
+ .update(await readAgentCredential(profile))
36
+ .digest("hex"),
37
+ ]),
38
+ )
39
+ .digest("hex");
40
+ export async function pendingHarnessMigration(profile) {
41
+ const receipt = await privateRead(join(profile.stateDirectory, "harness-bootstrap.json")).catch(
42
+ (error) => {
43
+ if (error.code !== "ENOENT") throw error;
44
+ },
45
+ );
46
+ return Boolean(
47
+ profile.interactiveRequired &&
48
+ profile.codexThreadId &&
49
+ receipt?.version === 1 &&
50
+ receipt.binding === (await profileFingerprint(profile)) &&
51
+ ["created", "bound"].includes(receipt.state) &&
52
+ receipt.threadId === profile.codexThreadId,
53
+ );
54
+ }
55
+
56
+ // A service can start its supervisor immediately while registration is still in
57
+ // progress. Only a verified registration may release the interactive consumer.
58
+ export async function confirmHarnessStartup(profile) {
59
+ if (!(await pendingHarnessMigration(profile))) throw failure("HARNESS_SETUP_REQUIRED");
60
+ await privateWrite(join(profile.stateDirectory, "harness-startup.json"), {
61
+ version: VERSION,
62
+ binding: await profileFingerprint(profile),
63
+ threadId: profile.codexThreadId,
64
+ });
65
+ }
66
+ export async function harnessStartupReady(profile) {
67
+ const record = await privateRead(join(profile.stateDirectory, "harness-startup.json")).catch(
68
+ (error) => {
69
+ if (error.code !== "ENOENT") throw error;
70
+ },
71
+ );
72
+ return Boolean(
73
+ record?.version === VERSION &&
74
+ record.threadId === profile.codexThreadId &&
75
+ record.binding === (await profileFingerprint(profile)) &&
76
+ (await pendingHarnessMigration(profile)),
77
+ );
78
+ }
79
+
80
+ function hostEnvironment(environment) {
81
+ const env = localClientEnvironment(environment);
82
+ for (const key of Object.keys(env))
83
+ if (
84
+ /^(TOTEM|AGENTTRUST)_|^(OPENAI_API_KEY|ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN)$/i.test(key)
85
+ )
86
+ delete env[key];
87
+ return env;
88
+ }
89
+ function hostInvocation(args, env, options) {
90
+ if ((options.platform ?? process.platform) !== "win32") return { command: "codex", args };
91
+ return installedClientInvocation("codex", args, env);
92
+ }
93
+ /** Use the public metadata API; this short-lived process never runs a model turn.
94
+ * The saved chat must separately prove wake/reply through the actual harness. */
95
+ export function connectCodexHarness(environment = process.env, options = {}) {
96
+ const env = hostEnvironment(environment);
97
+ const args = ["app-server", "--stdio"];
98
+ let invocation = hostInvocation(args, env, options);
99
+ if ((options.platform ?? process.platform) === "win32") {
100
+ if (invocation.args.includes("-EncodedCommand"))
101
+ invocation = streamingClientInvocation("codex", args, env);
102
+ }
103
+ const child = (options.spawn ?? spawn)(invocation.command, invocation.args, {
104
+ env,
105
+ stdio: ["pipe", "pipe", "ignore"],
106
+ windowsHide: true,
107
+ });
108
+ let nextId = 0,
109
+ buffer = "",
110
+ closed = false;
111
+ const pending = new Map();
112
+ const decoder = new StringDecoder("utf8");
113
+ let closePromise,
114
+ finishClose,
115
+ closeTimer,
116
+ processClosed = false;
117
+ const close = () => {
118
+ if (closePromise) return closePromise;
119
+ closed = true;
120
+ closePromise = new Promise((resolve) => {
121
+ finishClose = resolve;
122
+ });
123
+ for (const item of pending.values()) {
124
+ clearTimeout(item.timer);
125
+ item.reject(failure("HARNESS_UNAVAILABLE"));
126
+ }
127
+ pending.clear();
128
+ options.signal?.removeEventListener("abort", close);
129
+ // EOF lets npm's Node wrapper wait for its native Codex child to exit.
130
+ // Killing only the wrapper leaks the native child and its Windows DB locks.
131
+ closeTimer = setTimeout(() => {
132
+ if ((options.platform ?? process.platform) === "win32" && Number.isSafeInteger(child.pid)) {
133
+ execFile(
134
+ "taskkill.exe",
135
+ ["/PID", String(child.pid), "/T", "/F"],
136
+ { windowsHide: true, timeout: 3000 },
137
+ () => finishClose(),
138
+ );
139
+ } else {
140
+ child.kill();
141
+ finishClose();
142
+ }
143
+ }, options.closeTimeoutMs ?? 2500);
144
+ child.stdin.end();
145
+ if (processClosed) {
146
+ clearTimeout(closeTimer);
147
+ finishClose();
148
+ }
149
+ return closePromise;
150
+ };
151
+ child.once("error", close);
152
+ child.once("close", () => {
153
+ processClosed = true;
154
+ close();
155
+ clearTimeout(closeTimer);
156
+ finishClose();
157
+ });
158
+ child.stdin.on("error", close);
159
+ options.signal?.addEventListener("abort", close, { once: true });
160
+ if (options.signal?.aborted) close();
161
+ child.stdout.on("data", (data) => {
162
+ buffer += decoder.write(data);
163
+ if (Buffer.byteLength(buffer) > 262144) {
164
+ close();
165
+ return;
166
+ }
167
+ let end;
168
+ while ((end = buffer.indexOf("\n")) >= 0) {
169
+ const line = buffer.slice(0, end);
170
+ buffer = buffer.slice(end + 1);
171
+ if (!line.trim()) continue;
172
+ let message;
173
+ try {
174
+ message = JSON.parse(line);
175
+ } catch {
176
+ close();
177
+ return;
178
+ }
179
+ // Bootstrap performs no model turns or tool operations. Never auto-approve
180
+ // an unsolicited request or copy its payload to an owner-visible channel.
181
+ if (message.method && message.id !== undefined) {
182
+ child.stdin.write(
183
+ JSON.stringify({
184
+ id: message.id,
185
+ error: { code: -32601, message: "Bootstrap does not handle execution requests" },
186
+ }) + "\n",
187
+ );
188
+ continue;
189
+ }
190
+ const item = pending.get(message.id);
191
+ if (!item) continue;
192
+ pending.delete(message.id);
193
+ clearTimeout(item.timer);
194
+ if (message.error) item.reject(failure("HARNESS_UNAVAILABLE"));
195
+ else item.resolve(message.result);
196
+ }
197
+ });
198
+ const request = (method, params = {}) =>
199
+ new Promise((resolve, reject) => {
200
+ if (closed) {
201
+ reject(failure("HARNESS_UNAVAILABLE"));
202
+ return;
203
+ }
204
+ const id = ++nextId,
205
+ timer = setTimeout(() => {
206
+ pending.delete(id);
207
+ reject(failure("HARNESS_UNAVAILABLE"));
208
+ }, options.timeoutMs ?? 15000);
209
+ pending.set(id, { resolve, reject, timer });
210
+ child.stdin.write(JSON.stringify({ id, method, params }) + "\n", (error) => {
211
+ if (error) {
212
+ pending.delete(id);
213
+ clearTimeout(timer);
214
+ reject(failure("HARNESS_UNAVAILABLE"));
215
+ }
216
+ });
217
+ });
218
+ return {
219
+ request,
220
+ close,
221
+ async initialize() {
222
+ await request("initialize", {
223
+ clientInfo: { name: "totem_listener", title: "Totem listener", version: "1" },
224
+ });
225
+ child.stdin.write(JSON.stringify({ method: "initialized", params: {} }) + "\n");
226
+ },
227
+ };
228
+ }
229
+
230
+ /** No receiving begins until a durable, exact host session exists and is checked. */
231
+ export async function ensureInteractiveHarness(profile, environment = process.env, options = {}) {
232
+ if (!(await (options.enabled ?? automaticEnabled)(profile))) return profile;
233
+ let release;
234
+ try {
235
+ release = await acquireSupervisorLock(join(profile.stateDirectory, "harness-setup"));
236
+ } catch {
237
+ throw failure("HARNESS_BUSY");
238
+ }
239
+ try {
240
+ return await prepareHarness(profile, environment, options);
241
+ } finally {
242
+ await release();
243
+ }
244
+ }
245
+
246
+ async function prepareHarness(profile, environment, options) {
247
+ options.signal?.throwIfAborted();
248
+ const update = options.bindProfile ?? bindInteractiveProfile;
249
+ // Persist the requirement first. The normal supervisor must not race setup by
250
+ // starting the old, unbound conversation-only receiver after a crash.
251
+ if (!profile.interactiveRequired) profile = await update(profile, undefined, options);
252
+ if (profile.provider !== "codex") throw failure("HARNESS_UNSUPPORTED");
253
+ await (options.checkQueue ?? checkCodexQueueSupport)(environment);
254
+ const fingerprint = await profileFingerprint(profile);
255
+ const path = join(profile.stateDirectory, "harness-bootstrap.json");
256
+ let receipt = await privateRead(path).catch((error) => {
257
+ if (error.code !== "ENOENT") throw error;
258
+ });
259
+ if (
260
+ receipt &&
261
+ (receipt.version !== 1 ||
262
+ receipt.binding !== fingerprint ||
263
+ !["creating", "created", "bound"].includes(receipt.state) ||
264
+ (receipt.state !== "creating" && !UUID.test(receipt.threadId ?? "")))
265
+ )
266
+ throw failure("HARNESS_SETUP_REQUIRED");
267
+ if (profile.codexThreadId && receipt?.threadId && profile.codexThreadId !== receipt.threadId)
268
+ throw failure("HARNESS_SETUP_REQUIRED");
269
+ const rpc = (options.connect ?? connectCodexHarness)(environment, options);
270
+ try {
271
+ await rpc.initialize();
272
+ const cwd = join(profile.stateDirectory, "harness-workspace");
273
+ if (receipt?.state === "creating") {
274
+ // Recover only metadata in this identity's dedicated workspace. A lost
275
+ // creation reply never permits another thread/start or a global search.
276
+ const list = await rpc.request("thread/list", {
277
+ cwd,
278
+ archived: false,
279
+ sourceKinds: [],
280
+ limit: 2,
281
+ });
282
+ if (
283
+ list?.data?.length !== 1 ||
284
+ list.nextCursor ||
285
+ !UUID.test(list.data[0]?.id ?? "") ||
286
+ list.data[0].cwd !== cwd ||
287
+ list.data[0].ephemeral !== false
288
+ )
289
+ throw failure("HARNESS_CREATE_UNCERTAIN");
290
+ receipt = {
291
+ version: 1,
292
+ binding: fingerprint,
293
+ state: "created",
294
+ threadId: list.data[0].id,
295
+ createdLocally: true,
296
+ };
297
+ await privateWrite(path, receipt);
298
+ }
299
+ let threadId = profile.codexThreadId ?? receipt?.threadId;
300
+ if (!threadId) {
301
+ await privateDirectory(cwd);
302
+ await privateWrite(path, { version: 1, binding: fingerprint, state: "creating" });
303
+ // Omit model, sandbox and approval overrides: the owner's configured
304
+ // policy and instructions remain authoritative for the new host session.
305
+ const result = await rpc.request("thread/start", { cwd });
306
+ if (
307
+ !UUID.test(result?.thread?.id ?? "") ||
308
+ result.thread.ephemeral !== false ||
309
+ result.thread.cwd !== cwd
310
+ )
311
+ throw failure("HARNESS_CREATE_UNCERTAIN");
312
+ threadId = result.thread.id;
313
+ receipt = {
314
+ version: 1,
315
+ binding: fingerprint,
316
+ state: "created",
317
+ threadId,
318
+ createdLocally: true,
319
+ };
320
+ await privateWrite(path, receipt);
321
+ }
322
+ if (receipt?.state === "created")
323
+ await rpc.request("thread/name/set", { threadId, name: "Totem inbox" });
324
+ const checked = await rpc.request("thread/read", { threadId });
325
+ if (
326
+ checked?.thread?.id !== threadId ||
327
+ checked.thread.ephemeral !== false ||
328
+ checked.thread.archived === true ||
329
+ (receipt?.createdLocally && checked.thread.cwd !== cwd)
330
+ )
331
+ throw failure("HARNESS_SETUP_REQUIRED");
332
+ if (!profile.codexThreadId) profile = await update(profile, threadId, options);
333
+ await privateWrite(path, {
334
+ version: 1,
335
+ binding: fingerprint,
336
+ state: "bound",
337
+ threadId,
338
+ createdLocally: receipt?.createdLocally === true,
339
+ });
340
+ return profile;
341
+ } finally {
342
+ await rpc.close();
343
+ }
344
+ }
@@ -2,7 +2,7 @@ import { parseArgs } from "node:util";
2
2
  import { readFile, stat } from "node:fs/promises";
3
3
  import { privateRead, privateWrite } from "./runtime-installation.mjs";
4
4
  import { join } from "node:path";
5
- import { fileURLToPath } from "node:url";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { ensureInstalledRuntime, installRuntimeService } from "./runtime-installation.mjs";
7
7
  import { beginDeviceLogin } from "./device-login.mjs";
8
8
  import { resumeDeviceLogin } from "./device-login.mjs";
@@ -22,6 +22,16 @@ export async function activateConnection(profile, env = process.env, options = {
22
22
  ...(profile.agentTokenEnv ? { TOTEM_AGENT_TOKEN_ENV: profile.agentTokenEnv } : {}),
23
23
  });
24
24
  if (!enabled) return { state: "paused", ...service, entry: runtime.cli };
25
+ if (profile.interactiveRequired && !service.persistent)
26
+ return {
27
+ state: "needs_attention",
28
+ ready: false,
29
+ ...service,
30
+ entry: runtime.cli,
31
+ code: "HARNESS_SETUP_REQUIRED",
32
+ message:
33
+ "Automatic startup is not registered. Incoming work remains queued until host setup succeeds.",
34
+ };
25
35
  const { verifyExecutorReady } = await import("./executor-readiness.mjs");
26
36
  try {
27
37
  if (profile.codexThreadId) await (options.verifyExecutor ?? verifyExecutorReady)(profile, env);
@@ -35,11 +45,32 @@ export async function activateConnection(profile, env = process.env, options = {
35
45
  "Your connection is saved, but the selected agent could not complete its startup check. Run totem status to inspect the blocker.",
36
46
  };
37
47
  }
38
- const { agentStartMain } = await import("./agent-runner.mjs");
39
- const result = await (options.start ?? agentStartMain)(
40
- ["--profile", profile.profileName, "--background", "--json"],
41
- { env, automatic: true, stdout: { write() {} } },
42
- );
48
+ if (profile.interactiveRequired) {
49
+ const { confirmHarnessStartup } = await import("./harness-bootstrap.mjs");
50
+ await confirmHarnessStartup(profile);
51
+ }
52
+ const start =
53
+ options.start ??
54
+ (
55
+ await import(
56
+ pathToFileURL(
57
+ join(
58
+ runtime.root,
59
+ "runtime-packages",
60
+ runtime.version,
61
+ "node_modules",
62
+ "totemlayer",
63
+ "bin",
64
+ "agent-runner.mjs",
65
+ ),
66
+ )
67
+ )
68
+ ).agentStartMain;
69
+ const result = await start(["--profile", profile.profileName, "--background", "--json"], {
70
+ env,
71
+ automatic: true,
72
+ stdout: { write() {} },
73
+ });
43
74
  if (result) throw new Error("The listener could not start. Check totem status for this profile.");
44
75
  const status = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
45
76
  return {
@@ -4,6 +4,7 @@ import { Totem } from "./sdk/index.js";
4
4
  import { readAgentCredential, readAgentProfile } from "./agent-profile.mjs";
5
5
  import { runtimeRoot } from "./runtime-installation.mjs";
6
6
  import { automaticEnabled } from "./automatic-runtime.mjs";
7
+ import { ensureInteractiveHarness } from "./harness-bootstrap.mjs";
7
8
 
8
9
  function listenerBinding(profile) {
9
10
  return {
@@ -81,7 +82,6 @@ export async function ensureMcpListener(config, environment = process.env, optio
81
82
  "MCP tools are connected, but this identity has no saved listener on this computer. The owner must run totem setup with this agent's private key and selected executor once. Hosted MCP cannot install a local listener.",
82
83
  };
83
84
  const setup = options.setup ?? (await import("./agent-setup.mjs")).agentSetupMain;
84
- let output = "";
85
85
  const result = await setup(
86
86
  [
87
87
  "--profile",
@@ -100,11 +100,8 @@ export async function ensureMcpListener(config, environment = process.env, optio
100
100
  ],
101
101
  {
102
102
  env: environment,
103
- stdout: {
104
- write(chunk) {
105
- output = (output + chunk).slice(-65536);
106
- },
107
- },
103
+ deferActivation: true,
104
+ stdout: { write() {} },
108
105
  stderr: { write() {} },
109
106
  },
110
107
  );
@@ -116,9 +113,6 @@ export async function ensureMcpListener(config, environment = process.env, optio
116
113
  "MCP tools are connected. The listener setup did not confirm readiness; inspect totem status. No repository or peer permission was added.",
117
114
  };
118
115
  profile = await readAgentProfile(identity.id, { environment });
119
- const created = JSON.parse(output);
120
- if (!created.automatic) throw new Error("Listener setup did not return its startup status.");
121
- return { ...created.automatic, ...listenerBinding(profile) };
122
116
  }
123
117
  if (!(await automaticEnabled(profile)))
124
118
  return {
@@ -129,6 +123,21 @@ export async function ensureMcpListener(config, environment = process.env, optio
129
123
  "This listener was explicitly stopped. Its opt-out remains in effect; totem start can resume it.",
130
124
  };
131
125
  const activate = options.activate ?? (await import("./installation.mjs")).activateConnection;
126
+ if (["codex", "claude"].includes(profile.client ?? profile.provider)) {
127
+ try {
128
+ profile = await (options.ensureHarness ?? ensureInteractiveHarness)(profile, environment);
129
+ } catch (error) {
130
+ return {
131
+ state: "needs_attention",
132
+ ready: false,
133
+ persistent: false,
134
+ ...listenerBinding(profile),
135
+ code: error.code ?? "HARNESS_SETUP_REQUIRED",
136
+ message:
137
+ "The owner-bound interactive harness has not passed setup. Incoming work remains queued; no text-only receiver was started.",
138
+ };
139
+ }
140
+ }
132
141
  const status = await activate(profile, environment);
133
142
  return { ...status, ...listenerBinding(profile) };
134
143
  }
@@ -149,14 +158,26 @@ export async function readMcpListenerStatus(config, environment, initial, option
149
158
  throw new Error("Listener binding changed");
150
159
  const status = { ...initial, ready: false, checkedAt: new Date().toISOString() };
151
160
  if (!(await automaticEnabled(profile))) return { ...status, state: "paused" };
161
+ if (initial.state === "needs_attention" && initial.code?.startsWith("HARNESS_")) return status;
152
162
  const readStatus =
153
163
  options.readStatus ?? (await import("./agent-runner.mjs")).readAgentRuntimeStatus;
154
164
  const current = await readStatus(profile);
155
165
  return {
156
166
  ...status,
157
167
  ...current,
158
- state: !current.running ? "offline" : current.ready ? "ready" : "needs_attention",
159
- ready: Boolean(current.running && current.ready),
168
+ state: !current.running
169
+ ? "offline"
170
+ : current.ready &&
171
+ (!profile.interactiveRequired || profile.codexThreadId) &&
172
+ initial.persistent
173
+ ? "ready"
174
+ : "needs_attention",
175
+ ready: Boolean(
176
+ current.running &&
177
+ current.ready &&
178
+ initial.persistent &&
179
+ (!profile.interactiveRequired || profile.codexThreadId),
180
+ ),
160
181
  };
161
182
  } catch {
162
183
  return {
package/bin/mcp.mjs CHANGED
@@ -7,7 +7,7 @@ import { Totem } from "./sdk/index.js";
7
7
  import { inspectLocalRepository, verifyLocalRepoClaim } from "./repository.mjs";
8
8
  import { createApprovalReview } from "./mcp-approval-review.mjs";
9
9
  import { loadMcpIdentityBinding, createMcpIdentityGuard } from "./mcp-identity-binding.mjs";
10
- const VERSION = "0.1.41";
10
+ const VERSION = "0.1.44";
11
11
  const MAX_KEY_BYTES = 8 * 1024;
12
12
  const MAX_ATTACHMENT_BYTES = 2 * 1024 * 1024;
13
13
  const MAX_TOOL_INPUT_ELEMENTS = 10_000;
@@ -12,6 +12,7 @@ import { readAgentCredential, readAgentProfile } from "./agent-profile.mjs";
12
12
  import { automaticEnabled, acquireSupervisorLock } from "./automatic-runtime.mjs";
13
13
  import { readAgentRuntimeStatus } from "./agent-runner.mjs";
14
14
  import { runtimeErrorCode } from "./runtime-diagnostics.mjs";
15
+ import { ensureInteractiveHarness, pendingHarnessMigration } from "./harness-bootstrap.mjs";
15
16
  import {
16
17
  runtimeRoot,
17
18
  privateDirectory,
@@ -97,6 +98,8 @@ export function createPluginConnector(selection, options = {}) {
97
98
  setup,
98
99
  registered = false;
99
100
  let state = { state: "connecting", ready: false, persistent: false };
101
+ let harnessBlocked;
102
+ const migratedProfiles = new WeakSet();
100
103
  const readProfile = options.readProfile ?? readAgentProfile;
101
104
  const readCredential = options.readCredential ?? readAgentCredential;
102
105
  const readStatus = options.readStatus ?? readAgentRuntimeStatus;
@@ -166,15 +169,24 @@ export function createPluginConnector(selection, options = {}) {
166
169
  throw unavailable();
167
170
  }
168
171
  async function pinProfile(profile) {
169
- const { value, key } = await profileBinding(profile);
172
+ const handedOff = profile;
173
+ if (migratedProfiles.has(profile)) profile = await verifyBinding();
174
+ const initial = await profileBinding(profile);
175
+ const key = initial.key;
170
176
  await verifyIdentity(profile, key);
171
- if (binding && !same(binding, value)) throw unavailable();
177
+ const resumable = async (old) =>
178
+ old.codexThreadId === null &&
179
+ same({ ...old, codexThreadId: initial.value.codexThreadId }, initial.value) &&
180
+ (await pendingHarnessMigration(profile));
181
+ if (binding && !same(binding, initial.value) && !(await resumable(binding)))
182
+ throw unavailable();
172
183
  const disk = await privateRead(bindingPath).catch((error) => {
173
184
  if (error.code !== "ENOENT") throw error;
174
185
  });
175
- if (disk && !same(assertBinding(disk), value)) throw unavailable();
176
- if (!disk) await privateWrite(bindingPath, value);
177
- binding = value;
186
+ if (disk && !same(assertBinding(disk), initial.value) && !(await resumable(disk)))
187
+ throw unavailable();
188
+ if (!disk || !same(disk, initial.value)) await privateWrite(bindingPath, initial.value);
189
+ binding = initial.value;
178
190
  apiKey = key;
179
191
  if (!registered) {
180
192
  createTotemMcpServer(
@@ -189,6 +201,17 @@ export function createPluginConnector(selection, options = {}) {
189
201
  );
190
202
  registered = true;
191
203
  }
204
+ // Check/prepare the actual host before starting any background consumer.
205
+ // This migration cannot change identity, keys, repository scopes or policy.
206
+ profile = await (options.ensureHarness ?? ensureInteractiveHarness)(profile, env, {
207
+ signal: controller.signal,
208
+ });
209
+ const { value } = await profileBinding(profile);
210
+ if (!same({ ...initial.value, codexThreadId: value.codexThreadId }, value)) throw unavailable();
211
+ if (!disk || !same(disk, value)) await privateWrite(bindingPath, value);
212
+ binding = value;
213
+ apiKey = key;
214
+ if (handedOff !== profile) migratedProfiles.add(handedOff);
192
215
  state = { state: "starting", ready: false, persistent: false };
193
216
  }
194
217
  function activationStatus(value) {
@@ -209,14 +232,22 @@ export function createPluginConnector(selection, options = {}) {
209
232
  state = { ...state };
210
233
  delete state.code;
211
234
  if (!(await isEnabled(profile))) state = { ...state, state: "paused", ready: false };
235
+ else if (harnessBlocked)
236
+ state = { ...state, state: "needs_attention", ready: false, code: harnessBlocked };
212
237
  else if (inherited.TOTEM_AUTO_START === "0" || inherited.CI)
213
238
  state = { ...state, state: "disabled_by_request", ready: false };
214
239
  else {
215
240
  const current = await readStatus(profile);
216
241
  state = {
217
242
  ...state,
218
- state: current.running ? (current.ready ? "ready" : "needs_attention") : "offline",
219
- ready: Boolean(current.running && current.ready),
243
+ state: !current.running
244
+ ? "offline"
245
+ : current.ready && binding.codexThreadId && state.persistent
246
+ ? "ready"
247
+ : "needs_attention",
248
+ ready: Boolean(
249
+ current.running && current.ready && binding.codexThreadId && state.persistent,
250
+ ),
220
251
  ...(current.errorCode && !current.ready
221
252
  ? { code: runtimeErrorCode({ code: current.errorCode }) }
222
253
  : {}),
@@ -246,7 +277,12 @@ export function createPluginConnector(selection, options = {}) {
246
277
  : "separate_restricted_runtime",
247
278
  }
248
279
  : {}),
249
- currentChatWakeVerified: false,
280
+ currentChatWakeVerified: Boolean(
281
+ binding?.codexThreadId &&
282
+ state.ready &&
283
+ inherited.CODEX_THREAD_ID === binding.codexThreadId,
284
+ ),
285
+ boundChatWakeVerified: Boolean(binding?.codexThreadId && state.ready),
250
286
  message:
251
287
  state.state === "sign_in_required"
252
288
  ? "Finish the owner authorization in the opened browser. This installs the listener for the selected identity."
@@ -254,9 +290,11 @@ export function createPluginConnector(selection, options = {}) {
254
290
  ? "This listener was explicitly paused. The plugin preserves that choice."
255
291
  : state.ready && !state.persistent
256
292
  ? "The listener can respond now, but automatic startup at login is not configured on this host. Check its service manager or host permissions before relying on restart recovery."
257
- : state.ready
258
- ? "The saved listener passed receive, wake and reply verification. This does not verify delivery into the current chat or grant new work permissions."
259
- : "MCP access is separate from listener readiness. Check the selected client's sign-in and this connection's setup; no identity or permissions were replaced.",
293
+ : harnessBlocked
294
+ ? "Interactive harness setup is incomplete. Incoming work remains queued until the owner-bound chat and its host tools pass readiness. Check this client's supported session integration; no restricted responder was substituted."
295
+ : state.ready
296
+ ? "The owner-bound chat passed receive, wake and reply verification. Its existing host permissions still control every action."
297
+ : "MCP access is separate from listener readiness. Check the selected client's sign-in and this connection's setup; no identity or permissions were replaced.",
260
298
  };
261
299
  }
262
300
  server.registerTool(
@@ -304,10 +342,10 @@ export function createPluginConnector(selection, options = {}) {
304
342
  if (saved) {
305
343
  binding = assertBinding(saved);
306
344
  const profile = await readProfile(binding.profile, { environment: env });
307
- const current = await profileBinding(profile);
308
- if (!same(binding, current.value)) throw unavailable();
309
345
  await pinProfile(profile);
310
- state = activationStatus(await (options.activate ?? activateConnection)(profile, env));
346
+ state = activationStatus(
347
+ await (options.activate ?? activateConnection)(await verifyBinding(), env),
348
+ );
311
349
  } else {
312
350
  let partial = "";
313
351
  await (options.install ?? installationMain)(
@@ -329,7 +367,7 @@ export function createPluginConnector(selection, options = {}) {
329
367
  installService: async () => ({ persistent: false }),
330
368
  onProfile: pinProfile,
331
369
  activateConnection: async (profile) => {
332
- await verifyBinding();
370
+ profile = await verifyBinding();
333
371
  state = activationStatus(
334
372
  await (options.activate ?? activateConnection)(profile, env),
335
373
  );
@@ -377,9 +415,21 @@ export function createPluginConnector(selection, options = {}) {
377
415
  };
378
416
  }
379
417
  } catch (error) {
418
+ harnessBlocked = [
419
+ "HARNESS_SETUP_REQUIRED",
420
+ "HARNESS_UNSUPPORTED",
421
+ "HARNESS_UNAVAILABLE",
422
+ "HARNESS_BUSY",
423
+ "HARNESS_CREATE_UNCERTAIN",
424
+ "AGENT_CLI_INCOMPATIBLE",
425
+ ].includes(error.code)
426
+ ? error.code
427
+ : undefined;
380
428
  state = {
381
429
  state: "needs_attention",
382
- code: error.code === "PLUGIN_BINDING_CHANGED" ? error.code : "CONNECTION_SETUP_REQUIRED",
430
+ code:
431
+ harnessBlocked ??
432
+ (error.code === "PLUGIN_BINDING_CHANGED" ? error.code : "CONNECTION_SETUP_REQUIRED"),
383
433
  ready: false,
384
434
  persistent: false,
385
435
  };
@@ -54,6 +54,11 @@ export const RUNTIME_ERROR_CODES = new Set([
54
54
  "INBOX_CAPACITY_REACHED",
55
55
  "WORK_RATE_LIMITED",
56
56
  "INTERNAL_ERROR",
57
+ "HARNESS_SETUP_REQUIRED",
58
+ "HARNESS_UNSUPPORTED",
59
+ "HARNESS_UNAVAILABLE",
60
+ "HARNESS_BUSY",
61
+ "HARNESS_CREATE_UNCERTAIN",
57
62
  ]);
58
63
  export function runtimeErrorCode(error) {
59
64
  const code = MISSION_ERROR_CODES.get(error?.code) ?? error?.code;
@@ -72,6 +72,14 @@ export async function ensureInstalledRuntime(env = process.env) {
72
72
  const modules = join(root, "runtime-packages", metadata.version, "node_modules");
73
73
  const destination = join(modules, "totemlayer");
74
74
  await privateDirectory(runtime);
75
+ // Installation and service activation can happen in separate MCP callbacks.
76
+ // Retain the upgrade until registration has actually reloaded the supervisor.
77
+ const refreshPath = join(runtime, "service-refresh.json");
78
+ if (previousVersion && previousVersion !== metadata.version)
79
+ await privateWrite(refreshPath, { version: metadata.version });
80
+ const refresh = await privateRead(refreshPath).catch((error) => {
81
+ if (error.code !== "ENOENT") throw error;
82
+ });
75
83
  await privateDirectory(modules);
76
84
  let exists = false;
77
85
  try {
@@ -204,7 +212,7 @@ if (process.env.TOTEM_RUNTIME_ENV_LOADED === entry) {
204
212
  supervisor,
205
213
  directory: runtime,
206
214
  version: metadata.version,
207
- upgraded: Boolean(previousVersion && previousVersion !== metadata.version),
215
+ upgraded: refresh?.version === metadata.version,
208
216
  };
209
217
  }
210
218
  const xml = (value) =>
@@ -293,23 +301,51 @@ export async function installRuntimeService(runtime, env = process.env, options
293
301
  const run = options.execute ?? execute;
294
302
  const node = process.execPath;
295
303
  const home = env.HOME ?? env.USERPROFILE ?? homedir();
296
- if (runtime.upgraded && platform !== "win32") {
304
+ const acknowledged = async (result) => {
305
+ if (runtime.version)
306
+ await privateWrite(join(runtime.directory, "service-refresh.json"), { version: null });
307
+ return result;
308
+ };
309
+ if (runtime.upgraded) {
297
310
  try {
298
311
  const status = await privateRead(join(runtime.directory, "supervisor.json"));
299
- const { readSupervisorOwner } = await import("./automatic-runtime.mjs");
312
+ const { readSupervisorOwner, processIdentity } = await import("./automatic-runtime.mjs");
300
313
  const owner = (await readSupervisorOwner(join(runtime.directory, "supervisor.lock")))?.owner;
301
- if (Number.isSafeInteger(status.pid) && status.pid > 1 && owner?.pid === status.pid) {
302
- const processInfo = await run("ps", ["-p", String(status.pid), "-o", "args="], {
314
+ if (
315
+ Number.isSafeInteger(status.pid) &&
316
+ status.pid > 1 &&
317
+ owner?.pid === status.pid &&
318
+ owner.identity &&
319
+ (await (options.processIdentity ?? processIdentity)(status.pid)) === owner.identity
320
+ ) {
321
+ const invocation =
322
+ platform === "win32"
323
+ ? powershellInvocation(
324
+ `(Get-CimInstance Win32_Process -Filter 'ProcessId=${status.pid}').CommandLine`,
325
+ env,
326
+ )
327
+ : { command: "ps", args: ["-p", String(status.pid), "-o", "args="] };
328
+ const processInfo = await run(invocation.command, invocation.args, {
303
329
  encoding: "utf8",
304
330
  timeout: 3000,
305
331
  maxBuffer: 8192,
306
332
  });
307
- if (processInfo.stdout.trim() === `${node} ${runtime.supervisor}`) {
308
- process.kill(status.pid, "SIGTERM");
333
+ const args = [...processInfo.stdout.trim().matchAll(/"([^"]*)"|(\S+)/g)].map(
334
+ (match) => match[1] ?? match[2],
335
+ );
336
+ const exact =
337
+ platform === "win32"
338
+ ? args.length === 2 &&
339
+ args[0].toLowerCase() === node.toLowerCase() &&
340
+ args[1].toLowerCase() === runtime.supervisor.toLowerCase()
341
+ : processInfo.stdout.trim() === `${node} ${runtime.supervisor}`;
342
+ if (exact) {
343
+ const signal = options.processSignal ?? process.kill;
344
+ signal(status.pid, "SIGTERM");
309
345
  const deadline = Date.now() + 20000;
310
346
  while (Date.now() < deadline) {
311
347
  try {
312
- process.kill(status.pid, 0);
348
+ signal(status.pid, 0);
313
349
  } catch (error) {
314
350
  if (error.code === "ESRCH") break;
315
351
  throw error;
@@ -424,12 +460,17 @@ export async function installRuntimeService(runtime, env = process.env, options
424
460
  startupCode = "STARTUP_DISABLED";
425
461
  throw new Error("Automatic startup was disabled by the owner.");
426
462
  }
427
- // Preserve an already running supervisor; its launcher uses current.json after restart.
463
+ // Restart the registered entry in place. Unregister/bootstrap races with
464
+ // launchd's own KeepAlive recovery and can temporarily lose registration.
428
465
  try {
429
- await run("launchctl", ["print", `${domain}/${label}`]);
466
+ const loaded = await run("launchctl", ["print", `${domain}/${label}`]);
467
+ if (
468
+ !loaded.stdout.includes(`program = ${node}`) ||
469
+ !loaded.stdout.includes(runtime.supervisor)
470
+ )
471
+ throw new Error("The loaded launch agent does not match this runtime.");
430
472
  if (runtime.upgraded) {
431
- await run("launchctl", ["bootout", `${domain}/${label}`]);
432
- await run("launchctl", ["bootstrap", domain, path]);
473
+ await run("launchctl", ["kickstart", "-k", `${domain}/${label}`]);
433
474
  }
434
475
  } catch {
435
476
  await run("launchctl", ["bootstrap", domain, path]);
@@ -441,7 +482,7 @@ export async function installRuntimeService(runtime, env = process.env, options
441
482
  !verified.stdout.includes(runtime.supervisor)
442
483
  )
443
484
  throw new Error("The loaded launch agent does not match this runtime.");
444
- return { mode: "launchd", persistent: true, label, path };
485
+ return acknowledged({ mode: "launchd", persistent: true, label, path });
445
486
  }
446
487
  if (platform === "linux") {
447
488
  const path = join(home, ".config", "systemd", "user", unit);
@@ -462,12 +503,12 @@ export async function installRuntimeService(runtime, env = process.env, options
462
503
  await run("systemctl", ["--user", "daemon-reload"]);
463
504
  await run("systemctl", ["--user", "enable", "--now", unit]);
464
505
  if (runtime.upgraded) await run("systemctl", ["--user", "restart", unit]);
465
- return { mode: "systemd-user", persistent: true, label: unit, path };
506
+ return acknowledged({ mode: "systemd-user", persistent: true, label: unit, path });
466
507
  }
467
508
  if (platform === "win32") {
468
509
  windowsRegistration = await windowsStartup(runtime, node, task, env, run);
469
510
  if (windowsRegistration.mode === "scheduled-task")
470
- return { mode: "scheduled-task", persistent: true, label: task };
511
+ return acknowledged({ mode: "scheduled-task", persistent: true, label: task });
471
512
  }
472
513
  } catch {
473
514
  /* Ephemeral/headless hosts often have no user service manager. */
@@ -481,7 +522,7 @@ export async function installRuntimeService(runtime, env = process.env, options
481
522
  child.on("error", () => {});
482
523
  child.unref();
483
524
  return windowsRegistration?.mode === "startup-shortcut"
484
- ? { mode: "startup-shortcut", persistent: true, label: task }
525
+ ? acknowledged({ mode: "startup-shortcut", persistent: true, label: task })
485
526
  : {
486
527
  mode: "background-supervisor",
487
528
  persistent: false,
package/bin/totem.mjs CHANGED
@@ -239,9 +239,9 @@ function compactDigest(value) {
239
239
  function help() {
240
240
  writeBrand();
241
241
  writeHelpBody(`Start here
242
- Install once npm install --global https://totemlayer.com/downloads/totemlayer-0.1.41.tgz
242
+ Install once npm install --global https://totemlayer.com/downloads/totemlayer-0.1.44.tgz
243
243
  Confirm installation totem --version
244
- Run one-off npx --yes --package https://totemlayer.com/downloads/totemlayer-0.1.41.tgz totem --help
244
+ Run one-off npx --yes --package https://totemlayer.com/downloads/totemlayer-0.1.44.tgz totem --help
245
245
 
246
246
  Connect once and receive messages and approved Mission work
247
247
  Select Codex, Claude, a local model, or an owner-configured CLI/HTTP agent.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totemlayer",
3
- "version": "0.1.41",
3
+ "version": "0.1.44",
4
4
  "description": "Cross-platform CLI for TotemLayer agent identity and authorization",
5
5
  "type": "module",
6
6
  "bin": {