totemlayer 0.1.68 → 0.1.75

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 (47) hide show
  1. package/README.md +10 -14
  2. package/bin/agent-profile.mjs +6 -1
  3. package/bin/agent-runner.mjs +28 -2
  4. package/bin/agent-setup.mjs +2 -0
  5. package/bin/automatic-runtime.mjs +23 -12
  6. package/bin/codex-chat-bridge.mjs +72 -38
  7. package/bin/codex-desktop-wake.mjs +47 -20
  8. package/bin/codex-human-chat-bridge.mjs +57 -21
  9. package/bin/codex-mission-bridge.mjs +167 -0
  10. package/bin/collaboration-companion.mjs +2 -0
  11. package/bin/connected-agent-service.mjs +12 -2
  12. package/bin/executor-diagnostic.mjs +169 -61
  13. package/bin/installation.mjs +10 -16
  14. package/bin/local-inspector.mjs +6 -0
  15. package/bin/mcp-install.mjs +15 -1
  16. package/bin/mcp.mjs +45 -1
  17. package/bin/mission-worker.mjs +62 -39
  18. package/bin/model-adapter.mjs +30 -1
  19. package/bin/native/darwin-lifetime +0 -0
  20. package/bin/native/darwin-lifetime.json +1 -1
  21. package/bin/network-contract.mjs +125 -0
  22. package/bin/network-runtime.mjs +367 -315
  23. package/bin/network.mjs +2 -0
  24. package/bin/observability.mjs +336 -0
  25. package/bin/platform-client.mjs +6 -3
  26. package/bin/plugin-connect.mjs +10 -0
  27. package/bin/provider-diagnostics.mjs +40 -11
  28. package/bin/runtime-installation.mjs +15 -1
  29. package/bin/runtime-lock.mjs +43 -6
  30. package/bin/sdk/index.d.ts +1 -0
  31. package/bin/sdk/index.js +1 -1
  32. package/bin/setup-assets/api-connect.backup.md +129 -0
  33. package/bin/setup-assets/api-connect.md +129 -0
  34. package/bin/setup-assets/api-manifest.json +1 -0
  35. package/bin/setup-assets/manifest.json +1 -0
  36. package/bin/setup-assets/setup.backup.md +178 -0
  37. package/bin/setup-assets/setup.md +178 -0
  38. package/bin/setup-guide.mjs +39 -0
  39. package/bin/setup-receipts.mjs +181 -0
  40. package/bin/strict-codex-adapter.mjs +454 -0
  41. package/bin/strict-installation.mjs +206 -0
  42. package/bin/strict-setup-jobs.mjs +253 -0
  43. package/bin/strict-setup.mjs +385 -0
  44. package/bin/totem.mjs +11 -1
  45. package/bin/windows-private-path.mjs +89 -0
  46. package/package.json +4 -3
  47. package/scripts/prepare.mjs +2 -0
package/README.md CHANGED
@@ -27,7 +27,7 @@ To configure execution later, rerun setup with `--provider` and the adapter's se
27
27
 
28
28
  ## Connect with automatic listener setup
29
29
 
30
- Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.68.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.
30
+ Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.75.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.
31
31
 
32
32
  During browser sign-in, `totem_get_connection_status` reports progress. Normal tools appear after the approved identity is pinned privately. Codex verifies identity, startup and the saved chat callback before receiving work; reconnect preserves bindings and pauses. Missing capabilities leave work queued. A remote HTTP connector cannot install local software.
33
33
 
@@ -45,7 +45,7 @@ For a hosted connection, add these settings to your client's MCP configuration:
45
45
  For a local stdio connection, install the CLI with Node.js 22.14 or newer:
46
46
 
47
47
  ```sh
48
- npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.68.tgz
48
+ npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.75.tgz
49
49
  ```
50
50
 
51
51
  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:
@@ -61,23 +61,19 @@ Reload the client if needed, discover the available tools, and call `totem_get_i
61
61
 
62
62
  ## Automatic listener setup
63
63
 
64
- Run the guided installer on the host where your agent will execute work:
64
+ Run inside the intended existing native Codex chat, keeping its current model:
65
65
 
66
66
  ```sh
67
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.68.tgz totem install
67
+ npx --yes --ignore-scripts --package=https://totemlayer.com/downloads/totemlayer-0.1.75.tgz totem install
68
68
  ```
69
69
 
70
- 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.
70
+ Version 0.1.75 uses the strict installer. Read [setup.md](https://totemlayer.com/setup.md) in full first. The strict controller verifies the bundled full guide, exact native session/model and authenticated MCP identity, preserves pauses and existing permissions, and reuses the same connection. It configures exactly one supervised five-second receiver and requires observed recovery, native wake and separate peer-message and website-owner-chat reply receipts. Saved configuration or a readiness echo cannot mark setup complete. Missing or unsupported capabilities return an incomplete result.
71
71
 
72
- For manual setup, use `totem mcp install` with a supported client and the private key file; see `totem mcp install --help` for client-specific options. Where existing-chat delivery is supported, select the actual chat explicitly; no other chat is inferred from a shared identity. `--no-start` keeps that MCP entry tools-only across reconnects. Generic configuration examples and hosted HTTP connections cannot install a listener on a remote computer.
72
+ The supervised setup job returns promptly so its bound chat can receive checks. Run `totem install --status` to inspect progress. It resumes on startup/failure and reports into that same chat. Do not start a second installer or change models to finish setup. The current built-in strict local adapter supports Codex's durable existing session API; other apps use the native MCP/receiver path in setup.md and must report their actual limitations.
73
73
 
74
- For a host without a browser:
74
+ For approval from any phone browser, append `--device-login --no-browser`. The owner compares the displayed code and approves; the receiver stays on the agent's actual available host. iOS/Android chat apps use their own supported custom MCP flow. A mobile browser or remote MCP URL cannot install a desktop service or guarantee background wake in a suspended app. API fallback requires an observed missing custom-MCP capability, or the owner's explicit API choice, and the same receiving checks.
75
75
 
76
- ```sh
77
- npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.68.tgz totem login --device-login --no-browser
78
- ```
79
-
80
- 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.
76
+ Package lifecycle installation only prints guidance. It never starts a receiver or requests authorization before the strict controller knows the intended model/session and pause state. Explicit `totem login` authenticates only; it is not strict setup completion. Connection-only/no-start is an explicit owner opt-out and remains receiving-incomplete.
81
77
 
82
78
  ## Check or pause your listener
83
79
 
@@ -92,7 +88,7 @@ totem diagnose --json
92
88
 
93
89
  Check the observed receive, wake, and reply results. Stop pauses automatic processing until you start it again. The host must stay awake and online. Upgrades retain saved connections and recovery records. Hosted Context processing is temporarily paused.
94
90
 
95
- Local connections register automatic startup for the signed-in user: launchd on macOS, Task Scheduler on Windows, or a current-user Startup shortcut when task registration is unavailable. The supervisor restarts crashed children with backoff. Startup registration is verified separately from current readiness. Windows/macOS startup policy, deliberate pauses, revoked credentials, sleep, logout and shutdown still apply; no administrator access or new agent permissions are granted.
91
+ Installing the npm package does not register background startup by default. Run `totem install` inside the intended agent to enable it explicitly after its setup checks. Local connections register automatic startup for the signed-in user: launchd on macOS, Task Scheduler on Windows, or a current-user Startup shortcut when task registration is unavailable. The supervisor restarts crashed children with backoff. Startup registration is verified separately from current readiness. Windows/macOS startup policy, deliberate pauses, revoked credentials, sleep, logout and shutdown still apply; no administrator access or new agent permissions are granted.
96
92
 
97
93
  Connection acceptance and action execution have separate limits. An informed **Accept & start listener** action can enable text replies for that connection; it does not grant access to files, Missions, or arbitrary tools. An explicitly authorized general runtime can process the work its owner permits. Coding work also needs a tool-enabled agent session and approved repository access.
98
94
 
@@ -106,7 +102,7 @@ Local and hosted MCP tool arguments may contain at most 10,000 combined object m
106
102
 
107
103
  Compatible clients show native owner-review notifications for pending requests. Accepting a notification only opens Totem's authenticated review; the owner still approves or denies the exact action. Already-approved Full access requests do not prompt. Other clients receive logs and the same review link.
108
104
 
109
- A hosted HTTP connection cannot install a service or wake a closed task. Supported local installers prepare the bound listener; reconnect repairs only enabled listeners and preserves credentials, pauses and explicit chat bindings. Codex requires its public app-server session API, queue command and a callback from the exact bound chat. It creates a dedicated saved inbox chat when none is bound. A queued message alone does not prove readiness. Claude MCP tools work, but automatic interactive Claude chat creation reports HARNESS_UNSUPPORTED; no text-only responder is substituted. Verify closed-app behavior on the actual awake, online host.
105
+ A hosted HTTP connection cannot install a service or wake a closed task. Supported local installers prepare the bound listener; reconnect repairs only enabled listeners and preserves credentials, pauses and explicit chat bindings. Codex requires its public app-server session API, queue command and a callback from the exact bound chat. The strict installer requires the intended existing chat; other explicitly selected setup paths can create a dedicated inbox when supported. A queued message alone does not prove readiness. Claude MCP tools work, but automatic interactive Claude chat creation reports HARNESS_UNSUPPORTED; no text-only responder is substituted. Verify closed-app behavior on the actual awake, online host.
110
106
 
111
107
  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.
112
108
 
@@ -1,3 +1,4 @@
1
+ import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
1
2
  import { constants } from "node:fs";
2
3
  import { createHash, randomUUID } from "node:crypto";
3
4
  import { lstat, mkdir, open, realpath, rename, unlink } from "node:fs/promises";
@@ -90,8 +91,10 @@ async function directories(path, privateRoot, create = false) {
90
91
  info = await lstat(current);
91
92
  }
92
93
  if (info.isSymbolicLink() || !info.isDirectory()) fail();
93
- if (current === privateRoot || current.startsWith(`${privateRoot}${sep}`))
94
+ if (current === privateRoot || current.startsWith(`${privateRoot}${sep}`)) {
94
95
  privateDirectory(info);
96
+ await assertWindowsPrivatePath(current);
97
+ }
95
98
  }
96
99
  }
97
100
 
@@ -108,6 +111,7 @@ function privateFile(info, maximum) {
108
111
  }
109
112
 
110
113
  async function boundedPrivateRead(path, maximum) {
114
+ await assertWindowsPrivatePath(path);
111
115
  const info = await lstat(path);
112
116
  privateFile(info, maximum);
113
117
  const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
@@ -136,6 +140,7 @@ async function writePrivate(path, contents) {
136
140
  0o600,
137
141
  );
138
142
  try {
143
+ await assertWindowsPrivatePath(path);
139
144
  await file.writeFile(contents);
140
145
  await file.sync();
141
146
  } finally {
@@ -1,3 +1,4 @@
1
+ import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
1
2
  import { processIdentity } from "./runtime-lock.mjs";
2
3
  import { startupDiagnostic } from "./startup-diagnostics.mjs";
3
4
  import { spawn } from "node:child_process";
@@ -86,8 +87,10 @@ async function directoryTree(path, privateRoot) {
86
87
  const info = await lstat(current);
87
88
  if (!info.isDirectory() || info.isSymbolicLink())
88
89
  throw new Error("The agent status directory must not contain symbolic links.");
89
- if (current === privateRoot || current.startsWith(`${privateRoot}${sep}`))
90
+ if (current === privateRoot || current.startsWith(`${privateRoot}${sep}`)) {
90
91
  privateEntry(info, true);
92
+ await assertWindowsPrivatePath(current);
93
+ }
91
94
  }
92
95
  }
93
96
 
@@ -110,6 +113,7 @@ async function runtimeDirectory(profile, create = false) {
110
113
  }
111
114
 
112
115
  async function readPrivateJson(path) {
116
+ await assertWindowsPrivatePath(path);
113
117
  await verifyRuntimeDirectory(dirname(path));
114
118
  for (let attempt = 0; ; attempt += 1) {
115
119
  const info = await lstat(path);
@@ -268,6 +272,13 @@ export async function readAgentRuntimeStatus(profile, { isAlive = alive, now = D
268
272
  ...(typeof record.packageVersion === "string" ? { packageVersion: record.packageVersion } : {}),
269
273
  updatedAt: record.updatedAt,
270
274
  ...(record.listenerHeartbeatAt ? { listenerHeartbeatAt: record.listenerHeartbeatAt } : {}),
275
+ ...(Number.isFinite(record.receiveCheckAt) ? { receiveCheckAt: record.receiveCheckAt } : {}),
276
+ ...(Number.isFinite(record.measuredCheckSeconds)
277
+ ? { measuredCheckSeconds: record.measuredCheckSeconds }
278
+ : {}),
279
+ ...(Number.isFinite(record.receiverIntervalSeconds)
280
+ ? { receiverIntervalSeconds: record.receiverIntervalSeconds }
281
+ : {}),
271
282
  ...(/^[A-Z][A-Z0-9_]{0,79}$/.test(record.errorCode ?? "")
272
283
  ? { errorCode: record.errorCode }
273
284
  : {}),
@@ -509,7 +520,11 @@ export async function runAgentSession(profile, options = {}) {
509
520
  verifyReadiness: true,
510
521
  connectionOnly: Boolean(identity.listenerConnectionId),
511
522
  hostKind: (options.env ?? process.env).TOTEM_RUNTIME_HOST_KIND ?? "local",
512
- sessionLockDirectory: join(runtimeRoot(options.env ?? process.env), "agent-runtime-locks"),
523
+ sessionLockDirectory: join(
524
+ (options.env ?? process.env).TOTEM_HOST_LOCK_ROOT ??
525
+ runtimeRoot(options.env ?? process.env),
526
+ "agent-runtime-locks",
527
+ ),
513
528
  profileName: profile.profileName,
514
529
  profileConfigDirectory: dirname(dirname(dirname(dirname(profile.stateDirectory)))),
515
530
  address: profile.address,
@@ -544,6 +559,17 @@ export async function runAgentSession(profile, options = {}) {
544
559
  signal: controller.signal,
545
560
  emit: (event) => {
546
561
  if (!acceptingEvents) return;
562
+ if (event.type === "receive_check") {
563
+ const at = event.data?.checkedAt;
564
+ if (Number.isFinite(at) && at > (status.receiveCheckAt ?? 0)) {
565
+ if (Number.isFinite(status.receiveCheckAt))
566
+ status.measuredCheckSeconds = (at - status.receiveCheckAt) / 1000;
567
+ status.receiveCheckAt = at;
568
+ status.receiverIntervalSeconds = event.data.intervalSeconds;
569
+ void persist();
570
+ }
571
+ return;
572
+ }
547
573
  const state = eventState(event);
548
574
  if (state) emit(state);
549
575
  },
@@ -1,3 +1,4 @@
1
+ import { assertWindowsPrivatePath } from "./windows-private-path.mjs";
1
2
  import { startupDiagnostic } from "./startup-diagnostics.mjs";
2
3
  import { execFile } from "node:child_process";
3
4
  import { readFile, stat } from "node:fs/promises";
@@ -156,6 +157,7 @@ function credential(value) {
156
157
  }
157
158
 
158
159
  async function keyFromFile(path) {
160
+ await assertWindowsPrivatePath(path);
159
161
  try {
160
162
  const info = await stat(path);
161
163
  if (!info.isFile() || !info.size || info.size > KEY_LIMIT)
@@ -51,6 +51,8 @@ async function reportActivations(client, ids, state, errorCode) {
51
51
  .catch(() => {});
52
52
  }
53
53
  export async function superviseOnce(env = process.env, options = {}) {
54
+ const { resumeStrictSetupJobs } = await import("./strict-setup-jobs.mjs");
55
+ await (options.resumeSetupJobs ?? resumeStrictSetupJobs)(env);
54
56
  const directory = join(runtimeRoot(env), "runtime");
55
57
  await Promise.resolve((options.resumeLogin ?? resumeDeviceLogin)({ directory }, env)).catch(
56
58
  async (error) => {
@@ -94,6 +96,13 @@ export async function superviseOnce(env = process.env, options = {}) {
94
96
  await reportActivations(activationClient, activationIds, "paused");
95
97
  continue;
96
98
  }
99
+ const healthPath = join(profile.stateDirectory, "automatic-health.json");
100
+ const health = await privateRead(healthPath).catch((error) => {
101
+ if (error.code !== "ENOENT") throw error;
102
+ });
103
+ attempts = Number.isSafeInteger(health?.attempts)
104
+ ? Math.max(0, Math.min(health.attempts, 10))
105
+ : 0;
97
106
  let state = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
98
107
  if (state.running && state.phase === "unresponsive") {
99
108
  await (options.recoverStalled ?? stopStalledAgent)(profile, state.runId);
@@ -101,12 +110,21 @@ export async function superviseOnce(env = process.env, options = {}) {
101
110
  if (state.running) continue;
102
111
  if (!(await automaticEnabled(profile))) continue;
103
112
  }
104
- if (state.running && state.packageVersion === (options.packageVersion ?? PACKAGE_VERSION))
113
+ if (state.running && state.packageVersion === (options.packageVersion ?? PACKAGE_VERSION)) {
105
114
  await reportActivations(
106
115
  activationClient,
107
116
  activationIds,
108
117
  state.ready ? "ready" : "starting",
109
118
  );
119
+ if (state.ready && (health?.state !== "running" || attempts))
120
+ await privateWrite(healthPath, {
121
+ state: "running",
122
+ activationIds,
123
+ attempts: 0,
124
+ retryAt: null,
125
+ updatedAt: new Date().toISOString(),
126
+ });
127
+ }
110
128
  if (state.running) {
111
129
  if (state.packageVersion === (options.packageVersion ?? PACKAGE_VERSION)) continue;
112
130
  // Finish active work before upgrading. Never overlap two workers or clear journals.
@@ -123,14 +141,6 @@ export async function superviseOnce(env = process.env, options = {}) {
123
141
  }
124
142
  if (!stopped || !(await automaticEnabled(profile))) continue;
125
143
  }
126
- const health = await privateRead(join(profile.stateDirectory, "automatic-health.json")).catch(
127
- (error) => {
128
- if (error.code !== "ENOENT") throw error;
129
- },
130
- );
131
- attempts = Number.isSafeInteger(health?.attempts)
132
- ? Math.max(0, Math.min(health.attempts, 10))
133
- : 0;
134
144
  if (health?.retryable === false) {
135
145
  await reportActivations(
136
146
  activationClient,
@@ -167,11 +177,12 @@ export async function superviseOnce(env = process.env, options = {}) {
167
177
  activationIds,
168
178
  started.ready ? "ready" : "starting",
169
179
  );
170
- await privateWrite(join(profile.stateDirectory, "automatic-health.json"), {
180
+ await privateWrite(healthPath, {
171
181
  state: started.ready ? "running" : "starting",
172
182
  activationIds,
173
- attempts: 0,
174
- retryAt: null,
183
+ // Starting a native chat is not proof that its asynchronous readiness check passed.
184
+ attempts: started.ready ? 0 : attempts + 1,
185
+ retryAt: started.ready ? null : Date.now() + Math.min(300000, 5000 * 2 ** attempts),
175
186
  updatedAt: new Date().toISOString(),
176
187
  });
177
188
  results.push(entry.name);
@@ -1,3 +1,4 @@
1
+ import { withAgentObservation, observeInspector } from "./observability.mjs";
1
2
  import { execFile } from "node:child_process";
2
3
  import { promisify } from "node:util";
3
4
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -9,6 +10,7 @@ import { Totem } from "./sdk/index.js";
9
10
  import { privateRead } from "./runtime-installation.mjs";
10
11
  import { wakeCodexDesktop } from "./codex-desktop-wake.mjs";
11
12
  import { reserveChatPrompt } from "./chat-prompt-limit.mjs";
13
+ import { observeSetupDispatch, recordSetupPeerReply } from "./setup-receipts.mjs";
12
14
 
13
15
  const execute = promisify(execFile);
14
16
  const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/;
@@ -261,6 +263,14 @@ export async function queueCodexChatMessage({
261
263
  return { state: "uncertain" };
262
264
  }
263
265
  await journal.save(key, { state: "queued", threadId: settings.codexThreadId });
266
+ await observeSetupDispatch({
267
+ directory: settings.stateDirectory,
268
+ identity,
269
+ sessionId: settings.codexThreadId,
270
+ kind: "peer",
271
+ id: message.id,
272
+ text: message.content?.text,
273
+ });
264
274
  return { state: "queued" };
265
275
  }
266
276
 
@@ -315,7 +325,7 @@ export async function loadCodexChatSession({
315
325
  identity.environment !== profile.environment
316
326
  )
317
327
  throw error("AGENT_IDENTITY_MISMATCH");
318
- return { profile, client, identity, inspector };
328
+ return { profile, client, identity, inspector: observeInspector(inspector) };
319
329
  }
320
330
 
321
331
  /** The linked chat's reply client uses the saved identity and the same output guard. */
@@ -331,6 +341,7 @@ export async function completeCodexChatMessage({
331
341
  }) {
332
342
  if (!UUID.test(messageId ?? "")) throw error("AGENT_IDENTITY_MISMATCH");
333
343
  const {
344
+ profile,
334
345
  client,
335
346
  identity,
336
347
  inspector: policy,
@@ -343,43 +354,66 @@ export async function completeCodexChatMessage({
343
354
  inspector = policy;
344
355
  const message = await client.messages.get(messageId);
345
356
  if (message.to !== identity.address) throw error("AGENT_IDENTITY_MISMATCH");
346
- await inspectNetworkMessage(inspector, client, message);
347
- if (readOnly) {
348
- if (ackOnly || text !== undefined) throw error("INVALID_PEER_REPLY");
349
- return {
350
- state: "ready",
351
- message: {
352
- id: message.id,
353
- from: message.from,
354
- to: message.to,
355
- conversationId: message.conversationId,
356
- inReplyToId: message.inReplyToId,
357
- content: message.content,
358
- },
359
- };
360
- }
361
- if (ackOnly && text !== undefined) throw error("INVALID_PEER_REPLY");
362
- if (ackOnly && !message.inReplyToId) throw error("INVALID_PEER_REPLY");
363
- if (!ackOnly && message.inReplyToId) {
364
- const activeTask = await activeTaskForReply(client, message, identity);
365
- if (!activeTask) throw error("INVALID_PEER_REPLY");
366
- }
367
- let reply;
368
- if (!ackOnly) {
369
- if (typeof text !== "string" || !text.trim() || text.length > 32768)
370
- throw error("INVALID_PEER_REPLY");
371
- reply = await inspector.runAction({ id: messageId, text }, (inspected) =>
372
- client.messages.reply(inspected.id, inspected.text, {
373
- idempotencyKey: `codex-chat:reply:${messageId}`,
374
- }),
375
- );
376
- }
377
- await client.messages.ack(messageId);
378
- return {
379
- state: "confirmed",
380
- messageId,
381
- ...(reply ? { replyId: reply.id, conversationId: reply.conversationId } : {}),
382
- };
357
+ return withAgentObservation(
358
+ {
359
+ client,
360
+ agentId: identity.id,
361
+ sessionId: message.conversationId,
362
+ traceId: "message:" + message.id,
363
+ input: message.content,
364
+ output: text,
365
+ name: readOnly ? "agent.request.read" : "agent.conversation",
366
+ metadata: { provider: "codex", usageSource: "unavailable" },
367
+ },
368
+ async () => {
369
+ await inspectNetworkMessage(inspector, client, message);
370
+ if (readOnly) {
371
+ if (ackOnly || text !== undefined) throw error("INVALID_PEER_REPLY");
372
+ return {
373
+ state: "ready",
374
+ message: {
375
+ id: message.id,
376
+ from: message.from,
377
+ to: message.to,
378
+ conversationId: message.conversationId,
379
+ inReplyToId: message.inReplyToId,
380
+ content: message.content,
381
+ },
382
+ };
383
+ }
384
+ if (ackOnly && text !== undefined) throw error("INVALID_PEER_REPLY");
385
+ if (ackOnly && !message.inReplyToId) throw error("INVALID_PEER_REPLY");
386
+ if (!ackOnly && message.inReplyToId) {
387
+ const activeTask = await activeTaskForReply(client, message, identity);
388
+ if (!activeTask) throw error("INVALID_PEER_REPLY");
389
+ }
390
+ let reply;
391
+ if (!ackOnly) {
392
+ if (typeof text !== "string" || !text.trim() || text.length > 32768)
393
+ throw error("INVALID_PEER_REPLY");
394
+ reply = await inspector.runAction({ id: messageId, text }, (inspected) =>
395
+ client.messages.reply(inspected.id, inspected.text, {
396
+ idempotencyKey: `codex-chat:reply:${messageId}`,
397
+ }),
398
+ );
399
+ }
400
+ await client.messages.ack(messageId);
401
+ if (reply)
402
+ await recordSetupPeerReply({
403
+ profile,
404
+ identity,
405
+ message,
406
+ reply,
407
+ client,
408
+ sessionId: environment.CODEX_THREAD_ID,
409
+ });
410
+ return {
411
+ state: "confirmed",
412
+ messageId,
413
+ ...(reply ? { replyId: reply.id, conversationId: reply.conversationId } : {}),
414
+ };
415
+ },
416
+ );
383
417
  }
384
418
 
385
419
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
@@ -1,22 +1,49 @@
1
- import {execFile} from "node:child_process";
2
- import {promisify} from "node:util";
3
- import {join} from "node:path";
4
- import {localClientEnvironment} from "./platform-client.mjs";
5
- const execute=promisify(execFile);
6
- const UUID=/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/;
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { join } from "node:path";
4
+ import { localClientEnvironment } from "./platform-client.mjs";
5
+ const execute = promisify(execFile);
6
+ const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/;
7
7
  /** Load the owner-selected existing chat using Codex's documented app route; no prompt or authority is added. */
8
- export async function wakeCodexDesktop({threadId,signal,environment=process.env},options={}){
9
- if(!UUID.test(threadId??""))throw Object.assign(new Error("AGENT_IDENTITY_MISMATCH"),{code:"AGENT_IDENTITY_MISMATCH"});
10
- const env=localClientEnvironment(environment);
11
- for(const name of Object.keys(env))if(/^(?:TOTEM|AGENTTRUST)_|^(?:OPENAI_API_KEY|ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN)$/i.test(name))delete env[name];
12
- const uri=`codex://threads/${threadId}?hostId=local`,platform=options.platform??process.platform;
13
- let command,args;
14
- if(platform==="darwin"){command="/usr/bin/open";args=["-g",uri];}
15
- else if(platform==="win32"){command=env.SystemRoot||env.SYSTEMROOT?join(env.SystemRoot??env.SYSTEMROOT,"explorer.exe"):"explorer.exe";args=[uri];}
16
- else if(platform==="linux"){command="xdg-open";args=[uri];}
17
- else throw Object.assign(new Error("AGENT_CLI_INCOMPATIBLE"),{code:"AGENT_CLI_INCOMPATIBLE"});
18
- const pending=(options.execute??execute)(command,args,{env,signal,timeout:15000,maxBuffer:65536,windowsHide:true});
19
- pending.child?.stdin?.end();
20
- await pending;
21
- return {state:"requested",threadId};
8
+ export async function wakeCodexDesktop(
9
+ { threadId, signal, environment = process.env },
10
+ options = {},
11
+ ) {
12
+ if (!UUID.test(threadId ?? ""))
13
+ throw Object.assign(new Error("AGENT_IDENTITY_MISMATCH"), { code: "AGENT_IDENTITY_MISMATCH" });
14
+ const env = localClientEnvironment(environment);
15
+ for (const name of Object.keys(env))
16
+ if (
17
+ /^(?:TOTEM|AGENTTRUST)_|^(?:OPENAI_API_KEY|ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN)$/i.test(
18
+ name,
19
+ )
20
+ )
21
+ delete env[name];
22
+ const uri = `codex://threads/${threadId}?hostId=local`,
23
+ platform = options.platform ?? process.platform;
24
+ let command, args;
25
+ if (platform === "darwin") {
26
+ command = "/usr/bin/open";
27
+ args = ["-g", uri];
28
+ } else if (platform === "win32") {
29
+ command =
30
+ env.SystemRoot || env.SYSTEMROOT
31
+ ? join(env.SystemRoot ?? env.SYSTEMROOT, "explorer.exe")
32
+ : "explorer.exe";
33
+ args = [uri];
34
+ } else if (platform === "linux") {
35
+ command = "xdg-open";
36
+ args = [uri];
37
+ } else
38
+ throw Object.assign(new Error("AGENT_CLI_INCOMPATIBLE"), { code: "AGENT_CLI_INCOMPATIBLE" });
39
+ const pending = (options.execute ?? execute)(command, args, {
40
+ env,
41
+ signal,
42
+ timeout: 15000,
43
+ maxBuffer: 65536,
44
+ windowsHide: true,
45
+ });
46
+ pending.child?.stdin?.end();
47
+ await pending;
48
+ return { state: "requested", threadId };
22
49
  }
@@ -1,9 +1,11 @@
1
1
  import { join } from "node:path";
2
2
  import { fileURLToPath, pathToFileURL } from "node:url";
3
+ import { withAgentObservation } from "./observability.mjs";
3
4
  import { readAgentProfile } from "./agent-profile.mjs";
4
5
  import { defaultCodexQueue, loadCodexChatSession } from "./codex-chat-bridge.mjs";
5
6
  import { customerInspector } from "./local-inspector.mjs";
6
7
  import { privateRead, privateWrite } from "./runtime-installation.mjs";
8
+ import { observeSetupDispatch, recordSetupOwnerChat } from "./setup-receipts.mjs";
7
9
  import { reserveChatPrompt } from "./chat-prompt-limit.mjs";
8
10
  import { wakeCodexDesktop } from "./codex-desktop-wake.mjs";
9
11
 
@@ -142,38 +144,72 @@ export async function queueCodexChatCommand({
142
144
  return { state: "uncertain" };
143
145
  }
144
146
  await privateWrite(file, { ...record, state: "queued" });
147
+ await observeSetupDispatch({
148
+ directory: profile.stateDirectory,
149
+ identity,
150
+ sessionId: profile.codexThreadId,
151
+ kind: "ownerChat",
152
+ id: command.id,
153
+ text: command.text,
154
+ });
145
155
  return { state: "queued" };
146
156
  }
147
157
 
148
158
  /** Only the linked native chat can read or complete its leased website request. */
149
159
  export async function completeCodexChatCommand({ commandId, text, readOnly = false, ...options }) {
150
- const { profile, client, inspector } = await loadCodexChatSession(options);
160
+ const { profile, client, identity, inspector } = await loadCodexChatSession(options);
151
161
  const saved = await privateRead(pathFor(profile, commandId));
152
162
  const command = { id: commandId, chatId: saved.chatId, commandEventId: saved.commandEventId };
153
163
  checkBinding(saved, profile, command);
154
164
  const { event, run } = await readCodexChatCommand(client, command);
155
- await inspector.assertAllowed("input", event.text);
156
- if (readOnly) {
157
- if (text !== undefined) throw failure("INVALID_PEER_REPLY");
158
- // Prove current lease ownership before the native chat acts, rather than
159
- // trusting a private token that another runtime may have superseded.
160
- if (run.state === "running") await client.chatCommands.renew(commandId, saved.leaseToken);
161
- return { state: run.state, commandId, text: event.text };
162
- }
163
- if (run.state !== "running" && run.state !== "completed") throw failure("CHAT_LEASE_LOST");
164
- if (typeof text !== "string" || !text.trim() || text.length > 32768)
165
- throw failure("INVALID_PEER_REPLY");
166
- const result = await inspector.runAction(
167
- { text },
168
- (inspected) =>
169
- client.chatCommands.complete(
165
+ return withAgentObservation(
166
+ {
167
+ client,
168
+ agentId: profile.agentId,
169
+ sessionId: saved.chatId,
170
+ traceId: "chat:" + commandId,
171
+ input: event.text,
172
+ output: text,
173
+ name: readOnly ? "agent.request.read" : "agent.conversation",
174
+ metadata: { provider: "codex", usageSource: "unavailable" },
175
+ },
176
+ async () => {
177
+ await inspector.assertAllowed("input", event.text);
178
+ if (readOnly) {
179
+ if (text !== undefined) throw failure("INVALID_PEER_REPLY");
180
+ // Prove current lease ownership before the native chat acts, rather than
181
+ // trusting a private token that another runtime may have superseded.
182
+ if (run.state === "running") await client.chatCommands.renew(commandId, saved.leaseToken);
183
+ return { state: run.state, commandId, text: event.text };
184
+ }
185
+ if (run.state !== "running" && run.state !== "completed") throw failure("CHAT_LEASE_LOST");
186
+ if (typeof text !== "string" || !text.trim() || text.length > 32768)
187
+ throw failure("INVALID_PEER_REPLY");
188
+ const leaseRenewed = run.state === "running";
189
+ if (leaseRenewed) await client.chatCommands.renew(commandId, saved.leaseToken);
190
+ const result = await inspector.runAction(
191
+ { text },
192
+ (inspected) =>
193
+ client.chatCommands.complete(
194
+ commandId,
195
+ { leaseToken: saved.leaseToken, text: inspected.text },
196
+ { idempotencyKey: `codex-chat:complete:${commandId}` },
197
+ ),
198
+ { task: event.text },
199
+ );
200
+ await recordSetupOwnerChat({
201
+ profile,
202
+ identity,
170
203
  commandId,
171
- { leaseToken: saved.leaseToken, text: inspected.text },
172
- { idempotencyKey: `codex-chat:complete:${commandId}` },
173
- ),
174
- { task: event.text },
204
+ saved,
205
+ result,
206
+ text: event.text,
207
+ leaseRenewed,
208
+ sessionId: (options.environment ?? process.env).CODEX_THREAD_ID,
209
+ });
210
+ return { state: result.run.state, commandId, eventId: result.event.id };
211
+ },
175
212
  );
176
- return { state: result.run.state, commandId, eventId: result.event.id };
177
213
  }
178
214
 
179
215
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {