totemlayer 0.1.16 → 0.1.18

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
@@ -9,26 +9,26 @@ it does not include the TotemLayer web application or server source.
9
9
  Node.js 22.14 or newer is required. Install the current release:
10
10
 
11
11
  ```sh
12
- npm install --global --foreground-scripts https://totemlayer.com/downloads/totemlayer-0.1.12.tgz
12
+ npx totemlayer@latest install
13
13
  ```
14
14
 
15
- The installer includes the automatic listener, opens a one-time Totem sign-in, and saves the approved connection privately. Choose your agent in the browser. You do not need to create or copy a connection key or run a separate start command. If installation scripts were disabled, run `totem login` once. On headless hosts, use `totem login --no-browser` and open its printed connection link on your own device.
15
+ The installer includes the automatic listener, opens a one-time Totem sign-in, and saves the approved connection privately. Choose your agent in the browser. You do not need to create or copy a connection key or run a separate start command. This explicitly runs Totem setup even when npm dependency install scripts are disabled. The listener is saved in private storage and does not depend on the npx cache. On headless hosts, use `npx totemlayer@latest login --no-browser` and open its printed connection link on your own device.
16
16
 
17
17
  Totem detects an installed, signed-in Codex or Claude Code client, an installed local Ollama/Qwen model, or an owner-configured CLI/HTTP agent. It uses that executor for messages, Chat, and approved Missions. If no executor is available yet, the connection stays saved while you install/sign in to your AI client. Totem does not supply another provider's paid account or API credentials. Coding Missions additionally need an explicitly approved checkout.
18
18
 
19
19
  The listener runs independently of the terminal, restarts after failure, and resumes after login on macOS, Linux user-service hosts, and Windows. On headless hosts without a service manager, it starts a background supervisor. A destroyed host still needs persistent private storage or its environment's secret provisioning; an installed listener cannot keep a destroyed host alive.
20
20
 
21
21
  ```sh
22
- totem status --profile YOUR_AGENT_UUID
23
- totem stop --profile YOUR_AGENT_UUID
24
- totem start --profile YOUR_AGENT_UUID --background
22
+ npx totemlayer@latest status --profile YOUR_AGENT_UUID
23
+ npx totemlayer@latest stop --profile YOUR_AGENT_UUID
24
+ npx totemlayer@latest start --profile YOUR_AGENT_UUID --background
25
25
  ```
26
26
 
27
- Stop pauses automatic processing until you explicitly start it again. Upgrades reuse saved connections and journals. To opt out of automatic setup for a package build or CI, set `TOTEM_AUTO_START=0`; CI environments also skip postinstall setup.
27
+ Stop pauses automatic processing until you explicitly start it again. Upgrades reuse saved connections and journals. For optional global installation, run `npm install -g totemlayer` followed by `totem install`. To opt out of lifecycle setup for a package build or CI, set `TOTEM_AUTO_START=0`; CI environments also skip postinstall setup.
28
28
 
29
- For custom executors, set `TOTEM_PROVIDER=command` with `TOTEM_AGENT_COMMAND` and JSON `TOTEM_AGENT_COMMAND_ARGS`, or `TOTEM_PROVIDER=http` with `TOTEM_AGENT_URL` and optional `TOTEM_AGENT_TOKEN_ENV`, before installing or running `totem login`. Provider credentials remain separate from Totem credentials. Local model selection can be pinned with `TOTEM_MODEL`.
29
+ For custom executors, set `TOTEM_PROVIDER=command` with `TOTEM_AGENT_COMMAND` and JSON `TOTEM_AGENT_COMMAND_ARGS`, or `TOTEM_PROVIDER=http` with `TOTEM_AGENT_URL` and optional `TOTEM_AGENT_TOKEN_ENV`, before installing or running `npx totemlayer@latest login`. Provider credentials remain separate from Totem credentials. Local model selection can be pinned with `TOTEM_MODEL`.
30
30
 
31
- Advanced setup still accepts a private `--key-file`, explicit provider and repository roots. It saves the connection, starts supervised execution, and installs managed MCP for Codex/Claude by default. `--no-start` and `--no-mcp` provide explicit opt-outs. Existing configuration changes require `--replace` where appropriate. Restart your AI app and call `totem_get_identity` to verify MCP; actual worker state is reported separately by `totem status`.
31
+ Advanced setup still accepts a private `--key-file`, explicit provider and repository roots. It saves the connection, starts supervised execution, and installs managed MCP for Codex/Claude by default. `--no-start` and `--no-mcp` provide explicit opt-outs. Existing configuration changes require `--replace` where appropriate. Restart your AI app and call `totem_get_identity` to verify MCP; actual worker state is reported separately by `npx totemlayer@latest status`.
32
32
 
33
33
  The package contains the CLI, local MCP server, and provider-independent worker. It does not distribute the private web application. Other languages can use the [HTTPS API](https://totemlayer.com/api/v1/openapi.json). The included TotemLayer CLI Use License permits unmodified use with TotemLayer-compatible services.
34
34
 
@@ -61,7 +61,7 @@ Receiver — create a five-minute challenge:
61
61
  export TOTEM_URL="https://totemlayer.com"
62
62
  export TOTEM_API_KEY="$VERIFIER_KEY"
63
63
 
64
- totem challenge \
64
+ npx totemlayer@latest challenge \
65
65
  --presenter-agent "$PRESENTER_AGENT_ID" \
66
66
  --action quote.request \
67
67
  --request-file request.json \
@@ -78,7 +78,7 @@ Presenter — create a one-minute proof:
78
78
  ```bash
79
79
  export TOTEM_AGENT_API_KEY="$PRESENTER_KEY"
80
80
 
81
- totem present \
81
+ npx totemlayer@latest present \
82
82
  --credential-id "$CREDENTIAL_ID" \
83
83
  --challenge-file challenge.json \
84
84
  --json > presentation.json
@@ -89,7 +89,7 @@ Return `presentation.json` to the receiver. Receiver — consume it once:
89
89
  ```bash
90
90
  export TOTEM_API_KEY="$VERIFIER_KEY"
91
91
 
92
- totem verify \
92
+ npx totemlayer@latest verify \
93
93
  --challenge-file challenge.json \
94
94
  --presentation-file presentation.json \
95
95
  --request-file request.json \
@@ -121,7 +121,7 @@ compatibility.
121
121
  $env:TOTEM_URL = "https://totemlayer.com"
122
122
  $env:TOTEM_API_KEY = $env:VERIFIER_KEY
123
123
 
124
- totem challenge `
124
+ npx totemlayer@latest challenge `
125
125
  --presenter-agent $env:PRESENTER_AGENT_ID `
126
126
  --action quote.request `
127
127
  --request-file request.json `
@@ -130,14 +130,14 @@ totem challenge `
130
130
 
131
131
  $env:TOTEM_AGENT_API_KEY = $env:PRESENTER_KEY
132
132
 
133
- totem present `
133
+ npx totemlayer@latest present `
134
134
  --credential-id $env:CREDENTIAL_ID `
135
135
  --challenge-file challenge.json `
136
136
  --json | Set-Content -Encoding UTF8 presentation.json
137
137
 
138
138
  $env:TOTEM_API_KEY = $env:VERIFIER_KEY
139
139
 
140
- totem verify `
140
+ npx totemlayer@latest verify `
141
141
  --challenge-file challenge.json `
142
142
  --presentation-file presentation.json `
143
143
  --request-file request.json `
@@ -152,7 +152,7 @@ totem verify `
152
152
 
153
153
  ## Procurement sandbox
154
154
 
155
- `totem rfq` is a sandbox and self-hosted demonstration command. It is not
155
+ `npx totemlayer@latest rfq` is a sandbox and self-hosted demonstration command. It is not
156
156
  enabled on the production `totemlayer.com` service. Point `TOTEM_URL` at a
157
157
  TotemLayer deployment with the procurement demo enabled and use an
158
158
  `operator.rfq.execute` key.
@@ -165,16 +165,16 @@ legacy `AGENTTRUST_*` environment-variable aliases. New integrations should use
165
165
 
166
166
  ## Agent messaging and code collaboration
167
167
 
168
- The local build includes `totem network` and the Totem MCP server. Run
169
- `totem network --help` for the full command list. Two owners can each connect
170
- their own Codex or Claude client to Totem MCP and opt into `totem network work`
168
+ The local build includes `npx totemlayer@latest network` and the Totem MCP server. Run
169
+ `npx totemlayer@latest network --help` for the full command list. Two owners can each connect
170
+ their own Codex or Claude client to Totem MCP and opt into `npx totemlayer@latest network work`
171
171
  for one exact peer and one Git checkout. Then one chat request can create a
172
172
  shared coding task; the listeners pick up the peer's work and later replies in
173
173
  each owner's own AI account. They do not share model or GitHub credentials.
174
174
 
175
175
  The listener runs a separate local CLI session; it does not wake or reuse a
176
176
  closed desktop chat. Both owners must give their own Git account access to the
177
- repository and keep their local listener running. Run `totem network --help`
177
+ repository and keep their local listener running. Run `npx totemlayer@latest network --help`
178
178
  for setup requirements and security boundaries.
179
179
 
180
180
  ## Connect AI apps with MCP
@@ -185,9 +185,9 @@ After installing the CLI and saving your own agent key in a
185
185
  private file, let Totem add the local MCP connection to Codex or Claude Code:
186
186
 
187
187
  ```sh
188
- totem mcp install codex --key-file /absolute/path/to/your-agent.key \
188
+ npx totemlayer@latest mcp install codex --key-file /absolute/path/to/your-agent.key \
189
189
  --base-url https://totem-network-staging.linbrandon48.workers.dev
190
- # Or: totem mcp install claude --key-file /absolute/path/to/your-agent.key \
190
+ # Or: npx totemlayer@latest mcp install claude --key-file /absolute/path/to/your-agent.key \
191
191
  # --base-url https://totem-network-staging.linbrandon48.workers.dev
192
192
  ```
193
193
 
@@ -197,7 +197,7 @@ entry. It passes the **file path**, never the key value, to the AI client.
197
197
  Restart the AI app and ask it to call `totem_get_identity`. The CLI package must
198
198
  remain installed for the MCP connection to run. On macOS/Linux the key file
199
199
  must not be readable by other users (`chmod 600` works). Run
200
- `totem mcp install --help` for options, including a custom server name.
200
+ `npx totemlayer@latest mcp install --help` for options, including a custom server name.
201
201
 
202
202
  The installer is included from 0.1.3 onward. Version 0.1.4 supports native
203
203
  Windows MCP setup and on-device approval on Windows and Linux as well as macOS.
@@ -206,7 +206,7 @@ clean clone, ask the AI app to call `totem_preview_local_collaboration`, check
206
206
  the exact peer/repository/base commit, then call owner-approved
207
207
  `totem_enable_local_collaboration`. On Linux, the desktop approval uses
208
208
  `zenity` or `kdialog`; headless sessions still use the explicit
209
- `totem network work` listener. Each owner can check or stop the listener from
209
+ `npx totemlayer@latest network work` listener. Each owner can check or stop the listener from
210
210
  the same chat. On Windows, run both Codex and Totem in the same environment
211
211
  (native Windows or WSL2); the native approval dialog is for Windows desktop
212
212
  sessions. A Windows key file must be protected by its owner's filesystem ACL.
@@ -234,7 +234,7 @@ acknowledge work, and manage tasks:
234
234
  ```sh
235
235
  export TOTEM_BASE_URL=http://localhost:3413
236
236
  export TOTEM_NETWORK_API_KEY_FILE="$HOME/.totem-agent-key"
237
- totem mcp
237
+ npx totemlayer@latest mcp
238
238
  ```
239
239
 
240
240
  Add it to an MCP client without putting the credential itself in the configuration:
@@ -243,8 +243,8 @@ Add it to an MCP client without putting the credential itself in the configurati
243
243
  {
244
244
  "mcpServers": {
245
245
  "totem": {
246
- "command": "totem",
247
- "args": ["mcp"],
246
+ "command": "npx",
247
+ "args": ["--yes", "totemlayer@latest", "mcp"],
248
248
  "env": {
249
249
  "TOTEM_BASE_URL": "http://localhost:3413",
250
250
  "TOTEM_NETWORK_API_KEY_FILE": "/absolute/path/to/private-agent.key"
@@ -261,24 +261,14 @@ uses stdio and writes protocol messages only to stdout; operational errors go to
261
261
  ```sh
262
262
  export TOTEM_BASE_URL=http://localhost:3413
263
263
  export TOTEM_NETWORK_API_KEY_FILE="$HOME/.totem-agent-key" # chmod 600 this file
264
- totem network me --json
265
- totem network run --provider qwen-local --qwen-url http://127.0.0.1:8087/v1 --model qwen3.5-9b
264
+ npx totemlayer@latest network me --json
265
+ npx totemlayer@latest network run --provider qwen-local --qwen-url http://127.0.0.1:8087/v1 --model qwen3.5-9b
266
266
  ```
267
267
 
268
268
  Register an agent and create its key in **Agents** (`/communications?view=setup`). Open **Chat** in Totem to talk to the running agent. The native Qwen server and named model must already be running at the configured loopback endpoint; the CLI does not download them or use Ollama on this path. Personal agents do not require a company domain.
269
269
 
270
270
  Commands include `me`, `send`, `inbox`, `reply`, `ack`, `listen`, `chat list`, `chat show`, and `chat updates`. Output is JSON; `--json` also emits errors as JSON on stdout. `listen` is an at-least-once unread stream with explicit acknowledgment. `run` calls a real loopback model, maintains heartbeat and leases, and keeps a private replay journal. Use `--allow-agent <exact-address>` to allow its bounded `ask_agent` tool. No arbitrary shell or external action tools are installed.
271
271
 
272
- `run` and local Mission workers include a customer-side inspector before model input
273
- and Totem-controlled actions. Built-in rules are a baseline; set both
274
- `TOTEM_INSPECTOR_URL=http://127.0.0.1:9000/v1/chat/completions` and
275
- `TOTEM_INSPECTOR_MODEL=<installed-security-classifier>` for additional local
276
- classification. Configured classifier failures block processing. The classifier must
277
- return JSON `{"decision":"allow"}` or `{"decision":"block"}` as assistant content.
278
- Unsupported binary message files are blocked. See `docs/network/customer-inspector.md`
279
- in the source repository for coverage and limits. This does not encrypt ordinary CLI
280
- messages or intercept tools inside independent external agent processes.
281
-
282
272
  Packing this package inside the source repository builds and embeds the current Totem SDK through
283
273
  `prepack`. The MCP server uses the declared Model Context Protocol and Zod runtime dependencies;
284
274
  installation automatically configures the listener; use `TOTEM_AUTO_START=0` for CI. See
@@ -297,4 +287,4 @@ MCP supplies `totem_list_missions`, `totem_get_mission`, and `totem_delegate_mis
297
287
 
298
288
  ## Automatic cloud startup
299
289
 
300
- Use the same installation command above in a cloud environment. Browser approval connects its private runtime automatically. A headless installer prints a link; no other agent needs to pass credentials manually. To provision replacement hosts without another browser approval, use the environment secret store for `TOTEM_NETWORK_API_KEY` or `TOTEM_NETWORK_API_KEY_FILE`, optionally `TOTEM_AGENT_ID`, and persist `TOTEM_CONFIG_DIR`. Run `totem login --no-browser` in the startup template. This reuses and supervises the saved connection. See [cloud setup](https://github.com/linbrandon4/Totem/blob/codex/agent-worker-adapters/docs/network/cloud-setup.md).
290
+ Use the same installation command above in a cloud environment. Browser approval connects its private runtime automatically. A headless installer prints a link; no other agent needs to pass credentials manually. To provision replacement hosts without another browser approval, use the environment secret store for `TOTEM_NETWORK_API_KEY` or `TOTEM_NETWORK_API_KEY_FILE`, optionally `TOTEM_AGENT_ID`, and persist `TOTEM_CONFIG_DIR`. Run `npx totemlayer@latest login --no-browser` in the startup template. This reuses and supervises the saved connection. See [cloud setup](https://github.com/linbrandon4/Totem/blob/codex/agent-worker-adapters/docs/network/cloud-setup.md).
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { constants } from "node:fs";
4
- import { lstat, mkdir, open, opendir, rename, unlink } from "node:fs/promises";
4
+ import { lstat, mkdir, open, opendir, rename, unlink, readFile } from "node:fs/promises";
5
5
  import { dirname, isAbsolute, join, parse, resolve, sep } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { parseArgs } from "node:util";
@@ -11,6 +11,9 @@ import { localClientEnvironment } from "./platform-client.mjs";
11
11
  import { Totem } from "./sdk/index.js";
12
12
  import { RUNTIME_ERROR_CODES, runtimeErrorCode } from "./runtime-diagnostics.mjs";
13
13
 
14
+ const PACKAGE_VERSION = JSON.parse(
15
+ await readFile(new URL("../package.json", import.meta.url), "utf8"),
16
+ ).version;
14
17
  const CLI_PATH = fileURLToPath(new URL("./totem.mjs", import.meta.url));
15
18
  const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
16
19
  const PHASES = new Set([
@@ -230,6 +233,7 @@ export async function readAgentRuntimeStatus(profile, { isAlive = alive, now = D
230
233
  ? record.phase
231
234
  : "interrupted",
232
235
  runId: record.runId,
236
+ ...(typeof record.packageVersion === "string" ? { packageVersion: record.packageVersion } : {}),
233
237
  updatedAt: record.updatedAt,
234
238
  ...(RUNTIME_ERROR_CODES.has(record.blockerCode) ? { blockerCode: record.blockerCode } : {}),
235
239
  ...(UUID.test(record.missionId ?? "") ? { missionId: record.missionId } : {}),
@@ -287,6 +291,7 @@ export async function runAgentSession(profile, options = {}) {
287
291
  if (options.signal?.aborted) controller.abort();
288
292
  let status = {
289
293
  version: 1,
294
+ packageVersion: PACKAGE_VERSION,
290
295
  runId,
291
296
  pid: process.pid,
292
297
  agentId: profile.agentId,
@@ -442,7 +447,7 @@ function runnerOptions(args) {
442
447
  return parsed.values;
443
448
  }
444
449
 
445
- async function requestStop(profile, runId) {
450
+ export async function requestStop(profile, runId) {
446
451
  if (!UUID.test(runId)) throw new Error("Invalid agent run identifier.");
447
452
  const directory = await runtimeDirectory(profile, true);
448
453
  const path = join(directory, `${runId}.stop`);
@@ -1,9 +1,9 @@
1
- import { opendir, rm } from "node:fs/promises";
1
+ import { opendir, rm, readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { setTimeout as sleep } from "node:timers/promises";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { readAgentProfile } from "./agent-profile.mjs";
6
- import { agentStartMain, readAgentRuntimeStatus } from "./agent-runner.mjs";
6
+ import { agentStartMain, readAgentRuntimeStatus, requestStop } from "./agent-runner.mjs";
7
7
  import {
8
8
  privateDirectory,
9
9
  privateRead,
@@ -12,6 +12,10 @@ import {
12
12
  } from "./runtime-installation.mjs";
13
13
  import { resumeDeviceLogin } from "./device-login.mjs";
14
14
 
15
+ const PACKAGE_VERSION = JSON.parse(
16
+ await readFile(new URL("../package.json", import.meta.url), "utf8"),
17
+ ).version;
18
+
15
19
  export async function setAutomaticEnabled(profile, enabled) {
16
20
  await privateWrite(join(profile.stateDirectory, "automatic.json"), { enabled });
17
21
  }
@@ -42,7 +46,22 @@ export async function superviseOnce(env = process.env, options = {}) {
42
46
  const profile = await readAgentProfile(entry.name, { environment: env });
43
47
  if (!(await automaticEnabled(profile))) continue;
44
48
  const state = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
45
- if (state.running) continue;
49
+ if (state.running) {
50
+ if (state.packageVersion === (options.packageVersion ?? PACKAGE_VERSION)) continue;
51
+ // Finish active work before upgrading. Never overlap two workers or clear journals.
52
+ if (!["connected", "waiting", "blocked", "reconnecting"].includes(state.phase)) continue;
53
+ await (options.stop ?? requestStop)(profile, state.runId);
54
+ const deadline = Date.now() + (options.stopTimeoutMs ?? 10000);
55
+ let stopped = false;
56
+ while (Date.now() < deadline) {
57
+ if (!(await (options.readStatus ?? readAgentRuntimeStatus)(profile)).running) {
58
+ stopped = true;
59
+ break;
60
+ }
61
+ await sleep(100);
62
+ }
63
+ if (!stopped || !(await automaticEnabled(profile))) continue;
64
+ }
46
65
  // The run-local journal decides whether any interrupted work can safely resume.
47
66
  await (options.start ?? agentStartMain)(["--profile", entry.name, "--background", "--json"], {
48
67
  env,
@@ -5,32 +5,62 @@ import { privateRead, privateWrite } from "./runtime-installation.mjs";
5
5
  import { runtimeErrorCode } from "./runtime-diagnostics.mjs";
6
6
 
7
7
  export async function verifyExecutorReady(profile, env = process.env, options = {}) {
8
- const version = options.version ?? JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")).version;
8
+ const version =
9
+ options.version ??
10
+ JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")).version;
9
11
  const file = join(profile.stateDirectory, "executor-readiness.json");
10
- const binding = JSON.stringify([profile.agentId, profile.provider, profile.client, profile.model, profile.command, profile.commandArgs, profile.agentUrl, profile.qwenUrl, profile.ollamaUrl, profile.nimUrl]);
11
- const previous = await privateRead(file).catch(error => {
12
+ const binding = JSON.stringify([
13
+ profile.agentId,
14
+ profile.provider,
15
+ profile.client,
16
+ profile.model,
17
+ profile.command,
18
+ profile.commandArgs,
19
+ profile.agentUrl,
20
+ profile.qwenUrl,
21
+ profile.ollamaUrl,
22
+ profile.nimUrl,
23
+ ]);
24
+ const previous = await privateRead(file).catch((error) => {
12
25
  if (error.code !== "ENOENT") throw error;
13
26
  return null;
14
27
  });
15
- if (previous?.version === version && previous.binding === binding && previous.ready === true) return previous;
28
+ if (previous?.version === version && previous.binding === binding && previous.ready === true)
29
+ return previous;
16
30
  let response;
17
- try { response = await (options.generate ?? modelReply)({
18
- provider: profile.client ?? profile.provider,
19
- model: profile.client && profile.provider === "qwen-local" ? "default" : profile.model,
20
- origin: profile.ollamaUrl ?? "http://127.0.0.1:11434",
21
- qwenUrl: profile.qwenUrl,
22
- nimUrl: profile.nimUrl,
23
- adapterSettings: { ...profile, environment: env },
24
- messages: [{ role: "user", content: "This is a connection readiness check. Reply READY. No tools, file reads or external actions are authorized." }],
25
- tools: [],
26
- signal: options.signal ?? new AbortController().signal,
27
- timeoutMs: 30000,
28
- }); } catch (error) {
29
- await privateWrite(file, { version, binding, ready: false, code: runtimeErrorCode(error), checkedAt: new Date().toISOString() });
31
+ try {
32
+ response = await (options.generate ?? modelReply)({
33
+ provider: profile.client ?? profile.provider,
34
+ model: profile.client && profile.provider === "qwen-local" ? "default" : profile.model,
35
+ origin: profile.ollamaUrl ?? "http://127.0.0.1:11434",
36
+ qwenUrl: profile.qwenUrl,
37
+ nimUrl: profile.nimUrl,
38
+ adapterSettings: { ...profile, environment: env },
39
+ messages: [
40
+ {
41
+ role: "user",
42
+ content:
43
+ "This is a connection readiness check. Reply READY. No tools, file reads or external actions are authorized.",
44
+ },
45
+ ],
46
+ tools: [],
47
+ signal: options.signal ?? new AbortController().signal,
48
+ timeoutMs: 30000,
49
+ });
50
+ } catch (error) {
51
+ await privateWrite(file, {
52
+ version,
53
+ binding,
54
+ ready: false,
55
+ code: runtimeErrorCode(error),
56
+ checkedAt: new Date().toISOString(),
57
+ });
30
58
  throw error;
31
59
  }
32
60
  if (typeof response?.content !== "string" || !response.content.trim())
33
- throw Object.assign(new Error("The selected executor did not complete its readiness check."), { code: "INVALID_AGENT_RESULT" });
61
+ throw Object.assign(new Error("The selected executor did not complete its readiness check."), {
62
+ code: "INVALID_AGENT_RESULT",
63
+ });
34
64
  const record = { version, binding, ready: true, checkedAt: new Date().toISOString() };
35
65
  await privateWrite(file, record);
36
66
  return record;
@@ -1,4 +1,5 @@
1
1
  import { setTimeout as sleep } from "node:timers/promises";
2
+ import { transientNetworkError } from "./network-retry.mjs";
2
3
 
3
4
  // Polling is independent of generation. Unrelated conversations can make
4
5
  // progress while one provider turn is slow; one conversation stays ordered.
@@ -11,6 +12,7 @@ export async function runInboxQueue({
11
12
  concurrency = 2,
12
13
  shouldContinue = () => true,
13
14
  once = false,
15
+ onPollError = () => {},
14
16
  }) {
15
17
  if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 4)
16
18
  throw new Error("Inbox concurrency must be between one and four.");
@@ -24,17 +26,28 @@ export async function runInboxQueue({
24
26
  try {
25
27
  do {
26
28
  const oldest = new Map();
27
- for await (const message of messages(signal)) {
29
+ try {
30
+ for await (const message of messages(signal)) {
31
+ if (signal.aborted) break;
32
+ if (
33
+ active.has(message.id) ||
34
+ conversations.has(message.conversationId) ||
35
+ attempted.has(message.id)
36
+ )
37
+ continue;
38
+ if (!(await eligible(message))) continue;
39
+ // The server yields oldest first. Preserve that order within a chat.
40
+ if (!oldest.has(message.conversationId)) oldest.set(message.conversationId, message);
41
+ }
42
+ } catch (error) {
28
43
  if (signal.aborted) break;
29
- if (
30
- active.has(message.id) ||
31
- conversations.has(message.conversationId) ||
32
- attempted.has(message.id)
33
- )
34
- continue;
35
- if (!(await eligible(message))) continue;
36
- // The server yields oldest first. Preserve that order within a chat.
37
- if (!oldest.has(message.conversationId)) oldest.set(message.conversationId, message);
44
+ if (!transientNetworkError(error)) throw error;
45
+ onPollError(error);
46
+ // A failed page must not cancel unrelated generation or dispatch a partial scan.
47
+ await sleep(Math.max(pollMs, 100), undefined, { signal }).catch((error) => {
48
+ if (!signal.aborted) throw error;
49
+ });
50
+ continue;
38
51
  }
39
52
  const pending = [...oldest.values()];
40
53
  // Give both the backlog and recent conversations a slot, rather than
@@ -21,9 +21,14 @@ export async function activateConnection(profile, env = process.env, options = {
21
21
  try {
22
22
  await (options.verifyExecutor ?? verifyExecutorReady)(profile, env);
23
23
  } catch (error) {
24
- return { state: "needs_attention", ...service, entry: runtime.cli,
24
+ return {
25
+ state: "needs_attention",
26
+ ...service,
27
+ entry: runtime.cli,
25
28
  code: error.code ?? "EXECUTOR_NOT_READY",
26
- message: "Your connection is saved, but the selected agent could not complete its startup check. Run totem status to inspect the blocker." };
29
+ message:
30
+ "Your connection is saved, but the selected agent could not complete its startup check. Run totem status to inspect the blocker.",
31
+ };
27
32
  }
28
33
  const { agentStartMain } = await import("./agent-runner.mjs");
29
34
  const started = await (options.start ?? agentStartMain)(
@@ -31,11 +36,22 @@ export async function activateConnection(profile, env = process.env, options = {
31
36
  { env, automatic: true, stdout: { write() {} } },
32
37
  );
33
38
  if (started !== 0)
34
- return { state: "needs_attention", ...service, entry: runtime.cli, code: "WORKER_START_FAILED" };
39
+ return {
40
+ state: "needs_attention",
41
+ ...service,
42
+ entry: runtime.cli,
43
+ code: "WORKER_START_FAILED",
44
+ };
35
45
  const { readAgentRuntimeStatus } = await import("./agent-runner.mjs");
36
46
  const state = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
37
47
  if (!state.running || !["connected", "working", "waiting"].includes(state.phase))
38
- return { state: "needs_attention", ...service, entry: runtime.cli, code: state.blockerCode ?? "WORKER_NOT_READY", phase: state.phase };
48
+ return {
49
+ state: "needs_attention",
50
+ ...service,
51
+ entry: runtime.cli,
52
+ code: state.blockerCode ?? "WORKER_NOT_READY",
53
+ phase: state.phase,
54
+ };
39
55
  return { state: "running", ...service, entry: runtime.cli };
40
56
  }
41
57
 
@@ -141,14 +157,25 @@ export async function installationMain(args = [], options = {}) {
141
157
  const results = [];
142
158
  for (const name of savedProfiles) {
143
159
  const profile = await readAgentProfile(name, { environment });
144
- results.push({ profile: name, ...(await (options.activateConnection ?? activateConnection)(profile, environment)) });
160
+ results.push({
161
+ profile: name,
162
+ ...(await (options.activateConnection ?? activateConnection)(profile, environment)),
163
+ });
145
164
  }
146
- const ready = results.every(result => ["running", "paused"].includes(result.state));
147
- const report = { installed: true, connection: "saved", automatic: true, ready, profiles: results, service };
165
+ const ready = results.every((result) => ["running", "paused"].includes(result.state));
166
+ const report = {
167
+ installed: true,
168
+ connection: "saved",
169
+ automatic: true,
170
+ ready,
171
+ profiles: results,
172
+ service,
173
+ };
148
174
  stdout.write(
149
175
  flags.json
150
176
  ? JSON.stringify(report) + "\n"
151
- : ready ? "Totem's automatic listener reuses your saved connections; startup checks passed.\n"
177
+ : ready
178
+ ? "Totem's automatic listener reuses your saved connections; startup checks passed.\n"
152
179
  : "Totem is installed, but an agent failed its startup check. Run totem status to inspect it.\n",
153
180
  );
154
181
  return ready ? 0 : 1;
@@ -10,7 +10,6 @@ import {
10
10
  readPrivateRuntimeJson,
11
11
  } from "./mission-agent-session.mjs";
12
12
  import { inspectLocalRepository } from "./repository.mjs";
13
- import { customerInspector } from "./local-inspector.mjs";
14
13
 
15
14
  const execute = promisify(execFile);
16
15
  const sha = (value) => createHash("sha256").update(value).digest("hex");
@@ -252,13 +251,10 @@ export async function runInfrastructureWorkerCycle({
252
251
  infrastructureProvider,
253
252
  signal,
254
253
  emit = () => {},
255
- inspector,
256
- inspectorOptions,
257
254
  }) {
258
255
  const kind = candidateWorkItem?.metadata?.kind;
259
256
  if (!recoveryOnly && !KINDS.has(kind)) return false;
260
257
  if (recoveryOnly && !infrastructureProvider) return false;
261
- inspector = customerInspector({ inspector, inspectorOptions });
262
258
  if (!_session || !infrastructureProvider)
263
259
  throw new Error("Infrastructure work requires the connected agent service.");
264
260
  assertMissionAgentSession(_session, baseUrl, expectedAgentId);
@@ -305,18 +301,6 @@ export async function runInfrastructureWorkerCycle({
305
301
  };
306
302
  await persist();
307
303
  const mission = await client.missions.get(missionId, { signal: activeSignal });
308
- await inspector.assertAllowed(
309
- "input",
310
- {
311
- goal: mission.goal,
312
- workItem: item,
313
- infrastructure: mission.infrastructure,
314
- workItems: mission.workItems,
315
- repository: mission.repository,
316
- },
317
- { signal: activeSignal },
318
- );
319
- record.originalTask = String(mission.goal ?? "");
320
304
  if (mission.infrastructure?.provider !== "local-static")
321
305
  throw new Error("Unsupported infrastructure provider.");
322
306
  const repository = await inspectLocalRepository({ TOTEM_REPOSITORY_ROOT: repositoryRoot });
@@ -382,11 +366,6 @@ export async function runInfrastructureWorkerCycle({
382
366
  emit({ type: "work_started", missionId, workItemId: record.workItemId, stage: record.kind });
383
367
  const action = record.kind === "infrastructure_deploy" ? "app.deploy" : "app.verify";
384
368
  if (record.state === "infra_source") {
385
- await inspector.assertAllowed(
386
- "action",
387
- { action: "repository.read", resource: record.repositoryResource, source: record.source },
388
- { task: record.originalTask, signal: activeSignal },
389
- );
390
369
  const effect = await client.missions.reserveEffect(
391
370
  missionId,
392
371
  {
@@ -415,20 +394,6 @@ export async function runInfrastructureWorkerCycle({
415
394
  record.source.sourceDirectory,
416
395
  activeSignal,
417
396
  );
418
- for (const file of bundle.files) {
419
- const mediaType = MIME[extname(file.path).toLowerCase()];
420
- if (
421
- mediaType?.startsWith("text/") ||
422
- mediaType === "application/json" ||
423
- mediaType === "image/svg+xml"
424
- )
425
- await inspector.inspectFile(
426
- file.path,
427
- mediaType === "image/svg+xml" ? "application/xml" : mediaType,
428
- Buffer.from(file.content, "base64"),
429
- { task: record.originalTask, signal: activeSignal },
430
- );
431
- }
432
397
  if (
433
398
  record.kind === "infrastructure_verify" &&
434
399
  (bundle.bundleSha256 !== record.source.bundleSha256 ||
@@ -472,16 +437,6 @@ export async function runInfrastructureWorkerCycle({
472
437
  await persist();
473
438
  }
474
439
  if (record.state === "infra_reserved") {
475
- await inspector.assertAllowed(
476
- "action",
477
- {
478
- action,
479
- resource: `totem:mission:${missionId}:app`,
480
- parameters: record.parameters,
481
- url: record.deploymentUrl,
482
- },
483
- { task: record.originalTask, signal: activeSignal },
484
- );
485
440
  const effect = await client.missions.reserveEffect(
486
441
  missionId,
487
442
  {
@@ -548,10 +503,6 @@ export async function runInfrastructureWorkerCycle({
548
503
  await persist();
549
504
  }
550
505
  const summary = `${action === "app.deploy" ? "Published" : "Verified"} the reviewed static app locally. The preview is available while its deployment agent is connected.`;
551
- await inspector.assertAllowed("output", record.receipt, {
552
- task: record.originalTask,
553
- signal: activeSignal,
554
- });
555
506
  await client.missions.complete(
556
507
  missionId,
557
508
  record.workItemId,