totemlayer 0.1.66 → 0.1.67
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 +4 -4
- package/bin/agent-runner.mjs +91 -1
- package/bin/agent-setup.mjs +7 -1
- package/bin/automatic-runtime.mjs +19 -7
- package/bin/connected-agent-service.mjs +1 -1
- package/bin/installation.mjs +12 -8
- package/bin/native/darwin-lifetime +0 -0
- package/bin/native/darwin-lifetime.json +1 -1
- package/bin/network-runtime.mjs +1 -0
- package/bin/runtime-diagnostics.mjs +3 -0
- package/bin/runtime-installation.mjs +28 -9
- package/bin/startup-diagnostics.mjs +13 -0
- package/bin/totem.mjs +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ To configure execution later, rerun setup with `--provider` and the adapter's se
|
|
|
27
27
|
|
|
28
28
|
## Connect with automatic listener setup
|
|
29
29
|
|
|
30
|
-
Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.
|
|
30
|
+
Configure a local MCP server using command `npx` and arguments `--yes`, `--ignore-scripts`, `--package=https://totemlayer.com/downloads/totemlayer-0.1.67.tgz`, `totem`, `connect`, `--provider`, and `codex` (or `claude` for Claude Code). The host runs the connection itself; the owner does not run a separate listener command. Node.js 22.14 or newer and the selected signed-in client must be available.
|
|
31
31
|
|
|
32
32
|
During browser sign-in, `totem_get_connection_status` reports progress. Normal tools appear after the approved identity is pinned privately. Codex verifies identity, startup and the saved chat callback before receiving work; reconnect preserves bindings and pauses. Missing capabilities leave work queued. A remote HTTP connector cannot install local software.
|
|
33
33
|
|
|
@@ -45,7 +45,7 @@ For a hosted connection, add these settings to your client's MCP configuration:
|
|
|
45
45
|
For a local stdio connection, install the CLI with Node.js 22.14 or newer:
|
|
46
46
|
|
|
47
47
|
```sh
|
|
48
|
-
npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.
|
|
48
|
+
npm install --global --ignore-scripts https://totemlayer.com/downloads/totemlayer-0.1.67.tgz
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
Save your agent's connection key in a private file. Protect it with mode 0600 on macOS/Linux or your user's filesystem ACL on Windows. Configure these fields in your client's MCP settings:
|
|
@@ -64,7 +64,7 @@ Reload the client if needed, discover the available tools, and call `totem_get_i
|
|
|
64
64
|
Run the guided installer on the host where your agent will execute work:
|
|
65
65
|
|
|
66
66
|
```sh
|
|
67
|
-
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.
|
|
67
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.67.tgz totem install
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
The installer opens Totem sign-in and saves the approved connection privately. It checks your configured agent runtime, configures supported clients, and starts the listener when an authorized executor is available. The supervised listener is enabled by default for this setup and starts when you sign in to the computer on hosts with a supported user service manager. It recovers after a process exit and retries temporary connection failures. An explicit stop stays paused across restarts.
|
|
@@ -74,7 +74,7 @@ For manual setup, use `totem mcp install` with a supported client and the privat
|
|
|
74
74
|
For a host without a browser:
|
|
75
75
|
|
|
76
76
|
```sh
|
|
77
|
-
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.
|
|
77
|
+
npx --yes --package=https://totemlayer.com/downloads/totemlayer-0.1.67.tgz totem login --device-login --no-browser
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
Open the printed link on your own device and compare the connection code. Preserve existing MCP entries; inspect conflicts before replacing them. If an executor is unavailable, the saved connection remains available while you complete runtime setup.
|
package/bin/agent-runner.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { processIdentity } from "./runtime-lock.mjs";
|
|
1
2
|
import { startupDiagnostic } from "./startup-diagnostics.mjs";
|
|
2
3
|
import { spawn } from "node:child_process";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -225,11 +226,25 @@ export async function readAgentRuntimeStatus(profile, { isAlive = alive, now = D
|
|
|
225
226
|
const record = running ?? records[0];
|
|
226
227
|
if (!record) return { running: false, phase: "not_started" };
|
|
227
228
|
const fresh = now() - Date.parse(record.updatedAt) <= 30_000;
|
|
229
|
+
const receiving =
|
|
230
|
+
record.listenerHealthRequired !== true ||
|
|
231
|
+
(typeof record.listenerHeartbeatAt === "string" &&
|
|
232
|
+
Number.isFinite(Date.parse(record.listenerHeartbeatAt)) &&
|
|
233
|
+
now() - Date.parse(record.listenerHeartbeatAt) >= -5000 &&
|
|
234
|
+
now() - Date.parse(record.listenerHeartbeatAt) <= 30_000);
|
|
228
235
|
return {
|
|
229
236
|
running: Boolean(running),
|
|
237
|
+
listenerReady: Boolean(
|
|
238
|
+
running &&
|
|
239
|
+
fresh &&
|
|
240
|
+
receiving &&
|
|
241
|
+
record.ready &&
|
|
242
|
+
!["reconnecting", "starting", "failed", "stopped"].includes(record.phase),
|
|
243
|
+
),
|
|
230
244
|
receiveWakeReplyVerified: Boolean(
|
|
231
245
|
running &&
|
|
232
246
|
fresh &&
|
|
247
|
+
receiving &&
|
|
233
248
|
record.ready === true &&
|
|
234
249
|
!["reconnecting", "starting", "failed", "stopped"].includes(record.phase),
|
|
235
250
|
),
|
|
@@ -238,6 +253,7 @@ export async function readAgentRuntimeStatus(profile, { isAlive = alive, now = D
|
|
|
238
253
|
ready: Boolean(
|
|
239
254
|
running &&
|
|
240
255
|
fresh &&
|
|
256
|
+
receiving &&
|
|
241
257
|
record.ready === true &&
|
|
242
258
|
!["blocked", "reconnecting", "starting", "failed", "stopped"].includes(record.phase),
|
|
243
259
|
),
|
|
@@ -251,6 +267,7 @@ export async function readAgentRuntimeStatus(profile, { isAlive = alive, now = D
|
|
|
251
267
|
runId: record.runId,
|
|
252
268
|
...(typeof record.packageVersion === "string" ? { packageVersion: record.packageVersion } : {}),
|
|
253
269
|
updatedAt: record.updatedAt,
|
|
270
|
+
...(record.listenerHeartbeatAt ? { listenerHeartbeatAt: record.listenerHeartbeatAt } : {}),
|
|
254
271
|
...(/^[A-Z][A-Z0-9_]{0,79}$/.test(record.errorCode ?? "")
|
|
255
272
|
? { errorCode: record.errorCode }
|
|
256
273
|
: {}),
|
|
@@ -261,8 +278,57 @@ export async function readAgentRuntimeStatus(profile, { isAlive = alive, now = D
|
|
|
261
278
|
};
|
|
262
279
|
}
|
|
263
280
|
|
|
281
|
+
/** Recover only this run's verified worker; never signal a reused or unrelated PID. */
|
|
282
|
+
export async function stopStalledAgent(profile, runId, options = {}) {
|
|
283
|
+
if (!UUID.test(runId)) throw new Error("Invalid stalled run identifier.");
|
|
284
|
+
const readStatus = options.readStatus ?? readAgentRuntimeStatus;
|
|
285
|
+
const identity = options.processIdentity ?? processIdentity;
|
|
286
|
+
const signal = options.processSignal ?? process.kill;
|
|
287
|
+
const current = await readStatus(profile);
|
|
288
|
+
if (!current.running || current.runId !== runId || current.phase !== "unresponsive") return false;
|
|
289
|
+
const directory = await runtimeDirectory(profile);
|
|
290
|
+
const record = await readPrivateJson(join(directory, `${runId}.json`));
|
|
291
|
+
const command = `${CLI_PATH} start --profile ${profile.profileName} --run-id ${runId} --json`;
|
|
292
|
+
const owned = async () => {
|
|
293
|
+
const actual = await identity(record.pid);
|
|
294
|
+
if (!actual) return false;
|
|
295
|
+
if (
|
|
296
|
+
record.pid === process.pid ||
|
|
297
|
+
record.runId !== runId ||
|
|
298
|
+
record.agentId !== profile.agentId ||
|
|
299
|
+
record.baseUrl !== profile.baseUrl ||
|
|
300
|
+
typeof record.processIdentity !== "string" ||
|
|
301
|
+
actual !== record.processIdentity ||
|
|
302
|
+
!actual.replaceAll('"', "").includes(command)
|
|
303
|
+
)
|
|
304
|
+
throw Object.assign(new Error("The stalled worker's ownership could not be verified."), {
|
|
305
|
+
code: "LISTENER_PROCESS_UNVERIFIED",
|
|
306
|
+
});
|
|
307
|
+
return true;
|
|
308
|
+
};
|
|
309
|
+
if (!(await owned())) return true;
|
|
310
|
+
await requestStop(profile, runId);
|
|
311
|
+
if (!(await owned())) return true;
|
|
312
|
+
signal(record.pid, "SIGTERM");
|
|
313
|
+
if (process.platform !== "win32" && (await owned())) signal(record.pid, "SIGCONT");
|
|
314
|
+
const deadline = Date.now() + (options.stopTimeoutMs ?? 3000);
|
|
315
|
+
while (Date.now() < deadline) {
|
|
316
|
+
const state = await readStatus(profile);
|
|
317
|
+
if (!state.running || state.runId !== runId || !(await owned())) return true;
|
|
318
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
319
|
+
}
|
|
320
|
+
const state = await readStatus(profile);
|
|
321
|
+
if (!state.running || state.runId !== runId || !(await owned())) return true;
|
|
322
|
+
// A fresh heartbeat means the worker recovered; leave it and its journal intact.
|
|
323
|
+
if (state.phase !== "unresponsive") return false;
|
|
324
|
+
if (await owned()) signal(record.pid, "SIGKILL");
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
|
|
264
328
|
function eventState(event) {
|
|
265
|
-
if (event?.type === "runtime_ready")
|
|
329
|
+
if (event?.type === "runtime_ready")
|
|
330
|
+
return { ready: true, listenerHeartbeatAt: new Date().toISOString() };
|
|
331
|
+
if (event?.type === "runtime_heartbeat") return { listenerHeartbeatAt: new Date().toISOString() };
|
|
266
332
|
const candidate = event?.state ?? event?.status ?? event?.type;
|
|
267
333
|
const phase =
|
|
268
334
|
typeof candidate === "string" ? candidate.replace(/^mission_agent_|^agent_/, "") : "";
|
|
@@ -328,6 +394,8 @@ export async function runAgentSession(profile, options = {}) {
|
|
|
328
394
|
baseUrl: profile.baseUrl,
|
|
329
395
|
startedAt: new Date().toISOString(),
|
|
330
396
|
updatedAt: new Date().toISOString(),
|
|
397
|
+
processIdentity: await (options.processIdentity ?? processIdentity)(process.pid),
|
|
398
|
+
listenerHealthRequired: true,
|
|
331
399
|
phase: "starting",
|
|
332
400
|
};
|
|
333
401
|
let writes = Promise.resolve();
|
|
@@ -383,7 +451,25 @@ export async function runAgentSession(profile, options = {}) {
|
|
|
383
451
|
}
|
|
384
452
|
let polling;
|
|
385
453
|
let stopReadFailed = false;
|
|
454
|
+
let listenerExpired = false;
|
|
455
|
+
const listenerWatchdogMs = options.listenerWatchdogMs ?? 30_000;
|
|
456
|
+
if (
|
|
457
|
+
!Number.isInteger(listenerWatchdogMs) ||
|
|
458
|
+
listenerWatchdogMs < 50 ||
|
|
459
|
+
listenerWatchdogMs > 120_000
|
|
460
|
+
)
|
|
461
|
+
throw new Error("Invalid listener watchdog deadline.");
|
|
386
462
|
const poll = async () => {
|
|
463
|
+
if (status.ready && Date.now() - Date.parse(status.listenerHeartbeatAt) > listenerWatchdogMs) {
|
|
464
|
+
listenerExpired = true;
|
|
465
|
+
status = {
|
|
466
|
+
...status,
|
|
467
|
+
ready: false,
|
|
468
|
+
phase: "reconnecting",
|
|
469
|
+
errorCode: "LISTENER_HEARTBEAT_TIMEOUT",
|
|
470
|
+
};
|
|
471
|
+
controller.abort();
|
|
472
|
+
}
|
|
387
473
|
try {
|
|
388
474
|
const request = await readPrivateJson(join(directory, `${runId}.stop`));
|
|
389
475
|
if (
|
|
@@ -463,6 +549,10 @@ export async function runAgentSession(profile, options = {}) {
|
|
|
463
549
|
},
|
|
464
550
|
});
|
|
465
551
|
}
|
|
552
|
+
if (listenerExpired)
|
|
553
|
+
throw Object.assign(new Error("The listener stopped renewing its receive heartbeat."), {
|
|
554
|
+
code: "LISTENER_HEARTBEAT_TIMEOUT",
|
|
555
|
+
});
|
|
466
556
|
if (persistenceFailed || stopReadFailed)
|
|
467
557
|
throw new Error("Agent status could not be saved; the worker was stopped.");
|
|
468
558
|
} catch (error) {
|
package/bin/agent-setup.mjs
CHANGED
|
@@ -551,7 +551,13 @@ export async function agentSetupMain(args, options = {}) {
|
|
|
551
551
|
? `Connection saved. Automatic receiving is not configured. To add an executor, run totem setup --profile ${profile.profileName} --provider <provider> --replace with its settings.`
|
|
552
552
|
: `Run totem status --profile ${profile.profileName} to check your connection and automatic listener.`;
|
|
553
553
|
const report = {
|
|
554
|
-
ok:
|
|
554
|
+
ok:
|
|
555
|
+
(!mcp || mcp.state !== "needs_attention") &&
|
|
556
|
+
(connectionOnly || flags["no-start"] || io.deferActivation
|
|
557
|
+
? true
|
|
558
|
+
: automatic?.ready === true &&
|
|
559
|
+
automatic?.persistent === true &&
|
|
560
|
+
automatic?.running === true),
|
|
555
561
|
profile: profile.profileName,
|
|
556
562
|
agent: identity.address,
|
|
557
563
|
origin: baseUrl,
|
|
@@ -9,12 +9,13 @@ import { setTimeout as sleep } from "node:timers/promises";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { Totem } from "./sdk/index.js";
|
|
11
11
|
import { readAgentCredential, readAgentProfile } from "./agent-profile.mjs";
|
|
12
|
-
import { agentStartMain, readAgentRuntimeStatus, requestStop } from "./agent-runner.mjs";
|
|
13
12
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
agentStartMain,
|
|
14
|
+
readAgentRuntimeStatus,
|
|
15
|
+
requestStop,
|
|
16
|
+
stopStalledAgent,
|
|
17
|
+
} from "./agent-runner.mjs";
|
|
18
|
+
import { privateRead, privateWrite, runtimeRoot } from "./runtime-installation.mjs";
|
|
18
19
|
import { resumeDeviceLogin } from "./device-login.mjs";
|
|
19
20
|
|
|
20
21
|
const PACKAGE_VERSION = JSON.parse(
|
|
@@ -93,7 +94,13 @@ export async function superviseOnce(env = process.env, options = {}) {
|
|
|
93
94
|
await reportActivations(activationClient, activationIds, "paused");
|
|
94
95
|
continue;
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
+
let state = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
|
|
98
|
+
if (state.running && state.phase === "unresponsive") {
|
|
99
|
+
await (options.recoverStalled ?? stopStalledAgent)(profile, state.runId);
|
|
100
|
+
state = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
|
|
101
|
+
if (state.running) continue;
|
|
102
|
+
if (!(await automaticEnabled(profile))) continue;
|
|
103
|
+
}
|
|
97
104
|
if (state.running && state.packageVersion === (options.packageVersion ?? PACKAGE_VERSION))
|
|
98
105
|
await reportActivations(
|
|
99
106
|
activationClient,
|
|
@@ -150,13 +157,18 @@ export async function superviseOnce(env = process.env, options = {}) {
|
|
|
150
157
|
throw Object.assign(new Error("START_FAILED"), failure);
|
|
151
158
|
}
|
|
152
159
|
const started = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
|
|
160
|
+
if (!started.running)
|
|
161
|
+
throw Object.assign(
|
|
162
|
+
new Error("The listener process did not remain running after startup."),
|
|
163
|
+
{ code: "LISTENER_START_FAILED" },
|
|
164
|
+
);
|
|
153
165
|
await reportActivations(
|
|
154
166
|
activationClient,
|
|
155
167
|
activationIds,
|
|
156
168
|
started.ready ? "ready" : "starting",
|
|
157
169
|
);
|
|
158
170
|
await privateWrite(join(profile.stateDirectory, "automatic-health.json"), {
|
|
159
|
-
state: "running",
|
|
171
|
+
state: started.ready ? "running" : "starting",
|
|
160
172
|
activationIds,
|
|
161
173
|
attempts: 0,
|
|
162
174
|
retryAt: null,
|
|
@@ -76,7 +76,7 @@ export async function runConnectedAgentService(settings, dependencies = {}) {
|
|
|
76
76
|
replyTimeoutMs: 90000,
|
|
77
77
|
pollMs: settings.pollMs ?? DEFAULT_LISTENER_POLL_MS,
|
|
78
78
|
emit(event) {
|
|
79
|
-
if (event.type
|
|
79
|
+
if (["runtime_ready", "runtime_heartbeat"].includes(event.type)) emit(event);
|
|
80
80
|
else if (event.type === "runtime_reconnecting")
|
|
81
81
|
report("messages", { type: "reconnecting" });
|
|
82
82
|
else if (event.type === "runtime_resumed") report("messages", { type: "waiting" });
|
package/bin/installation.mjs
CHANGED
|
@@ -31,13 +31,13 @@ export async function activateConnection(profile, env = process.env, options = {
|
|
|
31
31
|
...(profile.agentTokenEnv ? { TOTEM_AGENT_TOKEN_ENV: profile.agentTokenEnv } : {}),
|
|
32
32
|
});
|
|
33
33
|
if (!enabled) return { state: "paused", ...service, entry: runtime.cli };
|
|
34
|
-
if (
|
|
34
|
+
if (!service.persistent)
|
|
35
35
|
return {
|
|
36
36
|
state: "needs_attention",
|
|
37
37
|
ready: false,
|
|
38
38
|
...service,
|
|
39
39
|
entry: runtime.cli,
|
|
40
|
-
code: "
|
|
40
|
+
code: "LISTENER_STARTUP_UNAVAILABLE",
|
|
41
41
|
message:
|
|
42
42
|
"Automatic startup is not registered. Incoming work remains queued until host setup succeeds.",
|
|
43
43
|
};
|
|
@@ -89,8 +89,9 @@ export async function activateConnection(profile, env = process.env, options = {
|
|
|
89
89
|
}
|
|
90
90
|
const status = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
|
|
91
91
|
return {
|
|
92
|
-
state: status.ready ? "ready" : "needs_attention",
|
|
93
|
-
ready: Boolean(status.ready),
|
|
92
|
+
state: status.running && status.ready ? "ready" : "needs_attention",
|
|
93
|
+
ready: Boolean(status.running && status.ready),
|
|
94
|
+
running: status.running === true,
|
|
94
95
|
...service,
|
|
95
96
|
entry: runtime.cli,
|
|
96
97
|
};
|
|
@@ -232,14 +233,15 @@ export async function installationMain(args = [], options = {}) {
|
|
|
232
233
|
return (options.activateConnection ?? activateConnection)(profile, environment, options);
|
|
233
234
|
};
|
|
234
235
|
const reportActivation = (status) => {
|
|
236
|
+
const ready = status.ready === true && status.persistent === true && status.running === true;
|
|
235
237
|
stdout.write(
|
|
236
238
|
flags.json
|
|
237
239
|
? JSON.stringify(status) + "\n"
|
|
238
|
-
:
|
|
240
|
+
: ready
|
|
239
241
|
? "Ready. The listener passed its controlled receive, wake and reply check. This host must remain awake and online.\n"
|
|
240
242
|
: `Runtime ${status.state}. Run totem status for diagnostics; an installed or paused listener is not ready.\n`,
|
|
241
243
|
);
|
|
242
|
-
return
|
|
244
|
+
return ready ? 0 : 1;
|
|
243
245
|
};
|
|
244
246
|
if (environment.TOTEM_CONNECTION_ID) {
|
|
245
247
|
for (const file of ["connection-ready.json", "connection-request.json"]) {
|
|
@@ -328,8 +330,10 @@ export async function installationMain(args = [], options = {}) {
|
|
|
328
330
|
for (const name of saved) results.push({ profile: name, ...(await activate(name)) });
|
|
329
331
|
return reportActivation({
|
|
330
332
|
installed: true,
|
|
331
|
-
ready: results.every((r) => r.ready),
|
|
332
|
-
|
|
333
|
+
ready: results.every((r) => r.ready && r.persistent && r.running),
|
|
334
|
+
persistent: results.every((r) => r.persistent),
|
|
335
|
+
running: results.every((r) => r.running),
|
|
336
|
+
state: results.every((r) => r.ready && r.persistent && r.running) ? "ready" : "needs_attention",
|
|
333
337
|
profiles: results,
|
|
334
338
|
service,
|
|
335
339
|
});
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"sourceSha256":"e15dc6cfd6f828dbcb829fc6fa3f5f6ef51062f45462ec6532b8c52f532c7951","binarySha256":"
|
|
1
|
+
{"sourceSha256":"e15dc6cfd6f828dbcb829fc6fa3f5f6ef51062f45462ec6532b8c52f532c7951","binarySha256":"611fb4d6e7fae5b5e3fc0b39b37b304070a81c0a2e6b8140c73fbe72a5be88b1","architectures":["arm64","x86_64"],"minimumMacOS":"13.0"}
|
package/bin/network-runtime.mjs
CHANGED
|
@@ -793,6 +793,7 @@ export async function runRuntime(settings) {
|
|
|
793
793
|
},
|
|
794
794
|
{ signal },
|
|
795
795
|
);
|
|
796
|
+
emit({ type: "runtime_heartbeat" });
|
|
796
797
|
diagnosticsSupported = result?.diagnosticsSupported === true;
|
|
797
798
|
operationalStatesSupported = result?.operationalStatesSupported === true;
|
|
798
799
|
return result;
|
|
@@ -7,6 +7,9 @@ const MISSION_ERROR_CODES = new Map([
|
|
|
7
7
|
]);
|
|
8
8
|
|
|
9
9
|
export const RUNTIME_ERROR_CODES = new Set([
|
|
10
|
+
"LISTENER_STARTUP_UNAVAILABLE",
|
|
11
|
+
"LISTENER_HEARTBEAT_TIMEOUT",
|
|
12
|
+
"LISTENER_PROCESS_UNVERIFIED",
|
|
10
13
|
...MISSION_ERROR_CODES.values(),
|
|
11
14
|
"AGENT_START_FAILED",
|
|
12
15
|
"AGENT_EXIT_FAILED",
|
|
@@ -562,6 +562,22 @@ export async function installRuntimeService(runtime, env = process.env, options
|
|
|
562
562
|
await run("systemctl", ["--user", "daemon-reload"]);
|
|
563
563
|
await run("systemctl", ["--user", "enable", "--now", unit]);
|
|
564
564
|
if (runtime.upgraded) await run("systemctl", ["--user", "restart", unit]);
|
|
565
|
+
const enabled = await run("systemctl", ["--user", "is-enabled", unit]);
|
|
566
|
+
const active = await run("systemctl", ["--user", "is-active", unit]);
|
|
567
|
+
const loaded = await run("systemctl", [
|
|
568
|
+
"--user",
|
|
569
|
+
"show",
|
|
570
|
+
unit,
|
|
571
|
+
"--property=ExecStart",
|
|
572
|
+
"--value",
|
|
573
|
+
]);
|
|
574
|
+
if (
|
|
575
|
+
enabled.stdout.trim() !== "enabled" ||
|
|
576
|
+
active.stdout.trim() !== "active" ||
|
|
577
|
+
!loaded.stdout.includes(node) ||
|
|
578
|
+
!loaded.stdout.includes(runtime.supervisor)
|
|
579
|
+
)
|
|
580
|
+
throw new Error("The exact persistent listener service is not enabled and active.");
|
|
565
581
|
return acknowledged({ mode: "systemd-user", persistent: true, label: unit, path });
|
|
566
582
|
}
|
|
567
583
|
if (platform === "win32") {
|
|
@@ -572,6 +588,17 @@ export async function installRuntimeService(runtime, env = process.env, options
|
|
|
572
588
|
} catch {
|
|
573
589
|
/* Ephemeral/headless hosts often have no user service manager. */
|
|
574
590
|
}
|
|
591
|
+
if (windowsRegistration?.mode !== "startup-shortcut")
|
|
592
|
+
return {
|
|
593
|
+
mode: "unavailable",
|
|
594
|
+
persistent: false,
|
|
595
|
+
code:
|
|
596
|
+
startupCode ??
|
|
597
|
+
windowsRegistration?.code ??
|
|
598
|
+
(platform === "darwin" || platform === "win32"
|
|
599
|
+
? "STARTUP_REGISTRATION_FAILED"
|
|
600
|
+
: "LISTENER_STARTUP_UNAVAILABLE"),
|
|
601
|
+
};
|
|
575
602
|
const child = (options.spawn ?? spawn)(node, [runtime.supervisor], {
|
|
576
603
|
env: { ...env, TOTEM_CONFIG_DIR: runtime.root },
|
|
577
604
|
stdio: "ignore",
|
|
@@ -580,13 +607,5 @@ export async function installRuntimeService(runtime, env = process.env, options
|
|
|
580
607
|
});
|
|
581
608
|
child.on("error", () => {});
|
|
582
609
|
child.unref();
|
|
583
|
-
return
|
|
584
|
-
? acknowledged({ mode: "startup-shortcut", persistent: true, label: task })
|
|
585
|
-
: {
|
|
586
|
-
mode: "background-supervisor",
|
|
587
|
-
persistent: false,
|
|
588
|
-
...(["win32", "darwin"].includes(platform)
|
|
589
|
-
? { code: startupCode ?? windowsRegistration?.code ?? "STARTUP_REGISTRATION_FAILED" }
|
|
590
|
-
: {}),
|
|
591
|
-
};
|
|
610
|
+
return acknowledged({ mode: "startup-shortcut", persistent: true, label: task });
|
|
592
611
|
}
|
|
@@ -4,6 +4,19 @@ export const HOST_APPROVAL_RECOVERY =
|
|
|
4
4
|
"Stop retrying. Ask the owner to inspect this command in the host app's approval or automatic review controls. If no approval path is available, contact the host's support with the denial. Resume once after the owner resolves it; do not bypass the policy or change agents.";
|
|
5
5
|
|
|
6
6
|
const causes = {
|
|
7
|
+
LISTENER_STARTUP_UNAVAILABLE: [
|
|
8
|
+
"The host has no verified enabled persistent listener service.",
|
|
9
|
+
"Restore the host's supported startup service, then rerun setup. A temporary background process is not a complete installation.",
|
|
10
|
+
],
|
|
11
|
+
LISTENER_HEARTBEAT_TIMEOUT: [
|
|
12
|
+
"The listener stopped renewing its receive heartbeat.",
|
|
13
|
+
"The supervisor will restart this same saved profile. Inspect network availability if receiving repeatedly stalls.",
|
|
14
|
+
],
|
|
15
|
+
LISTENER_PROCESS_UNVERIFIED: [
|
|
16
|
+
"The stalled listener process could not be safely identified.",
|
|
17
|
+
"Inspect this profile's saved process and stop the exact worker before resuming; no unrelated process was signaled.",
|
|
18
|
+
false,
|
|
19
|
+
],
|
|
7
20
|
HOST_APPROVAL_DENIED: [
|
|
8
21
|
"The host denied approval to execute the startup command.",
|
|
9
22
|
HOST_APPROVAL_RECOVERY,
|
package/bin/totem.mjs
CHANGED
|
@@ -240,9 +240,9 @@ function help() {
|
|
|
240
240
|
writeBrand();
|
|
241
241
|
writeHelpBody(`Start here
|
|
242
242
|
Updates totem update status | check | pause | resume
|
|
243
|
-
Install once npm install --global https://totemlayer.com/downloads/totemlayer
|
|
243
|
+
Install once npm install --global https://totemlayer.com/downloads/totemlayer-${CLI_VERSION}.tgz
|
|
244
244
|
Confirm installation totem --version
|
|
245
|
-
Run one-off npx --yes --package https://totemlayer.com/downloads/totemlayer
|
|
245
|
+
Run one-off npx --yes --package https://totemlayer.com/downloads/totemlayer-${CLI_VERSION}.tgz totem --help
|
|
246
246
|
|
|
247
247
|
Connect once and receive messages and approved Mission work
|
|
248
248
|
Select Codex, Claude, a local model, or an owner-configured CLI/HTTP agent.
|