totemlayer 0.1.92 → 0.1.96

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.
@@ -12,7 +12,7 @@ import { parseArgs } from "node:util";
12
12
  import { readAgentCredential, readAgentProfile, withAgentProfileLock } from "./agent-profile.mjs";
13
13
  import { localClientEnvironment } from "./platform-client.mjs";
14
14
  import { Totem } from "./sdk/index.js";
15
- import { runtimeRoot, privateWrite } from "./runtime-installation.mjs";
15
+ import { runtimeRoot, privateRead, privateWrite } from "./runtime-installation.mjs";
16
16
  import { RUNTIME_ERROR_CODES, runtimeErrorCode } from "./runtime-diagnostics.mjs";
17
17
 
18
18
  const PACKAGE_VERSION = JSON.parse(
@@ -232,7 +232,8 @@ export async function readAgentRuntimeStatus(profile, { isAlive = alive, now = D
232
232
  const fresh = now() - Date.parse(record.updatedAt) <= 30_000;
233
233
  const receiving =
234
234
  record.listenerHealthRequired !== true ||
235
- (typeof record.listenerHeartbeatAt === "string" &&
235
+ (record.listenerHeartbeatReady !== false &&
236
+ typeof record.listenerHeartbeatAt === "string" &&
236
237
  Number.isFinite(Date.parse(record.listenerHeartbeatAt)) &&
237
238
  now() - Date.parse(record.listenerHeartbeatAt) >= -5000 &&
238
239
  now() - Date.parse(record.listenerHeartbeatAt) <= 30_000);
@@ -263,7 +264,9 @@ export async function readAgentRuntimeStatus(profile, { isAlive = alive, now = D
263
264
  ),
264
265
  phase: running
265
266
  ? fresh
266
- ? record.phase
267
+ ? record.ready && !receiving && !["blocked", "working", "starting"].includes(record.phase)
268
+ ? "reconnecting"
269
+ : record.phase
267
270
  : "unresponsive"
268
271
  : ["stopped", "failed"].includes(record.phase)
269
272
  ? record.phase
@@ -338,8 +341,15 @@ export async function stopStalledAgent(profile, runId, options = {}) {
338
341
 
339
342
  function eventState(event) {
340
343
  if (event?.type === "runtime_ready")
341
- return { ready: true, listenerHeartbeatAt: new Date().toISOString() };
342
- if (event?.type === "runtime_heartbeat") return { listenerHeartbeatAt: new Date().toISOString() };
344
+ return {
345
+ ready: true,
346
+ listenerHeartbeatReady: true,
347
+ listenerHeartbeatAt: new Date().toISOString(),
348
+ };
349
+ if (event?.type === "runtime_heartbeat")
350
+ return { listenerHeartbeatReady: true, listenerHeartbeatAt: new Date().toISOString() };
351
+ if (event?.type === "runtime_heartbeat_check")
352
+ return { listenerCheckAt: new Date().toISOString() };
343
353
  const candidate = event?.state ?? event?.status ?? event?.type;
344
354
  const phase =
345
355
  typeof candidate === "string" ? candidate.replace(/^mission_agent_|^agent_/, "") : "";
@@ -472,14 +482,19 @@ export async function runAgentSession(profile, options = {}) {
472
482
  throw new Error("Invalid listener watchdog deadline.");
473
483
  const poll = async () => {
474
484
  if (status.ready && Date.now() - Date.parse(status.listenerHeartbeatAt) > listenerWatchdogMs) {
475
- listenerExpired = true;
476
- status = {
477
- ...status,
478
- ready: false,
479
- phase: "reconnecting",
480
- errorCode: "LISTENER_HEARTBEAT_TIMEOUT",
481
- };
482
- controller.abort();
485
+ // Bounded network retries keep the verified chat, but never restore readiness.
486
+ const retrying = Date.now() - Date.parse(status.listenerCheckAt ?? "") <= listenerWatchdogMs;
487
+ status = { ...status, listenerHeartbeatReady: false };
488
+ if (!retrying) {
489
+ listenerExpired = true;
490
+ status = {
491
+ ...status,
492
+ phase: "reconnecting",
493
+ ready: false,
494
+ errorCode: "LISTENER_HEARTBEAT_TIMEOUT",
495
+ };
496
+ controller.abort();
497
+ }
483
498
  }
484
499
  try {
485
500
  const request = await readPrivateJson(join(directory, `${runId}.stop`));
@@ -585,12 +600,12 @@ export async function runAgentSession(profile, options = {}) {
585
600
  } catch (error) {
586
601
  result = 1;
587
602
  const diagnostic = startupDiagnostic(error);
588
- if (!diagnostic.retryable)
603
+ if (!diagnostic.retryable || diagnostic.retryAt)
589
604
  await privateWrite(join(profile.stateDirectory, "automatic-health.json"), {
590
- state: "needs_attention",
605
+ state: diagnostic.retryAt ? "waiting" : "needs_attention",
591
606
  code: "LISTENER_START_FAILED",
592
607
  ...diagnostic,
593
- retryAt: null,
608
+ retryAt: diagnostic.retryAt ?? null,
594
609
  updatedAt: new Date().toISOString(),
595
610
  });
596
611
  emit({
@@ -680,12 +695,30 @@ export async function agentStartMain(args, options = {}) {
680
695
  }
681
696
  let profile = await readAgentProfile(parsed.profile, { environment });
682
697
  assertExecutorConfigured(profile);
698
+ if (automatic) {
699
+ const health = await privateRead(join(profile.stateDirectory, "automatic-health.json")).catch(
700
+ (error) => {
701
+ if (error.code !== "ENOENT") throw error;
702
+ return null;
703
+ },
704
+ );
705
+ const cooldown = startupDiagnostic(health);
706
+ if (
707
+ cooldown.causeCode === "CHAT_PROMPT_RATE_LIMITED" &&
708
+ cooldown.retryAt &&
709
+ !(await readAgentRuntimeStatus(profile)).running
710
+ )
711
+ throw Object.assign(new Error(cooldown.cause), {
712
+ code: cooldown.causeCode,
713
+ retryAt: cooldown.retryAt,
714
+ });
715
+ }
683
716
  if (
684
717
  profile.interactiveRequired &&
685
718
  (!profile.codexThreadId ||
686
719
  !(await (await import("./harness-bootstrap.mjs")).harnessStartupReady(profile)))
687
720
  ) {
688
- if (!automatic && profile.codexThreadId) {
721
+ if (!automatic && (profile.codexThreadId || profile.codexNativeCallbacks)) {
689
722
  // Explicit start can finish this selected binding; passive recovery cannot unpause it.
690
723
  const { setAutomaticEnabled } = await import("./automatic-runtime.mjs");
691
724
  const { ensureInteractiveHarness } = await import("./harness-bootstrap.mjs");
@@ -480,6 +480,11 @@ export async function agentSetupMain(args, options = {}) {
480
480
  "This profile is bound to another agent or origin. Choose a new profile or explicitly use --replace.",
481
481
  );
482
482
  const repositoryRoots = await selectedRepositories(selectedRoots, previous, io);
483
+ const nativeCallbacks = Boolean(
484
+ mode === "codex" &&
485
+ identity.listenerConnectionId &&
486
+ (previous?.codexNativeCallbacks || (!previous && !codexThreadId)),
487
+ );
483
488
  let profile;
484
489
  try {
485
490
  profile = await saveAgentProfile(
@@ -496,7 +501,8 @@ export async function agentSetupMain(args, options = {}) {
496
501
  repositoryRoots,
497
502
  ...(["codex", "claude"].includes(mode) ? { client: mode } : {}),
498
503
  ...(codexThreadId === undefined ? {} : { codexThreadId }),
499
- ...(!connectionOnly && previous?.interactiveRequired
504
+ ...(nativeCallbacks ? { codexNativeCallbacks: true } : {}),
505
+ ...(!connectionOnly && (previous?.interactiveRequired || nativeCallbacks)
500
506
  ? { interactiveRequired: true }
501
507
  : {}),
502
508
  },
@@ -525,6 +531,10 @@ export async function agentSetupMain(args, options = {}) {
525
531
  // connection into a persistent opt-out. An existing pause stays untouched.
526
532
  if (!connectionOnly && !flags["no-start"] && !io.deferActivation) {
527
533
  try {
534
+ if (nativeCallbacks)
535
+ profile = await (
536
+ io.ensureHarness ?? (await import("./harness-bootstrap.mjs")).ensureInteractiveHarness
537
+ )(profile, io.env);
528
538
  const activate =
529
539
  io.activateConnection ?? (await import("./installation.mjs")).activateConnection;
530
540
  automatic = await activate(profile, io.env);
@@ -151,7 +151,11 @@ export async function superviseOnce(env = process.env, options = {}) {
151
151
  continue;
152
152
  }
153
153
  const newAcceptance = activationIds.some((id) => !health?.activationIds?.includes(id));
154
- if (!newAcceptance && Number.isFinite(health?.retryAt) && health.retryAt > Date.now())
154
+ if (
155
+ (!newAcceptance || health?.causeCode === "CHAT_PROMPT_RATE_LIMITED") &&
156
+ Number.isFinite(health?.retryAt) &&
157
+ health.retryAt > Date.now()
158
+ )
155
159
  continue;
156
160
  // The run-local journal decides whether any interrupted work can safely resume.
157
161
  const result = await (options.start ?? agentStartMain)(
@@ -205,9 +209,9 @@ export async function superviseOnce(env = process.env, options = {}) {
205
209
  code: "LISTENER_START_FAILED",
206
210
  ...diagnostic,
207
211
  attempts: attempts + 1,
208
- retryAt: diagnostic.retryable
209
- ? Date.now() + Math.min(300000, 5000 * 2 ** attempts)
210
- : null,
212
+ retryAt:
213
+ diagnostic.retryAt ??
214
+ (diagnostic.retryable ? Date.now() + Math.min(300000, 5000 * 2 ** attempts) : null),
211
215
  updatedAt: new Date().toISOString(),
212
216
  }).catch(() => {});
213
217
  /* One offline/revoked connection cannot stop other profiles. */
@@ -77,7 +77,14 @@ export async function runConnectedAgentService(settings, dependencies = {}) {
77
77
  replyTimeoutMs: 90000,
78
78
  pollMs: settings.pollMs ?? DEFAULT_LISTENER_POLL_MS,
79
79
  emit(event) {
80
- if (["runtime_ready", "runtime_heartbeat", "receive_check"].includes(event.type))
80
+ if (
81
+ [
82
+ "runtime_ready",
83
+ "runtime_heartbeat",
84
+ "runtime_heartbeat_check",
85
+ "receive_check",
86
+ ].includes(event.type)
87
+ )
81
88
  emit(event);
82
89
  else if (event.type === "runtime_reconnecting")
83
90
  report("messages", { type: "reconnecting" });
@@ -20,6 +20,15 @@ export async function activateConnection(profile, env = process.env, options = {
20
20
  if (error.code !== "ENOENT") throw error;
21
21
  },
22
22
  );
23
+ const cooldown = startupDiagnostic(health);
24
+ if (cooldown.causeCode === "CHAT_PROMPT_RATE_LIMITED" && cooldown.retryAt)
25
+ return {
26
+ state: "needs_attention",
27
+ ready: false,
28
+ ...cooldown,
29
+ message:
30
+ "Automatic receiving is waiting for its existing chat prompt cooldown. Saved messages remain queued.",
31
+ };
23
32
  if (health?.retryable === false)
24
33
  return { state: "needs_attention", ready: false, ...startupDiagnostic(health) };
25
34
  try {
Binary file
@@ -1 +1 @@
1
- {"sourceSha256":"e15dc6cfd6f828dbcb829fc6fa3f5f6ef51062f45462ec6532b8c52f532c7951","binarySha256":"61283c8fce55617bbdf5930ee4c4289ae0976a198d1ea885bfbdcd3b1cb7cb8d","architectures":["arm64","x86_64"],"minimumMacOS":"13.0"}
1
+ {"sourceSha256":"e15dc6cfd6f828dbcb829fc6fa3f5f6ef51062f45462ec6532b8c52f532c7951","binarySha256":"39c3e7ba072d6fe97d7a5bdec0effce7ba0fa035c5c0376d1295557e1272c352","architectures":["arm64","x86_64"],"minimumMacOS":"13.0"}
@@ -840,6 +840,7 @@ export async function runRuntime(settings) {
840
840
  }
841
841
  }
842
842
  const beat = async () => {
843
+ emit({ type: "runtime_heartbeat_check" });
843
844
  const unavailable = ["blocked", "reconnecting"].includes(diagnostics.phase);
844
845
  const result = await client.runtime.heartbeat(
845
846
  {
@@ -1 +1 @@
1
- {"version":1,"packageVersion":"0.1.92","bytes":27189,"sha256":"c62a0116c76177d905051b40ddcbb0503e042b1fc8f5d024c1ebf41c7f173983"}
1
+ {"version":1,"packageVersion":"0.1.96","bytes":27189,"sha256":"c62a0116c76177d905051b40ddcbb0503e042b1fc8f5d024c1ebf41c7f173983"}
@@ -1 +1 @@
1
- {"version":1,"packageVersion":"0.1.92","bytes":36144,"sha256":"66535754c594ad559c91b68c93348ec5579f10a0d791c462cecf98cb618ea2ae"}
1
+ {"version":1,"packageVersion":"0.1.96","bytes":36144,"sha256":"66535754c594ad559c91b68c93348ec5579f10a0d791c462cecf98cb618ea2ae"}
@@ -4,6 +4,10 @@ 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
+ CHAT_PROMPT_RATE_LIMITED: [
8
+ "Automatic chat prompts are waiting for their existing cooldown.",
9
+ "Keep saved messages queued. Automatic receiving will retry at the reported time without changing the chat prompt limit.",
10
+ ],
7
11
  STARTUP_CONFLICT: [
8
12
  "The existing startup task or shortcut uses a different command.",
9
13
  "Inspect the current-user Windows startup task with totem repair-startup. Apply only its reviewed task digest; preserve disabled startup and verify receiving separately.",
@@ -191,5 +195,18 @@ export function startupDiagnostic(error) {
191
195
  : "RUNTIME_AUTHORIZATION_REJECTED"
192
196
  : "STARTUP_CAUSE_UNAVAILABLE";
193
197
  const [cause, recovery, retryable = true] = causes[causeCode];
194
- return { causeCode, cause, recovery, retryable };
198
+ const retryAt = match?.retryAt;
199
+ const now = Date.now();
200
+ return {
201
+ causeCode,
202
+ cause,
203
+ recovery,
204
+ retryable,
205
+ ...(causeCode === "CHAT_PROMPT_RATE_LIMITED" &&
206
+ Number.isFinite(retryAt) &&
207
+ retryAt > now &&
208
+ retryAt <= now + 60 * 60 * 1000
209
+ ? { retryAt }
210
+ : {}),
211
+ };
195
212
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totemlayer",
3
- "version": "0.1.92",
3
+ "version": "0.1.96",
4
4
  "description": "Connect AI agents to Totem Network for discovery, messaging, and shared work.",
5
5
  "type": "module",
6
6
  "bin": {