totemlayer 0.1.15 → 0.1.17
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 +29 -39
- package/bin/agent-runner.mjs +5 -1
- package/bin/agent-setup.mjs +45 -11
- package/bin/connected-agent-service.mjs +8 -2
- package/bin/executor-readiness.mjs +67 -0
- package/bin/inbox-queue.mjs +23 -10
- package/bin/installation.mjs +57 -5
- package/bin/mission-infrastructure.mjs +0 -49
- package/bin/mission-worker.mjs +11 -129
- package/bin/model-adapter.mjs +35 -2
- package/bin/network-retry.mjs +26 -0
- package/bin/network-runtime.mjs +89 -19
- package/bin/platform-client.mjs +30 -2
- package/bin/runtime-diagnostics.mjs +38 -0
- package/bin/sdk/index.js +11 -40
- package/bin/totem.mjs +1 -1
- package/bin/worker-core/adapters.mjs +25 -3
- package/bin/worker-core/errors.mjs +5 -4
- package/package.json +1 -1
- package/scripts/prepare.mjs +20 -0
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
|
-
|
|
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.
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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 `
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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 `
|
|
169
|
-
`
|
|
170
|
-
their own Codex or Claude client to Totem MCP and opt into `
|
|
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 `
|
|
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
|
-
|
|
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:
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
|
|
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": "
|
|
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
|
-
|
|
265
|
-
|
|
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 `
|
|
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).
|
package/bin/agent-runner.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { parseArgs } from "node:util";
|
|
|
9
9
|
import { readAgentCredential, readAgentProfile, withAgentProfileLock } from "./agent-profile.mjs";
|
|
10
10
|
import { localClientEnvironment } from "./platform-client.mjs";
|
|
11
11
|
import { Totem } from "./sdk/index.js";
|
|
12
|
+
import { RUNTIME_ERROR_CODES, runtimeErrorCode } from "./runtime-diagnostics.mjs";
|
|
12
13
|
|
|
13
14
|
const CLI_PATH = fileURLToPath(new URL("./totem.mjs", import.meta.url));
|
|
14
15
|
const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
|
|
@@ -29,7 +30,7 @@ const LABELS = {
|
|
|
29
30
|
reconnecting: "Connection interrupted. Waiting to reconnect from saved progress.",
|
|
30
31
|
working: "Working on an assigned Mission task.",
|
|
31
32
|
waiting: "Connected and waiting for approved work.",
|
|
32
|
-
blocked: "
|
|
33
|
+
blocked: "Agent execution needs attention. Check status for the blocker.",
|
|
33
34
|
stopping: "Stopping the agent and saving its progress…",
|
|
34
35
|
stopped: "Agent stopped. Saved progress will be checked when it starts again.",
|
|
35
36
|
failed:
|
|
@@ -230,6 +231,7 @@ export async function readAgentRuntimeStatus(profile, { isAlive = alive, now = D
|
|
|
230
231
|
: "interrupted",
|
|
231
232
|
runId: record.runId,
|
|
232
233
|
updatedAt: record.updatedAt,
|
|
234
|
+
...(RUNTIME_ERROR_CODES.has(record.blockerCode) ? { blockerCode: record.blockerCode } : {}),
|
|
233
235
|
...(UUID.test(record.missionId ?? "") ? { missionId: record.missionId } : {}),
|
|
234
236
|
...(UUID.test(record.workItemId ?? "") ? { workItemId: record.workItemId } : {}),
|
|
235
237
|
};
|
|
@@ -254,6 +256,7 @@ function eventState(event) {
|
|
|
254
256
|
if (!["connected", "working", "waiting", "blocked"].includes(phase)) return null;
|
|
255
257
|
return {
|
|
256
258
|
phase,
|
|
259
|
+
blockerCode: phase === "blocked" ? runtimeErrorCode({ code: event.code }) : undefined,
|
|
257
260
|
...(UUID.test(event.missionId ?? "") ? { missionId: event.missionId } : {}),
|
|
258
261
|
...(UUID.test(event.workItemId ?? "") ? { workItemId: event.workItemId } : {}),
|
|
259
262
|
};
|
|
@@ -311,6 +314,7 @@ export async function runAgentSession(profile, options = {}) {
|
|
|
311
314
|
phase: status.phase,
|
|
312
315
|
message: LABELS[status.phase],
|
|
313
316
|
...(status.missionId ? { missionId: status.missionId } : {}),
|
|
317
|
+
...(status.blockerCode ? { blockerCode: status.blockerCode } : {}),
|
|
314
318
|
});
|
|
315
319
|
} catch {
|
|
316
320
|
persistenceFailed = true;
|
package/bin/agent-setup.mjs
CHANGED
|
@@ -12,10 +12,12 @@ import { installedClientInvocation, localClientEnvironment } from "./platform-cl
|
|
|
12
12
|
import { inspectLocalRepository } from "./repository.mjs";
|
|
13
13
|
import { Totem } from "./sdk/index.js";
|
|
14
14
|
import { detectInstalledProvider } from "./provider-detection.mjs";
|
|
15
|
+
import { RUNTIME_ERROR_CODES } from "./runtime-diagnostics.mjs";
|
|
15
16
|
|
|
16
17
|
const execute = promisify(execFile);
|
|
17
18
|
const ORIGIN = "https://totemlayer.com";
|
|
18
19
|
const KEY_LIMIT = 8192;
|
|
20
|
+
const IDENTITY_TIMEOUT_MS = 15000;
|
|
19
21
|
|
|
20
22
|
export const AGENT_SETUP_HELP = `Connect your agent once
|
|
21
23
|
|
|
@@ -195,7 +197,8 @@ function dependencies(options) {
|
|
|
195
197
|
stdin: process.stdin,
|
|
196
198
|
stdout: process.stdout,
|
|
197
199
|
stderr: process.stderr,
|
|
198
|
-
createClient: (config) =>
|
|
200
|
+
createClient: (config) =>
|
|
201
|
+
new Totem({ ...config, timeoutMs: IDENTITY_TIMEOUT_MS, maxRetries: 0 }),
|
|
199
202
|
fetch: globalThis.fetch,
|
|
200
203
|
inspectRepository: inspectLocalRepository,
|
|
201
204
|
installMcp: installTotemMcp,
|
|
@@ -224,20 +227,43 @@ function safeOutput(value, secret) {
|
|
|
224
227
|
}
|
|
225
228
|
|
|
226
229
|
function identityError(error) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
+
if (error?.status === 401 || error?.status === 403)
|
|
231
|
+
return [
|
|
232
|
+
"The connection key was rejected. For a saved profile, rerun setup with --replace to enter a new key, or use --replace --key-file with the replacement key.",
|
|
233
|
+
"CONNECTION_KEY_REJECTED",
|
|
234
|
+
];
|
|
235
|
+
if (["TIMEOUT", "CANCELED"].includes(error?.code))
|
|
236
|
+
return [
|
|
237
|
+
"Totem identity verification timed out. Check connectivity and retry status; this does not mean the key was rejected.",
|
|
238
|
+
"IDENTITY_TIMEOUT",
|
|
239
|
+
];
|
|
240
|
+
if (error?.code === "NETWORK_ERROR")
|
|
241
|
+
return [
|
|
242
|
+
"Totem identity verification could not reach the saved origin. Check connectivity, proxy and TLS settings.",
|
|
243
|
+
"IDENTITY_NETWORK_ERROR",
|
|
244
|
+
];
|
|
245
|
+
if (error?.status >= 500)
|
|
246
|
+
return [
|
|
247
|
+
"Totem identity verification encountered a server error. Retry status; this does not mean the key was rejected.",
|
|
248
|
+
"IDENTITY_SERVER_ERROR",
|
|
249
|
+
];
|
|
250
|
+
if (error?.code === "INVALID_RESPONSE")
|
|
251
|
+
return [
|
|
252
|
+
"The saved origin returned an invalid identity response. Check the origin and server availability.",
|
|
253
|
+
"IDENTITY_INVALID_RESPONSE",
|
|
254
|
+
];
|
|
255
|
+
return [
|
|
256
|
+
"Totem identity could not be verified. Check the saved origin and server availability.",
|
|
257
|
+
"IDENTITY_VERIFICATION_FAILED",
|
|
258
|
+
];
|
|
230
259
|
}
|
|
231
260
|
|
|
232
261
|
async function verifiedIdentity(client) {
|
|
233
262
|
let identity;
|
|
234
263
|
try {
|
|
235
|
-
identity = await client.agent({ signal: AbortSignal.timeout(
|
|
264
|
+
identity = await client.agent({ signal: AbortSignal.timeout(IDENTITY_TIMEOUT_MS) });
|
|
236
265
|
} catch (error) {
|
|
237
|
-
fail(
|
|
238
|
-
identityError(error),
|
|
239
|
-
error?.status === 401 || error?.status === 403 ? "CONNECTION_KEY_REJECTED" : undefined,
|
|
240
|
-
);
|
|
266
|
+
fail(...identityError(error));
|
|
241
267
|
}
|
|
242
268
|
if (
|
|
243
269
|
!identity ||
|
|
@@ -248,7 +274,10 @@ async function verifiedIdentity(client) {
|
|
|
248
274
|
!["SANDBOX", "PRODUCTION"].includes(identity.environment) ||
|
|
249
275
|
(identity.status && !["ACTIVE", "OFFLINE"].includes(identity.status))
|
|
250
276
|
)
|
|
251
|
-
fail(
|
|
277
|
+
fail(
|
|
278
|
+
"Totem did not return an active agent identity. Check this agent in Totem.",
|
|
279
|
+
"IDENTITY_INVALID_RESPONSE",
|
|
280
|
+
);
|
|
252
281
|
return identity;
|
|
253
282
|
}
|
|
254
283
|
|
|
@@ -600,6 +629,7 @@ export async function agentStatusMain(args, options = {}) {
|
|
|
600
629
|
)
|
|
601
630
|
fail(
|
|
602
631
|
"The saved key no longer matches this profile's agent. Run setup with a separate profile or explicitly replace it.",
|
|
632
|
+
"AGENT_IDENTITY_MISMATCH",
|
|
603
633
|
);
|
|
604
634
|
checks.credential = {
|
|
605
635
|
state: "verified",
|
|
@@ -608,6 +638,9 @@ export async function agentStatusMain(args, options = {}) {
|
|
|
608
638
|
} catch (error) {
|
|
609
639
|
checks.credential = {
|
|
610
640
|
state: "failed",
|
|
641
|
+
code: error?.setupSafe
|
|
642
|
+
? (error.code ?? "IDENTITY_VERIFICATION_FAILED")
|
|
643
|
+
: "IDENTITY_VERIFICATION_FAILED",
|
|
611
644
|
message: error?.setupSafe
|
|
612
645
|
? error.message
|
|
613
646
|
: "The saved agent identity could not be verified.",
|
|
@@ -688,6 +721,7 @@ export async function agentStatusMain(args, options = {}) {
|
|
|
688
721
|
: "needs_attention";
|
|
689
722
|
checks.worker = {
|
|
690
723
|
state,
|
|
724
|
+
...(RUNTIME_ERROR_CODES.has(worker?.blockerCode) ? { code: worker.blockerCode } : {}),
|
|
691
725
|
...(phase ? { phase } : {}),
|
|
692
726
|
message:
|
|
693
727
|
state === "running"
|
|
@@ -697,7 +731,7 @@ export async function agentStatusMain(args, options = {}) {
|
|
|
697
731
|
: phase === "stopping"
|
|
698
732
|
? "The worker is stopping and preserving progress. Wait for it to stop before changing setup or restarting."
|
|
699
733
|
: phase === "blocked"
|
|
700
|
-
? "The running worker has
|
|
734
|
+
? "The running worker has an execution blocker. Check its error code; starting another worker will not resolve an uncertain run."
|
|
701
735
|
: phase === "unresponsive"
|
|
702
736
|
? "The worker process is present but its status is stale. Inspect or stop it before starting another worker."
|
|
703
737
|
: "The local worker is not confirmed ready. Check it with totem start for this profile.",
|
|
@@ -11,13 +11,17 @@ export async function runConnectedAgentService(settings, dependencies = {}) {
|
|
|
11
11
|
: controller.signal;
|
|
12
12
|
const emit = settings.emit ?? (() => {});
|
|
13
13
|
const phases = { messages: "starting", missions: "starting" };
|
|
14
|
+
const blockers = {};
|
|
14
15
|
const report = (queue, event) => {
|
|
15
16
|
phases[queue] = event.type === "completed" ? "waiting" : event.type;
|
|
17
|
+
if (event.type === "blocked") blockers[queue] = { code: event.code, messageId: event.messageId };
|
|
18
|
+
else delete blockers[queue];
|
|
16
19
|
const values = Object.values(phases);
|
|
17
20
|
const type =
|
|
18
21
|
["reconnecting", "starting", "blocked", "working"].find((phase) => values.includes(phase)) ??
|
|
19
22
|
"waiting";
|
|
20
|
-
|
|
23
|
+
const blocked = type === "blocked" ? blockers.messages ?? blockers.missions : undefined;
|
|
24
|
+
emit({ ...event, type, ...(blocked ?? {}) });
|
|
21
25
|
};
|
|
22
26
|
const messages = async () => {
|
|
23
27
|
let attempts = 0;
|
|
@@ -36,7 +40,9 @@ export async function runConnectedAgentService(settings, dependencies = {}) {
|
|
|
36
40
|
attempts = 0;
|
|
37
41
|
report("messages", { type: "waiting" });
|
|
38
42
|
} else if (event.type === "runtime_error")
|
|
39
|
-
report("messages", { type: "blocked", code: event.error?.code });
|
|
43
|
+
report("messages", { type: "blocked", code: event.error?.code, messageId: event.messageId });
|
|
44
|
+
else if (event.type === "message_started")
|
|
45
|
+
report("messages", { type: "working", messageId: event.data?.messageId });
|
|
40
46
|
else if (["message_handled", "chat_completed"].includes(event.type))
|
|
41
47
|
report("messages", { type: "waiting" });
|
|
42
48
|
},
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { modelReply } from "./network-runtime.mjs";
|
|
4
|
+
import { privateRead, privateWrite } from "./runtime-installation.mjs";
|
|
5
|
+
import { runtimeErrorCode } from "./runtime-diagnostics.mjs";
|
|
6
|
+
|
|
7
|
+
export async function verifyExecutorReady(profile, env = process.env, options = {}) {
|
|
8
|
+
const version =
|
|
9
|
+
options.version ??
|
|
10
|
+
JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
11
|
+
const file = join(profile.stateDirectory, "executor-readiness.json");
|
|
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) => {
|
|
25
|
+
if (error.code !== "ENOENT") throw error;
|
|
26
|
+
return null;
|
|
27
|
+
});
|
|
28
|
+
if (previous?.version === version && previous.binding === binding && previous.ready === true)
|
|
29
|
+
return previous;
|
|
30
|
+
let response;
|
|
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
|
+
});
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
if (typeof response?.content !== "string" || !response.content.trim())
|
|
61
|
+
throw Object.assign(new Error("The selected executor did not complete its readiness check."), {
|
|
62
|
+
code: "INVALID_AGENT_RESULT",
|
|
63
|
+
});
|
|
64
|
+
const record = { version, binding, ready: true, checkedAt: new Date().toISOString() };
|
|
65
|
+
await privateWrite(file, record);
|
|
66
|
+
return record;
|
|
67
|
+
}
|
package/bin/inbox-queue.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
package/bin/installation.mjs
CHANGED
|
@@ -17,11 +17,41 @@ export async function activateConnection(profile, env = process.env, options = {
|
|
|
17
17
|
...(profile.agentTokenEnv ? { TOTEM_AGENT_TOKEN_ENV: profile.agentTokenEnv } : {}),
|
|
18
18
|
});
|
|
19
19
|
if (!enabled) return { state: "paused", ...service, entry: runtime.cli };
|
|
20
|
+
const { verifyExecutorReady } = await import("./executor-readiness.mjs");
|
|
21
|
+
try {
|
|
22
|
+
await (options.verifyExecutor ?? verifyExecutorReady)(profile, env);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return {
|
|
25
|
+
state: "needs_attention",
|
|
26
|
+
...service,
|
|
27
|
+
entry: runtime.cli,
|
|
28
|
+
code: error.code ?? "EXECUTOR_NOT_READY",
|
|
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
|
+
};
|
|
32
|
+
}
|
|
20
33
|
const { agentStartMain } = await import("./agent-runner.mjs");
|
|
21
|
-
await (options.start ?? agentStartMain)(
|
|
34
|
+
const started = await (options.start ?? agentStartMain)(
|
|
22
35
|
["--profile", profile.profileName, "--background", "--json"],
|
|
23
36
|
{ env, automatic: true, stdout: { write() {} } },
|
|
24
37
|
);
|
|
38
|
+
if (started !== 0)
|
|
39
|
+
return {
|
|
40
|
+
state: "needs_attention",
|
|
41
|
+
...service,
|
|
42
|
+
entry: runtime.cli,
|
|
43
|
+
code: "WORKER_START_FAILED",
|
|
44
|
+
};
|
|
45
|
+
const { readAgentRuntimeStatus } = await import("./agent-runner.mjs");
|
|
46
|
+
const state = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
|
|
47
|
+
if (!state.running || !["connected", "working", "waiting"].includes(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
|
+
};
|
|
25
55
|
return { state: "running", ...service, entry: runtime.cli };
|
|
26
56
|
}
|
|
27
57
|
|
|
@@ -108,25 +138,47 @@ export async function installationMain(args = [], options = {}) {
|
|
|
108
138
|
// An upgrade reuses private profiles and starts their existing connections.
|
|
109
139
|
const { opendir } = await import("node:fs/promises");
|
|
110
140
|
let saved = false;
|
|
141
|
+
const savedProfiles = [];
|
|
111
142
|
try {
|
|
112
143
|
const profiles = await opendir(join(runtime.root, "connections"));
|
|
113
144
|
for await (const entry of profiles)
|
|
114
145
|
if (
|
|
115
146
|
entry.isDirectory() &&
|
|
116
147
|
(!environment.TOTEM_AGENT_ID || entry.name === environment.TOTEM_AGENT_ID)
|
|
117
|
-
)
|
|
148
|
+
) {
|
|
118
149
|
saved = true;
|
|
150
|
+
savedProfiles.push(entry.name);
|
|
151
|
+
}
|
|
119
152
|
} catch (error) {
|
|
120
153
|
if (error.code !== "ENOENT") throw error;
|
|
121
154
|
}
|
|
122
155
|
if (saved && options.forceLogin !== true) {
|
|
123
|
-
const
|
|
156
|
+
const { readAgentProfile } = await import("./agent-profile.mjs");
|
|
157
|
+
const results = [];
|
|
158
|
+
for (const name of savedProfiles) {
|
|
159
|
+
const profile = await readAgentProfile(name, { environment });
|
|
160
|
+
results.push({
|
|
161
|
+
profile: name,
|
|
162
|
+
...(await (options.activateConnection ?? activateConnection)(profile, environment)),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
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
|
+
};
|
|
124
174
|
stdout.write(
|
|
125
175
|
flags.json
|
|
126
176
|
? JSON.stringify(report) + "\n"
|
|
127
|
-
:
|
|
177
|
+
: ready
|
|
178
|
+
? "Totem's automatic listener reuses your saved connections; startup checks passed.\n"
|
|
179
|
+
: "Totem is installed, but an agent failed its startup check. Run totem status to inspect it.\n",
|
|
128
180
|
);
|
|
129
|
-
return 0;
|
|
181
|
+
return ready ? 0 : 1;
|
|
130
182
|
}
|
|
131
183
|
const connection = await (options.beginLogin ?? beginDeviceLogin)(runtime, environment, {
|
|
132
184
|
openBrowser: !flags["no-browser"] && environment.TOTEM_NO_BROWSER !== "1",
|