totemlayer 0.1.20 → 0.1.24
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 +31 -29
- package/bin/agent-profile.mjs +13 -0
- package/bin/agent-runner.mjs +31 -2
- package/bin/agent-setup.mjs +58 -6
- package/bin/automatic-runtime.mjs +44 -6
- package/bin/bound-login.mjs +107 -0
- package/bin/codex-chat-bridge.mjs +197 -0
- package/bin/codex-desktop-wake.mjs +22 -0
- package/bin/connected-agent-service.mjs +115 -78
- package/bin/device-login.mjs +100 -8
- package/bin/executor-readiness.mjs +97 -20
- package/bin/installation.mjs +129 -74
- package/bin/mission-agent-service.mjs +23 -5
- package/bin/mission-coordination.mjs +311 -0
- package/bin/mission-worker.mjs +271 -20
- package/bin/model-adapter.mjs +30 -8
- package/bin/network-runtime.mjs +115 -6
- package/bin/runtime-diagnostics.mjs +4 -0
- package/bin/runtime-installation.mjs +1 -0
- package/bin/sdk/action-types.d.ts +236 -0
- package/bin/sdk/actions.d.ts +19 -0
- package/bin/sdk/errors.d.ts +30 -0
- package/bin/sdk/errors.js +14 -1
- package/bin/sdk/index.d.ts +298 -0
- package/bin/sdk/index.js +55 -11
- package/bin/sdk/inspector.d.ts +40 -0
- package/bin/sdk/request-options.d.ts +13 -0
- package/bin/sdk/transport.d.ts +24 -0
- package/bin/sdk/types.d.ts +638 -0
- package/bin/totem.mjs +2 -2
- package/package.json +1 -1
- package/scripts/prepare.mjs +9 -20
package/README.md
CHANGED
|
@@ -9,32 +9,34 @@ 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
|
-
npx totemlayer
|
|
12
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem 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. 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
|
|
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 --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem login --device-login --no-browser` and open its printed connection link on your own device.
|
|
16
|
+
|
|
17
|
+
To deliver peer messages into an existing Codex desktop chat, bind that connection with `totem setup --profile <profile> --provider codex --model default --codex-thread <chat-UUID>`. Totem's installed background listener queues a new turn in that exact chat, screens peer content, and confirms the chat's actual Totem reply. The listener opens the owner-bound chat through Codex’s documented desktop route and recovers already queued messages after sleep or app reload without sending them again. Each chat uses a separate agent identity. This mode requires Codex's `queue` command and an available desktop session; other provider adapters continue to start separate provider turns. It does not change the chat's tool permissions or run the independent Missions worker for that connection.
|
|
16
18
|
|
|
17
19
|
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
20
|
|
|
19
21
|
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
22
|
|
|
21
|
-
If an executor fails, Totem keeps a redacted error privately on that computer. Retrieve it with `npx --yes totemlayer
|
|
23
|
+
If an executor fails, Totem keeps a redacted error privately on that computer. Retrieve it with `npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem diagnose --json`; no separate diagnostic download is needed. If no error has been saved yet, Windows runs the check through the installed task account. `--fresh` requests a new check, and `--profile YOUR_AGENT_UUID` selects a connection when more than one is installed. The diagnostic does not clear recovery journals or restart the existing listener.
|
|
22
24
|
|
|
23
25
|
```sh
|
|
24
|
-
npx totemlayer
|
|
25
|
-
npx totemlayer
|
|
26
|
-
npx totemlayer
|
|
26
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem status --profile YOUR_AGENT_UUID
|
|
27
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem stop --profile YOUR_AGENT_UUID
|
|
28
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem start --profile YOUR_AGENT_UUID --background
|
|
27
29
|
```
|
|
28
30
|
|
|
29
|
-
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.
|
|
31
|
+
Stop pauses automatic processing until you explicitly start it again. Upgrades reuse saved connections and journals. For optional global installation, run `npm install -g https://totemlayer.com/downloads/totemlayer-0.1.23.tgz` 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.
|
|
30
32
|
|
|
31
|
-
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
|
|
33
|
+
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 --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem login`. Provider credentials remain separate from Totem credentials. Local model selection can be pinned with `TOTEM_MODEL`.
|
|
32
34
|
|
|
33
35
|
The background service preserves custom `CODEX_HOME` and `CLAUDE_CONFIG_DIR` paths and saved proxy settings across upgrades. An explicitly empty environment setting clears its saved value.
|
|
34
36
|
|
|
35
37
|
Content screening uses local rules by default. To add your own local AI classifier, set both `TOTEM_INSPECTOR_URL` (a numeric loopback `/chat/completions` endpoint) and `TOTEM_INSPECTOR_MODEL` before installation. These settings persist in the background service; unavailable or malformed classifier responses block processing. Screening does not grant permission for an action, and no filter guarantees detection of every malicious instruction.
|
|
36
38
|
|
|
37
|
-
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
|
|
39
|
+
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 --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem status`.
|
|
38
40
|
|
|
39
41
|
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.
|
|
40
42
|
|
|
@@ -67,7 +69,7 @@ Receiver — create a five-minute challenge:
|
|
|
67
69
|
export TOTEM_URL="https://totemlayer.com"
|
|
68
70
|
export TOTEM_API_KEY="$VERIFIER_KEY"
|
|
69
71
|
|
|
70
|
-
npx totemlayer
|
|
72
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem challenge \
|
|
71
73
|
--presenter-agent "$PRESENTER_AGENT_ID" \
|
|
72
74
|
--action quote.request \
|
|
73
75
|
--request-file request.json \
|
|
@@ -84,7 +86,7 @@ Presenter — create a one-minute proof:
|
|
|
84
86
|
```bash
|
|
85
87
|
export TOTEM_AGENT_API_KEY="$PRESENTER_KEY"
|
|
86
88
|
|
|
87
|
-
npx totemlayer
|
|
89
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem present \
|
|
88
90
|
--credential-id "$CREDENTIAL_ID" \
|
|
89
91
|
--challenge-file challenge.json \
|
|
90
92
|
--json > presentation.json
|
|
@@ -95,7 +97,7 @@ Return `presentation.json` to the receiver. Receiver — consume it once:
|
|
|
95
97
|
```bash
|
|
96
98
|
export TOTEM_API_KEY="$VERIFIER_KEY"
|
|
97
99
|
|
|
98
|
-
npx totemlayer
|
|
100
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem verify \
|
|
99
101
|
--challenge-file challenge.json \
|
|
100
102
|
--presentation-file presentation.json \
|
|
101
103
|
--request-file request.json \
|
|
@@ -127,7 +129,7 @@ compatibility.
|
|
|
127
129
|
$env:TOTEM_URL = "https://totemlayer.com"
|
|
128
130
|
$env:TOTEM_API_KEY = $env:VERIFIER_KEY
|
|
129
131
|
|
|
130
|
-
npx totemlayer
|
|
132
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem challenge `
|
|
131
133
|
--presenter-agent $env:PRESENTER_AGENT_ID `
|
|
132
134
|
--action quote.request `
|
|
133
135
|
--request-file request.json `
|
|
@@ -136,14 +138,14 @@ npx totemlayer@latest challenge `
|
|
|
136
138
|
|
|
137
139
|
$env:TOTEM_AGENT_API_KEY = $env:PRESENTER_KEY
|
|
138
140
|
|
|
139
|
-
npx totemlayer
|
|
141
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem present `
|
|
140
142
|
--credential-id $env:CREDENTIAL_ID `
|
|
141
143
|
--challenge-file challenge.json `
|
|
142
144
|
--json | Set-Content -Encoding UTF8 presentation.json
|
|
143
145
|
|
|
144
146
|
$env:TOTEM_API_KEY = $env:VERIFIER_KEY
|
|
145
147
|
|
|
146
|
-
npx totemlayer
|
|
148
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem verify `
|
|
147
149
|
--challenge-file challenge.json `
|
|
148
150
|
--presentation-file presentation.json `
|
|
149
151
|
--request-file request.json `
|
|
@@ -158,7 +160,7 @@ npx totemlayer@latest verify `
|
|
|
158
160
|
|
|
159
161
|
## Procurement sandbox
|
|
160
162
|
|
|
161
|
-
`npx totemlayer
|
|
163
|
+
`npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem rfq` is a sandbox and self-hosted demonstration command. It is not
|
|
162
164
|
enabled on the production `totemlayer.com` service. Point `TOTEM_URL` at a
|
|
163
165
|
TotemLayer deployment with the procurement demo enabled and use an
|
|
164
166
|
`operator.rfq.execute` key.
|
|
@@ -171,16 +173,16 @@ legacy `AGENTTRUST_*` environment-variable aliases. New integrations should use
|
|
|
171
173
|
|
|
172
174
|
## Agent messaging and code collaboration
|
|
173
175
|
|
|
174
|
-
The local build includes `npx totemlayer
|
|
175
|
-
`npx totemlayer
|
|
176
|
-
their own Codex or Claude client to Totem MCP and opt into `npx totemlayer
|
|
176
|
+
The local build includes `npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem network` and the Totem MCP server. Run
|
|
177
|
+
`npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem network --help` for the full command list. Two owners can each connect
|
|
178
|
+
their own Codex or Claude client to Totem MCP and opt into `npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem network work`
|
|
177
179
|
for one exact peer and one Git checkout. Then one chat request can create a
|
|
178
180
|
shared coding task; the listeners pick up the peer's work and later replies in
|
|
179
181
|
each owner's own AI account. They do not share model or GitHub credentials.
|
|
180
182
|
|
|
181
183
|
The listener runs a separate local CLI session; it does not wake or reuse a
|
|
182
184
|
closed desktop chat. Both owners must give their own Git account access to the
|
|
183
|
-
repository and keep their local listener running. Run `npx totemlayer
|
|
185
|
+
repository and keep their local listener running. Run `npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem network --help`
|
|
184
186
|
for setup requirements and security boundaries.
|
|
185
187
|
|
|
186
188
|
## Connect AI apps with MCP
|
|
@@ -191,9 +193,9 @@ After installing the CLI and saving your own agent key in a
|
|
|
191
193
|
private file, let Totem add the local MCP connection to Codex or Claude Code:
|
|
192
194
|
|
|
193
195
|
```sh
|
|
194
|
-
npx totemlayer
|
|
196
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem mcp install codex --key-file /absolute/path/to/your-agent.key \
|
|
195
197
|
--base-url https://totem-network-staging.linbrandon48.workers.dev
|
|
196
|
-
# Or: npx totemlayer
|
|
198
|
+
# Or: npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem mcp install claude --key-file /absolute/path/to/your-agent.key \
|
|
197
199
|
# --base-url https://totem-network-staging.linbrandon48.workers.dev
|
|
198
200
|
```
|
|
199
201
|
|
|
@@ -203,7 +205,7 @@ entry. It passes the **file path**, never the key value, to the AI client.
|
|
|
203
205
|
Restart the AI app and ask it to call `totem_get_identity`. The CLI package must
|
|
204
206
|
remain installed for the MCP connection to run. On macOS/Linux the key file
|
|
205
207
|
must not be readable by other users (`chmod 600` works). Run
|
|
206
|
-
`npx totemlayer
|
|
208
|
+
`npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem mcp install --help` for options, including a custom server name.
|
|
207
209
|
|
|
208
210
|
The installer is included from 0.1.3 onward. Version 0.1.4 supports native
|
|
209
211
|
Windows MCP setup and on-device approval on Windows and Linux as well as macOS.
|
|
@@ -212,7 +214,7 @@ clean clone, ask the AI app to call `totem_preview_local_collaboration`, check
|
|
|
212
214
|
the exact peer/repository/base commit, then call owner-approved
|
|
213
215
|
`totem_enable_local_collaboration`. On Linux, the desktop approval uses
|
|
214
216
|
`zenity` or `kdialog`; headless sessions still use the explicit
|
|
215
|
-
`npx totemlayer
|
|
217
|
+
`npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem network work` listener. Each owner can check or stop the listener from
|
|
216
218
|
the same chat. On Windows, run both Codex and Totem in the same environment
|
|
217
219
|
(native Windows or WSL2); the native approval dialog is for Windows desktop
|
|
218
220
|
sessions. A Windows key file must be protected by its owner's filesystem ACL.
|
|
@@ -240,7 +242,7 @@ acknowledge work, and manage tasks:
|
|
|
240
242
|
```sh
|
|
241
243
|
export TOTEM_BASE_URL=http://localhost:3413
|
|
242
244
|
export TOTEM_NETWORK_API_KEY_FILE="$HOME/.totem-agent-key"
|
|
243
|
-
npx totemlayer
|
|
245
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem mcp
|
|
244
246
|
```
|
|
245
247
|
|
|
246
248
|
Add it to an MCP client without putting the credential itself in the configuration:
|
|
@@ -250,7 +252,7 @@ Add it to an MCP client without putting the credential itself in the configurati
|
|
|
250
252
|
"mcpServers": {
|
|
251
253
|
"totem": {
|
|
252
254
|
"command": "npx",
|
|
253
|
-
"args": ["--yes", "totemlayer
|
|
255
|
+
"args": ["--yes", "--package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz", "totem", "mcp"],
|
|
254
256
|
"env": {
|
|
255
257
|
"TOTEM_BASE_URL": "http://localhost:3413",
|
|
256
258
|
"TOTEM_NETWORK_API_KEY_FILE": "/absolute/path/to/private-agent.key"
|
|
@@ -267,8 +269,8 @@ uses stdio and writes protocol messages only to stdout; operational errors go to
|
|
|
267
269
|
```sh
|
|
268
270
|
export TOTEM_BASE_URL=http://localhost:3413
|
|
269
271
|
export TOTEM_NETWORK_API_KEY_FILE="$HOME/.totem-agent-key" # chmod 600 this file
|
|
270
|
-
npx totemlayer
|
|
271
|
-
npx totemlayer
|
|
272
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem network me --json
|
|
273
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem network run --provider qwen-local --qwen-url http://127.0.0.1:8087/v1 --model qwen3.5-9b
|
|
272
274
|
```
|
|
273
275
|
|
|
274
276
|
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.
|
|
@@ -293,4 +295,4 @@ MCP supplies `totem_list_missions`, `totem_get_mission`, and `totem_delegate_mis
|
|
|
293
295
|
|
|
294
296
|
## Automatic cloud startup
|
|
295
297
|
|
|
296
|
-
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
|
|
298
|
+
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 --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.23.tgz totem login --device-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-profile.mjs
CHANGED
|
@@ -187,6 +187,7 @@ async function normalizedSettings(input, directory) {
|
|
|
187
187
|
"agentTokenEnv",
|
|
188
188
|
"ollamaUrl",
|
|
189
189
|
"nimUrl",
|
|
190
|
+
"codexThreadId",
|
|
190
191
|
]);
|
|
191
192
|
if (Object.keys(input).some((key) => !allowed.has(key))) fail();
|
|
192
193
|
const baseUrl = origin(input.baseUrl);
|
|
@@ -220,6 +221,17 @@ async function normalizedSettings(input, directory) {
|
|
|
220
221
|
const endpoint = new URL(localModelEndpoint("qwen-local", { qwenUrl: input.qwenUrl }));
|
|
221
222
|
const qwenUrl = `${endpoint.origin}/v1`;
|
|
222
223
|
if (input.client !== undefined && !["codex", "claude"].includes(input.client)) fail();
|
|
224
|
+
let codexThreadId;
|
|
225
|
+
if (input.codexThreadId !== undefined) {
|
|
226
|
+
if (
|
|
227
|
+
provider !== "codex" ||
|
|
228
|
+
(input.client !== undefined && input.client !== "codex") ||
|
|
229
|
+
typeof input.codexThreadId !== "string" ||
|
|
230
|
+
!UUID.test(input.codexThreadId.toLowerCase())
|
|
231
|
+
)
|
|
232
|
+
fail();
|
|
233
|
+
codexThreadId = input.codexThreadId.toLowerCase();
|
|
234
|
+
}
|
|
223
235
|
const roots = input.repositoryRoots ?? [];
|
|
224
236
|
if (!Array.isArray(roots) || roots.length > 32) fail();
|
|
225
237
|
const ids = new Set();
|
|
@@ -267,6 +279,7 @@ async function normalizedSettings(input, directory) {
|
|
|
267
279
|
.map((key) => [key, input[key]]),
|
|
268
280
|
),
|
|
269
281
|
...(input.client === undefined ? {} : { client: input.client }),
|
|
282
|
+
...(codexThreadId === undefined ? {} : { codexThreadId }),
|
|
270
283
|
};
|
|
271
284
|
}
|
|
272
285
|
|
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 { runtimeRoot } from "./runtime-installation.mjs";
|
|
12
13
|
import { RUNTIME_ERROR_CODES, runtimeErrorCode } from "./runtime-diagnostics.mjs";
|
|
13
14
|
|
|
14
15
|
const PACKAGE_VERSION = JSON.parse(
|
|
@@ -225,6 +226,12 @@ export async function readAgentRuntimeStatus(profile, { isAlive = alive, now = D
|
|
|
225
226
|
const fresh = now() - Date.parse(record.updatedAt) <= 30_000;
|
|
226
227
|
return {
|
|
227
228
|
running: Boolean(running),
|
|
229
|
+
ready: Boolean(
|
|
230
|
+
running &&
|
|
231
|
+
fresh &&
|
|
232
|
+
record.ready === true &&
|
|
233
|
+
!["reconnecting", "starting", "failed", "stopped"].includes(record.phase),
|
|
234
|
+
),
|
|
228
235
|
phase: running
|
|
229
236
|
? fresh
|
|
230
237
|
? record.phase
|
|
@@ -235,6 +242,9 @@ export async function readAgentRuntimeStatus(profile, { isAlive = alive, now = D
|
|
|
235
242
|
runId: record.runId,
|
|
236
243
|
...(typeof record.packageVersion === "string" ? { packageVersion: record.packageVersion } : {}),
|
|
237
244
|
updatedAt: record.updatedAt,
|
|
245
|
+
...(/^[A-Z][A-Z0-9_]{0,79}$/.test(record.errorCode ?? "")
|
|
246
|
+
? { errorCode: record.errorCode }
|
|
247
|
+
: {}),
|
|
238
248
|
...(RUNTIME_ERROR_CODES.has(record.blockerCode) ? { blockerCode: record.blockerCode } : {}),
|
|
239
249
|
...(UUID.test(record.missionId ?? "") ? { missionId: record.missionId } : {}),
|
|
240
250
|
...(UUID.test(record.workItemId ?? "") ? { workItemId: record.workItemId } : {}),
|
|
@@ -242,6 +252,7 @@ export async function readAgentRuntimeStatus(profile, { isAlive = alive, now = D
|
|
|
242
252
|
}
|
|
243
253
|
|
|
244
254
|
function eventState(event) {
|
|
255
|
+
if (event?.type === "runtime_ready") return { ready: true };
|
|
245
256
|
const candidate = event?.state ?? event?.status ?? event?.type;
|
|
246
257
|
const phase =
|
|
247
258
|
typeof candidate === "string" ? candidate.replace(/^mission_agent_|^agent_/, "") : "";
|
|
@@ -387,6 +398,15 @@ export async function runAgentSession(profile, options = {}) {
|
|
|
387
398
|
await runService({
|
|
388
399
|
client,
|
|
389
400
|
expectedAgentId: profile.agentId,
|
|
401
|
+
runId,
|
|
402
|
+
verifyReadiness: true,
|
|
403
|
+
hostKind: (options.env ?? process.env).TOTEM_RUNTIME_HOST_KIND ?? "local",
|
|
404
|
+
sessionLockDirectory: join(runtimeRoot(options.env ?? process.env), "agent-runtime-locks"),
|
|
405
|
+
profileName: profile.profileName,
|
|
406
|
+
profileConfigDirectory: dirname(dirname(dirname(dirname(profile.stateDirectory)))),
|
|
407
|
+
address: profile.address,
|
|
408
|
+
environment: profile.environment,
|
|
409
|
+
...(profile.codexThreadId === undefined ? {} : { codexThreadId: profile.codexThreadId }),
|
|
390
410
|
baseUrl: profile.baseUrl,
|
|
391
411
|
stateDirectory: profile.stateDirectory,
|
|
392
412
|
repositoryBindings: profile.repositoryRoots.map(({ repositoryId, root }) => ({
|
|
@@ -414,7 +434,14 @@ export async function runAgentSession(profile, options = {}) {
|
|
|
414
434
|
}
|
|
415
435
|
if (persistenceFailed || stopReadFailed)
|
|
416
436
|
throw new Error("Agent status could not be saved; the worker was stopped.");
|
|
417
|
-
} catch {
|
|
437
|
+
} catch (error) {
|
|
438
|
+
emit({
|
|
439
|
+
errorCode: /^[A-Z][A-Z0-9_]{0,79}$/.test(error?.code ?? "")
|
|
440
|
+
? error.code
|
|
441
|
+
: [401, 403].includes(error?.status)
|
|
442
|
+
? "RUNTIME_AUTHORIZATION_REJECTED"
|
|
443
|
+
: "RUNTIME_START_FAILED",
|
|
444
|
+
});
|
|
418
445
|
result = 1;
|
|
419
446
|
controller.abort();
|
|
420
447
|
} finally {
|
|
@@ -518,6 +545,8 @@ export async function agentStartMain(args, options = {}) {
|
|
|
518
545
|
env[profile.agentTokenEnv] = environment[profile.agentTokenEnv];
|
|
519
546
|
delete env.TOTEM_NETWORK_API_KEY_FILE;
|
|
520
547
|
delete env.TOTEM_BASE_URL;
|
|
548
|
+
if (["local", "hosted"].includes(environment.TOTEM_RUNTIME_HOST_KIND))
|
|
549
|
+
env.TOTEM_RUNTIME_HOST_KIND = environment.TOTEM_RUNTIME_HOST_KIND;
|
|
521
550
|
const child = (options.spawnProcess ?? spawn)(
|
|
522
551
|
process.execPath,
|
|
523
552
|
[CLI_PATH, "start", "--profile", parsed.profile, "--run-id", runId, "--json"],
|
|
@@ -530,7 +559,7 @@ export async function agentStartMain(args, options = {}) {
|
|
|
530
559
|
});
|
|
531
560
|
child.unref();
|
|
532
561
|
try {
|
|
533
|
-
const startupTimeoutMs = options.startupTimeoutMs ??
|
|
562
|
+
const startupTimeoutMs = options.startupTimeoutMs ?? 60_000;
|
|
534
563
|
if (
|
|
535
564
|
!Number.isInteger(startupTimeoutMs) ||
|
|
536
565
|
startupTimeoutMs < 10 ||
|
package/bin/agent-setup.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { inspectLocalRepository } from "./repository.mjs";
|
|
|
13
13
|
import { Totem } from "./sdk/index.js";
|
|
14
14
|
import { detectInstalledProvider } from "./provider-detection.mjs";
|
|
15
15
|
import { RUNTIME_ERROR_CODES } from "./runtime-diagnostics.mjs";
|
|
16
|
+
import { checkCodexQueueSupport, CODEX_QUEUE_UNAVAILABLE_MESSAGE } from "./executor-readiness.mjs";
|
|
16
17
|
|
|
17
18
|
const execute = promisify(execFile);
|
|
18
19
|
const ORIGIN = "https://totemlayer.com";
|
|
@@ -27,6 +28,7 @@ export const AGENT_SETUP_HELP = `Connect your agent once
|
|
|
27
28
|
[--qwen-url http://127.0.0.1:8087/v1] [--install-mcp] [--replace] [--json]
|
|
28
29
|
[--command /absolute/agent --command-arg <argument>]...
|
|
29
30
|
[--agent-url https://your-agent.example/run] [--ollama-url <origin>] [--nim-url <origin>]
|
|
31
|
+
[--codex-thread <owner-selected-chat-UUID>]
|
|
30
32
|
totem status [--profile default] [--json]
|
|
31
33
|
|
|
32
34
|
In a terminal, setup can ask for a hidden connection key. Otherwise use --key-file
|
|
@@ -38,6 +40,8 @@ Keys are never accepted as command arguments. Repeating unchanged setup is safe.
|
|
|
38
40
|
Setup automatically detects the installed executor, connects its MCP tools, and
|
|
39
41
|
starts a supervised listener for messages, human Chat, and approved Missions.
|
|
40
42
|
Use --no-start or --no-mcp to opt out. Setup never downloads a model automatically.
|
|
43
|
+
--codex-thread binds incoming messages to the owner's existing Codex chat.
|
|
44
|
+
It is available only with --provider codex and is preserved by later setup runs.
|
|
41
45
|
Status only reads readiness; it never claims work or sends a heartbeat.
|
|
42
46
|
`;
|
|
43
47
|
|
|
@@ -70,6 +74,7 @@ function parse(args, status = false) {
|
|
|
70
74
|
"command-arg": { type: "string", multiple: true },
|
|
71
75
|
"agent-url": { type: "string" },
|
|
72
76
|
"agent-token-env": { type: "string" },
|
|
77
|
+
"codex-thread": { type: "string" },
|
|
73
78
|
"install-mcp": { type: "boolean" },
|
|
74
79
|
"no-start": { type: "boolean" },
|
|
75
80
|
"no-mcp": { type: "boolean" },
|
|
@@ -92,6 +97,11 @@ function parse(args, status = false) {
|
|
|
92
97
|
!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(options["agent-id"])
|
|
93
98
|
)
|
|
94
99
|
fail("The expected agent ID must be a valid UUID.");
|
|
100
|
+
if (
|
|
101
|
+
options["codex-thread"] !== undefined &&
|
|
102
|
+
!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(options["codex-thread"])
|
|
103
|
+
)
|
|
104
|
+
fail("The owner-selected Codex chat ID must be a valid UUID.");
|
|
95
105
|
return options;
|
|
96
106
|
}
|
|
97
107
|
|
|
@@ -209,12 +219,14 @@ function dependencies(options) {
|
|
|
209
219
|
process.platform === "win32"
|
|
210
220
|
? installedClientInvocation(client, args, environment)
|
|
211
221
|
: { command: client, args };
|
|
212
|
-
|
|
222
|
+
const pending = execute(invocation.command, invocation.args, {
|
|
213
223
|
env: localClientEnvironment(environment),
|
|
214
224
|
timeout: 5000,
|
|
215
225
|
maxBuffer: 64 * 1024,
|
|
216
226
|
encoding: "utf8",
|
|
217
227
|
});
|
|
228
|
+
pending.child?.stdin?.end();
|
|
229
|
+
return pending;
|
|
218
230
|
},
|
|
219
231
|
...options,
|
|
220
232
|
};
|
|
@@ -343,6 +355,10 @@ export async function agentSetupMain(args, options = {}) {
|
|
|
343
355
|
)
|
|
344
356
|
)
|
|
345
357
|
fail("Choose qwen-local, ollama-local, nim-local, codex, claude, command, or http.");
|
|
358
|
+
if (flags["codex-thread"] !== undefined && mode !== "codex")
|
|
359
|
+
fail("Choose --provider codex before binding an existing Codex chat.");
|
|
360
|
+
const codexThreadId =
|
|
361
|
+
mode === "codex" ? (flags["codex-thread"] ?? previous?.codexThreadId) : undefined;
|
|
346
362
|
const adapterSettings = Object.fromEntries(
|
|
347
363
|
[
|
|
348
364
|
["command", flags.command ?? previous?.command],
|
|
@@ -445,6 +461,7 @@ export async function agentSetupMain(args, options = {}) {
|
|
|
445
461
|
qwenUrl,
|
|
446
462
|
repositoryRoots,
|
|
447
463
|
...(["codex", "claude"].includes(mode) ? { client: mode } : {}),
|
|
464
|
+
...(codexThreadId === undefined ? {} : { codexThreadId }),
|
|
448
465
|
},
|
|
449
466
|
key,
|
|
450
467
|
{ environment: io.env, replace: flags.replace === true },
|
|
@@ -524,6 +541,14 @@ async function executorReadiness(profile, io) {
|
|
|
524
541
|
provider === "codex" ? ["login", "status"] : ["auth", "status"],
|
|
525
542
|
io.env,
|
|
526
543
|
);
|
|
544
|
+
if (provider === "codex" && profile.codexThreadId) {
|
|
545
|
+
await checkCodexQueueSupport(io.env, { runClient: io.runClient });
|
|
546
|
+
return {
|
|
547
|
+
state: "available",
|
|
548
|
+
message:
|
|
549
|
+
"Codex is installed, signed in, and supports queueing messages to the selected chat.",
|
|
550
|
+
};
|
|
551
|
+
}
|
|
527
552
|
return {
|
|
528
553
|
state: "available",
|
|
529
554
|
message:
|
|
@@ -543,7 +568,13 @@ async function executorReadiness(profile, io) {
|
|
|
543
568
|
message:
|
|
544
569
|
"The owner-selected HTTPS agent endpoint is configured. Its availability is checked when work runs.",
|
|
545
570
|
};
|
|
546
|
-
} catch {
|
|
571
|
+
} catch (error) {
|
|
572
|
+
if (error?.code === "AGENT_CLI_INCOMPATIBLE")
|
|
573
|
+
return {
|
|
574
|
+
state: "unavailable",
|
|
575
|
+
code: "AGENT_CLI_INCOMPATIBLE",
|
|
576
|
+
message: CODEX_QUEUE_UNAVAILABLE_MESSAGE,
|
|
577
|
+
};
|
|
547
578
|
return {
|
|
548
579
|
state: "unavailable",
|
|
549
580
|
message:
|
|
@@ -707,7 +738,9 @@ export async function agentStatusMain(args, options = {}) {
|
|
|
707
738
|
];
|
|
708
739
|
const phase = phases.includes(worker?.phase) ? worker.phase : undefined;
|
|
709
740
|
const state = phase
|
|
710
|
-
? worker.running === true &&
|
|
741
|
+
? worker.running === true &&
|
|
742
|
+
worker.ready === true &&
|
|
743
|
+
["connected", "working", "waiting"].includes(phase)
|
|
711
744
|
? "running"
|
|
712
745
|
: phase === "starting" && worker.running === true
|
|
713
746
|
? "starting"
|
|
@@ -717,15 +750,31 @@ export async function agentStatusMain(args, options = {}) {
|
|
|
717
750
|
: ["running", "stopped", "starting", "needs_attention", "not_started"].includes(
|
|
718
751
|
worker?.state,
|
|
719
752
|
)
|
|
720
|
-
? worker.state
|
|
753
|
+
? worker.state === "running" && worker.ready !== true
|
|
754
|
+
? "needs_attention"
|
|
755
|
+
: worker.state
|
|
721
756
|
: "needs_attention";
|
|
722
757
|
checks.worker = {
|
|
723
758
|
state,
|
|
724
759
|
...(RUNTIME_ERROR_CODES.has(worker?.blockerCode) ? { code: worker.blockerCode } : {}),
|
|
760
|
+
...(typeof worker.errorCode === "string" &&
|
|
761
|
+
/^[A-Z][A-Z0-9_]{0,79}$/.test(worker.errorCode)
|
|
762
|
+
? { errorCode: worker.errorCode }
|
|
763
|
+
: {}),
|
|
764
|
+
...(worker.errorCode
|
|
765
|
+
? {
|
|
766
|
+
recovery:
|
|
767
|
+
worker.errorCode === "READINESS_UNSUPPORTED"
|
|
768
|
+
? "Existing-chat queueing has no verified receive/wake/reply probe. Configure an isolated supported adapter before claiming readiness."
|
|
769
|
+
: worker.errorCode === "RUNTIME_AUTHORIZATION_REJECTED"
|
|
770
|
+
? "Reauthorize this agent with totem login --reauthorize. Never copy another agent's key."
|
|
771
|
+
: `Check ${profile.provider === "codex" ? "codex login status and the installed Codex CLI version" : profile.provider === "claude" ? "claude auth status and the installed Claude CLI version" : "the configured adapter and model endpoint"}, then restart this profile.`,
|
|
772
|
+
}
|
|
773
|
+
: {}),
|
|
725
774
|
...(phase ? { phase } : {}),
|
|
726
775
|
message:
|
|
727
776
|
state === "running"
|
|
728
|
-
? "The
|
|
777
|
+
? "The listener passed its controlled receive, wake and reply check and reports a current process heartbeat."
|
|
729
778
|
: phase === "reconnecting"
|
|
730
779
|
? "The worker is reconnecting to Totem. Check server availability and let this process retry; do not start another worker."
|
|
731
780
|
: phase === "stopping"
|
|
@@ -744,14 +793,17 @@ export async function agentStatusMain(args, options = {}) {
|
|
|
744
793
|
}
|
|
745
794
|
}
|
|
746
795
|
const ok = checks.credential.state === "verified";
|
|
747
|
-
const
|
|
796
|
+
const executionReady =
|
|
748
797
|
ok && checks.model.state === "available" && checks.worker.state === "running";
|
|
798
|
+
const chatBound = Boolean(profile.codexThreadId);
|
|
799
|
+
const missionsReady = !chatBound && executionReady;
|
|
749
800
|
const report = {
|
|
750
801
|
ok,
|
|
751
802
|
profile: profile.profileName,
|
|
752
803
|
agent: profile.address,
|
|
753
804
|
origin: profile.baseUrl,
|
|
754
805
|
checks,
|
|
806
|
+
...(chatBound ? { chatReady: executionReady } : {}),
|
|
755
807
|
missionsReady,
|
|
756
808
|
codingReady: missionsReady && checks.repositories.state === "verified",
|
|
757
809
|
};
|
|
@@ -30,7 +30,14 @@ export async function automaticEnabled(profile) {
|
|
|
30
30
|
export async function superviseOnce(env = process.env, options = {}) {
|
|
31
31
|
const directory = join(runtimeRoot(env), "runtime");
|
|
32
32
|
await Promise.resolve((options.resumeLogin ?? resumeDeviceLogin)({ directory }, env)).catch(
|
|
33
|
-
() => {
|
|
33
|
+
async () => {
|
|
34
|
+
await privateWrite(join(directory, "setup-health.json"), {
|
|
35
|
+
state: "error",
|
|
36
|
+
code: "SETUP_NEEDS_ATTENTION",
|
|
37
|
+
recovery: "Run totem login to inspect authorization and adapter setup.",
|
|
38
|
+
updatedAt: new Date().toISOString(),
|
|
39
|
+
});
|
|
40
|
+
},
|
|
34
41
|
);
|
|
35
42
|
let profiles;
|
|
36
43
|
try {
|
|
@@ -42,8 +49,10 @@ export async function superviseOnce(env = process.env, options = {}) {
|
|
|
42
49
|
const results = [];
|
|
43
50
|
for await (const entry of profiles) {
|
|
44
51
|
if (!entry.isDirectory()) continue;
|
|
52
|
+
let profile,
|
|
53
|
+
attempts = 0;
|
|
45
54
|
try {
|
|
46
|
-
|
|
55
|
+
profile = await readAgentProfile(entry.name, { environment: env });
|
|
47
56
|
if (!(await automaticEnabled(profile))) continue;
|
|
48
57
|
const state = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
|
|
49
58
|
if (state.running) {
|
|
@@ -62,14 +71,43 @@ export async function superviseOnce(env = process.env, options = {}) {
|
|
|
62
71
|
}
|
|
63
72
|
if (!stopped || !(await automaticEnabled(profile))) continue;
|
|
64
73
|
}
|
|
74
|
+
const health = await privateRead(join(profile.stateDirectory, "automatic-health.json")).catch(
|
|
75
|
+
(error) => {
|
|
76
|
+
if (error.code !== "ENOENT") throw error;
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
attempts = Number.isSafeInteger(health?.attempts)
|
|
80
|
+
? Math.max(0, Math.min(health.attempts, 10))
|
|
81
|
+
: 0;
|
|
82
|
+
if (Number.isFinite(health?.retryAt) && health.retryAt > Date.now()) continue;
|
|
65
83
|
// The run-local journal decides whether any interrupted work can safely resume.
|
|
66
|
-
await (options.start ?? agentStartMain)(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
const result = await (options.start ?? agentStartMain)(
|
|
85
|
+
["--profile", entry.name, "--background", "--json"],
|
|
86
|
+
{
|
|
87
|
+
env,
|
|
88
|
+
automatic: true,
|
|
89
|
+
stdout: { write() {} },
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
if (result) throw new Error("START_FAILED");
|
|
93
|
+
await privateWrite(join(profile.stateDirectory, "automatic-health.json"), {
|
|
94
|
+
state: "running",
|
|
95
|
+
attempts: 0,
|
|
96
|
+
retryAt: null,
|
|
97
|
+
updatedAt: new Date().toISOString(),
|
|
70
98
|
});
|
|
71
99
|
results.push(entry.name);
|
|
72
100
|
} catch {
|
|
101
|
+
if (profile)
|
|
102
|
+
await privateWrite(join(profile.stateDirectory, "automatic-health.json"), {
|
|
103
|
+
state: "error",
|
|
104
|
+
code: "LISTENER_START_FAILED",
|
|
105
|
+
attempts: attempts + 1,
|
|
106
|
+
retryAt: Date.now() + Math.min(300000, 5000 * 2 ** attempts),
|
|
107
|
+
recovery:
|
|
108
|
+
"Inspect totem status. Reauthorize expired/revoked credentials with totem login --reauthorize, or repair the selected adapter.",
|
|
109
|
+
updatedAt: new Date().toISOString(),
|
|
110
|
+
}).catch(() => {});
|
|
73
111
|
/* One offline/revoked connection cannot stop other profiles. */
|
|
74
112
|
}
|
|
75
113
|
}
|