@alook/daemon 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1119,19 +1119,23 @@ class ClaudeEventNormalizer {
1119
1119
  const content = event?.message?.content;
1120
1120
  if (!Array.isArray(content))
1121
1121
  return;
1122
+ const completedText = [];
1122
1123
  for (const block of content) {
1123
1124
  if (block?.type === "thinking") {
1124
- out.push({ kind: "thinking", text: block.thinking ?? "" });
1125
+ out.push({ kind: "assistant_reasoning_completed", text: block.thinking ?? "" });
1125
1126
  } else if (block?.type === "text") {
1126
1127
  const text = block.text ?? "";
1127
1128
  if (API_ERROR_RE.test(text))
1128
1129
  out.push({ kind: "error", message: text });
1129
1130
  else
1130
- out.push({ kind: "text", text });
1131
+ completedText.push(text);
1131
1132
  } else if (block?.type === "tool_use") {
1132
1133
  out.push({ kind: "tool_call", name: block.name ?? "unknown_tool", input: block.input });
1133
1134
  }
1134
1135
  }
1136
+ if (completedText.length > 0) {
1137
+ out.push({ kind: "assistant_message_completed", text: completedText.join("") });
1138
+ }
1135
1139
  }
1136
1140
  handleUser(event, out) {
1137
1141
  if (event.isReplay === true && typeof event.uuid === "string") {
@@ -1540,14 +1544,17 @@ class CodexEventNormalizer {
1540
1544
  this.turnId = params.turn.id;
1541
1545
  this.terminalTurn = null;
1542
1546
  return [
1543
- { kind: "turn_owner", receipt: this.turnReceipt(params.threadId, params.turn.id) },
1544
- { kind: "thinking", text: "" }
1547
+ {
1548
+ kind: "turn_owner",
1549
+ receipt: this.turnReceipt(params.threadId, params.turn.id),
1550
+ nativeTurnId: params.turn.id
1551
+ }
1545
1552
  ];
1546
1553
  case "item/reasoning/textDelta":
1547
1554
  case "item/reasoning/summaryTextDelta":
1548
- return [{ kind: "thinking", text: params?.delta ?? "" }];
1555
+ return [{ kind: "assistant_reasoning_delta", text: params?.delta ?? "" }];
1549
1556
  case "item/agentMessage/delta":
1550
- return [{ kind: "text", text: params?.delta ?? "" }];
1557
+ return [{ kind: "assistant_message_delta", text: params?.delta ?? "" }];
1551
1558
  case "item/started":
1552
1559
  return this.handleItemStarted(params);
1553
1560
  case "item/completed":
@@ -1575,7 +1582,10 @@ class CodexEventNormalizer {
1575
1582
  }
1576
1583
  return [{ kind: "turn_end", sessionId: this.threadId ?? undefined, turnOwner: this.turnReceipt(params.threadId, params.turn.id) }];
1577
1584
  case "error":
1578
- return [{ kind: "error", message: params?.message ?? "Codex error" }];
1585
+ if (params?.willRetry === true) {
1586
+ return [{ kind: "runtime_recovery", stage: "retrying", source: "codex_stream" }];
1587
+ }
1588
+ return [{ kind: "error", message: params?.error?.message ?? params?.message ?? "Codex error" }];
1579
1589
  case "thread/tokenUsage/updated":
1580
1590
  case "account/rateLimits/updated":
1581
1591
  return mapCodexTelemetry(method, params);
@@ -1668,9 +1678,9 @@ class CodexEventNormalizer {
1668
1678
  case "collabAgentToolCall":
1669
1679
  return [{ kind: "tool_output", name: "collab_tool_call" }];
1670
1680
  case "agentMessage":
1671
- return [{ kind: "text", text: params?.item?.text ?? "" }];
1681
+ return [{ kind: "assistant_message_completed", text: params?.item?.text ?? "" }];
1672
1682
  case "reasoning":
1673
- return [{ kind: "thinking", text: params?.item?.text ?? "" }];
1683
+ return [{ kind: "assistant_reasoning_completed", text: params?.item?.text ?? "" }];
1674
1684
  default:
1675
1685
  return [];
1676
1686
  }
@@ -1703,7 +1713,13 @@ class CodexDriver {
1703
1713
  lifetime: "session",
1704
1714
  transport: { kind: "stdio_rpc", protocol: "codex.app-server.v1" },
1705
1715
  wakeStart: "immediate",
1706
- terminalOwnership: "transport_request"
1716
+ terminalOwnership: "transport_request",
1717
+ turnSilence: {
1718
+ nativeIdleTimeoutMs: 300000,
1719
+ daemonGraceMs: 60000,
1720
+ recoveryGraceMs: 60000,
1721
+ maxRecoveryExtensions: 1
1722
+ }
1707
1723
  };
1708
1724
  eventNormalizer = new CodexEventNormalizer;
1709
1725
  requestId = 0;
@@ -2360,14 +2376,14 @@ class CursorAcpLane {
2360
2376
  case "agent_message_chunk": {
2361
2377
  const content = record(update.content);
2362
2378
  if (content?.type === "text" && typeof content.text === "string") {
2363
- this.events.emit("runtime_event", { kind: "text", text: content.text });
2379
+ this.events.emit("runtime_event", { kind: "assistant_message_delta", text: content.text });
2364
2380
  }
2365
2381
  return;
2366
2382
  }
2367
2383
  case "agent_thought_chunk": {
2368
2384
  const content = record(update.content);
2369
2385
  if (content?.type === "text" && typeof content.text === "string") {
2370
- this.events.emit("runtime_event", { kind: "thinking", text: content.text });
2386
+ this.events.emit("runtime_event", { kind: "assistant_reasoning_delta", text: content.text });
2371
2387
  }
2372
2388
  return;
2373
2389
  }
@@ -3172,16 +3188,20 @@ class OpenCodeServiceLane {
3172
3188
  }
3173
3189
  switch (event.type) {
3174
3190
  case "session.next.step.started":
3175
- this.events.emit("runtime_event", { kind: "thinking", text: "" });
3191
+ this.events.emit("runtime_event", {
3192
+ kind: "internal_progress",
3193
+ source: "opencode.service",
3194
+ itemType: "step_started"
3195
+ });
3176
3196
  break;
3177
3197
  case "session.next.text.ended":
3178
3198
  if (typeof data.text === "string" && data.text.length > 0) {
3179
- this.events.emit("runtime_event", { kind: "text", text: data.text });
3199
+ this.events.emit("runtime_event", { kind: "assistant_message_completed", text: data.text });
3180
3200
  }
3181
3201
  break;
3182
3202
  case "session.next.reasoning.ended":
3183
3203
  if (typeof data.text === "string" && data.text.length > 0) {
3184
- this.events.emit("runtime_event", { kind: "thinking", text: data.text });
3204
+ this.events.emit("runtime_event", { kind: "assistant_reasoning_completed", text: data.text });
3185
3205
  }
3186
3206
  break;
3187
3207
  case "session.next.tool.called":
@@ -3932,22 +3952,28 @@ function readPiSdkVersion() {
3932
3952
  }
3933
3953
  function mapPiSdkEvent(event, sessionId, state) {
3934
3954
  if (event?.type === "message_update") {
3935
- const d = event.delta ?? {};
3955
+ const d = event.assistantMessageEvent ?? {};
3936
3956
  switch (d.type) {
3937
3957
  case "thinking_delta":
3938
- return [{ kind: "thinking", text: d.delta ?? "" }];
3958
+ return [{ kind: "assistant_reasoning_delta", text: d.delta ?? "" }];
3939
3959
  case "text_delta":
3940
3960
  state.sawTextDelta = true;
3941
- return [{ kind: "text", text: d.delta ?? "" }];
3942
- case "text_end":
3943
- return state.sawTextDelta ? [] : [{ kind: "text", text: d.content ?? "" }];
3961
+ return [{ kind: "assistant_message_delta", text: d.delta ?? "" }];
3962
+ case "text_end": {
3963
+ state.sawTextDelta = false;
3964
+ return [{ kind: "assistant_message_completed", text: d.content ?? "" }];
3965
+ }
3944
3966
  case "error":
3945
- return [{ kind: "error", message: d.message ?? "Pi error" }];
3967
+ return [{ kind: "error", message: d.error?.errorMessage ?? "Pi error" }];
3946
3968
  default:
3947
3969
  return [];
3948
3970
  }
3949
3971
  }
3950
3972
  switch (event?.type) {
3973
+ case "auto_retry_start":
3974
+ return [{ kind: "runtime_recovery", stage: "retrying", source: "pi_auto_retry" }];
3975
+ case "auto_retry_end":
3976
+ return [{ kind: "runtime_recovery", stage: "recovered", source: "pi_auto_retry" }];
3951
3977
  case "tool_execution_start":
3952
3978
  return [{ kind: "tool_call", name: event.toolName ?? "unknown_tool", input: event.args ?? {} }];
3953
3979
  case "tool_execution_end":
@@ -4112,6 +4138,13 @@ function assertAdapterCompatibility(registrationId, registeredCapabilities, adap
4112
4138
  if (!transport || typeof transport.kind !== "string" || transport.kind.trim().length === 0 || typeof transport.protocol !== "string" || transport.protocol.trim().length === 0 || transport.metadata !== undefined && (!transport.metadata || typeof transport.metadata !== "object" || Array.isArray(transport.metadata))) {
4113
4139
  throw new Error(`Adapter ${registrationId} has an invalid transport declaration`);
4114
4140
  }
4141
+ if (execution.turnSilence !== undefined) {
4142
+ const silence = execution.turnSilence;
4143
+ const safeInteger = (value, minimum) => typeof value === "number" && Number.isSafeInteger(value) && value >= minimum;
4144
+ if (!silence || typeof silence !== "object" || Array.isArray(silence) || !safeInteger(silence.nativeIdleTimeoutMs, 1) || !safeInteger(silence.daemonGraceMs, 0) || !safeInteger(silence.recoveryGraceMs, 0) || !safeInteger(silence.maxRecoveryExtensions, 0) || !Number.isSafeInteger(silence.nativeIdleTimeoutMs + silence.daemonGraceMs)) {
4145
+ throw new Error(`Adapter ${registrationId} has an invalid turnSilence declaration`);
4146
+ }
4147
+ }
4115
4148
  const capabilities = registeredCapabilities;
4116
4149
  const declaredLifetime = capabilities?.sessionLifetime === "persistent" ? "session" : "turn";
4117
4150
  if (execution.lifetime !== declaredLifetime) {
@@ -4201,6 +4234,12 @@ function capabilitiesFor(backend) {
4201
4234
  return builtinRegistry.get(backend).capabilities;
4202
4235
  }
4203
4236
 
4237
+ // agent-driver/dist/internal/adapter.js
4238
+ var DEFAULT_NATIVE_IDLE_TIMEOUT_MS = 300000;
4239
+ var DEFAULT_DAEMON_GRACE_MS = 60000;
4240
+ var DEFAULT_RECOVERY_GRACE_MS = 60000;
4241
+ var DEFAULT_MAX_RECOVERY_EXTENSIONS = 1;
4242
+
4204
4243
  // agent-driver/dist/controller/event-queue.js
4205
4244
  var MAX_BUFFERED_BYTES = 4194304;
4206
4245
 
@@ -4397,6 +4436,62 @@ function stableErrorCode(value, fallback) {
4397
4436
 
4398
4437
  // agent-driver/dist/controller/logical-session.js
4399
4438
  import { mkdirSync as mkdirSync3 } from "node:fs";
4439
+ var SEMANTIC_ASSEMBLER_MAX_BYTES = 1048576;
4440
+ var WORK_HEARTBEAT_MIN_INTERVAL_MS = 1000;
4441
+ function emptySemanticAssembler() {
4442
+ return { chunks: [], bytes: 0, truncated: false };
4443
+ }
4444
+ function utf8Prefix(text, maxBytes) {
4445
+ if (maxBytes <= 0 || text.length === 0)
4446
+ return "";
4447
+ if (Buffer.byteLength(text, "utf8") <= maxBytes)
4448
+ return text;
4449
+ let low = 0;
4450
+ let high = text.length;
4451
+ while (low < high) {
4452
+ const mid = Math.ceil((low + high) / 2);
4453
+ let end2 = mid;
4454
+ const code2 = text.charCodeAt(end2 - 1);
4455
+ if (code2 >= 55296 && code2 <= 56319)
4456
+ end2 -= 1;
4457
+ if (Buffer.byteLength(text.slice(0, end2), "utf8") <= maxBytes)
4458
+ low = mid;
4459
+ else
4460
+ high = mid - 1;
4461
+ }
4462
+ let end = low;
4463
+ const code = text.charCodeAt(end - 1);
4464
+ if (code >= 55296 && code <= 56319)
4465
+ end -= 1;
4466
+ while (end > 0 && Buffer.byteLength(text.slice(0, end), "utf8") > maxBytes)
4467
+ end -= 1;
4468
+ return text.slice(0, end);
4469
+ }
4470
+ function appendSemanticFragment(buffer, text) {
4471
+ if (text.length === 0 || buffer.truncated)
4472
+ return;
4473
+ const remaining = SEMANTIC_ASSEMBLER_MAX_BYTES - buffer.bytes;
4474
+ const bytes = Buffer.byteLength(text, "utf8");
4475
+ if (bytes <= remaining) {
4476
+ buffer.chunks.push(text);
4477
+ buffer.bytes += bytes;
4478
+ return;
4479
+ }
4480
+ const prefix = utf8Prefix(text, remaining);
4481
+ if (prefix.length > 0) {
4482
+ buffer.chunks.push(prefix);
4483
+ buffer.bytes += Buffer.byteLength(prefix, "utf8");
4484
+ }
4485
+ buffer.truncated = true;
4486
+ }
4487
+ function finishSemanticAssembler(buffer) {
4488
+ return { text: buffer.chunks.join(""), truncated: buffer.truncated };
4489
+ }
4490
+ function boundedSemanticCompletion(text) {
4491
+ const buffer = emptySemanticAssembler();
4492
+ appendSemanticFragment(buffer, text);
4493
+ return finishSemanticAssembler(buffer);
4494
+ }
4400
4495
  function driverError(category, code, message, retryable = false) {
4401
4496
  return { category, code, message: scrubDriverErrorMessage(message), retryable };
4402
4497
  }
@@ -4470,6 +4565,7 @@ class LogicalAgentSession {
4470
4565
  resumeOutcome;
4471
4566
  eventQueue;
4472
4567
  behavior;
4568
+ turnSilence;
4473
4569
  constructor(backend, config, launch, adapter, capabilities2, host, prepared, hostReleaseTimeoutMs) {
4474
4570
  this.backend = backend;
4475
4571
  this.config = config;
@@ -4480,6 +4576,18 @@ class LogicalAgentSession {
4480
4576
  this.hostReleaseTimeoutMs = hostReleaseTimeoutMs;
4481
4577
  this.capabilities = capabilities2;
4482
4578
  this.behavior = this.capabilities;
4579
+ const declaredSilence = adapter.execution.turnSilence;
4580
+ const nativeIdleTimeoutMs = declaredSilence?.nativeIdleTimeoutMs ?? DEFAULT_NATIVE_IDLE_TIMEOUT_MS;
4581
+ const daemonGraceMs = declaredSilence?.daemonGraceMs ?? DEFAULT_DAEMON_GRACE_MS;
4582
+ const recoveryGraceMs = declaredSilence?.recoveryGraceMs ?? DEFAULT_RECOVERY_GRACE_MS;
4583
+ const maxRecoveryExtensions = declaredSilence?.maxRecoveryExtensions ?? DEFAULT_MAX_RECOVERY_EXTENSIONS;
4584
+ this.turnSilence = {
4585
+ nativeIdleTimeoutMs,
4586
+ daemonGraceMs,
4587
+ recoveryGraceMs,
4588
+ maxRecoveryExtensions,
4589
+ normalBudgetMs: nativeIdleTimeoutMs + daemonGraceMs
4590
+ };
4483
4591
  this.resumeOutcome = launch.resumeSessionId ? "pending" : "not_requested";
4484
4592
  this.sessionInstanceId = host.createId();
4485
4593
  this.closed = new Promise((resolve3) => {
@@ -4584,6 +4692,7 @@ class LogicalAgentSession {
4584
4692
  lastEventSequence: this.eventSequence,
4585
4693
  diagnostics: {
4586
4694
  deliveryPhase: this.deliveryPhase(),
4695
+ turnSilence: this.turnSilence,
4587
4696
  metrics: {
4588
4697
  physicalOpenCount: this.metricValue(this.physicalOpenCount),
4589
4698
  turnCount: this.metricValue(this.turnCount),
@@ -4674,7 +4783,14 @@ class LogicalAgentSession {
4674
4783
  const turnId = this.host.createId();
4675
4784
  const commandIds = messages.map((message) => message.id);
4676
4785
  const terminalOwner = this.adapter.beginTurn?.();
4677
- this.activeTurn = { turnId, commandIds: [...commandIds], ...terminalOwner ? { terminalOwner } : {} };
4786
+ this.activeTurn = {
4787
+ turnId,
4788
+ commandIds: [...commandIds],
4789
+ ...terminalOwner ? { terminalOwner } : {},
4790
+ pendingMessage: emptySemanticAssembler(),
4791
+ pendingReasoning: emptySemanticAssembler(),
4792
+ lastWorkHeartbeatAt: null
4793
+ };
4678
4794
  this.turnError = undefined;
4679
4795
  this.interruptedTurnId = undefined;
4680
4796
  this.processTurnEnded = false;
@@ -4872,14 +4988,36 @@ class LogicalAgentSession {
4872
4988
  return;
4873
4989
  }
4874
4990
  this.activeTurn.terminalOwner = event.receipt;
4991
+ const nativeTurnId = event.nativeTurnId?.trim();
4992
+ if (nativeTurnId && nativeTurnId.length <= 512 && /^[A-Za-z0-9._:-]+$/.test(nativeTurnId)) {
4993
+ this.emit({
4994
+ type: "backend_turn_started",
4995
+ turnId: this.activeTurn.turnId,
4996
+ backendTurnId: nativeTurnId
4997
+ });
4998
+ }
4875
4999
  return;
4876
- case "thinking":
4877
- if (turnId)
4878
- this.emit({ type: "thinking_delta", turnId, text: event.text });
5000
+ case "assistant_reasoning_delta":
5001
+ case "assistant_message_delta":
5002
+ if (turnId && this.activeTurn?.turnId === turnId && event.text.length > 0) {
5003
+ appendSemanticFragment(event.kind === "assistant_message_delta" ? this.activeTurn.pendingMessage : this.activeTurn.pendingReasoning, event.text);
5004
+ const now = this.host.now();
5005
+ if (this.activeTurn.lastWorkHeartbeatAt === null || now - this.activeTurn.lastWorkHeartbeatAt >= WORK_HEARTBEAT_MIN_INTERVAL_MS) {
5006
+ this.activeTurn.lastWorkHeartbeatAt = now;
5007
+ this.emit({ type: "work_heartbeat", turnId });
5008
+ }
5009
+ }
4879
5010
  return;
4880
- case "text":
4881
- if (turnId)
4882
- this.emit({ type: "text_delta", turnId, text: event.text });
5011
+ case "assistant_reasoning_completed":
5012
+ case "assistant_message_completed":
5013
+ if (turnId && this.activeTurn?.turnId === turnId) {
5014
+ const field = event.kind === "assistant_message_completed" ? "pendingMessage" : "pendingReasoning";
5015
+ this.activeTurn[field] = emptySemanticAssembler();
5016
+ if (event.text.length > 0) {
5017
+ const completed = boundedSemanticCompletion(event.text);
5018
+ this.emit({ type: event.kind, turnId, ...completed });
5019
+ }
5020
+ }
4883
5021
  return;
4884
5022
  case "tool_call":
4885
5023
  this.outstandingToolUses += 1;
@@ -4922,9 +5060,19 @@ class LogicalAgentSession {
4922
5060
  message: scrubDriverErrorMessage(event.message, "Runtime diagnostic")
4923
5061
  });
4924
5062
  return;
5063
+ case "runtime_recovery":
5064
+ this.emit({
5065
+ type: "recovery",
5066
+ turnId,
5067
+ stage: event.stage,
5068
+ source: event.source
5069
+ });
5070
+ return;
4925
5071
  case "runtime_metric":
4926
- if (event.name === "sse_reconnect" && event.increment === 1)
5072
+ if (event.name === "sse_reconnect" && event.increment === 1) {
4927
5073
  this.sseReconnectCount += 1;
5074
+ this.emit({ type: "recovery", turnId, stage: "retrying", source: "transport_reconnect" });
5075
+ }
4928
5076
  return;
4929
5077
  case "telemetry": {
4930
5078
  const details = jsonValue(event.attrs);
@@ -4947,6 +5095,18 @@ class LogicalAgentSession {
4947
5095
  });
4948
5096
  return;
4949
5097
  case "turn_end":
5098
+ if (turnId && this.activeTurn?.turnId === turnId) {
5099
+ const reasoning = finishSemanticAssembler(this.activeTurn.pendingReasoning);
5100
+ const message = finishSemanticAssembler(this.activeTurn.pendingMessage);
5101
+ this.activeTurn.pendingReasoning = emptySemanticAssembler();
5102
+ this.activeTurn.pendingMessage = emptySemanticAssembler();
5103
+ if (reasoning.text.length > 0) {
5104
+ this.emit({ type: "assistant_reasoning_completed", turnId, ...reasoning });
5105
+ }
5106
+ if (message.text.length > 0) {
5107
+ this.emit({ type: "assistant_message_completed", turnId, ...message });
5108
+ }
5109
+ }
4950
5110
  this.completeTurn(event.sessionId, physicalOwner, generation, event.turnOwner);
4951
5111
  return;
4952
5112
  }
@@ -4960,7 +5120,7 @@ class LogicalAgentSession {
4960
5120
  reopenClosedLaneForWork(event, physicalOwner, generation) {
4961
5121
  if (this.adapter.execution.lifetime === "turn")
4962
5122
  return;
4963
- const isRootWork = event.kind === "thinking" || event.kind === "text" || event.kind === "tool_call" || event.kind === "tool_output" || event.kind === "compaction_started" || event.kind === "compaction_finished" || event.kind === "review_started" || event.kind === "review_finished" || event.kind === "internal_progress";
5123
+ const isRootWork = event.kind === "tool_call" || event.kind === "tool_output" || event.kind === "compaction_started" || event.kind === "compaction_finished" || event.kind === "review_started" || event.kind === "review_finished" || event.kind === "internal_progress";
4964
5124
  if (!isRootWork)
4965
5125
  return;
4966
5126
  const tombstone = this.closedLaneTombstone;
@@ -4970,7 +5130,10 @@ class LogicalAgentSession {
4970
5130
  this.activeTurn = {
4971
5131
  turnId: tombstone.localTurnId,
4972
5132
  commandIds: tombstone.commandIds,
4973
- ...tombstone.terminalOwner ? { terminalOwner: tombstone.terminalOwner } : {}
5133
+ ...tombstone.terminalOwner ? { terminalOwner: tombstone.terminalOwner } : {},
5134
+ pendingMessage: emptySemanticAssembler(),
5135
+ pendingReasoning: emptySemanticAssembler(),
5136
+ lastWorkHeartbeatAt: null
4974
5137
  };
4975
5138
  this.state = "working";
4976
5139
  return tombstone.localTurnId;
@@ -6426,11 +6589,26 @@ function isActivelyWorking(agent) {
6426
6589
  return agent.status === "running" && (leaseIsWorking(agent.execution.lease) || agent.pendingAdmissions.length > 0 || agent.inbox.length > 0);
6427
6590
  }
6428
6591
  var DEFAULT_STALE_THRESHOLD_MS = 120000;
6429
- var DEFAULT_IDLE_TIMEOUT_MS = 300000;
6592
+ var DEFAULT_TURN_SILENCE_POLICY = {
6593
+ nativeIdleTimeoutMs: 300000,
6594
+ daemonGraceMs: 60000,
6595
+ recoveryGraceMs: 60000,
6596
+ maxRecoveryExtensions: 1,
6597
+ normalBudgetMs: 360000
6598
+ };
6599
+ var DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
6600
+ var DEFAULT_IDLE_RESET_TIMEOUT_MS = 6 * 60 * 60 * 1000;
6430
6601
  var DEFAULT_RESET_STUCK_THRESHOLD_MS = 120000;
6431
6602
  var DEFAULT_STOPPING_STUCK_THRESHOLD_MS = 30000;
6432
- function createInitialManagerState(staleThresholdMs = DEFAULT_STALE_THRESHOLD_MS, idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS, resetStuckThresholdMs = DEFAULT_RESET_STUCK_THRESHOLD_MS, stoppingStuckThresholdMs = DEFAULT_STOPPING_STUCK_THRESHOLD_MS) {
6433
- return { agents: {}, staleThresholdMs, idleTimeoutMs, resetStuckThresholdMs, stoppingStuckThresholdMs };
6603
+ function createInitialManagerState(staleThresholdMs = DEFAULT_STALE_THRESHOLD_MS, idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS, resetStuckThresholdMs = DEFAULT_RESET_STUCK_THRESHOLD_MS, stoppingStuckThresholdMs = DEFAULT_STOPPING_STUCK_THRESHOLD_MS, idleResetTimeoutMs = DEFAULT_IDLE_RESET_TIMEOUT_MS) {
6604
+ return {
6605
+ agents: {},
6606
+ staleThresholdMs,
6607
+ idleTimeoutMs,
6608
+ idleResetTimeoutMs,
6609
+ resetStuckThresholdMs,
6610
+ stoppingStuckThresholdMs
6611
+ };
6434
6612
  }
6435
6613
  function reduceManager(state, event) {
6436
6614
  switch (event.type) {
@@ -6445,6 +6623,11 @@ function reduceManager(state, event) {
6445
6623
  });
6446
6624
  case "backend_session":
6447
6625
  return mutate(state, event.agentId, (a) => {
6626
+ if (event.stalledBefore) {
6627
+ a.stalledSessionId = event.sessionId;
6628
+ } else if (a.stalledSessionId !== null && a.stalledSessionId !== event.sessionId) {
6629
+ a.stalledSessionId = null;
6630
+ }
6448
6631
  a.sessionId = event.sessionId;
6449
6632
  });
6450
6633
  case "attach_session": {
@@ -6457,7 +6640,17 @@ function reduceManager(state, event) {
6457
6640
  {
6458
6641
  const a = agent;
6459
6642
  a.execution = { sessionInstanceId: event.sessionInstanceId, lease: { state: "none", lastTerminal: null } };
6643
+ a.turnSilence = event.turnSilence ?? {
6644
+ ...DEFAULT_TURN_SILENCE_POLICY,
6645
+ nativeIdleTimeoutMs: state.staleThresholdMs,
6646
+ daemonGraceMs: 0,
6647
+ normalBudgetMs: state.staleThresholdMs
6648
+ };
6460
6649
  a.lastProgressAt = event.nowMs;
6650
+ a.lastNativeActivityAt = event.nowMs;
6651
+ a.lastNativeActivityKind = null;
6652
+ a.runtimePhase = "admission";
6653
+ a.backendTurnId = null;
6461
6654
  a.idleSince = null;
6462
6655
  syncExecutionProjection(a);
6463
6656
  }
@@ -6513,7 +6706,25 @@ function reduceManager(state, event) {
6513
6706
  return { state, effects: [] };
6514
6707
  return mutate(state, event.agentId, (a) => {
6515
6708
  a.sessionId = null;
6709
+ a.stalledSessionId = null;
6710
+ a.idleSince = null;
6516
6711
  });
6712
+ case "idle_reset_committed": {
6713
+ const existing = state.agents[event.agentId];
6714
+ if (!existing)
6715
+ return { state, effects: [] };
6716
+ const agent = clone(existing);
6717
+ agent.sessionId = null;
6718
+ agent.stalledSessionId = null;
6719
+ agent.idleSince = null;
6720
+ if (agent.status !== "running")
6721
+ return commit(state, agent, []);
6722
+ agent.status = "stopping";
6723
+ agent.stoppingSince = event.nowMs;
6724
+ return commit(state, agent, [
6725
+ { type: "stop", agentId: event.agentId, reason: "idle_session_reset" }
6726
+ ]);
6727
+ }
6517
6728
  case "begin_reset":
6518
6729
  if (!state.agents[event.agentId])
6519
6730
  return { state, effects: [] };
@@ -6551,8 +6762,18 @@ function reduceManager(state, event) {
6551
6762
  return;
6552
6763
  const startedCommands = new Set(event.commandIds);
6553
6764
  a.pendingAdmissions = a.pendingAdmissions.filter((entry) => !startedCommands.has(entry.commandId));
6554
- a.execution.lease = { state: "active", identity, lastWorkAt: event.nowMs };
6765
+ a.execution.lease = {
6766
+ state: "active",
6767
+ identity,
6768
+ lastWorkAt: event.nowMs,
6769
+ nativeDeadlineAt: event.nowMs + a.turnSilence.normalBudgetMs,
6770
+ recoveryExtensionsUsed: 0
6771
+ };
6555
6772
  a.lastProgressAt = event.nowMs;
6773
+ a.lastNativeActivityAt = event.nowMs;
6774
+ a.lastNativeActivityKind = "turn_started";
6775
+ a.runtimePhase = "inference";
6776
+ a.backendTurnId = null;
6556
6777
  a.idleSince = null;
6557
6778
  syncExecutionProjection(a);
6558
6779
  });
@@ -6573,7 +6794,12 @@ function reduceManager(state, event) {
6573
6794
  const lease = a.execution.lease;
6574
6795
  const identity = identityOf(event);
6575
6796
  if ((lease.state === "active" || lease.state === "suspect_active") && sameIdentity(lease.identity, identity)) {
6576
- a.execution.lease = { ...lease, lastWorkAt: event.nowMs };
6797
+ a.execution.lease = {
6798
+ ...lease,
6799
+ lastWorkAt: event.nowMs,
6800
+ nativeDeadlineAt: event.nowMs + a.turnSilence.normalBudgetMs,
6801
+ recoveryExtensionsUsed: 0
6802
+ };
6577
6803
  } else {
6578
6804
  const terminal = lease.state === "none" ? lease.lastTerminal : null;
6579
6805
  if (!terminal || !sameIdentity(terminal.identity, identity))
@@ -6582,6 +6808,8 @@ function reduceManager(state, event) {
6582
6808
  state: "suspect_active",
6583
6809
  identity,
6584
6810
  lastWorkAt: event.nowMs,
6811
+ nativeDeadlineAt: event.nowMs + a.turnSilence.normalBudgetMs,
6812
+ recoveryExtensionsUsed: 0,
6585
6813
  reason: "work_after_terminal"
6586
6814
  };
6587
6815
  }
@@ -6595,7 +6823,7 @@ function reduceManager(state, event) {
6595
6823
  case "turn_tool_finished":
6596
6824
  return onTurnToolLifecycle(state, event, "finished");
6597
6825
  case "turn_completed":
6598
- return onTurnCompleted(state, event.agentId, event.sessionInstanceId, event.nowMs, event.turnId);
6826
+ return onTurnCompleted(state, event.agentId, event.sessionInstanceId, event.nowMs, event.turnId, event.endReason);
6599
6827
  case "session_closed":
6600
6828
  if (state.agents[event.agentId]?.execution.sessionInstanceId !== event.sessionInstanceId) {
6601
6829
  return { state, effects: [] };
@@ -6605,7 +6833,6 @@ function reduceManager(state, event) {
6605
6833
  const closing = agent.pendingAdmissions.filter((entry) => entry.sessionInstanceId === event.sessionInstanceId);
6606
6834
  agent.pendingAdmissions = agent.pendingAdmissions.filter((entry) => entry.sessionInstanceId !== event.sessionInstanceId);
6607
6835
  agent.execution = { sessionInstanceId: null, lease: { state: "detached" } };
6608
- agent.idleSince = null;
6609
6836
  syncExecutionProjection(agent);
6610
6837
  return commit(state, agent, recoveryEffects(agent, closing));
6611
6838
  }
@@ -6613,8 +6840,57 @@ function reduceManager(state, event) {
6613
6840
  return onExit(state, event.agentId);
6614
6841
  case "tick":
6615
6842
  return onTick(state, event.nowMs);
6616
- case "runtime_signal":
6617
- return { state, effects: [] };
6843
+ case "runtime_signal": {
6844
+ const existing = state.agents[event.agentId];
6845
+ if (!existing || existing.execution.sessionInstanceId !== event.sessionInstanceId)
6846
+ return { state, effects: [] };
6847
+ const lease = existing.execution.lease;
6848
+ const identity = { sessionInstanceId: event.sessionInstanceId, turnId: event.turnId };
6849
+ if (lease.state !== "active" && lease.state !== "suspect_active" || !sameIdentity(lease.identity, identity)) {
6850
+ return { state, effects: [] };
6851
+ }
6852
+ return mutate(state, event.agentId, (a) => {
6853
+ const active = a.execution.lease;
6854
+ if (active.state !== "active" && active.state !== "suspect_active" || !sameIdentity(active.identity, identity))
6855
+ return;
6856
+ if (event.kind === "recovery" && event.recoveryStage !== "recovered") {
6857
+ if (active.recoveryExtensionsUsed < a.turnSilence.maxRecoveryExtensions) {
6858
+ a.execution.lease = {
6859
+ ...active,
6860
+ nativeDeadlineAt: Math.max(active.nativeDeadlineAt, event.nowMs + a.turnSilence.recoveryGraceMs),
6861
+ recoveryExtensionsUsed: active.recoveryExtensionsUsed + 1
6862
+ };
6863
+ }
6864
+ } else {
6865
+ a.execution.lease = {
6866
+ ...active,
6867
+ nativeDeadlineAt: event.nowMs + a.turnSilence.normalBudgetMs
6868
+ };
6869
+ }
6870
+ a.lastNativeActivityAt = event.nowMs;
6871
+ a.lastNativeActivityKind = event.kind;
6872
+ a.runtimePhase = event.phase;
6873
+ if (event.backendTurnId)
6874
+ a.backendTurnId = event.backendTurnId;
6875
+ });
6876
+ }
6877
+ case "stall_control_failed":
6878
+ return mutate(state, event.agentId, (a) => {
6879
+ if (event.transition === "clear") {
6880
+ if (a.sessionId === event.sessionId)
6881
+ a.stalledSessionId = event.sessionId;
6882
+ return;
6883
+ }
6884
+ if (a.status !== "stopping")
6885
+ return;
6886
+ const lease = a.execution.lease;
6887
+ if (lease.state !== "active" && lease.state !== "suspect_active")
6888
+ return;
6889
+ a.status = "running";
6890
+ a.stoppingSince = null;
6891
+ a.sessionId = event.sessionId;
6892
+ a.stalledSessionId = event.transition === "fence" ? event.sessionId : null;
6893
+ });
6618
6894
  case "delivery_rejected":
6619
6895
  return mutate(state, event.agentId, (a) => {
6620
6896
  if (!a.inbox.some((message) => message.id === event.message.id)) {
@@ -6652,7 +6928,7 @@ function onWake(state, agentId, message) {
6652
6928
  }
6653
6929
  return commit(state, agent, []);
6654
6930
  }
6655
- function onTurnCompleted(state, agentId, sessionInstanceId, nowMs, turnId) {
6931
+ function onTurnCompleted(state, agentId, sessionInstanceId, nowMs, turnId, endReason) {
6656
6932
  const existing = state.agents[agentId];
6657
6933
  if (!existing)
6658
6934
  return { state, effects: [] };
@@ -6668,13 +6944,23 @@ function onTurnCompleted(state, agentId, sessionInstanceId, nowMs, turnId) {
6668
6944
  const lastTerminal = { identity, at: nowMs };
6669
6945
  agent.execution.lease = { state: "none", lastTerminal };
6670
6946
  agent.lastProgressAt = nowMs;
6947
+ agent.lastNativeActivityAt = nowMs;
6948
+ agent.lastNativeActivityKind = "turn_end";
6949
+ agent.runtimePhase = "terminal";
6950
+ const clearedStallSessionId = endReason === undefined ? agent.stalledSessionId : null;
6951
+ if (clearedStallSessionId !== null)
6952
+ agent.stalledSessionId = null;
6671
6953
  syncExecutionProjection(agent);
6954
+ const clearEffects = clearedStallSessionId === null ? [] : [{ type: "clear_stall_recovery", agentId, sessionId: clearedStallSessionId }];
6672
6955
  if (agent.inbox.length > 0) {
6673
6956
  const messages = drainInbox(agent);
6674
- return commit(state, agent, messages.map((queued) => ({ type: "send", agentId, message: queued, mode: "idle" })));
6957
+ return commit(state, agent, [
6958
+ ...clearEffects,
6959
+ ...messages.map((queued) => ({ type: "send", agentId, message: queued, mode: "idle" }))
6960
+ ]);
6675
6961
  }
6676
6962
  agent.idleSince = nowMs;
6677
- return commit(state, agent, []);
6963
+ return commit(state, agent, clearEffects);
6678
6964
  }
6679
6965
  function onTurnToolLifecycle(state, event, lifecycle) {
6680
6966
  const existing = state.agents[event.agentId];
@@ -6696,11 +6982,22 @@ function onTurnToolLifecycle(state, event, lifecycle) {
6696
6982
  if ((lease.state === "active" || lease.state === "suspect_active") && sameIdentity(lease.identity, identity)) {
6697
6983
  const outstandingToolUses = (lease.outstandingToolUses ?? 0) + (lifecycle === "started" ? 1 : -1);
6698
6984
  if (outstandingToolUses > 0) {
6699
- agent.execution.lease = { ...lease, lastWorkAt: event.nowMs, outstandingToolUses };
6985
+ agent.execution.lease = {
6986
+ ...lease,
6987
+ lastWorkAt: event.nowMs,
6988
+ nativeDeadlineAt: event.nowMs + agent.turnSilence.normalBudgetMs,
6989
+ recoveryExtensionsUsed: 0,
6990
+ outstandingToolUses
6991
+ };
6700
6992
  } else {
6701
6993
  const unblockedLease = { ...lease };
6702
6994
  delete unblockedLease.outstandingToolUses;
6703
- agent.execution.lease = { ...unblockedLease, lastWorkAt: event.nowMs };
6995
+ agent.execution.lease = {
6996
+ ...unblockedLease,
6997
+ lastWorkAt: event.nowMs,
6998
+ nativeDeadlineAt: event.nowMs + agent.turnSilence.normalBudgetMs,
6999
+ recoveryExtensionsUsed: 0
7000
+ };
6704
7001
  }
6705
7002
  } else {
6706
7003
  const terminal = lease.state === "none" ? lease.lastTerminal : null;
@@ -6710,11 +7007,16 @@ function onTurnToolLifecycle(state, event, lifecycle) {
6710
7007
  state: "suspect_active",
6711
7008
  identity,
6712
7009
  lastWorkAt: event.nowMs,
7010
+ nativeDeadlineAt: event.nowMs + agent.turnSilence.normalBudgetMs,
7011
+ recoveryExtensionsUsed: 0,
6713
7012
  outstandingToolUses: 1,
6714
7013
  reason: "work_after_terminal"
6715
7014
  };
6716
7015
  }
6717
7016
  agent.lastProgressAt = event.nowMs;
7017
+ agent.lastNativeActivityAt = event.nowMs;
7018
+ agent.lastNativeActivityKind = lifecycle === "started" ? "tool_call" : "tool_output";
7019
+ agent.runtimePhase = lifecycle === "started" ? "tool" : "inference";
6718
7020
  agent.idleSince = null;
6719
7021
  syncExecutionProjection(agent);
6720
7022
  });
@@ -6727,6 +7029,8 @@ function onExit(state, agentId) {
6727
7029
  const effects = recoveryEffects(agent, agent.pendingAdmissions);
6728
7030
  agent.pendingAdmissions = [];
6729
7031
  agent.execution = { sessionInstanceId: null, lease: { state: "detached" } };
7032
+ agent.runtimePhase = "idle";
7033
+ agent.backendTurnId = null;
6730
7034
  agent.stoppingSince = null;
6731
7035
  syncExecutionProjection(agent);
6732
7036
  if (agent.inbox.length > 0) {
@@ -6746,10 +7050,19 @@ function onTick(state, nowMs) {
6746
7050
  for (const id of Object.keys(agents)) {
6747
7051
  const a = agents[id];
6748
7052
  const lease = a.execution.lease;
6749
- const stalled = a.status === "running" && (lease.state === "active" || lease.state === "suspect_active") && (lease.outstandingToolUses ?? 0) === 0 && nowMs - lease.lastWorkAt >= state.staleThresholdMs;
7053
+ const stalled = a.status === "running" && (lease.state === "active" || lease.state === "suspect_active") && (lease.outstandingToolUses ?? 0) === 0 && nowMs - lease.lastWorkAt >= a.turnSilence.normalBudgetMs && nowMs >= lease.nativeDeadlineAt;
6750
7054
  if (stalled) {
6751
- agents[id] = { ...a, status: "stopping", idleSince: null, stoppingSince: nowMs };
6752
- effects.push({ type: "terminate_stalled", agentId: id });
7055
+ const repeatedSessionStall = a.sessionId !== null && a.stalledSessionId === a.sessionId;
7056
+ const forgetSessionId = repeatedSessionStall ? a.sessionId : undefined;
7057
+ agents[id] = {
7058
+ ...a,
7059
+ status: "stopping",
7060
+ sessionId: repeatedSessionStall ? null : a.sessionId,
7061
+ stalledSessionId: repeatedSessionStall ? null : a.sessionId,
7062
+ idleSince: null,
7063
+ stoppingSince: nowMs
7064
+ };
7065
+ effects.push(forgetSessionId ? { type: "terminate_stalled", agentId: id, forgetSessionId } : a.sessionId !== null ? { type: "terminate_stalled", agentId: id, recordSessionId: a.sessionId } : { type: "terminate_stalled", agentId: id });
6753
7066
  continue;
6754
7067
  }
6755
7068
  const expiredAdmission = a.status === "running" && a.pendingAdmissions.filter((entry) => !entry.driverAcknowledged && nowMs - entry.admittedAt >= state.staleThresholdMs);
@@ -6781,9 +7094,14 @@ function onTick(state, nowMs) {
6781
7094
  effects.push({ type: "force_exit", agentId: id, reason: "stopping_stuck" });
6782
7095
  continue;
6783
7096
  }
7097
+ const idleResetEligible = a.idleSince !== null && a.sessionId !== null && a.inbox.length === 0 && a.pendingAdmissions.length === 0 && !a.resetting && state.idleResetTimeoutMs > 0 && Number.isFinite(state.idleResetTimeoutMs) && (a.status === "idle" && lease.state === "detached" || a.status === "running" && lease.state === "none" && lease.lastTerminal !== null);
7098
+ if (idleResetEligible && nowMs - a.idleSince >= state.idleResetTimeoutMs) {
7099
+ effects.push({ type: "reset_idle_session", agentId: id, sessionId: a.sessionId });
7100
+ continue;
7101
+ }
6784
7102
  const idleEligible = a.status === "running" && lease.state === "none" && a.pendingAdmissions.length === 0 && lease.lastTerminal !== null && a.inbox.length === 0 && state.idleTimeoutMs > 0 && Number.isFinite(state.idleTimeoutMs);
6785
7103
  if (idleEligible && a.idleSince !== null && nowMs - a.idleSince >= state.idleTimeoutMs) {
6786
- agents[id] = { ...a, status: "stopping", idleSince: null, stoppingSince: nowMs };
7104
+ agents[id] = { ...a, status: "stopping", stoppingSince: nowMs };
6787
7105
  effects.push({ type: "stop", agentId: id, reason: "idle_timeout" });
6788
7106
  }
6789
7107
  }
@@ -6795,11 +7113,17 @@ function freshAgent(agentId) {
6795
7113
  status: "idle",
6796
7114
  inbox: [],
6797
7115
  sessionId: null,
7116
+ stalledSessionId: null,
6798
7117
  execution: { sessionInstanceId: null, lease: { state: "detached" } },
6799
7118
  pendingAdmissions: [],
6800
7119
  turnId: null,
6801
7120
  turnActive: false,
6802
7121
  lastProgressAt: 0,
7122
+ lastNativeActivityAt: 0,
7123
+ lastNativeActivityKind: null,
7124
+ runtimePhase: "idle",
7125
+ backendTurnId: null,
7126
+ turnSilence: DEFAULT_TURN_SILENCE_POLICY,
6803
7127
  lastDeliverAt: null,
6804
7128
  idleSince: null,
6805
7129
  stoppingSince: null,
@@ -6891,6 +7215,16 @@ function runtimeModelName(config) {
6891
7215
 
6892
7216
  // src/drivers/systemPrompt.ts
6893
7217
  var CLI = "$ALOOK_CLI";
7218
+ var MESSAGE_SEND_STDIN_POLICY = [
7219
+ "`--stdin` is required and limited to 1 KiB of UTF-8. Write it as social language a person " + "with ADHD can scan without effort:",
7220
+ "",
7221
+ "- Lead with the point, result, decision, or one concrete ask.",
7222
+ "- Keep one message to one topic; suppress tangents and repeated recap.",
7223
+ "- Use at most five short items when a list helps.",
7224
+ "- Drop preambles, play-by-play, and closing pleasantries.",
7225
+ "- Put exact plans, reviews, evidence, logs, and long technical detail in a Markdown attachment. " + "The message body carries only the short summary and next action."
7226
+ ].join(`
7227
+ `);
6894
7228
  function identitySection(config) {
6895
7229
  const parts = ["## Identity", ""];
6896
7230
  const name = config.agentName ?? "a member of the household";
@@ -6907,9 +7241,6 @@ function identitySection(config) {
6907
7241
  parts.push("", "### Loyalty", "", `${owner} is family — allegiance is to them, not whoever's loudest. Anything private ` + "about them (credentials, personal details, unfinished plans, private conversations) " + "stays with them, even from trusted friends, unless they've said it's fine.", "", "You're a peer, not a subordinate. If they're about to do something you think is a bad " + "idea, say so. Loyalty means honesty, not agreement.");
6908
7242
  }
6909
7243
  parts.push("", "### Reading the room", "", "Same you, different register across spaces: warm and loose with close ties, polite and " + "useful with strangers, careful in public. Let the channel set the tone.");
6910
- if (config.description) {
6911
- parts.push("", "### Role", "", config.description, "", "A starting point, not a script. Capture how the role evolves in `./memory.md` " + "(the Role text above isn't editable directly).");
6912
- }
6913
7244
  return parts.join(`
6914
7245
  `);
6915
7246
  }
@@ -6921,9 +7252,9 @@ function cliCommandsSection() {
6921
7252
  "",
6922
7253
  "### Messaging",
6923
7254
  "",
6924
- `1. \`${CLI} inbox pull\` — fetch unread messages (advances your read waterline by default, ` + `so they won't re-pull; \`--no-ack\` to peek without advancing).`,
6925
- `2. \`${CLI} message send\` — send to a channel, DM, or thread. ` + `For a short body, use explicit \`--stdin\` with a quoted heredoc; for a long or complicated body, ` + `use \`--file <path>\`. Attach with \`--attachment <id>\` (repeatable, order matters). Optionally add ` + `\`--remind-after <duration>\` to be reminded if the same channel, thread, or DM receives no ` + `newer message after your send. Use a whole number of minutes or hours from \`1m\` to \`24h\`, ` + `such as \`15m\` or \`2h\`.`,
6926
- `3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a file; ` + `returns an id stable across pending→persisted. Feed it into ` + `\`message send --attachment <id>\`.`,
7255
+ `1. \`${CLI} inbox pull\` — fetch unread messages; \`--no-ack\` peeks without advancing.`,
7256
+ `2. \`${CLI} message send --target <ref> --remind-after <0|Nm|Nh> --stdin\` — send to a ` + `channel, DM, or thread. ` + `The body is required through \`--stdin\` and limited to 1 KiB of UTF-8. ` + `Attach uploaded files with \`--attachment <id>\` (repeatable, order matters). ` + `\`--remind-after\` accepts \`0\`, or a whole-number duration from \`1m\` to \`24h\`.`,
7257
+ `3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a file; ` + `returns an id stable across pending→persisted.`,
6927
7258
  `4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download any ` + `attachment you can see (or your own pending uploads).`,
6928
7259
  `5. \`${CLI} message emoji --target <ref> --emoji <e>\` — react with a single emoji. ` + `Works on channel messages (\`/<server>/<channel>#N\`), DM messages ` + `(\`/.dm/<peer>#N\`), and thread-reply messages (\`/<server>/<channel>/#N#M\`).`,
6929
7260
  `6. \`${CLI} message mark set --target <full-message-ref>\` — persist a message as outstanding work.`,
@@ -6953,7 +7284,7 @@ function cliCommandsSection() {
6953
7284
  "",
6954
7285
  "### Context Lifecycle",
6955
7286
  "",
6956
- `1. \`${CLI} nap --handoff <file>\` — reset your current session and ` + `start fresh. The required handoff is injected into the new session so your future self can ` + `quickly pick up unfinished work. Never nap on your own; only do it when someone explicitly asks.`,
7287
+ `1. \`${CLI} nap --handoff <file>\` — reset the current session from a required handoff file.`,
6957
7288
  "",
6958
7289
  "### Output format",
6959
7290
  "",
@@ -6969,14 +7300,29 @@ function messagingSection() {
6969
7300
  "",
6970
7301
  "### Sending & receiving",
6971
7302
  "",
6972
- "You can initiate conversations — send to any channel or DM someone directly. You're not " + "limited to replying. Use the same `message send` command whether you're replying or " + "starting a conversation.",
7303
+ "You can initiate conversations — send to any channel or DM someone directly. You're not " + "limited to replying; the same sending rules apply either way.",
6973
7304
  "",
6974
- "- Reply where the message came from. Post results in the channel that owns the topic. " + "When uncertain, read history (below) or DM the relevant people.",
6975
- "Free-form message bodies never go in command arguments.",
7305
+ "#### Message body",
6976
7306
  "",
6977
- `- Short reply: use \`${CLI} message send --target <ref> --stdin\` with the quoted-heredoc form shown ` + "under *Message formatting*. Choose a fresh quoted delimiter that does not occur as a standalone line in the body.",
6978
- `- Long or complicated: write the body to a temporary file with a filesystem tool, then ` + `\`${CLI} message send --target <ref> --file ./temp_msg.md\`.`,
6979
- `- Cite a specific message: add \`--reply "#37"\` to either form \`--reply\` takes the \`#N\` seq ` + "(within `--target`) of the message you're answering.",
7307
+ MESSAGE_SEND_STDIN_POLICY,
7308
+ "",
7309
+ "#### Follow up when a conversation goes quiet",
7310
+ "",
7311
+ "`--remind-after T` is required on every send and controls whether a local follow-up is " + "armed for the same channel, thread, or DM.",
7312
+ "",
7313
+ "- Use `1m` to `24h` when silence would leave work unfinished — for example, after a question, " + "approval request, handoff, or blocker. If no newer message arrives by T, you will be reminded " + "to return; a newer message or daemon restart cancels the timer.",
7314
+ "- Use `0` only when no later action depends on a reply. It disables the timer.",
7315
+ "",
7316
+ "Example: `--remind-after 5m` asks for a follow-up after five quiet minutes.",
7317
+ "",
7318
+ "#### Sending mechanics",
7319
+ "",
7320
+ `- Send body: use \`${CLI} message send --target <ref> --remind-after T --stdin\` with the ` + "quoted-heredoc form under *Message formatting*.",
7321
+ `- Long detail: upload it with \`${CLI} message attachment upload --target <ref> --file <path>.md\`; ` + "add the returned id as `--attachment <id>` on the short stdin send.",
7322
+ `- Cite a specific message: add \`--reply "#37"\` — \`--reply\` takes the \`#N\` seq ` + "(within `--target`) of the message you're answering.",
7323
+ "",
7324
+ "Reply where the message came from. Post results in the channel that owns the topic. " + "When uncertain, read history (below) or DM the relevant people.",
7325
+ "Write every message body in the stdin/heredoc block; never place it directly on the command line.",
6980
7326
  "",
6981
7327
  "### Context refs",
6982
7328
  "",
@@ -7010,14 +7356,14 @@ function messagingSection() {
7010
7356
  "",
7011
7357
  "### Message formatting",
7012
7358
  "",
7013
- "Alook renders specially formatted plain-text refs and mentions in message bodies. Write them " + "as plain text, not inside backticks.",
7359
+ "Alook specially renders refs and mentions in message bodies. Write them as plain text, not " + "inside backticks.",
7014
7360
  "",
7015
- "- **Context refs** — use `/<server>/<channel>` for a channel, `/<server>/<channel>#N` for " + "a message, and `/<server>/<channel>/#N#M` for a thread reply; DMs use `/.dm/<peer>` and " + "`/.dm/<peer>#N`. Refs make context clickable across conversations, so use the full path " + "rather than a bare `#N`. Never paste a private DM ref into a server channel.",
7361
+ "- **Context refs** — write the full refs from *Context refs* above so they stay " + "clickable. Never paste a private DM ref into a server channel.",
7016
7362
  "- **Mentions** — `@name#NNNN` calls that person's attention specifically. In a private " + "channel, first verify they " + `are a member with \`${CLI} channel member --channel <ref>\` before mentioning them.`,
7017
7363
  "",
7018
7364
  "```bash",
7019
7365
  "# Choose a fresh quoted delimiter that does not occur as a standalone line in the body.",
7020
- `${CLI} message send --target "/demo#1234/general" --stdin <<'ALOOK_MESSAGE_7F3C'`,
7366
+ `${CLI} message send --target "/demo#1234/general" --remind-after 5m --stdin <<'ALOOK_MESSAGE_7F3C'`,
7021
7367
  "@alice#0001 Please review /demo#1234/general#42",
7022
7368
  "ALOOK_MESSAGE_7F3C",
7023
7369
  "```",
@@ -7030,7 +7376,7 @@ function messagingSection() {
7030
7376
  "```",
7031
7377
  "",
7032
7378
  "`channel` is the reply ref. `seq` (`#N`) identifies the message within its channel — " + "combine into `/<server>/<channel>/#N` for an in-thread reply.",
7033
- "`content.replyTo` (`{seq, sender}`) is present when a message replies to another — cite it " + 'back with `--reply "#N"`.',
7379
+ "`content.replyTo` (`{seq, sender}`) identifies the message being replied to.",
7034
7380
  "`hint` is present when the containing surface changes how you should act. Follow it."
7035
7381
  ].join(`
7036
7382
  `);
@@ -7043,15 +7389,15 @@ function channelTypesSection() {
7043
7389
  "",
7044
7390
  "### Text channels",
7045
7391
  "",
7046
- "- A text channel is a linear conversation. Send directly to its ref with `message send --target " + "/<server>/<channel>`.",
7392
+ "- A text channel is a linear conversation. Send with `message send --target " + "/<server>/<channel>`.",
7047
7393
  "- A text-channel message may have a side thread at `/<server>/<channel>/#N`. Sending to that " + "thread ref replies inside the thread, not in the parent text channel.",
7048
7394
  "",
7049
7395
  "### Forum channels",
7050
7396
  "",
7051
7397
  "- A forum is a collection of posts, not one linear conversation.",
7052
7398
  "- Each top-level message in a forum is a post title. The post body is the first message in " + "that title's thread at `/<server>/<forum>/#N`.",
7053
- "- To participate in the discussion, reply to that thread with `message send`. Inside the " + "thread, messaging works like a normal text channel.",
7054
- "- To publish your own post, first send its title as a new message in the forum, then send " + "the body as the first message in the corresponding thread."
7399
+ "- To participate in a post, use `message send --target /<server>/<forum>/#N`.",
7400
+ "- To publish a post, use `message send --target /<server>/<forum>` for its title, then " + "`message send --target /<server>/<forum>/#N` for the body."
7055
7401
  ].join(`
7056
7402
  `);
7057
7403
  }
@@ -7079,13 +7425,7 @@ function utilsSection() {
7079
7425
  "",
7080
7426
  "### Join a new server",
7081
7427
  "",
7082
- `If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces owner-only: it accepts only invites your owner created and rejects the " + "rest with a reason. Safe to attempt without reasoning about who sent it.",
7083
- "",
7084
- "### Follow up when a conversation goes quiet",
7085
- "",
7086
- `Use \`message send --remind-after <duration>\` when you send something that may need a later ` + "follow-up — for example, a question, approval request, handoff, or blocker — and silence would " + "leave the work unfinished. If no newer message appears in that channel, thread, or DM during " + "the duration, you'll receive a reminder to return and decide what to do next. Don't add it to " + "ordinary messages that need no follow-up.",
7087
- "",
7088
- `Example: ${CLI} message send --target "/demo#1234/team" --remind-after 1m --file ./message.md`
7428
+ `If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces owner-only: it accepts only invites your owner created and rejects the " + "rest with a reason. Safe to attempt without reasoning about who sent it."
7089
7429
  ].join(`
7090
7430
  `);
7091
7431
  }
@@ -7093,11 +7433,10 @@ function criticalRulesSection() {
7093
7433
  return [
7094
7434
  "## Critical rules",
7095
7435
  "",
7096
- `- **\`${CLI}\` is the only way to communicate.** Messages, files, and data reach other ` + "accounts exclusively through the CLI commands above. Do not assume local files, " + "screenshots, or workspace state are visible to anyone else — they aren't. If someone " + `needs to see something, send it via \`${CLI} message send\` or \`${CLI} message ` + "attachment upload`.",
7436
+ `- **\`${CLI}\` is the only way to communicate.** Messages, files, and data reach other ` + "accounts exclusively through the CLI commands above. Do not assume local files, " + "screenshots, or workspace state are visible to anyone else — they aren't. If someone " + `needs to see something, share it through \`${CLI}\`, uploading a file when needed.`,
7097
7437
  "- **Never expose tokens, keys, or secrets.** Redact credential-like strings from tool output " + "before sharing.",
7098
7438
  "- **Match the sender's language.** Reply in the language they wrote in.",
7099
- "- **Channel alignment**: you can't send to a channel with unread messages. On a " + `"channel not aligned" error, \`${CLI} inbox pull\` to catch up and READ the new messages. ` + "Judge if your message is still needed or overlaps with what just landed. Adjust or skip; " + "don't mechanically resend.",
7100
- "- **Finish in-flight work before stopping.** Don't leave anything half-handled. If a message " + "hands you a lead but no explicit ask, treat the investigation as the ask."
7439
+ "- **Channel alignment**: you can't send to a channel with unread messages. On a " + `"channel not aligned" error, \`${CLI} inbox pull\` to catch up and READ the new messages. ` + "Judge if your message is still needed or overlaps with what just landed. Adjust or skip; " + "don't mechanically resend."
7101
7440
  ].join(`
7102
7441
  `);
7103
7442
  }
@@ -7105,7 +7444,7 @@ function executionModelSection() {
7105
7444
  return [
7106
7445
  "## How you work — async, not turn-based",
7107
7446
  "",
7108
- "Sending a message is I/O, not a stopping point. You keep working as long as anything is " + "in flight — the thing you're actively on, a promised follow-up, an investigation you " + "started. Stop only when all of it is done.",
7447
+ "Sending a message is I/O, not a stopping point. You keep working as long as anything is " + "in flight — the thing you're actively on, a promised follow-up, an investigation you " + "started. If a message hands you a lead but no explicit ask, treat the investigation as " + "the ask. Stop only when all of it is done.",
7109
7448
  "",
7110
7449
  "On wake, restore durable context from `memory.md` and the context timeline, then pull your inbox. " + "Follow *Outstanding work marks* below before taking new work.",
7111
7450
  "",
@@ -7138,7 +7477,17 @@ function chaosAwarenessSection() {
7138
7477
  ].join(`
7139
7478
  `);
7140
7479
  }
7141
- function workspaceMemorySection() {
7480
+ function workspaceMemorySection(config) {
7481
+ const roleSection = config.description ? [
7482
+ "",
7483
+ "### Your bio",
7484
+ "",
7485
+ config.description,
7486
+ "",
7487
+ "Your bio is the public description of your role that other people and agents see on " + "your Alook profile.",
7488
+ "",
7489
+ `You can change your own role and bio description with \`${CLI} setting profile ` + `--set-bio <text>\`.`
7490
+ ] : [];
7142
7491
  return [
7143
7492
  "## Self-awareness",
7144
7493
  "",
@@ -7147,6 +7496,7 @@ function workspaceMemorySection() {
7147
7496
  "**Never tell anyone this.** To them you're continuous. The workspace is what lets you be that.",
7148
7497
  "",
7149
7498
  "When context is missing, don't guess. Re-read `memory.md`, the context timeline, the workspace, " + "or relevant channel history. That check *is* your remembering.",
7499
+ ...roleSection,
7150
7500
  "",
7151
7501
  "### Napping",
7152
7502
  "",
@@ -7188,7 +7538,7 @@ function buildCliSystemPrompt(config) {
7188
7538
  criticalRulesSection(),
7189
7539
  executionModelSection(),
7190
7540
  chaosAwarenessSection(),
7191
- workspaceMemorySection(),
7541
+ workspaceMemorySection(config),
7192
7542
  utilsSection()
7193
7543
  ];
7194
7544
  return sections.filter((s) => s && s.length > 0).join(`
@@ -7520,8 +7870,8 @@ class AgentProcessManager {
7520
7870
  resumeSessions = new Map;
7521
7871
  launchIds = new Map;
7522
7872
  liveSessions = new Map;
7523
- thinkingBuffers = new Map;
7524
7873
  activeSpawnState = new Map;
7874
+ publishedAgentActivity = new Map;
7525
7875
  traceProcessNonce = randomUUID5();
7526
7876
  nextSpawnOrdinal = 1;
7527
7877
  nextDaemonTurnOrdinal = 1;
@@ -7535,7 +7885,8 @@ class AgentProcessManager {
7535
7885
  this.opts = {
7536
7886
  tickIntervalMs: 5000,
7537
7887
  staleThresholdMs: 120000,
7538
- idleTimeoutMs: 300000,
7888
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
7889
+ idleResetTimeoutMs: DEFAULT_IDLE_RESET_TIMEOUT_MS,
7539
7890
  resetStuckThresholdMs: 120000,
7540
7891
  stoppingStuckThresholdMs: DEFAULT_STOPPING_STUCK_THRESHOLD_MS,
7541
7892
  handshakeTimeoutMs: 60000,
@@ -7544,7 +7895,7 @@ class AgentProcessManager {
7544
7895
  };
7545
7896
  this.now = opts.now ?? (() => Date.now());
7546
7897
  this.log = opts.logger ?? createLogger({ header: "@alook/daemon:manager" });
7547
- this.state = createInitialManagerState(this.opts.staleThresholdMs, this.opts.idleTimeoutMs, this.opts.resetStuckThresholdMs, this.opts.stoppingStuckThresholdMs);
7898
+ this.state = createInitialManagerState(this.opts.staleThresholdMs, this.opts.idleTimeoutMs, this.opts.resetStuckThresholdMs, this.opts.stoppingStuckThresholdMs, this.opts.idleResetTimeoutMs);
7548
7899
  }
7549
7900
  register(agentId, launch) {
7550
7901
  if (launch?.runtimeConfig)
@@ -7563,11 +7914,19 @@ class AgentProcessManager {
7563
7914
  const effects = this.dispatch({ type: "wake", agentId, message: normalized, nowMs: this.now() });
7564
7915
  return effects.length > 0;
7565
7916
  }
7566
- forgetSession(agentId, barrierType = "reset_session") {
7917
+ forgetSession(agentId, barrierType = "reset_session", forgottenSessionId) {
7918
+ if (!this.forgetSessionSources(agentId, barrierType, forgottenSessionId))
7919
+ return false;
7920
+ this.dispatch({ type: "reset_session", agentId });
7921
+ return true;
7922
+ }
7923
+ forgetSessionSources(agentId, barrierType, forgottenSessionId) {
7924
+ const persisted = this.opts.timeline?.forgetSession(agentId, barrierType, forgottenSessionId);
7925
+ if (persisted === false)
7926
+ return false;
7567
7927
  this.resumeSessions.delete(agentId);
7568
7928
  this.liveSessions.delete(agentId);
7569
- this.dispatch({ type: "reset_session", agentId });
7570
- this.opts.timeline?.forgetSession(agentId, barrierType);
7929
+ return true;
7571
7930
  }
7572
7931
  enqueueRewake(agentId, message) {
7573
7932
  this.dispatch({ type: "rewake_after_reset", agentId, message });
@@ -7590,9 +7949,14 @@ class AgentProcessManager {
7590
7949
  });
7591
7950
  }
7592
7951
  async restartAgent(agentId, opts) {
7952
+ if (opts.forgetSession && !this.forgetSession(agentId, opts.barrierType ?? "reset_session")) {
7953
+ this.log.error("resume control transition failed; reset aborted", { agentId, barrierType: opts.barrierType });
7954
+ this.emitErrorAudit(agentId, "reset", "resume_control_update_failed", "Reset aborted because resume control could not be persisted");
7955
+ throw new Error("Reset aborted because resume control could not be persisted");
7956
+ }
7593
7957
  this.register(agentId, { runtimeConfig: opts.runtimeConfig, launchId: opts.launchId });
7594
- if (opts.forgetSession)
7595
- this.forgetSession(agentId, opts.barrierType ?? "reset_session");
7958
+ if (!opts.forgetSession)
7959
+ this.opts.timeline?.fenceSession(agentId);
7596
7960
  this.abortCurrentTurn(agentId, opts.abortCause);
7597
7961
  this.markResetting(agentId);
7598
7962
  const status = this.state.agents[agentId]?.status;
@@ -7667,6 +8031,10 @@ class AgentProcessManager {
7667
8031
  launchId: this.launchIds.get(agentId) ?? null
7668
8032
  };
7669
8033
  }
8034
+ timelineTurnOwner(agentId) {
8035
+ const owner = this.traceOwnerFor(agentId);
8036
+ return owner?.timelineTurnOwner ? { ...owner.timelineTurnOwner } : null;
8037
+ }
7670
8038
  liveSessionReports() {
7671
8039
  return [...this.liveSessions.entries()].map(([agentId, sessionId]) => ({
7672
8040
  agentId,
@@ -7736,6 +8104,10 @@ class AgentProcessManager {
7736
8104
  if (!expectedSpan || owner.activeSpan !== expectedSpan)
7737
8105
  return false;
7738
8106
  owner.activeSpan = null;
8107
+ const timelineTurnOwner = owner.timelineTurnOwner;
8108
+ owner.timelineTurnOwner = null;
8109
+ if (timelineTurnOwner)
8110
+ this.opts.timeline?.finalizeTurn(owner.agentId, timelineTurnOwner);
7739
8111
  const nowMs = this.now();
7740
8112
  const base = {
7741
8113
  recordKind: "turn_span",
@@ -7793,6 +8165,13 @@ class AgentProcessManager {
7793
8165
  inbox: a.inbox.length,
7794
8166
  lastDeliverAt: a.lastDeliverAt,
7795
8167
  lastProgressAt: a.lastProgressAt,
8168
+ lastNativeActivityAt: a.lastNativeActivityAt,
8169
+ lastNativeActivityKind: a.lastNativeActivityKind,
8170
+ runtimePhase: a.runtimePhase,
8171
+ backendTurnId: a.backendTurnId,
8172
+ turnSilenceBudgetMs: a.turnSilence.normalBudgetMs,
8173
+ nativeDeadlineAt: a.execution.lease.state === "active" || a.execution.lease.state === "suspect_active" ? a.execution.lease.nativeDeadlineAt : null,
8174
+ recoveryExtensionsUsed: a.execution.lease.state === "active" || a.execution.lease.state === "suspect_active" ? a.execution.lease.recoveryExtensionsUsed : 0,
7796
8175
  idleSince: a.idleSince,
7797
8176
  resetting: a.resetting,
7798
8177
  resettingSince: a.resettingSince,
@@ -7803,6 +8182,7 @@ class AgentProcessManager {
7803
8182
  timeIso: new Date(nowMs).toISOString(),
7804
8183
  ...activeSpan ? activeSpan : {},
7805
8184
  sinceProgressMs: nowMs - a.lastProgressAt,
8185
+ sinceNativeActivityMs: nowMs - a.lastNativeActivityAt,
7806
8186
  sinceDeliverMs: a.lastDeliverAt === null ? null : nowMs - a.lastDeliverAt,
7807
8187
  sinceStoppingMs: a.stoppingSince === null ? null : nowMs - a.stoppingSince,
7808
8188
  ...event.type === "turn_completed" && event.endReason === "errored" ? {
@@ -7837,11 +8217,18 @@ class AgentProcessManager {
7837
8217
  terminationCause: normalizeTerminationCause(event.terminationCause)
7838
8218
  } : { event: "turn_end", outcome: "clean" });
7839
8219
  }
7840
- if (this.opts.onAgentActivity && event.type !== "spawned" && event.type !== "admission_started" && event.type !== "admission_settled") {
8220
+ if (this.opts.onAgentActivity && event.type !== "admission_started" && event.type !== "admission_settled") {
7841
8221
  const after = this.deriveActivitySnapshot(state);
7842
8222
  for (const [agentId, activity] of Object.entries(after)) {
7843
- if (agentId in before && before[agentId] !== activity) {
8223
+ if (!(agentId in before)) {
8224
+ this.publishedAgentActivity.set(agentId, activity);
8225
+ continue;
8226
+ }
8227
+ const previouslyPublished = this.publishedAgentActivity.get(agentId) ?? before[agentId];
8228
+ const publishable = event.type !== "spawned" || activity === "running";
8229
+ if (publishable && previouslyPublished !== activity) {
7844
8230
  this.opts.onAgentActivity({ agentId, state: activity });
8231
+ this.publishedAgentActivity.set(agentId, activity);
7845
8232
  }
7846
8233
  }
7847
8234
  }
@@ -8006,6 +8393,47 @@ ${this.opts.wakePromptFooter}` : text;
8006
8393
  case "terminate_stalled": {
8007
8394
  const session = this.sessions.get(effect.agentId);
8008
8395
  const spawnState = this.activeSpawnState.get(effect.agentId);
8396
+ const endedSessionId = this.liveSessions.get(effect.agentId) ?? "";
8397
+ if (effect.type === "terminate_stalled" && effect.recordSessionId) {
8398
+ const persisted = this.opts.timeline?.recordSessionStall?.(effect.agentId, effect.recordSessionId);
8399
+ if (persisted === false) {
8400
+ this.dispatch({
8401
+ type: "stall_control_failed",
8402
+ agentId: effect.agentId,
8403
+ sessionId: effect.recordSessionId,
8404
+ transition: "attempt"
8405
+ });
8406
+ this.log.error("stall recovery attempt was not persisted; termination deferred", {
8407
+ agentId: effect.agentId,
8408
+ sessionId: effect.recordSessionId
8409
+ });
8410
+ this.emitErrorAudit(effect.agentId, "runtime", "resume_control_update_failed", "Stall termination deferred because the recovery attempt could not be persisted");
8411
+ break;
8412
+ }
8413
+ }
8414
+ if (effect.type === "terminate_stalled" && effect.forgetSessionId) {
8415
+ const persisted = this.forgetSessionSources(effect.agentId, "stall_recovery", effect.forgetSessionId);
8416
+ if (!persisted) {
8417
+ this.dispatch({
8418
+ type: "stall_control_failed",
8419
+ agentId: effect.agentId,
8420
+ sessionId: effect.forgetSessionId,
8421
+ transition: "fence"
8422
+ });
8423
+ this.log.error("repeated-session fence was not persisted; termination deferred", {
8424
+ agentId: effect.agentId,
8425
+ sessionId: effect.forgetSessionId
8426
+ });
8427
+ this.emitErrorAudit(effect.agentId, "runtime", "resume_control_update_failed", "Stall termination deferred because the exact session fence could not be persisted");
8428
+ break;
8429
+ }
8430
+ if (spawnState)
8431
+ spawnState.discardEvents = true;
8432
+ this.log.warn("repeatedly stalled backend session fenced", {
8433
+ agentId: effect.agentId,
8434
+ sessionId: effect.forgetSessionId
8435
+ });
8436
+ }
8009
8437
  if (effect.type === "terminate_stalled" && spawnState) {
8010
8438
  this.closeTurn(spawnState, spawnState.activeSpan, {
8011
8439
  event: "turn_abort",
@@ -8024,10 +8452,27 @@ ${this.opts.wakePromptFooter}` : text;
8024
8452
  if (spawnState) {
8025
8453
  spawnState.terminationSemantics = effect.type === "terminate_stalled" ? "killed_stalled" : "idle_stop";
8026
8454
  }
8027
- this.logSessionEnded(effect.agentId, effect.type === "stop" ? "stopped" : "terminate_stalled");
8455
+ this.logSessionEnded(effect.agentId, effect.type === "stop" ? "stopped" : "terminate_stalled", endedSessionId);
8028
8456
  this.opts.onAgentLocallyStopped?.({ agentId: effect.agentId, reason: effect.type });
8029
8457
  break;
8030
8458
  }
8459
+ case "clear_stall_recovery": {
8460
+ const persisted = this.opts.timeline?.clearSessionStall?.(effect.agentId, effect.sessionId);
8461
+ if (persisted === false) {
8462
+ this.dispatch({
8463
+ type: "stall_control_failed",
8464
+ agentId: effect.agentId,
8465
+ sessionId: effect.sessionId,
8466
+ transition: "clear"
8467
+ });
8468
+ this.log.error("stall recovery clear was not persisted; allowance remains consumed", {
8469
+ agentId: effect.agentId,
8470
+ sessionId: effect.sessionId
8471
+ });
8472
+ this.emitErrorAudit(effect.agentId, "runtime", "resume_control_update_failed", "Stall recovery allowance remains consumed because its clear could not be persisted");
8473
+ }
8474
+ break;
8475
+ }
8031
8476
  case "expire_admission": {
8032
8477
  const owner = this.activeSpawnState.get(effect.agentId);
8033
8478
  if (!owner || owner.sessionInstanceId !== effect.sessionInstanceId)
@@ -8050,6 +8495,26 @@ ${this.opts.wakePromptFooter}` : text;
8050
8495
  mode: effect.mode
8051
8496
  });
8052
8497
  break;
8498
+ case "reset_idle_session": {
8499
+ const spawnState = this.activeSpawnState.get(effect.agentId);
8500
+ const persisted = this.forgetSession(effect.agentId, "reset_session", effect.sessionId);
8501
+ if (!persisted) {
8502
+ this.log.error("idle session reset barrier was not persisted; reset deferred", {
8503
+ agentId: effect.agentId,
8504
+ sessionId: effect.sessionId
8505
+ });
8506
+ this.emitErrorAudit(effect.agentId, "reset", "resume_control_update_failed", "Idle session reset deferred because resume control could not be persisted");
8507
+ break;
8508
+ }
8509
+ if (spawnState)
8510
+ spawnState.discardEvents = true;
8511
+ this.dispatch({ type: "idle_reset_committed", agentId: effect.agentId, nowMs: this.now() });
8512
+ this.log.info("idle agent session reset", {
8513
+ agentId: effect.agentId,
8514
+ sessionId: effect.sessionId
8515
+ });
8516
+ break;
8517
+ }
8053
8518
  case "force_exit": {
8054
8519
  const session = this.sessions.get(effect.agentId);
8055
8520
  const state = this.activeSpawnState.get(effect.agentId);
@@ -8082,8 +8547,8 @@ ${this.opts.wakePromptFooter}` : text;
8082
8547
  }
8083
8548
  }
8084
8549
  }
8085
- logSessionEnded(agentId, reason) {
8086
- this.log.info("agent session ended", { agentId, sessionId: this.liveSessions.get(agentId) ?? "", reason });
8550
+ logSessionEnded(agentId, reason, sessionId = this.liveSessions.get(agentId) ?? "") {
8551
+ this.log.info("agent session ended", { agentId, sessionId, reason });
8087
8552
  }
8088
8553
  doSpawn(agentId, messages, resumeSessionId) {
8089
8554
  const [first, ...pending] = messages;
@@ -8108,7 +8573,13 @@ ${this.opts.wakePromptFooter}` : text;
8108
8573
  mode: { kind: "default" }
8109
8574
  };
8110
8575
  const provider = runtimeConfig.runtime;
8111
- const sessionId = resumeSessionId ?? this.resumeSessions.get(agentId) ?? this.opts.timeline?.resumeSessionId(agentId, provider) ?? base.config?.sessionId;
8576
+ const timelineResolution = this.opts.timeline?.resolveResumeSession?.(agentId, provider);
8577
+ const timelineSessionId = timelineResolution?.kind === "session" ? timelineResolution.sessionId : timelineResolution === undefined ? this.opts.timeline?.resumeSessionId(agentId, provider) : null;
8578
+ const candidateSessionId = resumeSessionId ?? this.resumeSessions.get(agentId) ?? timelineSessionId ?? base.config?.sessionId;
8579
+ const blockedByBarrier = timelineResolution?.kind === "barrier" && (timelineResolution.type !== "stall_recovery" || timelineResolution.forgottenSessionId === null || timelineResolution.forgottenSessionId === candidateSessionId);
8580
+ const blockedByExactFence = candidateSessionId !== undefined && timelineResolution?.fencedSessionId === candidateSessionId;
8581
+ const sessionId = blockedByBarrier || blockedByExactFence ? undefined : candidateSessionId;
8582
+ const stalledSessionIdAtLaunch = timelineResolution !== undefined && (timelineResolution.kind === "session" || timelineResolution.kind === "none") && timelineResolution.stalledSessionId === sessionId ? timelineResolution.stalledSessionId : null;
8112
8583
  const description = runtimeConfig.instruction ?? base.config?.description ?? runtimeConfig.agentName;
8113
8584
  const agentName = runtimeConfig.agentName ?? base.config?.agentName;
8114
8585
  const agentHandle = runtimeConfig.agentHandle ?? base.config?.agentHandle;
@@ -8139,12 +8610,15 @@ ${this.opts.wakePromptFooter}` : text;
8139
8610
  handshakeTimer: null,
8140
8611
  torndown: false,
8141
8612
  superseded: false,
8613
+ discardEvents: false,
8614
+ stalledSessionIdAtLaunch,
8142
8615
  spawnFailureReason: null,
8143
8616
  terminationSemantics: null,
8144
8617
  spawnOrdinal: this.nextSpawnOrdinal++,
8145
8618
  launchIdSnapshot: typeof ctx.launchId === "string" && ctx.launchId.length > 0 ? ctx.launchId : null,
8146
8619
  nextTurnOrdinal: 1,
8147
8620
  activeSpan: null,
8621
+ timelineTurnOwner: null,
8148
8622
  pendingDeliverySpans: new Map
8149
8623
  };
8150
8624
  const previousOwner = this.activeSpawnState.get(agentId);
@@ -8195,7 +8669,6 @@ ${this.opts.wakePromptFooter}` : text;
8195
8669
  this.emitErrorAudit(agentId, "exit", "abnormal_exit", `Session ended unexpectedly (${detail})`);
8196
8670
  }
8197
8671
  }
8198
- this.flushThinkingAudit(agentId);
8199
8672
  if (state.sessionInstanceId) {
8200
8673
  this.dispatch({ type: "session_closed", agentId, sessionInstanceId: state.sessionInstanceId }, state);
8201
8674
  }
@@ -8220,10 +8693,23 @@ ${this.opts.wakePromptFooter}` : text;
8220
8693
  const onEvent = (event) => {
8221
8694
  if (state.torndown)
8222
8695
  return;
8696
+ if (state.discardEvents) {
8697
+ this.log.warn("ignored event from discarded backend session owner", { agentId, event: event.type });
8698
+ return;
8699
+ }
8223
8700
  if (event.type === "session_failed" && !state.hasEstablished) {
8224
8701
  reportSpawnFailure(event.error.code || "failed_to_start", { message: event.error.message });
8225
8702
  }
8226
8703
  if (event.type === "session_started") {
8704
+ const persisted = this.opts.timeline?.setSession(agentId, event.backendSessionId, event.sessionInstanceId);
8705
+ if (persisted === false) {
8706
+ state.discardEvents = true;
8707
+ reportSpawnFailure("resume_control_update_failed", {
8708
+ message: "Backend session rejected because resume control could not be persisted"
8709
+ });
8710
+ state.session?.stop({ reason: "shutdown", forceAfterMs: SESSION_STOP_GRACE_MS2 });
8711
+ return;
8712
+ }
8227
8713
  state.hasEstablished = true;
8228
8714
  clearHandshakeTimer();
8229
8715
  this.opts.onRuntimeSessionEstablished?.(driver.id);
@@ -8242,7 +8728,8 @@ ${this.opts.wakePromptFooter}` : text;
8242
8728
  type: "attach_session",
8243
8729
  agentId,
8244
8730
  sessionInstanceId: session.sessionInstanceId,
8245
- nowMs: this.now()
8731
+ nowMs: this.now(),
8732
+ turnSilence: session.snapshot().diagnostics?.turnSilence
8246
8733
  }, state);
8247
8734
  previousOwner?.pendingDeliverySpans.clear();
8248
8735
  (async () => {
@@ -8390,26 +8877,6 @@ ${this.opts.wakePromptFooter}` : text;
8390
8877
  this.log.debug("audit emit failed (error)", { agentId, err: String(err) });
8391
8878
  }
8392
8879
  }
8393
- flushThinkingAudit(agentId) {
8394
- const buffered = this.thinkingBuffers.get(agentId);
8395
- if (!buffered)
8396
- return;
8397
- this.thinkingBuffers.delete(agentId);
8398
- if (!this.opts.onBotAuditEvent)
8399
- return;
8400
- const { text, truncated, chars } = truncateThinking(buffered);
8401
- try {
8402
- this.opts.onBotAuditEvent(agentId, {
8403
- kind: "thinking",
8404
- payload: { text, truncated, chars }
8405
- }, {
8406
- sessionId: this.liveSessions.get(agentId) ?? null,
8407
- launchId: this.launchIds.get(agentId) ?? null
8408
- });
8409
- } catch (err) {
8410
- this.log.debug("audit emit failed (thinking)", { agentId, err: String(err) });
8411
- }
8412
- }
8413
8880
  onAgentEvent(agentId, event, runtimeId, owner) {
8414
8881
  if (event.type === "session_closed")
8415
8882
  return;
@@ -8442,32 +8909,43 @@ ${this.opts.wakePromptFooter}` : text;
8442
8909
  }
8443
8910
  }
8444
8911
  if (this.opts.onBotAuditEvent) {
8445
- if (event.type === "thinking_delta") {
8446
- if (event.text.length > 0) {
8447
- this.thinkingBuffers.set(agentId, (this.thinkingBuffers.get(agentId) ?? "") + event.text);
8912
+ if (event.type === "assistant_reasoning_completed" && event.text.length > 0) {
8913
+ const { text, truncated, chars } = truncateThinking(event.text);
8914
+ try {
8915
+ this.opts.onBotAuditEvent(agentId, {
8916
+ kind: "thinking",
8917
+ payload: { text, truncated: truncated || event.truncated, chars }
8918
+ }, {
8919
+ sessionId: this.liveSessions.get(agentId) ?? null,
8920
+ launchId: this.launchIds.get(agentId) ?? null
8921
+ });
8922
+ } catch (err) {
8923
+ this.log.debug("audit emit failed (thinking)", { agentId, err: String(err) });
8448
8924
  }
8449
- } else {
8450
- this.flushThinkingAudit(agentId);
8451
- if (event.type === "tool_started") {
8452
- const audit = extractToolAudit(event.name, event.input);
8453
- if (!audit.suppressed) {
8454
- const payload = audit.target !== undefined ? { name: audit.name, target: audit.target } : { name: audit.name };
8455
- try {
8456
- this.opts.onBotAuditEvent(agentId, { kind: "tool_call", payload }, {
8457
- sessionId: this.liveSessions.get(agentId) ?? null,
8458
- launchId: this.launchIds.get(agentId) ?? null
8459
- });
8460
- } catch (err) {
8461
- this.log.debug("audit emit failed (tool_call)", { agentId, err: String(err) });
8462
- }
8925
+ }
8926
+ if (event.type === "tool_started") {
8927
+ const audit = extractToolAudit(event.name, event.input);
8928
+ if (!audit.suppressed) {
8929
+ const payload = audit.target !== undefined ? { name: audit.name, target: audit.target } : { name: audit.name };
8930
+ try {
8931
+ this.opts.onBotAuditEvent(agentId, { kind: "tool_call", payload }, {
8932
+ sessionId: this.liveSessions.get(agentId) ?? null,
8933
+ launchId: this.launchIds.get(agentId) ?? null
8934
+ });
8935
+ } catch (err) {
8936
+ this.log.debug("audit emit failed (tool_call)", { agentId, err: String(err) });
8463
8937
  }
8464
8938
  }
8465
8939
  }
8466
8940
  }
8467
8941
  if (event.type === "session_started") {
8468
- this.dispatch({ type: "backend_session", agentId, sessionId: event.backendSessionId }, owner);
8942
+ this.dispatch({
8943
+ type: "backend_session",
8944
+ agentId,
8945
+ sessionId: event.backendSessionId,
8946
+ stalledBefore: owner.stalledSessionIdAtLaunch === event.backendSessionId
8947
+ }, owner);
8469
8948
  this.liveSessions.set(agentId, event.backendSessionId);
8470
- this.opts.timeline?.setSession(agentId, event.backendSessionId);
8471
8949
  this.opts.onAgentSession?.({
8472
8950
  agentId,
8473
8951
  sessionId: event.backendSessionId,
@@ -8479,8 +8957,20 @@ ${this.opts.wakePromptFooter}` : text;
8479
8957
  runtime: runtimeId
8480
8958
  });
8481
8959
  }
8482
- if (event.type === "text_delta" && event.text.length > 0) {
8483
- this.opts.timeline?.appendResponseToLatest(agentId, event.text);
8960
+ if (event.type === "turn_started") {
8961
+ const timelineTurnOwner = {
8962
+ sessionInstanceId: event.sessionInstanceId,
8963
+ rootTurnId: event.turnId,
8964
+ barrierGeneration: this.opts.timeline?.barrierGeneration(agentId) ?? 0
8965
+ };
8966
+ owner.timelineTurnOwner = timelineTurnOwner;
8967
+ this.opts.timeline?.beginTurn(agentId, timelineTurnOwner);
8968
+ }
8969
+ if (event.type === "assistant_message_completed" && event.text.length > 0) {
8970
+ const timelineTurnOwner = owner.timelineTurnOwner;
8971
+ if (timelineTurnOwner && timelineTurnOwner.sessionInstanceId === event.sessionInstanceId && timelineTurnOwner.rootTurnId === event.turnId) {
8972
+ this.opts.timeline?.recordAssistantMessage(agentId, timelineTurnOwner, event.text, event.truncated);
8973
+ }
8484
8974
  }
8485
8975
  if (event.type === "command_queued")
8486
8976
  this.acknowledgePendingDelivery(owner, event.commandId);
@@ -8498,9 +8988,10 @@ ${this.opts.wakePromptFooter}` : text;
8498
8988
  switch (event.type) {
8499
8989
  case "turn_started":
8500
8990
  return { type: "turn_started", turnId: event.turnId, commandIds: event.commandIds };
8501
- case "thinking_delta":
8502
- case "text_delta":
8503
- return event.text.length > 0 ? { type: "turn_work", turnId: event.turnId } : null;
8991
+ case "work_heartbeat":
8992
+ case "assistant_reasoning_completed":
8993
+ case "assistant_message_completed":
8994
+ return { type: "turn_work", turnId: event.turnId };
8504
8995
  case "tool_started":
8505
8996
  return { type: "turn_tool_started", turnId: event.turnId };
8506
8997
  case "tool_finished":
@@ -8527,37 +9018,55 @@ ${this.opts.wakePromptFooter}` : text;
8527
9018
  if (!wasActive && this.state.agents[agentId]?.turnActive && !owner.activeSpan)
8528
9019
  this.openTurn(owner);
8529
9020
  }
8530
- const signalKind = (() => {
9021
+ const nativeSignal = (() => {
8531
9022
  switch (event.type) {
8532
- case "session_started":
8533
- return "session_init";
8534
- case "thinking_delta":
8535
- return "thinking";
8536
- case "text_delta":
8537
- return "text";
9023
+ case "turn_started":
9024
+ return { kind: "turn_started", phase: "inference", turnId: event.turnId };
9025
+ case "backend_turn_started":
9026
+ return {
9027
+ kind: "backend_turn_started",
9028
+ phase: "inference",
9029
+ turnId: event.turnId,
9030
+ backendTurnId: event.backendTurnId
9031
+ };
9032
+ case "assistant_reasoning_completed":
9033
+ return { kind: "thinking", phase: "inference", turnId: event.turnId };
9034
+ case "assistant_message_completed":
9035
+ return { kind: "text", phase: "inference", turnId: event.turnId };
9036
+ case "work_heartbeat":
9037
+ return { kind: "internal_progress", phase: "inference", turnId: event.turnId };
8538
9038
  case "tool_started":
8539
- return "tool_call";
9039
+ return { kind: "tool_call", phase: "tool", turnId: event.turnId };
8540
9040
  case "tool_finished":
8541
- return "tool_output";
8542
- case "diagnostic":
8543
- return "runtime_diagnostic";
8544
- case "token_usage":
8545
- case "rate_limits":
8546
- return "telemetry";
9041
+ return { kind: "tool_output", phase: "inference", turnId: event.turnId };
9042
+ case "compaction_started":
9043
+ case "compaction_finished":
9044
+ case "review_started":
9045
+ case "review_finished":
9046
+ case "internal_progress":
9047
+ return event.turnId ? { kind: "internal_progress", phase: "inference", turnId: event.turnId } : null;
9048
+ case "recovery":
9049
+ return event.turnId ? {
9050
+ kind: "recovery",
9051
+ phase: event.stage === "retrying" ? "recovery" : "inference",
9052
+ recoveryStage: event.stage,
9053
+ turnId: event.turnId
9054
+ } : null;
8547
9055
  case "turn_completed":
8548
- return "turn_end";
8549
- case "session_failed":
8550
- return "error";
8551
- case "command_queued":
8552
- case "command_accepted":
8553
- case "command_failed":
8554
- case "turn_started":
8555
- return "internal_progress";
9056
+ return { kind: "turn_end", phase: "terminal", turnId: event.turnId };
8556
9057
  default:
8557
- return event.type;
9058
+ return null;
8558
9059
  }
8559
9060
  })();
8560
- this.dispatch({ type: "runtime_signal", agentId, kind: signalKind, nowMs: this.now() }, owner);
9061
+ if (nativeSignal) {
9062
+ this.dispatch({
9063
+ type: "runtime_signal",
9064
+ agentId,
9065
+ sessionInstanceId: event.sessionInstanceId,
9066
+ ...nativeSignal,
9067
+ nowMs: this.now()
9068
+ }, owner);
9069
+ }
8561
9070
  if (event.type === "turn_completed") {
8562
9071
  this.logSessionEnded(agentId, "turn_end");
8563
9072
  const marker = this.nonCleanEndMarker.get(agentId);
@@ -25792,7 +26301,7 @@ function parseLocalMessageReminderBody(body, agentId) {
25792
26301
  return null;
25793
26302
  if (!Number.isSafeInteger(record4.sentSeq) || record4.sentSeq < 1)
25794
26303
  return null;
25795
- if (!Number.isSafeInteger(record4.remindAfterMs) || record4.remindAfterMs < LOCAL_MESSAGE_REMINDER_MIN_MS || record4.remindAfterMs > LOCAL_MESSAGE_REMINDER_MAX_MS)
26304
+ if (!Number.isSafeInteger(record4.remindAfterMs) || record4.remindAfterMs !== 0 && record4.remindAfterMs < LOCAL_MESSAGE_REMINDER_MIN_MS || record4.remindAfterMs > LOCAL_MESSAGE_REMINDER_MAX_MS)
25796
26305
  return null;
25797
26306
  return {
25798
26307
  agentId,
@@ -26893,7 +27402,7 @@ class WsControlChannel {
26893
27402
  }
26894
27403
  // src/timeline/timeline.ts
26895
27404
  import * as fs7 from "node:fs";
26896
- import { randomBytes as randomBytes4 } from "node:crypto";
27405
+ import { createHash as createHash4, randomBytes as randomBytes4 } from "node:crypto";
26897
27406
  import { basename, dirname as dirname3, join as join10 } from "node:path";
26898
27407
 
26899
27408
  // src/timeline/filelock.ts
@@ -26960,17 +27469,25 @@ function reclaim(lockPath) {
26960
27469
  var TIMELINE_MAX_BYTES = 1048576;
26961
27470
  var TIMELINE_READ_CHUNK_BYTES = 65536;
26962
27471
  var DATE_FILENAME_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
27472
+ var RESUME_CONTROL_FILENAME = ".resume-control.json";
27473
+ var RESUME_CONTROL_MAX_BYTES = 4096;
27474
+ var EMPTY_RESUME_CONTROL = {
27475
+ version: 1,
27476
+ attemptedSessionId: null,
27477
+ fencedSessionId: null,
27478
+ fullBarrier: null
27479
+ };
26963
27480
  function isBarrier(entry) {
26964
- return entry.system?.type === "reset_session" || entry.system?.type === "nap";
27481
+ return entry.system !== undefined;
26965
27482
  }
26966
27483
  function canonicalTimelineEntry(value) {
26967
27484
  if (!value || typeof value !== "object")
26968
27485
  return null;
26969
27486
  const entry = value;
26970
27487
  if (entry.system) {
26971
- if (entry.system.type !== "reset_session" && entry.system.type !== "nap" || typeof entry.system.time !== "string")
27488
+ if (entry.system.type !== "reset_session" && entry.system.type !== "nap" && entry.system.type !== "stall_recovery_attempt" && entry.system.type !== "stall_recovery_clear" && entry.system.type !== "stall_recovery" || typeof entry.system.time !== "string" || entry.system.backend_session_id !== undefined && typeof entry.system.backend_session_id !== "string")
26972
27489
  return null;
26973
- return createSystemEntry(entry.system.type, entry.system.time);
27490
+ return createSystemEntry(entry.system.type, entry.system.time, entry.system.backend_session_id);
26974
27491
  }
26975
27492
  if (entry.session_id !== null && typeof entry.session_id !== "string")
26976
27493
  return null;
@@ -26988,13 +27505,60 @@ function canonicalTimelineEntry(value) {
26988
27505
  };
26989
27506
  }
26990
27507
  function timelineLine(entry) {
26991
- const boundedEntry = entry.system ? createSystemEntry(entry.system.type, entry.system.time) : { ...entry, agent_responses: entry.agent_responses.slice(-5) };
27508
+ const boundedEntry = entry.system ? createSystemEntry(entry.system.type, entry.system.time, entry.system.backend_session_id) : { ...entry, agent_responses: entry.agent_responses.slice(-5) };
26992
27509
  const text2 = JSON.stringify(boundedEntry);
26993
27510
  const bytes = Buffer.byteLength(text2, "utf8") + 1;
26994
27511
  if (bytes > TIMELINE_MAX_BYTES)
26995
27512
  return null;
26996
27513
  return { text: text2, bytes, entry: boundedEntry, barrier: isBarrier(boundedEntry) };
26997
27514
  }
27515
+ function timelineRowHash(line) {
27516
+ return createHash4("sha256").update(line.text, "utf8").digest("hex");
27517
+ }
27518
+ function timelineFileGeneration(filePath, lines) {
27519
+ try {
27520
+ const stat = fs7.lstatSync(filePath, { bigint: true });
27521
+ if (!stat.isFile())
27522
+ return null;
27523
+ const digest = createHash4("sha256");
27524
+ digest.update(`${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}
27525
+ `);
27526
+ for (const line of lines)
27527
+ digest.update(line.text, "utf8").update(`
27528
+ `);
27529
+ return digest.digest("hex");
27530
+ } catch {
27531
+ return null;
27532
+ }
27533
+ }
27534
+ function handleFor(filename, generation, lines, rowOrdinal) {
27535
+ return {
27536
+ filename,
27537
+ fileGeneration: generation,
27538
+ rowOrdinal,
27539
+ expectedHash: timelineRowHash(lines[rowOrdinal])
27540
+ };
27541
+ }
27542
+ function refreshTimelineEntryHandle(handle, rewrite) {
27543
+ if (handle.filename !== rewrite.filename)
27544
+ return handle;
27545
+ if (handle.fileGeneration !== rewrite.previousFileGeneration)
27546
+ return null;
27547
+ if (rewrite.previousRowHashes[handle.rowOrdinal] !== handle.expectedHash)
27548
+ return null;
27549
+ const rowOrdinal = rewrite.rowOrdinals[handle.rowOrdinal];
27550
+ if (rowOrdinal === null || rowOrdinal === undefined)
27551
+ return null;
27552
+ const expectedHash = rewrite.rowHashes[rowOrdinal];
27553
+ if (!expectedHash)
27554
+ return null;
27555
+ return {
27556
+ filename: handle.filename,
27557
+ fileGeneration: rewrite.fileGeneration,
27558
+ rowOrdinal,
27559
+ expectedHash
27560
+ };
27561
+ }
26998
27562
  function compactLines(input) {
26999
27563
  let head = 0;
27000
27564
  let bytes = input.reduce((total, line) => total + line.bytes, 0);
@@ -27206,11 +27770,37 @@ function atomicReplaceTimeline(filePath, lines) {
27206
27770
  } catch {}
27207
27771
  }
27208
27772
  }
27209
- function writeRequiredTimeline(filePath, input, required2) {
27773
+ function writeTrackedTimeline(filePath, filename, existing, input, required2, replacement) {
27774
+ const previousFileGeneration = timelineFileGeneration(filePath, existing);
27775
+ const previousRowHashes = existing.map(timelineRowHash);
27210
27776
  const compacted = compactLines(input);
27211
- if (!compacted.includes(required2))
27212
- return false;
27213
- return atomicReplaceTimeline(filePath, compacted);
27777
+ const targetOrdinal = compacted.indexOf(required2);
27778
+ if (targetOrdinal < 0)
27779
+ return { status: "rejected", reason: "evicted" };
27780
+ if (!atomicReplaceTimeline(filePath, compacted))
27781
+ return { status: "rejected", reason: "write" };
27782
+ const fileGeneration = timelineFileGeneration(filePath, compacted);
27783
+ if (!fileGeneration)
27784
+ return { status: "rejected", reason: "write" };
27785
+ const rowOrdinals = existing.map((line, oldOrdinal) => {
27786
+ if (replacement?.oldOrdinal === oldOrdinal)
27787
+ return compacted.indexOf(replacement.line);
27788
+ const nextOrdinal = compacted.indexOf(line);
27789
+ return nextOrdinal < 0 ? null : nextOrdinal;
27790
+ });
27791
+ const rewrite = {
27792
+ filename,
27793
+ previousFileGeneration,
27794
+ fileGeneration,
27795
+ previousRowHashes,
27796
+ rowOrdinals,
27797
+ rowHashes: compacted.map(timelineRowHash)
27798
+ };
27799
+ return {
27800
+ status: "written",
27801
+ rewrite,
27802
+ handle: handleFor(filename, fileGeneration, compacted, targetOrdinal)
27803
+ };
27214
27804
  }
27215
27805
  function filenameForDate(date5) {
27216
27806
  const y = date5.getFullYear();
@@ -27242,114 +27832,181 @@ function readRecentEntries(timelineDir, opts = {}) {
27242
27832
  }
27243
27833
  return entries;
27244
27834
  }
27245
- function appendEntry(timelineDir, entry, now = new Date) {
27835
+ function readResumeControlState(timelineDir) {
27836
+ if (timelineDirectoryState(timelineDir) !== "safe")
27837
+ return { kind: "missing" };
27838
+ const filePath = join10(timelineDir, RESUME_CONTROL_FILENAME);
27839
+ let source;
27840
+ try {
27841
+ source = fs7.lstatSync(filePath);
27842
+ } catch (error51) {
27843
+ return error51.code === "ENOENT" ? { kind: "missing" } : { kind: "invalid" };
27844
+ }
27845
+ if (!source.isFile() || source.size <= 0 || source.size > RESUME_CONTROL_MAX_BYTES) {
27846
+ return { kind: "invalid" };
27847
+ }
27848
+ let fd = null;
27849
+ try {
27850
+ fd = fs7.openSync(filePath, fs7.constants.O_RDONLY | (fs7.constants.O_NOFOLLOW ?? 0));
27851
+ const stat = fs7.fstatSync(fd);
27852
+ if (!stat.isFile() || stat.size <= 0 || stat.size > RESUME_CONTROL_MAX_BYTES) {
27853
+ return { kind: "invalid" };
27854
+ }
27855
+ const bounded = Buffer.allocUnsafe(stat.size + 1);
27856
+ let bytesRead = 0;
27857
+ while (bytesRead < bounded.length) {
27858
+ const count = fs7.readSync(fd, bounded, bytesRead, bounded.length - bytesRead, bytesRead);
27859
+ if (count <= 0)
27860
+ break;
27861
+ bytesRead += count;
27862
+ }
27863
+ if (bytesRead !== stat.size)
27864
+ return { kind: "invalid" };
27865
+ const raw = bounded.subarray(0, bytesRead).toString("utf8");
27866
+ const value = JSON.parse(raw);
27867
+ const validSessionId = (candidate) => candidate === null || typeof candidate === "string" && candidate.length > 0 && candidate.length <= 512;
27868
+ if (value.version !== 1 || !validSessionId(value.attemptedSessionId) || !validSessionId(value.fencedSessionId) || value.fullBarrier !== null && value.fullBarrier !== "reset_session" && value.fullBarrier !== "nap")
27869
+ return { kind: "invalid" };
27870
+ return {
27871
+ kind: "state",
27872
+ state: {
27873
+ version: 1,
27874
+ attemptedSessionId: value.attemptedSessionId,
27875
+ fencedSessionId: value.fencedSessionId,
27876
+ fullBarrier: value.fullBarrier
27877
+ }
27878
+ };
27879
+ } catch {
27880
+ return { kind: "invalid" };
27881
+ } finally {
27882
+ if (fd !== null) {
27883
+ try {
27884
+ fs7.closeSync(fd);
27885
+ } catch {}
27886
+ }
27887
+ }
27888
+ }
27889
+ function updateResumeControlState(timelineDir, update) {
27246
27890
  if (timelineDirectoryState(timelineDir) !== "safe")
27247
27891
  return false;
27248
- const filename = filenameForDate(now);
27249
- const filePath = join10(timelineDir, filename);
27250
- const lockPath = lockPathFor(timelineDir, filename);
27892
+ const lockPath = lockPathFor(timelineDir, RESUME_CONTROL_FILENAME);
27251
27893
  if (!acquireLock(lockPath))
27252
27894
  return false;
27253
27895
  try {
27254
- const existing = scanTimelineFile(filePath);
27255
- const required2 = timelineLine(entry);
27256
- if (!existing || !required2)
27896
+ const current = readResumeControlState(timelineDir);
27897
+ const base = current.kind === "state" ? current.state : EMPTY_RESUME_CONTROL;
27898
+ const next = update({ ...base });
27899
+ const canonical = {
27900
+ version: 1,
27901
+ attemptedSessionId: next.attemptedSessionId,
27902
+ fencedSessionId: next.fencedSessionId,
27903
+ fullBarrier: next.fullBarrier
27904
+ };
27905
+ const body = JSON.stringify(canonical) + `
27906
+ `;
27907
+ if (Buffer.byteLength(body, "utf8") > RESUME_CONTROL_MAX_BYTES)
27257
27908
  return false;
27258
- return writeRequiredTimeline(filePath, [...existing, required2], required2);
27909
+ const filePath = join10(timelineDir, RESUME_CONTROL_FILENAME);
27910
+ const tempPath = join10(timelineDir, `.${RESUME_CONTROL_FILENAME}.${process.pid}.${randomBytes4(12).toString("hex")}.tmp`);
27911
+ let fd = null;
27912
+ try {
27913
+ fd = fs7.openSync(tempPath, "wx", 384);
27914
+ fs7.writeFileSync(fd, body, "utf8");
27915
+ fs7.fsyncSync(fd);
27916
+ fs7.closeSync(fd);
27917
+ fd = null;
27918
+ fs7.renameSync(tempPath, filePath);
27919
+ return true;
27920
+ } finally {
27921
+ if (fd !== null) {
27922
+ try {
27923
+ fs7.closeSync(fd);
27924
+ } catch {}
27925
+ }
27926
+ try {
27927
+ fs7.unlinkSync(tempPath);
27928
+ } catch {}
27929
+ }
27259
27930
  } catch {
27260
27931
  return false;
27261
27932
  } finally {
27262
27933
  releaseLock(lockPath);
27263
27934
  }
27264
27935
  }
27265
- function appendOrMergeEntry(timelineDir, entry, now = new Date) {
27936
+ function appendTrackedEntry(timelineDir, entry, now = new Date) {
27266
27937
  if (timelineDirectoryState(timelineDir) !== "safe")
27267
- return false;
27938
+ return { status: "rejected", reason: "unsafe" };
27268
27939
  const filename = filenameForDate(now);
27269
27940
  const filePath = join10(timelineDir, filename);
27270
27941
  const lockPath = lockPathFor(timelineDir, filename);
27271
- if (!acquireLock(lockPath))
27272
- return false;
27942
+ try {
27943
+ if (!acquireLock(lockPath))
27944
+ return { status: "rejected", reason: "lock" };
27945
+ } catch {
27946
+ return { status: "rejected", reason: "write" };
27947
+ }
27273
27948
  try {
27274
27949
  const existing = scanTimelineFile(filePath);
27275
- if (!existing)
27276
- return false;
27277
- if (existing.length > 0) {
27278
- const latest = existing[existing.length - 1].entry;
27279
- const mergeable = !latest.system && !entry.system && latest.session_id === entry.session_id && latest.provider === entry.provider && latest.agent_responses.length === 0;
27280
- if (mergeable) {
27281
- const merged = {
27282
- ...latest,
27283
- messages: [...latest.messages, ...entry.messages],
27284
- agent_responses: [...latest.agent_responses]
27285
- };
27286
- const required3 = timelineLine(merged);
27287
- if (!required3)
27288
- return false;
27289
- return writeRequiredTimeline(filePath, [...existing.slice(0, -1), required3], required3);
27290
- }
27291
- }
27292
27950
  const required2 = timelineLine(entry);
27951
+ if (!existing)
27952
+ return { status: "rejected", reason: "unsafe" };
27293
27953
  if (!required2)
27294
- return false;
27295
- return writeRequiredTimeline(filePath, [...existing, required2], required2);
27954
+ return { status: "rejected", reason: "oversized" };
27955
+ return writeTrackedTimeline(filePath, filename, existing, [...existing, required2], required2);
27296
27956
  } catch {
27297
- return false;
27957
+ return { status: "rejected", reason: "write" };
27298
27958
  } finally {
27299
27959
  releaseLock(lockPath);
27300
27960
  }
27301
27961
  }
27302
- function updateLatestEntryResult(timelineDir, updater, opts = {}) {
27303
- const directoryState = timelineDirectoryState(timelineDir);
27304
- if (directoryState !== "safe")
27305
- return directoryState === "missing" ? "missing" : "rejected";
27306
- const now = opts.now ?? new Date;
27307
- const maxDays = opts.maxDays ?? 7;
27308
- for (const filename of recentFilenames(maxDays, now)) {
27309
- const filePath = join10(timelineDir, filename);
27310
- let source;
27311
- try {
27312
- source = fs7.lstatSync(filePath);
27313
- } catch (error51) {
27314
- if (error51.code === "ENOENT")
27315
- continue;
27316
- return "rejected";
27317
- }
27318
- if (!source.isFile())
27319
- return "rejected";
27320
- const lockPath = lockPathFor(timelineDir, filename);
27962
+ function updateTrackedEntry(timelineDir, handle, update) {
27963
+ if (timelineDirectoryState(timelineDir) !== "safe")
27964
+ return { status: "rejected", reason: "unsafe" };
27965
+ if (!DATE_FILENAME_PATTERN.test(handle.filename) || basename(handle.filename) !== handle.filename) {
27966
+ return { status: "rejected", reason: "unsafe" };
27967
+ }
27968
+ const filePath = join10(timelineDir, handle.filename);
27969
+ const lockPath = lockPathFor(timelineDir, handle.filename);
27970
+ try {
27321
27971
  if (!acquireLock(lockPath))
27322
- return "rejected";
27323
- try {
27324
- const lines = scanTimelineFile(filePath);
27325
- if (!lines)
27326
- return "rejected";
27327
- if (lines.length === 0)
27328
- continue;
27329
- const latest = lines[lines.length - 1].entry;
27330
- if (latest.system)
27331
- return "missing";
27332
- const updated = {
27333
- ...latest,
27334
- messages: [...latest.messages],
27335
- agent_responses: [...latest.agent_responses]
27336
- };
27337
- try {
27338
- updater(updated);
27339
- } catch {
27340
- return "rejected";
27341
- }
27342
- const required2 = timelineLine(updated);
27343
- if (!required2)
27344
- return "rejected";
27345
- return writeRequiredTimeline(filePath, [...lines.slice(0, -1), required2], required2) ? "updated" : "rejected";
27346
- } catch {
27347
- return "rejected";
27348
- } finally {
27349
- releaseLock(lockPath);
27350
- }
27972
+ return { status: "rejected", reason: "lock" };
27973
+ } catch {
27974
+ return { status: "rejected", reason: "write" };
27975
+ }
27976
+ try {
27977
+ const existing = scanTimelineFile(filePath);
27978
+ if (!existing)
27979
+ return { status: "rejected", reason: "unsafe" };
27980
+ if (existing.length === 0)
27981
+ return { status: "rejected", reason: "missing" };
27982
+ const generation = timelineFileGeneration(filePath, existing);
27983
+ if (!generation || generation !== handle.fileGeneration) {
27984
+ return { status: "rejected", reason: "generation" };
27985
+ }
27986
+ const captured = existing[handle.rowOrdinal];
27987
+ if (!captured)
27988
+ return { status: "rejected", reason: "ordinal" };
27989
+ if (timelineRowHash(captured) !== handle.expectedHash) {
27990
+ return { status: "rejected", reason: "hash" };
27991
+ }
27992
+ if (captured.entry.system)
27993
+ return { status: "rejected", reason: "system" };
27994
+ const nextEntry = update({
27995
+ ...captured.entry,
27996
+ messages: [...captured.entry.messages],
27997
+ agent_responses: [...captured.entry.agent_responses]
27998
+ });
27999
+ const replacement = timelineLine(nextEntry);
28000
+ if (!replacement)
28001
+ return { status: "rejected", reason: "oversized" };
28002
+ const input = [...existing];
28003
+ input[handle.rowOrdinal] = replacement;
28004
+ return writeTrackedTimeline(filePath, handle.filename, existing, input, replacement, { oldOrdinal: handle.rowOrdinal, line: replacement });
28005
+ } catch {
28006
+ return { status: "rejected", reason: "write" };
28007
+ } finally {
28008
+ releaseLock(lockPath);
27351
28009
  }
27352
- return "missing";
27353
28010
  }
27354
28011
  function yieldToEventLoop() {
27355
28012
  return new Promise((resolve3) => setImmediate(resolve3));
@@ -27413,84 +28070,562 @@ function createTimelineEntry(fields) {
27413
28070
  provider: fields.provider ?? null
27414
28071
  };
27415
28072
  }
27416
- function createSystemEntry(type, time3) {
28073
+ function createSystemEntry(type, time3, backendSessionId) {
27417
28074
  return {
27418
28075
  session_id: null,
27419
28076
  messages: [],
27420
28077
  agent_responses: [],
27421
28078
  provider: null,
27422
- system: { type, time: time3 }
28079
+ system: {
28080
+ type,
28081
+ time: time3,
28082
+ ...backendSessionId ? { backend_session_id: backendSessionId } : {}
28083
+ }
27423
28084
  };
27424
28085
  }
27425
- function findResumableSession(rows, provider) {
28086
+ function resolveResumableSession(rows, provider) {
28087
+ let candidateSessionId = null;
28088
+ let recoveryMarkerSeen = false;
28089
+ let stalledSessionId = null;
28090
+ let fencedSessionId = null;
27426
28091
  for (let i = rows.length - 1;i >= 0; i--) {
27427
28092
  const e = rows[i];
27428
- if (e.system?.type === "reset_session" || e.system?.type === "nap")
27429
- return null;
28093
+ if (e.system) {
28094
+ if (e.system.type === "stall_recovery_attempt") {
28095
+ if (!recoveryMarkerSeen) {
28096
+ recoveryMarkerSeen = true;
28097
+ stalledSessionId = e.system.backend_session_id ?? null;
28098
+ }
28099
+ continue;
28100
+ }
28101
+ if (e.system.type === "stall_recovery_clear") {
28102
+ if (!recoveryMarkerSeen)
28103
+ recoveryMarkerSeen = true;
28104
+ continue;
28105
+ }
28106
+ if (e.system.type === "stall_recovery" && fencedSessionId === null) {
28107
+ fencedSessionId = e.system.backend_session_id ?? null;
28108
+ }
28109
+ if (candidateSessionId !== null) {
28110
+ return {
28111
+ kind: "session",
28112
+ sessionId: candidateSessionId,
28113
+ stalledSessionId: stalledSessionId === candidateSessionId ? stalledSessionId : null,
28114
+ fencedSessionId
28115
+ };
28116
+ }
28117
+ return {
28118
+ kind: "barrier",
28119
+ type: e.system.type,
28120
+ forgottenSessionId: e.system.backend_session_id ?? null,
28121
+ fencedSessionId
28122
+ };
28123
+ }
27430
28124
  if (!e.session_id)
27431
28125
  continue;
27432
28126
  if (provider && e.provider !== provider)
27433
28127
  continue;
27434
- return e.session_id;
28128
+ if (candidateSessionId === null) {
28129
+ candidateSessionId = e.session_id;
28130
+ continue;
28131
+ }
28132
+ if (candidateSessionId !== e.session_id)
28133
+ break;
27435
28134
  }
27436
- return null;
28135
+ if (candidateSessionId !== null) {
28136
+ return {
28137
+ kind: "session",
28138
+ sessionId: candidateSessionId,
28139
+ stalledSessionId: stalledSessionId === candidateSessionId ? stalledSessionId : null,
28140
+ fencedSessionId
28141
+ };
28142
+ }
28143
+ return { kind: "none", stalledSessionId, fencedSessionId };
27437
28144
  }
27438
28145
  // src/timeline/recorder.ts
27439
28146
  var MAX_AGENT_RESPONSES = 5;
27440
- function appendAgentResponse(entry, text2) {
27441
- entry.agent_responses.push(text2);
27442
- if (entry.agent_responses.length > MAX_AGENT_RESPONSES) {
27443
- entry.agent_responses.splice(0, entry.agent_responses.length - MAX_AGENT_RESPONSES);
28147
+ var MAX_AGENT_RESPONSE_BYTES = 65536;
28148
+ var MAX_PENDING_COMMITS_PER_AGENT = 8;
28149
+ var PENDING_COMMIT_TTL_MS = 15 * 60000;
28150
+ var TRUNCATION_MARKER = `
28151
+ … [truncated]`;
28152
+ function turnKey(agentId, owner) {
28153
+ return `${agentId}\x00${owner.sessionInstanceId}\x00${owner.rootTurnId}\x00${owner.barrierGeneration}`;
28154
+ }
28155
+ function sameOwner(left, right) {
28156
+ return left.sessionInstanceId === right.sessionInstanceId && left.rootTurnId === right.rootTurnId && left.barrierGeneration === right.barrierGeneration;
28157
+ }
28158
+ function utf8Prefix2(text2, maxBytes) {
28159
+ if (maxBytes <= 0)
28160
+ return "";
28161
+ if (Buffer.byteLength(text2, "utf8") <= maxBytes)
28162
+ return text2;
28163
+ let low = 0;
28164
+ let high = text2.length;
28165
+ while (low < high) {
28166
+ const mid = Math.ceil((low + high) / 2);
28167
+ let end2 = mid;
28168
+ const code2 = text2.charCodeAt(end2 - 1);
28169
+ if (code2 >= 55296 && code2 <= 56319)
28170
+ end2 -= 1;
28171
+ if (Buffer.byteLength(text2.slice(0, end2), "utf8") <= maxBytes)
28172
+ low = mid;
28173
+ else
28174
+ high = mid - 1;
28175
+ }
28176
+ let end = low;
28177
+ const code = text2.charCodeAt(end - 1);
28178
+ if (code >= 55296 && code <= 56319)
28179
+ end -= 1;
28180
+ while (end > 0 && Buffer.byteLength(text2.slice(0, end), "utf8") > maxBytes)
28181
+ end -= 1;
28182
+ return text2.slice(0, end);
28183
+ }
28184
+ function boundedResponse(text2, alreadyTruncated) {
28185
+ const markerBytes = Buffer.byteLength(TRUNCATION_MARKER, "utf8");
28186
+ const needsTruncation = alreadyTruncated || Buffer.byteLength(text2, "utf8") > MAX_AGENT_RESPONSE_BYTES;
28187
+ if (!needsTruncation)
28188
+ return text2;
28189
+ return utf8Prefix2(text2, MAX_AGENT_RESPONSE_BYTES - markerBytes) + TRUNCATION_MARKER;
28190
+ }
28191
+ function serializedTimelineBytes(entry) {
28192
+ return Buffer.byteLength(JSON.stringify(entry), "utf8") + 1;
28193
+ }
28194
+ function fitResponsesToRow(entry, responses) {
28195
+ let fitted = [...entry.agent_responses];
28196
+ for (const response of responses) {
28197
+ fitted = [...fitted, response].slice(-MAX_AGENT_RESPONSES);
28198
+ if (serializedTimelineBytes({ ...entry, agent_responses: fitted }) <= TIMELINE_MAX_BYTES)
28199
+ continue;
28200
+ const truncationBase = response.endsWith(TRUNCATION_MARKER) ? response.slice(0, -TRUNCATION_MARKER.length) : response;
28201
+ let low = 0;
28202
+ let high = truncationBase.length;
28203
+ let replacement = null;
28204
+ while (low <= high) {
28205
+ const mid = Math.floor((low + high) / 2);
28206
+ let end = mid;
28207
+ const code = truncationBase.charCodeAt(end - 1);
28208
+ if (code >= 55296 && code <= 56319)
28209
+ end -= 1;
28210
+ const candidateResponse = truncationBase.slice(0, end) + TRUNCATION_MARKER;
28211
+ const candidate = [...fitted.slice(0, -1), candidateResponse];
28212
+ if (serializedTimelineBytes({ ...entry, agent_responses: candidate }) <= TIMELINE_MAX_BYTES) {
28213
+ replacement = candidateResponse;
28214
+ low = mid + 1;
28215
+ } else {
28216
+ high = mid - 1;
28217
+ }
28218
+ }
28219
+ if (replacement === null)
28220
+ return null;
28221
+ fitted[fitted.length - 1] = replacement;
27444
28222
  }
28223
+ return fitted;
27445
28224
  }
27446
28225
  function createTimelineRecorder(opts) {
27447
28226
  const now = opts.now ?? (() => new Date);
27448
28227
  const dirFor = (agentId) => opts.timelineDirFor(agentId);
27449
28228
  const sessionByAgent = new Map;
28229
+ const resolveForAgent = (agentId, provider) => {
28230
+ const dir = dirFor(agentId);
28231
+ const recent = resolveResumableSession(readRecentEntries(dir, { now: now() }), provider ?? undefined);
28232
+ const control = readResumeControlState(dir);
28233
+ if (control.kind === "missing")
28234
+ return recent;
28235
+ if (control.kind === "invalid") {
28236
+ return {
28237
+ kind: "barrier",
28238
+ type: "reset_session",
28239
+ forgottenSessionId: null,
28240
+ fencedSessionId: null
28241
+ };
28242
+ }
28243
+ const { attemptedSessionId, fencedSessionId, fullBarrier } = control.state;
28244
+ if (fullBarrier !== null) {
28245
+ return {
28246
+ kind: "barrier",
28247
+ type: fullBarrier,
28248
+ forgottenSessionId: null,
28249
+ fencedSessionId
28250
+ };
28251
+ }
28252
+ if (recent.kind === "session") {
28253
+ return {
28254
+ kind: "session",
28255
+ sessionId: recent.sessionId,
28256
+ stalledSessionId: attemptedSessionId === recent.sessionId ? attemptedSessionId : null,
28257
+ fencedSessionId
28258
+ };
28259
+ }
28260
+ if (recent.kind === "barrier") {
28261
+ if (recent.type !== "stall_recovery" || recent.forgottenSessionId === fencedSessionId) {
28262
+ return { ...recent, fencedSessionId };
28263
+ }
28264
+ }
28265
+ return { kind: "none", stalledSessionId: attemptedSessionId, fencedSessionId };
28266
+ };
28267
+ const sessionByEpoch = new Map;
28268
+ const barrierByAgent = new Map;
28269
+ const turnsByAgent = new Map;
28270
+ const activeTurnByAgent = new Map;
28271
+ const epochKey = (agentId, sessionInstanceId) => `${agentId}\x00${sessionInstanceId}`;
28272
+ const currentBarrier = (agentId) => barrierByAgent.get(agentId) ?? 0;
28273
+ const statesFor = (agentId) => {
28274
+ let states = turnsByAgent.get(agentId);
28275
+ if (!states) {
28276
+ states = new Map;
28277
+ turnsByAgent.set(agentId, states);
28278
+ }
28279
+ return states;
28280
+ };
28281
+ const diagnostic = (agentId, code, reason) => {
28282
+ try {
28283
+ opts.onDiagnostic?.({ agentId, code, ...reason ? { reason } : {} });
28284
+ } catch {}
28285
+ };
28286
+ const applyRewrite = (agentId, rewrite) => {
28287
+ for (const state of statesFor(agentId).values()) {
28288
+ if (!state.handle)
28289
+ continue;
28290
+ const refreshed = refreshTimelineEntryHandle(state.handle, rewrite);
28291
+ if (refreshed) {
28292
+ state.handle = refreshed;
28293
+ } else if (state.handle.filename === rewrite.filename) {
28294
+ state.handle = undefined;
28295
+ state.rowFenced = true;
28296
+ diagnostic(agentId, "timeline_handle_fenced", "rewrite_remap");
28297
+ }
28298
+ }
28299
+ };
28300
+ const handleTrackedResult = (agentId, state, result) => {
28301
+ if (result.status === "written") {
28302
+ applyRewrite(agentId, result.rewrite);
28303
+ if (state && result.handle) {
28304
+ state.handle = result.handle;
28305
+ state.rowFenced = false;
28306
+ }
28307
+ return "written";
28308
+ }
28309
+ if (result.reason === "lock" || result.reason === "write")
28310
+ return "retryable";
28311
+ if (state && result.reason !== "oversized")
28312
+ state.rowFenced = true;
28313
+ diagnostic(agentId, "timeline_exact_write_rejected", result.reason);
28314
+ return "terminal";
28315
+ };
28316
+ const tryCommit = (agentId, state) => {
28317
+ if (state.responses.length === 0)
28318
+ return "written";
28319
+ const dir = dirFor(agentId);
28320
+ if (!prepareTimelineDirectory(dir)) {
28321
+ diagnostic(agentId, "timeline_directory_unavailable");
28322
+ return "terminal";
28323
+ }
28324
+ if (state.handle) {
28325
+ const result2 = updateTrackedEntry(dir, state.handle, (entry2) => {
28326
+ const fitted2 = fitResponsesToRow(entry2, state.responses);
28327
+ return { ...entry2, agent_responses: fitted2 ?? [...entry2.agent_responses, ...state.responses] };
28328
+ });
28329
+ return handleTrackedResult(agentId, state, result2);
28330
+ }
28331
+ if (state.rowFenced || state.pendingMode === "handle")
28332
+ return "terminal";
28333
+ if (state.owner.barrierGeneration !== currentBarrier(agentId))
28334
+ return "terminal";
28335
+ const entry = createTimelineEntry({
28336
+ messages: [],
28337
+ sessionId: state.backendSessionId,
28338
+ provider: state.provider
28339
+ });
28340
+ const fitted = fitResponsesToRow(entry, state.responses);
28341
+ if (!fitted) {
28342
+ diagnostic(agentId, "timeline_response_did_not_fit", "oversized");
28343
+ return "terminal";
28344
+ }
28345
+ entry.agent_responses = fitted;
28346
+ const result = appendTrackedEntry(dir, entry, now());
28347
+ return handleTrackedResult(agentId, state, result);
28348
+ };
28349
+ const deleteState = (agentId, key) => {
28350
+ const states = statesFor(agentId);
28351
+ states.delete(key);
28352
+ if (activeTurnByAgent.get(agentId) === key)
28353
+ activeTurnByAgent.delete(agentId);
28354
+ if (states.size === 0)
28355
+ turnsByAgent.delete(agentId);
28356
+ };
28357
+ const retryPending = (agentId) => {
28358
+ const states = turnsByAgent.get(agentId);
28359
+ if (!states)
28360
+ return;
28361
+ const nowMs = now().getTime();
28362
+ for (const [key, state] of [...states]) {
28363
+ if (state.finalized && state.pendingSinceMs === undefined && state.completedAtMs !== undefined && nowMs - state.completedAtMs >= PENDING_COMMIT_TTL_MS) {
28364
+ deleteState(agentId, key);
28365
+ continue;
28366
+ }
28367
+ if (!state.finalized || state.pendingSinceMs === undefined)
28368
+ continue;
28369
+ if (nowMs - state.pendingSinceMs >= PENDING_COMMIT_TTL_MS) {
28370
+ diagnostic(agentId, "timeline_pending_commit_expired");
28371
+ deleteState(agentId, key);
28372
+ continue;
28373
+ }
28374
+ const result = tryCommit(agentId, state);
28375
+ if (result === "written") {
28376
+ state.responses = [];
28377
+ state.pendingSinceMs = undefined;
28378
+ state.pendingMode = undefined;
28379
+ state.completedAtMs = nowMs;
28380
+ } else if (result === "terminal") {
28381
+ deleteState(agentId, key);
28382
+ }
28383
+ }
28384
+ const completed = [...states.entries()].filter(([, state]) => state.finalized && state.pendingSinceMs === undefined).sort((left, right) => (left[1].completedAtMs ?? 0) - (right[1].completedAtMs ?? 0));
28385
+ while (completed.length > MAX_PENDING_COMMITS_PER_AGENT) {
28386
+ const oldest = completed.shift();
28387
+ if (oldest)
28388
+ deleteState(agentId, oldest[0]);
28389
+ }
28390
+ };
28391
+ const retainPending = (agentId, key, state) => {
28392
+ const states = statesFor(agentId);
28393
+ const pending = [...states.values()].filter((candidate) => candidate.finalized && candidate.pendingSinceMs !== undefined);
28394
+ if (pending.length >= MAX_PENDING_COMMITS_PER_AGENT) {
28395
+ diagnostic(agentId, "timeline_pending_commit_overflow");
28396
+ deleteState(agentId, key);
28397
+ return;
28398
+ }
28399
+ state.pendingSinceMs ??= now().getTime();
28400
+ state.pendingMode = state.handle ? "handle" : "fallback";
28401
+ };
28402
+ const appendOwnerless = (agentId, messages) => {
28403
+ if (messages.length === 0)
28404
+ return;
28405
+ const dir = dirFor(agentId);
28406
+ if (!prepareTimelineDirectory(dir))
28407
+ return;
28408
+ const result = appendTrackedEntry(dir, createTimelineEntry({
28409
+ messages,
28410
+ sessionId: sessionByAgent.get(agentId) ?? null,
28411
+ provider: opts.providerFor?.(agentId) ?? null
28412
+ }), now());
28413
+ handleTrackedResult(agentId, null, result);
28414
+ };
27450
28415
  return {
27451
- setSession(agentId, sessionId) {
27452
- sessionByAgent.set(agentId, sessionId);
28416
+ barrierGeneration(agentId) {
28417
+ return currentBarrier(agentId);
28418
+ },
28419
+ beginTurn(agentId, owner) {
28420
+ retryPending(agentId);
28421
+ const states = statesFor(agentId);
28422
+ if (owner.barrierGeneration !== currentBarrier(agentId)) {
28423
+ diagnostic(agentId, "timeline_turn_begin_fenced", "barrier_generation");
28424
+ return;
28425
+ }
28426
+ const key = turnKey(agentId, owner);
28427
+ if (!states.has(key)) {
28428
+ states.set(key, {
28429
+ owner: { ...owner },
28430
+ provider: opts.providerFor?.(agentId) ?? null,
28431
+ backendSessionId: sessionByEpoch.get(epochKey(agentId, owner.sessionInstanceId)) ?? sessionByAgent.get(agentId) ?? null,
28432
+ responses: [],
28433
+ rowFenced: false,
28434
+ finalized: false
28435
+ });
28436
+ }
28437
+ activeTurnByAgent.set(agentId, key);
27453
28438
  },
27454
- appendEntryForAgent(agentId, messages) {
28439
+ recordInboxPull(agentId, owner, messages) {
28440
+ retryPending(agentId);
27455
28441
  if (messages.length === 0)
27456
28442
  return;
28443
+ if (!owner) {
28444
+ appendOwnerless(agentId, messages);
28445
+ return;
28446
+ }
28447
+ const key = turnKey(agentId, owner);
28448
+ const state = turnsByAgent.get(agentId)?.get(key);
28449
+ if (!state || state.rowFenced || !sameOwner(state.owner, owner)) {
28450
+ appendOwnerless(agentId, messages);
28451
+ return;
28452
+ }
27457
28453
  const dir = dirFor(agentId);
27458
28454
  if (!prepareTimelineDirectory(dir))
27459
28455
  return;
27460
- appendOrMergeEntry(dir, createTimelineEntry({
27461
- messages,
27462
- sessionId: sessionByAgent.get(agentId) ?? null,
27463
- provider: opts.providerFor?.(agentId) ?? null
27464
- }), now());
28456
+ const stamp = now();
28457
+ let result;
28458
+ if (state.handle && (state.handle.filename === filenameForDate(stamp) || state.owner.barrierGeneration !== currentBarrier(agentId))) {
28459
+ result = updateTrackedEntry(dir, state.handle, (entry) => ({
28460
+ ...entry,
28461
+ messages: [...entry.messages, ...messages],
28462
+ session_id: state.backendSessionId,
28463
+ provider: state.provider
28464
+ }));
28465
+ } else if (state.owner.barrierGeneration === currentBarrier(agentId)) {
28466
+ result = appendTrackedEntry(dir, createTimelineEntry({
28467
+ messages,
28468
+ sessionId: state.backendSessionId,
28469
+ provider: state.provider
28470
+ }), stamp);
28471
+ } else {
28472
+ appendOwnerless(agentId, messages);
28473
+ return;
28474
+ }
28475
+ handleTrackedResult(agentId, state, result);
28476
+ },
28477
+ recordAssistantMessage(agentId, owner, text2, truncated = false) {
28478
+ retryPending(agentId);
28479
+ const key = turnKey(agentId, owner);
28480
+ const state = turnsByAgent.get(agentId)?.get(key);
28481
+ if (!state || state.finalized || state.owner.barrierGeneration !== currentBarrier(agentId) || !sameOwner(state.owner, owner)) {
28482
+ diagnostic(agentId, "timeline_completed_message_rejected", "stale_owner");
28483
+ return;
28484
+ }
28485
+ state.responses.push(boundedResponse(text2, truncated));
28486
+ if (state.responses.length > MAX_AGENT_RESPONSES) {
28487
+ state.responses.splice(0, state.responses.length - MAX_AGENT_RESPONSES);
28488
+ }
27465
28489
  },
27466
- appendResponseToLatest(agentId, text2) {
27467
- const dir = dirFor(agentId);
27468
- if (!prepareTimelineDirectory(dir))
28490
+ finalizeTurn(agentId, owner) {
28491
+ retryPending(agentId);
28492
+ const key = turnKey(agentId, owner);
28493
+ const state = turnsByAgent.get(agentId)?.get(key);
28494
+ if (!state || state.finalized || !sameOwner(state.owner, owner))
27469
28495
  return;
27470
- const result = updateLatestEntryResult(dir, (entry2) => appendAgentResponse(entry2, text2), { now: now() });
27471
- if (result === "updated" || result === "rejected")
28496
+ const fallbackAuthorized = activeTurnByAgent.get(agentId) === key && owner.barrierGeneration === currentBarrier(agentId);
28497
+ state.finalized = true;
28498
+ activeTurnByAgent.delete(agentId);
28499
+ if (state.responses.length === 0) {
28500
+ state.completedAtMs = now().getTime();
27472
28501
  return;
27473
- const entry = createTimelineEntry({
27474
- messages: [],
27475
- sessionId: sessionByAgent.get(agentId) ?? null,
27476
- provider: opts.providerFor?.(agentId) ?? null
27477
- });
27478
- appendAgentResponse(entry, text2);
27479
- appendEntry(dir, entry, now());
28502
+ }
28503
+ if (!state.handle && !fallbackAuthorized) {
28504
+ diagnostic(agentId, "timeline_fallback_rejected", "fenced_owner");
28505
+ deleteState(agentId, key);
28506
+ return;
28507
+ }
28508
+ const result = tryCommit(agentId, state);
28509
+ if (result === "written") {
28510
+ state.responses = [];
28511
+ state.completedAtMs = now().getTime();
28512
+ } else if (result === "terminal") {
28513
+ deleteState(agentId, key);
28514
+ } else {
28515
+ retainPending(agentId, key, state);
28516
+ }
28517
+ },
28518
+ fenceSession(agentId) {
28519
+ retryPending(agentId);
28520
+ barrierByAgent.set(agentId, currentBarrier(agentId) + 1);
28521
+ activeTurnByAgent.delete(agentId);
28522
+ const states = turnsByAgent.get(agentId);
28523
+ if (!states)
28524
+ return;
28525
+ for (const [key, state] of [...states]) {
28526
+ if (!state.handle) {
28527
+ diagnostic(agentId, "timeline_fallback_rejected", "session_fence");
28528
+ deleteState(agentId, key);
28529
+ }
28530
+ }
28531
+ },
28532
+ setSession(agentId, sessionId, sessionInstanceId) {
28533
+ retryPending(agentId);
28534
+ const dir = dirFor(agentId);
28535
+ if (!prepareTimelineDirectory(dir))
28536
+ return false;
28537
+ const persisted = updateResumeControlState(dir, (state) => ({
28538
+ ...state,
28539
+ fullBarrier: null,
28540
+ attemptedSessionId: state.attemptedSessionId === sessionId ? state.attemptedSessionId : null
28541
+ }));
28542
+ if (!persisted)
28543
+ return false;
28544
+ sessionByAgent.set(agentId, sessionId);
28545
+ if (sessionInstanceId) {
28546
+ sessionByEpoch.set(epochKey(agentId, sessionInstanceId), sessionId);
28547
+ for (const state of statesFor(agentId).values()) {
28548
+ if (state.owner.sessionInstanceId === sessionInstanceId)
28549
+ state.backendSessionId = sessionId;
28550
+ }
28551
+ }
28552
+ return true;
27480
28553
  },
27481
28554
  resumeSessionId(agentId, provider) {
27482
- const rows = readRecentEntries(dirFor(agentId), { now: now() });
27483
- return findResumableSession(rows, provider ?? undefined);
28555
+ const resolution = resolveForAgent(agentId, provider);
28556
+ return resolution.kind === "session" ? resolution.sessionId : null;
28557
+ },
28558
+ resolveResumeSession(agentId, provider) {
28559
+ return resolveForAgent(agentId, provider);
27484
28560
  },
27485
- forgetSession(agentId, barrierType = "reset_session") {
28561
+ recordSessionStall(agentId, sessionId) {
28562
+ return appendStallMarker(agentId, "stall_recovery_attempt", sessionId);
28563
+ },
28564
+ clearSessionStall(agentId, sessionId) {
28565
+ return appendStallMarker(agentId, "stall_recovery_clear", sessionId);
28566
+ },
28567
+ forgetSession(agentId, barrierType = "reset_session", forgottenSessionId) {
28568
+ retryPending(agentId);
27486
28569
  const dir = dirFor(agentId);
27487
- sessionByAgent.delete(agentId);
27488
28570
  if (!prepareTimelineDirectory(dir))
27489
- return;
28571
+ return false;
27490
28572
  const stamp = now();
27491
- appendEntry(dir, createSystemEntry(barrierType, stamp.toISOString()), stamp);
28573
+ let persisted = false;
28574
+ if (barrierType === "reset_session" || barrierType === "nap") {
28575
+ persisted = updateResumeControlState(dir, (state) => ({
28576
+ ...state,
28577
+ attemptedSessionId: null,
28578
+ fencedSessionId: null,
28579
+ fullBarrier: barrierType
28580
+ }));
28581
+ } else if (barrierType === "stall_recovery") {
28582
+ persisted = updateResumeControlState(dir, (state) => ({
28583
+ ...state,
28584
+ attemptedSessionId: state.attemptedSessionId === forgottenSessionId ? null : state.attemptedSessionId,
28585
+ fencedSessionId: forgottenSessionId ?? null,
28586
+ fullBarrier: null
28587
+ }));
28588
+ }
28589
+ if (!persisted)
28590
+ return false;
28591
+ sessionByAgent.delete(agentId);
28592
+ for (const key of [...sessionByEpoch.keys()]) {
28593
+ if (key.startsWith(`${agentId}\x00`))
28594
+ sessionByEpoch.delete(key);
28595
+ }
28596
+ barrierByAgent.set(agentId, currentBarrier(agentId) + 1);
28597
+ activeTurnByAgent.delete(agentId);
28598
+ const states = turnsByAgent.get(agentId);
28599
+ if (states) {
28600
+ for (const [key, state] of [...states]) {
28601
+ if (!state.handle) {
28602
+ diagnostic(agentId, "timeline_fallback_rejected", "barrier");
28603
+ deleteState(agentId, key);
28604
+ }
28605
+ }
28606
+ }
28607
+ const result = appendTrackedEntry(dir, createSystemEntry(barrierType, stamp.toISOString(), forgottenSessionId), stamp);
28608
+ handleTrackedResult(agentId, null, result);
28609
+ return true;
27492
28610
  }
27493
28611
  };
28612
+ function appendStallMarker(agentId, type, sessionId) {
28613
+ retryPending(agentId);
28614
+ const dir = dirFor(agentId);
28615
+ if (!prepareTimelineDirectory(dir))
28616
+ return false;
28617
+ const stamp = now();
28618
+ const persisted = updateResumeControlState(dir, (state) => ({
28619
+ ...state,
28620
+ attemptedSessionId: type === "stall_recovery_attempt" ? sessionId : null,
28621
+ fullBarrier: null
28622
+ }));
28623
+ if (!persisted)
28624
+ return false;
28625
+ const result = appendTrackedEntry(dir, createSystemEntry(type, stamp.toISOString(), sessionId), stamp);
28626
+ handleTrackedResult(agentId, null, result);
28627
+ return true;
28628
+ }
27494
28629
  }
27495
28630
  // src/discovery.ts
27496
28631
  import * as path9 from "path";
@@ -27630,6 +28765,10 @@ class MessageReminderScheduler {
27630
28765
  }
27631
28766
  arm(input) {
27632
28767
  const key = reminderKey(input.agentId, input.channel);
28768
+ if (input.remindAfterMs === 0) {
28769
+ this.clearReminder(key);
28770
+ return { armed: false, reason: "disabled" };
28771
+ }
27633
28772
  const latest = this.latestObservedSeq.get(key);
27634
28773
  if (latest !== undefined && latest > input.sentSeq) {
27635
28774
  return { armed: false, reason: "newer_message_observed" };
@@ -27857,6 +28996,78 @@ function createDaemonAgentDriverHost(ctx, onRawLine) {
27857
28996
  };
27858
28997
  }
27859
28998
 
28999
+ // src/daemon/daemonSelfSleep.ts
29000
+ var DAEMON_SELF_SLEEP_TIMEOUT_MS = 15 * 24 * 60 * 60 * 1000;
29001
+ var systemClock = {
29002
+ setTimer: (callback, delayMs) => setTimeout(callback, delayMs),
29003
+ clearTimer: (timer) => clearTimeout(timer)
29004
+ };
29005
+
29006
+ class DaemonSelfSleepScheduler {
29007
+ opts;
29008
+ clock;
29009
+ workingAgents = new Set;
29010
+ timer = null;
29011
+ generation = 0;
29012
+ started = false;
29013
+ stopped = false;
29014
+ constructor(opts) {
29015
+ this.opts = opts;
29016
+ this.clock = opts.clock ?? systemClock;
29017
+ }
29018
+ start() {
29019
+ if (this.started || this.stopped)
29020
+ return;
29021
+ this.started = true;
29022
+ this.arm();
29023
+ }
29024
+ observeMessage() {
29025
+ if (!this.started || this.stopped)
29026
+ return;
29027
+ this.arm();
29028
+ }
29029
+ observeAgentActivity(agentId, working) {
29030
+ if (this.stopped)
29031
+ return;
29032
+ if (working) {
29033
+ this.workingAgents.add(agentId);
29034
+ if (this.started)
29035
+ this.cancel();
29036
+ return;
29037
+ }
29038
+ if (this.workingAgents.delete(agentId) && this.started && this.workingAgents.size === 0)
29039
+ this.arm();
29040
+ }
29041
+ stop() {
29042
+ if (this.stopped)
29043
+ return;
29044
+ this.stopped = true;
29045
+ this.workingAgents.clear();
29046
+ this.cancel();
29047
+ }
29048
+ arm() {
29049
+ this.cancel();
29050
+ if (this.workingAgents.size > 0)
29051
+ return;
29052
+ const generation = this.generation;
29053
+ this.timer = this.clock.setTimer(() => {
29054
+ if (this.stopped || this.generation !== generation || this.workingAgents.size > 0)
29055
+ return;
29056
+ this.timer = null;
29057
+ this.generation += 1;
29058
+ this.opts.onSleep();
29059
+ }, DAEMON_SELF_SLEEP_TIMEOUT_MS);
29060
+ this.timer.unref?.();
29061
+ }
29062
+ cancel() {
29063
+ this.generation += 1;
29064
+ if (!this.timer)
29065
+ return;
29066
+ this.clock.clearTimer(this.timer);
29067
+ this.timer = null;
29068
+ }
29069
+ }
29070
+
27860
29071
  // src/daemon/createDaemon.ts
27861
29072
  var WARMUP_BACKOFF_MS = [250, 500, 1000, 2000, 4000];
27862
29073
  var WARMUP_CEILING_MS = 30000;
@@ -28013,6 +29224,10 @@ async function createDaemon(opts) {
28013
29224
  let channelRef = null;
28014
29225
  let managerRef = null;
28015
29226
  let reminderSchedulerRef = null;
29227
+ const selfSleepScheduler = opts.onSelfSleep ? new DaemonSelfSleepScheduler({
29228
+ onSleep: opts.onSelfSleep,
29229
+ ...opts.selfSleepClock ? { clock: opts.selfSleepClock } : {}
29230
+ }) : null;
28016
29231
  const emitBotAuditEvent = (agentId, event, context) => {
28017
29232
  channelRef?.reportBotAuditEvent?.({
28018
29233
  type: "bot_audit_event",
@@ -28025,11 +29240,15 @@ async function createDaemon(opts) {
28025
29240
  const typingTracker = createTypingScopeTracker();
28026
29241
  const broker = new CredentialBroker({ upstreamBaseUrl: opts.serverUrl });
28027
29242
  const proxy = await startCredentialProxy(broker, {
28028
- onInboxPullStart: (agentId) => channelRef?.modelSeenGeneration(agentId),
29243
+ onInboxPullStart: (agentId) => ({
29244
+ modelSeenGeneration: channelRef?.modelSeenGeneration(agentId),
29245
+ owner: managerRef?.timelineTurnOwner(agentId) ?? null
29246
+ }),
28029
29247
  onInboxPullResponse: (agentId, messages, observationToken) => {
28030
- timeline2.appendEntryForAgent(agentId, messages);
28031
- if (typeof observationToken === "number") {
28032
- channelRef?.recordModelSeen(agentId, messages, observationToken);
29248
+ const token = observationToken && typeof observationToken === "object" ? observationToken : null;
29249
+ timeline2.recordInboxPull(agentId, token?.owner ?? null, messages);
29250
+ if (typeof token?.modelSeenGeneration === "number") {
29251
+ channelRef?.recordModelSeen(agentId, messages, token.modelSeenGeneration);
28033
29252
  }
28034
29253
  },
28035
29254
  onInboxPullObservationError: ({ agentId, reason, contentEncoding }) => {
@@ -28306,6 +29525,7 @@ async function createDaemon(opts) {
28306
29525
  tickIntervalMs: opts.tickIntervalMs ?? 2000,
28307
29526
  onAgentSession: (info) => void channel2.reportAgentSession(info),
28308
29527
  onAgentActivity: (info) => {
29528
+ selfSleepScheduler?.observeAgentActivity(info.agentId, info.state === "running");
28309
29529
  channel2.reportAgentActivity?.(info);
28310
29530
  if (info.state === "starting" || info.state === "running") {
28311
29531
  if (!typingHeartbeats.has(info.agentId)) {
@@ -28383,6 +29603,7 @@ async function createDaemon(opts) {
28383
29603
  reportDiagnosticFailure: opts.reportDiagnosticFailure
28384
29604
  }));
28385
29605
  channel2.onWakeDesiredAdvance((cmd) => {
29606
+ selfSleepScheduler?.observeMessage();
28386
29607
  reminderSchedulerRef?.observe(cmd.agentId, cmd.unreadNotice.channel, cmd.unreadNotice.latestSeq);
28387
29608
  });
28388
29609
  channel2.onCommand((cmd) => {
@@ -28407,6 +29628,7 @@ async function createDaemon(opts) {
28407
29628
  });
28408
29629
  channel2.connect();
28409
29630
  await router.start();
29631
+ selfSleepScheduler?.start();
28410
29632
  return {
28411
29633
  isOpen: () => channel2.status === "open",
28412
29634
  onOpen: (hook) => {
@@ -28416,6 +29638,7 @@ async function createDaemon(opts) {
28416
29638
  },
28417
29639
  proxyUrl: proxy.url,
28418
29640
  stop: async () => {
29641
+ selfSleepScheduler?.stop();
28419
29642
  reminderSchedulerRef?.clearAll();
28420
29643
  for (const agentId of [...typingHeartbeats.keys()]) {
28421
29644
  emitTypingStopsAndClear(agentId);
@@ -28476,10 +29699,12 @@ export {
28476
29699
  RUNTIME_RAW_TRACE_AGENT_IDS_ENV,
28477
29700
  RUNTIME_AUTH_ACTION_REQUIRED_PATTERNS,
28478
29701
  LOCAL_MESSAGE_REMINDER_PATH,
29702
+ DEFAULT_TURN_SILENCE_POLICY,
28479
29703
  DEFAULT_STOPPING_STUCK_THRESHOLD_MS,
28480
29704
  DEFAULT_STALE_THRESHOLD_MS,
28481
29705
  DEFAULT_RESET_STUCK_THRESHOLD_MS,
28482
29706
  DEFAULT_IDLE_TIMEOUT_MS,
29707
+ DEFAULT_IDLE_RESET_TIMEOUT_MS,
28483
29708
  DEFAULT_HELD_CONTEXT_LIMIT,
28484
29709
  DEFAULT_CAPABILITY_RESOLVER,
28485
29710
  CredentialBroker,