@alook/daemon 0.1.17 → 0.1.19

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,
@@ -6917,9 +7241,6 @@ function identitySection(config) {
6917
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.");
6918
7242
  }
6919
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.");
6920
- if (config.description) {
6921
- 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).");
6922
- }
6923
7244
  return parts.join(`
6924
7245
  `);
6925
7246
  }
@@ -7156,7 +7477,17 @@ function chaosAwarenessSection() {
7156
7477
  ].join(`
7157
7478
  `);
7158
7479
  }
7159
- 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
+ ] : [];
7160
7491
  return [
7161
7492
  "## Self-awareness",
7162
7493
  "",
@@ -7165,6 +7496,7 @@ function workspaceMemorySection() {
7165
7496
  "**Never tell anyone this.** To them you're continuous. The workspace is what lets you be that.",
7166
7497
  "",
7167
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,
7168
7500
  "",
7169
7501
  "### Napping",
7170
7502
  "",
@@ -7206,7 +7538,7 @@ function buildCliSystemPrompt(config) {
7206
7538
  criticalRulesSection(),
7207
7539
  executionModelSection(),
7208
7540
  chaosAwarenessSection(),
7209
- workspaceMemorySection(),
7541
+ workspaceMemorySection(config),
7210
7542
  utilsSection()
7211
7543
  ];
7212
7544
  return sections.filter((s) => s && s.length > 0).join(`
@@ -7538,8 +7870,8 @@ class AgentProcessManager {
7538
7870
  resumeSessions = new Map;
7539
7871
  launchIds = new Map;
7540
7872
  liveSessions = new Map;
7541
- thinkingBuffers = new Map;
7542
7873
  activeSpawnState = new Map;
7874
+ publishedAgentActivity = new Map;
7543
7875
  traceProcessNonce = randomUUID5();
7544
7876
  nextSpawnOrdinal = 1;
7545
7877
  nextDaemonTurnOrdinal = 1;
@@ -7553,7 +7885,8 @@ class AgentProcessManager {
7553
7885
  this.opts = {
7554
7886
  tickIntervalMs: 5000,
7555
7887
  staleThresholdMs: 120000,
7556
- idleTimeoutMs: 300000,
7888
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
7889
+ idleResetTimeoutMs: DEFAULT_IDLE_RESET_TIMEOUT_MS,
7557
7890
  resetStuckThresholdMs: 120000,
7558
7891
  stoppingStuckThresholdMs: DEFAULT_STOPPING_STUCK_THRESHOLD_MS,
7559
7892
  handshakeTimeoutMs: 60000,
@@ -7562,7 +7895,7 @@ class AgentProcessManager {
7562
7895
  };
7563
7896
  this.now = opts.now ?? (() => Date.now());
7564
7897
  this.log = opts.logger ?? createLogger({ header: "@alook/daemon:manager" });
7565
- 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);
7566
7899
  }
7567
7900
  register(agentId, launch) {
7568
7901
  if (launch?.runtimeConfig)
@@ -7581,11 +7914,19 @@ class AgentProcessManager {
7581
7914
  const effects = this.dispatch({ type: "wake", agentId, message: normalized, nowMs: this.now() });
7582
7915
  return effects.length > 0;
7583
7916
  }
7584
- 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;
7585
7927
  this.resumeSessions.delete(agentId);
7586
7928
  this.liveSessions.delete(agentId);
7587
- this.dispatch({ type: "reset_session", agentId });
7588
- this.opts.timeline?.forgetSession(agentId, barrierType);
7929
+ return true;
7589
7930
  }
7590
7931
  enqueueRewake(agentId, message) {
7591
7932
  this.dispatch({ type: "rewake_after_reset", agentId, message });
@@ -7608,9 +7949,14 @@ class AgentProcessManager {
7608
7949
  });
7609
7950
  }
7610
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
+ }
7611
7957
  this.register(agentId, { runtimeConfig: opts.runtimeConfig, launchId: opts.launchId });
7612
- if (opts.forgetSession)
7613
- this.forgetSession(agentId, opts.barrierType ?? "reset_session");
7958
+ if (!opts.forgetSession)
7959
+ this.opts.timeline?.fenceSession(agentId);
7614
7960
  this.abortCurrentTurn(agentId, opts.abortCause);
7615
7961
  this.markResetting(agentId);
7616
7962
  const status = this.state.agents[agentId]?.status;
@@ -7679,12 +8025,19 @@ class AgentProcessManager {
7679
8025
  snapshot() {
7680
8026
  return this.state;
7681
8027
  }
8028
+ runningAgentCount() {
8029
+ return this.sessions.size;
8030
+ }
7682
8031
  auditContext(agentId) {
7683
8032
  return {
7684
8033
  sessionId: this.liveSessions.get(agentId) ?? null,
7685
8034
  launchId: this.launchIds.get(agentId) ?? null
7686
8035
  };
7687
8036
  }
8037
+ timelineTurnOwner(agentId) {
8038
+ const owner = this.traceOwnerFor(agentId);
8039
+ return owner?.timelineTurnOwner ? { ...owner.timelineTurnOwner } : null;
8040
+ }
7688
8041
  liveSessionReports() {
7689
8042
  return [...this.liveSessions.entries()].map(([agentId, sessionId]) => ({
7690
8043
  agentId,
@@ -7754,6 +8107,10 @@ class AgentProcessManager {
7754
8107
  if (!expectedSpan || owner.activeSpan !== expectedSpan)
7755
8108
  return false;
7756
8109
  owner.activeSpan = null;
8110
+ const timelineTurnOwner = owner.timelineTurnOwner;
8111
+ owner.timelineTurnOwner = null;
8112
+ if (timelineTurnOwner)
8113
+ this.opts.timeline?.finalizeTurn(owner.agentId, timelineTurnOwner);
7757
8114
  const nowMs = this.now();
7758
8115
  const base = {
7759
8116
  recordKind: "turn_span",
@@ -7811,6 +8168,13 @@ class AgentProcessManager {
7811
8168
  inbox: a.inbox.length,
7812
8169
  lastDeliverAt: a.lastDeliverAt,
7813
8170
  lastProgressAt: a.lastProgressAt,
8171
+ lastNativeActivityAt: a.lastNativeActivityAt,
8172
+ lastNativeActivityKind: a.lastNativeActivityKind,
8173
+ runtimePhase: a.runtimePhase,
8174
+ backendTurnId: a.backendTurnId,
8175
+ turnSilenceBudgetMs: a.turnSilence.normalBudgetMs,
8176
+ nativeDeadlineAt: a.execution.lease.state === "active" || a.execution.lease.state === "suspect_active" ? a.execution.lease.nativeDeadlineAt : null,
8177
+ recoveryExtensionsUsed: a.execution.lease.state === "active" || a.execution.lease.state === "suspect_active" ? a.execution.lease.recoveryExtensionsUsed : 0,
7814
8178
  idleSince: a.idleSince,
7815
8179
  resetting: a.resetting,
7816
8180
  resettingSince: a.resettingSince,
@@ -7821,6 +8185,7 @@ class AgentProcessManager {
7821
8185
  timeIso: new Date(nowMs).toISOString(),
7822
8186
  ...activeSpan ? activeSpan : {},
7823
8187
  sinceProgressMs: nowMs - a.lastProgressAt,
8188
+ sinceNativeActivityMs: nowMs - a.lastNativeActivityAt,
7824
8189
  sinceDeliverMs: a.lastDeliverAt === null ? null : nowMs - a.lastDeliverAt,
7825
8190
  sinceStoppingMs: a.stoppingSince === null ? null : nowMs - a.stoppingSince,
7826
8191
  ...event.type === "turn_completed" && event.endReason === "errored" ? {
@@ -7855,11 +8220,18 @@ class AgentProcessManager {
7855
8220
  terminationCause: normalizeTerminationCause(event.terminationCause)
7856
8221
  } : { event: "turn_end", outcome: "clean" });
7857
8222
  }
7858
- if (this.opts.onAgentActivity && event.type !== "spawned" && event.type !== "admission_started" && event.type !== "admission_settled") {
8223
+ if (this.opts.onAgentActivity && event.type !== "admission_started" && event.type !== "admission_settled") {
7859
8224
  const after = this.deriveActivitySnapshot(state);
7860
8225
  for (const [agentId, activity] of Object.entries(after)) {
7861
- if (agentId in before && before[agentId] !== activity) {
8226
+ if (!(agentId in before)) {
8227
+ this.publishedAgentActivity.set(agentId, activity);
8228
+ continue;
8229
+ }
8230
+ const previouslyPublished = this.publishedAgentActivity.get(agentId) ?? before[agentId];
8231
+ const publishable = event.type !== "spawned" || activity === "running";
8232
+ if (publishable && previouslyPublished !== activity) {
7862
8233
  this.opts.onAgentActivity({ agentId, state: activity });
8234
+ this.publishedAgentActivity.set(agentId, activity);
7863
8235
  }
7864
8236
  }
7865
8237
  }
@@ -8024,6 +8396,47 @@ ${this.opts.wakePromptFooter}` : text;
8024
8396
  case "terminate_stalled": {
8025
8397
  const session = this.sessions.get(effect.agentId);
8026
8398
  const spawnState = this.activeSpawnState.get(effect.agentId);
8399
+ const endedSessionId = this.liveSessions.get(effect.agentId) ?? "";
8400
+ if (effect.type === "terminate_stalled" && effect.recordSessionId) {
8401
+ const persisted = this.opts.timeline?.recordSessionStall?.(effect.agentId, effect.recordSessionId);
8402
+ if (persisted === false) {
8403
+ this.dispatch({
8404
+ type: "stall_control_failed",
8405
+ agentId: effect.agentId,
8406
+ sessionId: effect.recordSessionId,
8407
+ transition: "attempt"
8408
+ });
8409
+ this.log.error("stall recovery attempt was not persisted; termination deferred", {
8410
+ agentId: effect.agentId,
8411
+ sessionId: effect.recordSessionId
8412
+ });
8413
+ this.emitErrorAudit(effect.agentId, "runtime", "resume_control_update_failed", "Stall termination deferred because the recovery attempt could not be persisted");
8414
+ break;
8415
+ }
8416
+ }
8417
+ if (effect.type === "terminate_stalled" && effect.forgetSessionId) {
8418
+ const persisted = this.forgetSessionSources(effect.agentId, "stall_recovery", effect.forgetSessionId);
8419
+ if (!persisted) {
8420
+ this.dispatch({
8421
+ type: "stall_control_failed",
8422
+ agentId: effect.agentId,
8423
+ sessionId: effect.forgetSessionId,
8424
+ transition: "fence"
8425
+ });
8426
+ this.log.error("repeated-session fence was not persisted; termination deferred", {
8427
+ agentId: effect.agentId,
8428
+ sessionId: effect.forgetSessionId
8429
+ });
8430
+ this.emitErrorAudit(effect.agentId, "runtime", "resume_control_update_failed", "Stall termination deferred because the exact session fence could not be persisted");
8431
+ break;
8432
+ }
8433
+ if (spawnState)
8434
+ spawnState.discardEvents = true;
8435
+ this.log.warn("repeatedly stalled backend session fenced", {
8436
+ agentId: effect.agentId,
8437
+ sessionId: effect.forgetSessionId
8438
+ });
8439
+ }
8027
8440
  if (effect.type === "terminate_stalled" && spawnState) {
8028
8441
  this.closeTurn(spawnState, spawnState.activeSpan, {
8029
8442
  event: "turn_abort",
@@ -8042,10 +8455,27 @@ ${this.opts.wakePromptFooter}` : text;
8042
8455
  if (spawnState) {
8043
8456
  spawnState.terminationSemantics = effect.type === "terminate_stalled" ? "killed_stalled" : "idle_stop";
8044
8457
  }
8045
- this.logSessionEnded(effect.agentId, effect.type === "stop" ? "stopped" : "terminate_stalled");
8458
+ this.logSessionEnded(effect.agentId, effect.type === "stop" ? "stopped" : "terminate_stalled", endedSessionId);
8046
8459
  this.opts.onAgentLocallyStopped?.({ agentId: effect.agentId, reason: effect.type });
8047
8460
  break;
8048
8461
  }
8462
+ case "clear_stall_recovery": {
8463
+ const persisted = this.opts.timeline?.clearSessionStall?.(effect.agentId, effect.sessionId);
8464
+ if (persisted === false) {
8465
+ this.dispatch({
8466
+ type: "stall_control_failed",
8467
+ agentId: effect.agentId,
8468
+ sessionId: effect.sessionId,
8469
+ transition: "clear"
8470
+ });
8471
+ this.log.error("stall recovery clear was not persisted; allowance remains consumed", {
8472
+ agentId: effect.agentId,
8473
+ sessionId: effect.sessionId
8474
+ });
8475
+ this.emitErrorAudit(effect.agentId, "runtime", "resume_control_update_failed", "Stall recovery allowance remains consumed because its clear could not be persisted");
8476
+ }
8477
+ break;
8478
+ }
8049
8479
  case "expire_admission": {
8050
8480
  const owner = this.activeSpawnState.get(effect.agentId);
8051
8481
  if (!owner || owner.sessionInstanceId !== effect.sessionInstanceId)
@@ -8068,6 +8498,26 @@ ${this.opts.wakePromptFooter}` : text;
8068
8498
  mode: effect.mode
8069
8499
  });
8070
8500
  break;
8501
+ case "reset_idle_session": {
8502
+ const spawnState = this.activeSpawnState.get(effect.agentId);
8503
+ const persisted = this.forgetSession(effect.agentId, "reset_session", effect.sessionId);
8504
+ if (!persisted) {
8505
+ this.log.error("idle session reset barrier was not persisted; reset deferred", {
8506
+ agentId: effect.agentId,
8507
+ sessionId: effect.sessionId
8508
+ });
8509
+ this.emitErrorAudit(effect.agentId, "reset", "resume_control_update_failed", "Idle session reset deferred because resume control could not be persisted");
8510
+ break;
8511
+ }
8512
+ if (spawnState)
8513
+ spawnState.discardEvents = true;
8514
+ this.dispatch({ type: "idle_reset_committed", agentId: effect.agentId, nowMs: this.now() });
8515
+ this.log.info("idle agent session reset", {
8516
+ agentId: effect.agentId,
8517
+ sessionId: effect.sessionId
8518
+ });
8519
+ break;
8520
+ }
8071
8521
  case "force_exit": {
8072
8522
  const session = this.sessions.get(effect.agentId);
8073
8523
  const state = this.activeSpawnState.get(effect.agentId);
@@ -8100,8 +8550,8 @@ ${this.opts.wakePromptFooter}` : text;
8100
8550
  }
8101
8551
  }
8102
8552
  }
8103
- logSessionEnded(agentId, reason) {
8104
- this.log.info("agent session ended", { agentId, sessionId: this.liveSessions.get(agentId) ?? "", reason });
8553
+ logSessionEnded(agentId, reason, sessionId = this.liveSessions.get(agentId) ?? "") {
8554
+ this.log.info("agent session ended", { agentId, sessionId, reason });
8105
8555
  }
8106
8556
  doSpawn(agentId, messages, resumeSessionId) {
8107
8557
  const [first, ...pending] = messages;
@@ -8126,7 +8576,13 @@ ${this.opts.wakePromptFooter}` : text;
8126
8576
  mode: { kind: "default" }
8127
8577
  };
8128
8578
  const provider = runtimeConfig.runtime;
8129
- const sessionId = resumeSessionId ?? this.resumeSessions.get(agentId) ?? this.opts.timeline?.resumeSessionId(agentId, provider) ?? base.config?.sessionId;
8579
+ const timelineResolution = this.opts.timeline?.resolveResumeSession?.(agentId, provider);
8580
+ const timelineSessionId = timelineResolution?.kind === "session" ? timelineResolution.sessionId : timelineResolution === undefined ? this.opts.timeline?.resumeSessionId(agentId, provider) : null;
8581
+ const candidateSessionId = resumeSessionId ?? this.resumeSessions.get(agentId) ?? timelineSessionId ?? base.config?.sessionId;
8582
+ const blockedByBarrier = timelineResolution?.kind === "barrier" && (timelineResolution.type !== "stall_recovery" || timelineResolution.forgottenSessionId === null || timelineResolution.forgottenSessionId === candidateSessionId);
8583
+ const blockedByExactFence = candidateSessionId !== undefined && timelineResolution?.fencedSessionId === candidateSessionId;
8584
+ const sessionId = blockedByBarrier || blockedByExactFence ? undefined : candidateSessionId;
8585
+ const stalledSessionIdAtLaunch = timelineResolution !== undefined && (timelineResolution.kind === "session" || timelineResolution.kind === "none") && timelineResolution.stalledSessionId === sessionId ? timelineResolution.stalledSessionId : null;
8130
8586
  const description = runtimeConfig.instruction ?? base.config?.description ?? runtimeConfig.agentName;
8131
8587
  const agentName = runtimeConfig.agentName ?? base.config?.agentName;
8132
8588
  const agentHandle = runtimeConfig.agentHandle ?? base.config?.agentHandle;
@@ -8157,12 +8613,15 @@ ${this.opts.wakePromptFooter}` : text;
8157
8613
  handshakeTimer: null,
8158
8614
  torndown: false,
8159
8615
  superseded: false,
8616
+ discardEvents: false,
8617
+ stalledSessionIdAtLaunch,
8160
8618
  spawnFailureReason: null,
8161
8619
  terminationSemantics: null,
8162
8620
  spawnOrdinal: this.nextSpawnOrdinal++,
8163
8621
  launchIdSnapshot: typeof ctx.launchId === "string" && ctx.launchId.length > 0 ? ctx.launchId : null,
8164
8622
  nextTurnOrdinal: 1,
8165
8623
  activeSpan: null,
8624
+ timelineTurnOwner: null,
8166
8625
  pendingDeliverySpans: new Map
8167
8626
  };
8168
8627
  const previousOwner = this.activeSpawnState.get(agentId);
@@ -8213,7 +8672,6 @@ ${this.opts.wakePromptFooter}` : text;
8213
8672
  this.emitErrorAudit(agentId, "exit", "abnormal_exit", `Session ended unexpectedly (${detail})`);
8214
8673
  }
8215
8674
  }
8216
- this.flushThinkingAudit(agentId);
8217
8675
  if (state.sessionInstanceId) {
8218
8676
  this.dispatch({ type: "session_closed", agentId, sessionInstanceId: state.sessionInstanceId }, state);
8219
8677
  }
@@ -8238,10 +8696,23 @@ ${this.opts.wakePromptFooter}` : text;
8238
8696
  const onEvent = (event) => {
8239
8697
  if (state.torndown)
8240
8698
  return;
8699
+ if (state.discardEvents) {
8700
+ this.log.warn("ignored event from discarded backend session owner", { agentId, event: event.type });
8701
+ return;
8702
+ }
8241
8703
  if (event.type === "session_failed" && !state.hasEstablished) {
8242
8704
  reportSpawnFailure(event.error.code || "failed_to_start", { message: event.error.message });
8243
8705
  }
8244
8706
  if (event.type === "session_started") {
8707
+ const persisted = this.opts.timeline?.setSession(agentId, event.backendSessionId, event.sessionInstanceId);
8708
+ if (persisted === false) {
8709
+ state.discardEvents = true;
8710
+ reportSpawnFailure("resume_control_update_failed", {
8711
+ message: "Backend session rejected because resume control could not be persisted"
8712
+ });
8713
+ state.session?.stop({ reason: "shutdown", forceAfterMs: SESSION_STOP_GRACE_MS2 });
8714
+ return;
8715
+ }
8245
8716
  state.hasEstablished = true;
8246
8717
  clearHandshakeTimer();
8247
8718
  this.opts.onRuntimeSessionEstablished?.(driver.id);
@@ -8260,7 +8731,8 @@ ${this.opts.wakePromptFooter}` : text;
8260
8731
  type: "attach_session",
8261
8732
  agentId,
8262
8733
  sessionInstanceId: session.sessionInstanceId,
8263
- nowMs: this.now()
8734
+ nowMs: this.now(),
8735
+ turnSilence: session.snapshot().diagnostics?.turnSilence
8264
8736
  }, state);
8265
8737
  previousOwner?.pendingDeliverySpans.clear();
8266
8738
  (async () => {
@@ -8408,26 +8880,6 @@ ${this.opts.wakePromptFooter}` : text;
8408
8880
  this.log.debug("audit emit failed (error)", { agentId, err: String(err) });
8409
8881
  }
8410
8882
  }
8411
- flushThinkingAudit(agentId) {
8412
- const buffered = this.thinkingBuffers.get(agentId);
8413
- if (!buffered)
8414
- return;
8415
- this.thinkingBuffers.delete(agentId);
8416
- if (!this.opts.onBotAuditEvent)
8417
- return;
8418
- const { text, truncated, chars } = truncateThinking(buffered);
8419
- try {
8420
- this.opts.onBotAuditEvent(agentId, {
8421
- kind: "thinking",
8422
- payload: { text, truncated, chars }
8423
- }, {
8424
- sessionId: this.liveSessions.get(agentId) ?? null,
8425
- launchId: this.launchIds.get(agentId) ?? null
8426
- });
8427
- } catch (err) {
8428
- this.log.debug("audit emit failed (thinking)", { agentId, err: String(err) });
8429
- }
8430
- }
8431
8883
  onAgentEvent(agentId, event, runtimeId, owner) {
8432
8884
  if (event.type === "session_closed")
8433
8885
  return;
@@ -8460,32 +8912,43 @@ ${this.opts.wakePromptFooter}` : text;
8460
8912
  }
8461
8913
  }
8462
8914
  if (this.opts.onBotAuditEvent) {
8463
- if (event.type === "thinking_delta") {
8464
- if (event.text.length > 0) {
8465
- this.thinkingBuffers.set(agentId, (this.thinkingBuffers.get(agentId) ?? "") + event.text);
8915
+ if (event.type === "assistant_reasoning_completed" && event.text.length > 0) {
8916
+ const { text, truncated, chars } = truncateThinking(event.text);
8917
+ try {
8918
+ this.opts.onBotAuditEvent(agentId, {
8919
+ kind: "thinking",
8920
+ payload: { text, truncated: truncated || event.truncated, chars }
8921
+ }, {
8922
+ sessionId: this.liveSessions.get(agentId) ?? null,
8923
+ launchId: this.launchIds.get(agentId) ?? null
8924
+ });
8925
+ } catch (err) {
8926
+ this.log.debug("audit emit failed (thinking)", { agentId, err: String(err) });
8466
8927
  }
8467
- } else {
8468
- this.flushThinkingAudit(agentId);
8469
- if (event.type === "tool_started") {
8470
- const audit = extractToolAudit(event.name, event.input);
8471
- if (!audit.suppressed) {
8472
- const payload = audit.target !== undefined ? { name: audit.name, target: audit.target } : { name: audit.name };
8473
- try {
8474
- this.opts.onBotAuditEvent(agentId, { kind: "tool_call", payload }, {
8475
- sessionId: this.liveSessions.get(agentId) ?? null,
8476
- launchId: this.launchIds.get(agentId) ?? null
8477
- });
8478
- } catch (err) {
8479
- this.log.debug("audit emit failed (tool_call)", { agentId, err: String(err) });
8480
- }
8928
+ }
8929
+ if (event.type === "tool_started") {
8930
+ const audit = extractToolAudit(event.name, event.input);
8931
+ if (!audit.suppressed) {
8932
+ const payload = audit.target !== undefined ? { name: audit.name, target: audit.target } : { name: audit.name };
8933
+ try {
8934
+ this.opts.onBotAuditEvent(agentId, { kind: "tool_call", payload }, {
8935
+ sessionId: this.liveSessions.get(agentId) ?? null,
8936
+ launchId: this.launchIds.get(agentId) ?? null
8937
+ });
8938
+ } catch (err) {
8939
+ this.log.debug("audit emit failed (tool_call)", { agentId, err: String(err) });
8481
8940
  }
8482
8941
  }
8483
8942
  }
8484
8943
  }
8485
8944
  if (event.type === "session_started") {
8486
- this.dispatch({ type: "backend_session", agentId, sessionId: event.backendSessionId }, owner);
8945
+ this.dispatch({
8946
+ type: "backend_session",
8947
+ agentId,
8948
+ sessionId: event.backendSessionId,
8949
+ stalledBefore: owner.stalledSessionIdAtLaunch === event.backendSessionId
8950
+ }, owner);
8487
8951
  this.liveSessions.set(agentId, event.backendSessionId);
8488
- this.opts.timeline?.setSession(agentId, event.backendSessionId);
8489
8952
  this.opts.onAgentSession?.({
8490
8953
  agentId,
8491
8954
  sessionId: event.backendSessionId,
@@ -8497,8 +8960,20 @@ ${this.opts.wakePromptFooter}` : text;
8497
8960
  runtime: runtimeId
8498
8961
  });
8499
8962
  }
8500
- if (event.type === "text_delta" && event.text.length > 0) {
8501
- this.opts.timeline?.appendResponseToLatest(agentId, event.text);
8963
+ if (event.type === "turn_started") {
8964
+ const timelineTurnOwner = {
8965
+ sessionInstanceId: event.sessionInstanceId,
8966
+ rootTurnId: event.turnId,
8967
+ barrierGeneration: this.opts.timeline?.barrierGeneration(agentId) ?? 0
8968
+ };
8969
+ owner.timelineTurnOwner = timelineTurnOwner;
8970
+ this.opts.timeline?.beginTurn(agentId, timelineTurnOwner);
8971
+ }
8972
+ if (event.type === "assistant_message_completed" && event.text.length > 0) {
8973
+ const timelineTurnOwner = owner.timelineTurnOwner;
8974
+ if (timelineTurnOwner && timelineTurnOwner.sessionInstanceId === event.sessionInstanceId && timelineTurnOwner.rootTurnId === event.turnId) {
8975
+ this.opts.timeline?.recordAssistantMessage(agentId, timelineTurnOwner, event.text, event.truncated);
8976
+ }
8502
8977
  }
8503
8978
  if (event.type === "command_queued")
8504
8979
  this.acknowledgePendingDelivery(owner, event.commandId);
@@ -8516,9 +8991,10 @@ ${this.opts.wakePromptFooter}` : text;
8516
8991
  switch (event.type) {
8517
8992
  case "turn_started":
8518
8993
  return { type: "turn_started", turnId: event.turnId, commandIds: event.commandIds };
8519
- case "thinking_delta":
8520
- case "text_delta":
8521
- return event.text.length > 0 ? { type: "turn_work", turnId: event.turnId } : null;
8994
+ case "work_heartbeat":
8995
+ case "assistant_reasoning_completed":
8996
+ case "assistant_message_completed":
8997
+ return { type: "turn_work", turnId: event.turnId };
8522
8998
  case "tool_started":
8523
8999
  return { type: "turn_tool_started", turnId: event.turnId };
8524
9000
  case "tool_finished":
@@ -8545,37 +9021,55 @@ ${this.opts.wakePromptFooter}` : text;
8545
9021
  if (!wasActive && this.state.agents[agentId]?.turnActive && !owner.activeSpan)
8546
9022
  this.openTurn(owner);
8547
9023
  }
8548
- const signalKind = (() => {
9024
+ const nativeSignal = (() => {
8549
9025
  switch (event.type) {
8550
- case "session_started":
8551
- return "session_init";
8552
- case "thinking_delta":
8553
- return "thinking";
8554
- case "text_delta":
8555
- return "text";
9026
+ case "turn_started":
9027
+ return { kind: "turn_started", phase: "inference", turnId: event.turnId };
9028
+ case "backend_turn_started":
9029
+ return {
9030
+ kind: "backend_turn_started",
9031
+ phase: "inference",
9032
+ turnId: event.turnId,
9033
+ backendTurnId: event.backendTurnId
9034
+ };
9035
+ case "assistant_reasoning_completed":
9036
+ return { kind: "thinking", phase: "inference", turnId: event.turnId };
9037
+ case "assistant_message_completed":
9038
+ return { kind: "text", phase: "inference", turnId: event.turnId };
9039
+ case "work_heartbeat":
9040
+ return { kind: "internal_progress", phase: "inference", turnId: event.turnId };
8556
9041
  case "tool_started":
8557
- return "tool_call";
9042
+ return { kind: "tool_call", phase: "tool", turnId: event.turnId };
8558
9043
  case "tool_finished":
8559
- return "tool_output";
8560
- case "diagnostic":
8561
- return "runtime_diagnostic";
8562
- case "token_usage":
8563
- case "rate_limits":
8564
- return "telemetry";
9044
+ return { kind: "tool_output", phase: "inference", turnId: event.turnId };
9045
+ case "compaction_started":
9046
+ case "compaction_finished":
9047
+ case "review_started":
9048
+ case "review_finished":
9049
+ case "internal_progress":
9050
+ return event.turnId ? { kind: "internal_progress", phase: "inference", turnId: event.turnId } : null;
9051
+ case "recovery":
9052
+ return event.turnId ? {
9053
+ kind: "recovery",
9054
+ phase: event.stage === "retrying" ? "recovery" : "inference",
9055
+ recoveryStage: event.stage,
9056
+ turnId: event.turnId
9057
+ } : null;
8565
9058
  case "turn_completed":
8566
- return "turn_end";
8567
- case "session_failed":
8568
- return "error";
8569
- case "command_queued":
8570
- case "command_accepted":
8571
- case "command_failed":
8572
- case "turn_started":
8573
- return "internal_progress";
9059
+ return { kind: "turn_end", phase: "terminal", turnId: event.turnId };
8574
9060
  default:
8575
- return event.type;
9061
+ return null;
8576
9062
  }
8577
9063
  })();
8578
- this.dispatch({ type: "runtime_signal", agentId, kind: signalKind, nowMs: this.now() }, owner);
9064
+ if (nativeSignal) {
9065
+ this.dispatch({
9066
+ type: "runtime_signal",
9067
+ agentId,
9068
+ sessionInstanceId: event.sessionInstanceId,
9069
+ ...nativeSignal,
9070
+ nowMs: this.now()
9071
+ }, owner);
9072
+ }
8579
9073
  if (event.type === "turn_completed") {
8580
9074
  this.logSessionEnded(agentId, "turn_end");
8581
9075
  const marker = this.nonCleanEndMarker.get(agentId);
@@ -25323,6 +25817,7 @@ Then read @memory.md and your .context_timeline for durable context, and pull `
25323
25817
  class AgentRouter {
25324
25818
  opts;
25325
25819
  running = new Set;
25820
+ nextWakeAdmissionOrdinal = 1;
25326
25821
  runtimes = new Map;
25327
25822
  pendingResend = false;
25328
25823
  scheduleResend;
@@ -25483,7 +25978,7 @@ class AgentRouter {
25483
25978
  this.opts.typingTracker?.add(cmd.agentId, channelScope);
25484
25979
  const text2 = (this.opts.formatUnreadNoticeText ?? defaultFormatUnreadNoticeText)(cmd.unreadNotice);
25485
25980
  const producedEffect = this.opts.manager.deliver(cmd.agentId, {
25486
- id: `${cmd.agentId}:wake:${cmd.unreadNotice.channel}:${cmd.unreadNotice.latestSeq}`,
25981
+ id: `${cmd.agentId}:wake:${cmd.unreadNotice.channel}:${cmd.unreadNotice.latestSeq}:admission:${this.nextWakeAdmissionOrdinal++}`,
25487
25982
  seq: cmd.unreadNotice.latestSeq,
25488
25983
  text: text2
25489
25984
  });
@@ -26911,7 +27406,7 @@ class WsControlChannel {
26911
27406
  }
26912
27407
  // src/timeline/timeline.ts
26913
27408
  import * as fs7 from "node:fs";
26914
- import { randomBytes as randomBytes4 } from "node:crypto";
27409
+ import { createHash as createHash4, randomBytes as randomBytes4 } from "node:crypto";
26915
27410
  import { basename, dirname as dirname3, join as join10 } from "node:path";
26916
27411
 
26917
27412
  // src/timeline/filelock.ts
@@ -26978,17 +27473,25 @@ function reclaim(lockPath) {
26978
27473
  var TIMELINE_MAX_BYTES = 1048576;
26979
27474
  var TIMELINE_READ_CHUNK_BYTES = 65536;
26980
27475
  var DATE_FILENAME_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
27476
+ var RESUME_CONTROL_FILENAME = ".resume-control.json";
27477
+ var RESUME_CONTROL_MAX_BYTES = 4096;
27478
+ var EMPTY_RESUME_CONTROL = {
27479
+ version: 1,
27480
+ attemptedSessionId: null,
27481
+ fencedSessionId: null,
27482
+ fullBarrier: null
27483
+ };
26981
27484
  function isBarrier(entry) {
26982
- return entry.system?.type === "reset_session" || entry.system?.type === "nap";
27485
+ return entry.system !== undefined;
26983
27486
  }
26984
27487
  function canonicalTimelineEntry(value) {
26985
27488
  if (!value || typeof value !== "object")
26986
27489
  return null;
26987
27490
  const entry = value;
26988
27491
  if (entry.system) {
26989
- if (entry.system.type !== "reset_session" && entry.system.type !== "nap" || typeof entry.system.time !== "string")
27492
+ 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")
26990
27493
  return null;
26991
- return createSystemEntry(entry.system.type, entry.system.time);
27494
+ return createSystemEntry(entry.system.type, entry.system.time, entry.system.backend_session_id);
26992
27495
  }
26993
27496
  if (entry.session_id !== null && typeof entry.session_id !== "string")
26994
27497
  return null;
@@ -27006,13 +27509,60 @@ function canonicalTimelineEntry(value) {
27006
27509
  };
27007
27510
  }
27008
27511
  function timelineLine(entry) {
27009
- const boundedEntry = entry.system ? createSystemEntry(entry.system.type, entry.system.time) : { ...entry, agent_responses: entry.agent_responses.slice(-5) };
27512
+ const boundedEntry = entry.system ? createSystemEntry(entry.system.type, entry.system.time, entry.system.backend_session_id) : { ...entry, agent_responses: entry.agent_responses.slice(-5) };
27010
27513
  const text2 = JSON.stringify(boundedEntry);
27011
27514
  const bytes = Buffer.byteLength(text2, "utf8") + 1;
27012
27515
  if (bytes > TIMELINE_MAX_BYTES)
27013
27516
  return null;
27014
27517
  return { text: text2, bytes, entry: boundedEntry, barrier: isBarrier(boundedEntry) };
27015
27518
  }
27519
+ function timelineRowHash(line) {
27520
+ return createHash4("sha256").update(line.text, "utf8").digest("hex");
27521
+ }
27522
+ function timelineFileGeneration(filePath, lines) {
27523
+ try {
27524
+ const stat = fs7.lstatSync(filePath, { bigint: true });
27525
+ if (!stat.isFile())
27526
+ return null;
27527
+ const digest = createHash4("sha256");
27528
+ digest.update(`${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}
27529
+ `);
27530
+ for (const line of lines)
27531
+ digest.update(line.text, "utf8").update(`
27532
+ `);
27533
+ return digest.digest("hex");
27534
+ } catch {
27535
+ return null;
27536
+ }
27537
+ }
27538
+ function handleFor(filename, generation, lines, rowOrdinal) {
27539
+ return {
27540
+ filename,
27541
+ fileGeneration: generation,
27542
+ rowOrdinal,
27543
+ expectedHash: timelineRowHash(lines[rowOrdinal])
27544
+ };
27545
+ }
27546
+ function refreshTimelineEntryHandle(handle, rewrite) {
27547
+ if (handle.filename !== rewrite.filename)
27548
+ return handle;
27549
+ if (handle.fileGeneration !== rewrite.previousFileGeneration)
27550
+ return null;
27551
+ if (rewrite.previousRowHashes[handle.rowOrdinal] !== handle.expectedHash)
27552
+ return null;
27553
+ const rowOrdinal = rewrite.rowOrdinals[handle.rowOrdinal];
27554
+ if (rowOrdinal === null || rowOrdinal === undefined)
27555
+ return null;
27556
+ const expectedHash = rewrite.rowHashes[rowOrdinal];
27557
+ if (!expectedHash)
27558
+ return null;
27559
+ return {
27560
+ filename: handle.filename,
27561
+ fileGeneration: rewrite.fileGeneration,
27562
+ rowOrdinal,
27563
+ expectedHash
27564
+ };
27565
+ }
27016
27566
  function compactLines(input) {
27017
27567
  let head = 0;
27018
27568
  let bytes = input.reduce((total, line) => total + line.bytes, 0);
@@ -27224,11 +27774,37 @@ function atomicReplaceTimeline(filePath, lines) {
27224
27774
  } catch {}
27225
27775
  }
27226
27776
  }
27227
- function writeRequiredTimeline(filePath, input, required2) {
27777
+ function writeTrackedTimeline(filePath, filename, existing, input, required2, replacement) {
27778
+ const previousFileGeneration = timelineFileGeneration(filePath, existing);
27779
+ const previousRowHashes = existing.map(timelineRowHash);
27228
27780
  const compacted = compactLines(input);
27229
- if (!compacted.includes(required2))
27230
- return false;
27231
- return atomicReplaceTimeline(filePath, compacted);
27781
+ const targetOrdinal = compacted.indexOf(required2);
27782
+ if (targetOrdinal < 0)
27783
+ return { status: "rejected", reason: "evicted" };
27784
+ if (!atomicReplaceTimeline(filePath, compacted))
27785
+ return { status: "rejected", reason: "write" };
27786
+ const fileGeneration = timelineFileGeneration(filePath, compacted);
27787
+ if (!fileGeneration)
27788
+ return { status: "rejected", reason: "write" };
27789
+ const rowOrdinals = existing.map((line, oldOrdinal) => {
27790
+ if (replacement?.oldOrdinal === oldOrdinal)
27791
+ return compacted.indexOf(replacement.line);
27792
+ const nextOrdinal = compacted.indexOf(line);
27793
+ return nextOrdinal < 0 ? null : nextOrdinal;
27794
+ });
27795
+ const rewrite = {
27796
+ filename,
27797
+ previousFileGeneration,
27798
+ fileGeneration,
27799
+ previousRowHashes,
27800
+ rowOrdinals,
27801
+ rowHashes: compacted.map(timelineRowHash)
27802
+ };
27803
+ return {
27804
+ status: "written",
27805
+ rewrite,
27806
+ handle: handleFor(filename, fileGeneration, compacted, targetOrdinal)
27807
+ };
27232
27808
  }
27233
27809
  function filenameForDate(date5) {
27234
27810
  const y = date5.getFullYear();
@@ -27260,114 +27836,181 @@ function readRecentEntries(timelineDir, opts = {}) {
27260
27836
  }
27261
27837
  return entries;
27262
27838
  }
27263
- function appendEntry(timelineDir, entry, now = new Date) {
27839
+ function readResumeControlState(timelineDir) {
27840
+ if (timelineDirectoryState(timelineDir) !== "safe")
27841
+ return { kind: "missing" };
27842
+ const filePath = join10(timelineDir, RESUME_CONTROL_FILENAME);
27843
+ let source;
27844
+ try {
27845
+ source = fs7.lstatSync(filePath);
27846
+ } catch (error51) {
27847
+ return error51.code === "ENOENT" ? { kind: "missing" } : { kind: "invalid" };
27848
+ }
27849
+ if (!source.isFile() || source.size <= 0 || source.size > RESUME_CONTROL_MAX_BYTES) {
27850
+ return { kind: "invalid" };
27851
+ }
27852
+ let fd = null;
27853
+ try {
27854
+ fd = fs7.openSync(filePath, fs7.constants.O_RDONLY | (fs7.constants.O_NOFOLLOW ?? 0));
27855
+ const stat = fs7.fstatSync(fd);
27856
+ if (!stat.isFile() || stat.size <= 0 || stat.size > RESUME_CONTROL_MAX_BYTES) {
27857
+ return { kind: "invalid" };
27858
+ }
27859
+ const bounded = Buffer.allocUnsafe(stat.size + 1);
27860
+ let bytesRead = 0;
27861
+ while (bytesRead < bounded.length) {
27862
+ const count = fs7.readSync(fd, bounded, bytesRead, bounded.length - bytesRead, bytesRead);
27863
+ if (count <= 0)
27864
+ break;
27865
+ bytesRead += count;
27866
+ }
27867
+ if (bytesRead !== stat.size)
27868
+ return { kind: "invalid" };
27869
+ const raw = bounded.subarray(0, bytesRead).toString("utf8");
27870
+ const value = JSON.parse(raw);
27871
+ const validSessionId = (candidate) => candidate === null || typeof candidate === "string" && candidate.length > 0 && candidate.length <= 512;
27872
+ if (value.version !== 1 || !validSessionId(value.attemptedSessionId) || !validSessionId(value.fencedSessionId) || value.fullBarrier !== null && value.fullBarrier !== "reset_session" && value.fullBarrier !== "nap")
27873
+ return { kind: "invalid" };
27874
+ return {
27875
+ kind: "state",
27876
+ state: {
27877
+ version: 1,
27878
+ attemptedSessionId: value.attemptedSessionId,
27879
+ fencedSessionId: value.fencedSessionId,
27880
+ fullBarrier: value.fullBarrier
27881
+ }
27882
+ };
27883
+ } catch {
27884
+ return { kind: "invalid" };
27885
+ } finally {
27886
+ if (fd !== null) {
27887
+ try {
27888
+ fs7.closeSync(fd);
27889
+ } catch {}
27890
+ }
27891
+ }
27892
+ }
27893
+ function updateResumeControlState(timelineDir, update) {
27264
27894
  if (timelineDirectoryState(timelineDir) !== "safe")
27265
27895
  return false;
27266
- const filename = filenameForDate(now);
27267
- const filePath = join10(timelineDir, filename);
27268
- const lockPath = lockPathFor(timelineDir, filename);
27896
+ const lockPath = lockPathFor(timelineDir, RESUME_CONTROL_FILENAME);
27269
27897
  if (!acquireLock(lockPath))
27270
27898
  return false;
27271
27899
  try {
27272
- const existing = scanTimelineFile(filePath);
27273
- const required2 = timelineLine(entry);
27274
- if (!existing || !required2)
27900
+ const current = readResumeControlState(timelineDir);
27901
+ const base = current.kind === "state" ? current.state : EMPTY_RESUME_CONTROL;
27902
+ const next = update({ ...base });
27903
+ const canonical = {
27904
+ version: 1,
27905
+ attemptedSessionId: next.attemptedSessionId,
27906
+ fencedSessionId: next.fencedSessionId,
27907
+ fullBarrier: next.fullBarrier
27908
+ };
27909
+ const body = JSON.stringify(canonical) + `
27910
+ `;
27911
+ if (Buffer.byteLength(body, "utf8") > RESUME_CONTROL_MAX_BYTES)
27275
27912
  return false;
27276
- return writeRequiredTimeline(filePath, [...existing, required2], required2);
27913
+ const filePath = join10(timelineDir, RESUME_CONTROL_FILENAME);
27914
+ const tempPath = join10(timelineDir, `.${RESUME_CONTROL_FILENAME}.${process.pid}.${randomBytes4(12).toString("hex")}.tmp`);
27915
+ let fd = null;
27916
+ try {
27917
+ fd = fs7.openSync(tempPath, "wx", 384);
27918
+ fs7.writeFileSync(fd, body, "utf8");
27919
+ fs7.fsyncSync(fd);
27920
+ fs7.closeSync(fd);
27921
+ fd = null;
27922
+ fs7.renameSync(tempPath, filePath);
27923
+ return true;
27924
+ } finally {
27925
+ if (fd !== null) {
27926
+ try {
27927
+ fs7.closeSync(fd);
27928
+ } catch {}
27929
+ }
27930
+ try {
27931
+ fs7.unlinkSync(tempPath);
27932
+ } catch {}
27933
+ }
27277
27934
  } catch {
27278
27935
  return false;
27279
27936
  } finally {
27280
27937
  releaseLock(lockPath);
27281
27938
  }
27282
27939
  }
27283
- function appendOrMergeEntry(timelineDir, entry, now = new Date) {
27940
+ function appendTrackedEntry(timelineDir, entry, now = new Date) {
27284
27941
  if (timelineDirectoryState(timelineDir) !== "safe")
27285
- return false;
27942
+ return { status: "rejected", reason: "unsafe" };
27286
27943
  const filename = filenameForDate(now);
27287
27944
  const filePath = join10(timelineDir, filename);
27288
27945
  const lockPath = lockPathFor(timelineDir, filename);
27289
- if (!acquireLock(lockPath))
27290
- return false;
27946
+ try {
27947
+ if (!acquireLock(lockPath))
27948
+ return { status: "rejected", reason: "lock" };
27949
+ } catch {
27950
+ return { status: "rejected", reason: "write" };
27951
+ }
27291
27952
  try {
27292
27953
  const existing = scanTimelineFile(filePath);
27293
- if (!existing)
27294
- return false;
27295
- if (existing.length > 0) {
27296
- const latest = existing[existing.length - 1].entry;
27297
- const mergeable = !latest.system && !entry.system && latest.session_id === entry.session_id && latest.provider === entry.provider && latest.agent_responses.length === 0;
27298
- if (mergeable) {
27299
- const merged = {
27300
- ...latest,
27301
- messages: [...latest.messages, ...entry.messages],
27302
- agent_responses: [...latest.agent_responses]
27303
- };
27304
- const required3 = timelineLine(merged);
27305
- if (!required3)
27306
- return false;
27307
- return writeRequiredTimeline(filePath, [...existing.slice(0, -1), required3], required3);
27308
- }
27309
- }
27310
27954
  const required2 = timelineLine(entry);
27955
+ if (!existing)
27956
+ return { status: "rejected", reason: "unsafe" };
27311
27957
  if (!required2)
27312
- return false;
27313
- return writeRequiredTimeline(filePath, [...existing, required2], required2);
27958
+ return { status: "rejected", reason: "oversized" };
27959
+ return writeTrackedTimeline(filePath, filename, existing, [...existing, required2], required2);
27314
27960
  } catch {
27315
- return false;
27961
+ return { status: "rejected", reason: "write" };
27316
27962
  } finally {
27317
27963
  releaseLock(lockPath);
27318
27964
  }
27319
27965
  }
27320
- function updateLatestEntryResult(timelineDir, updater, opts = {}) {
27321
- const directoryState = timelineDirectoryState(timelineDir);
27322
- if (directoryState !== "safe")
27323
- return directoryState === "missing" ? "missing" : "rejected";
27324
- const now = opts.now ?? new Date;
27325
- const maxDays = opts.maxDays ?? 7;
27326
- for (const filename of recentFilenames(maxDays, now)) {
27327
- const filePath = join10(timelineDir, filename);
27328
- let source;
27329
- try {
27330
- source = fs7.lstatSync(filePath);
27331
- } catch (error51) {
27332
- if (error51.code === "ENOENT")
27333
- continue;
27334
- return "rejected";
27335
- }
27336
- if (!source.isFile())
27337
- return "rejected";
27338
- const lockPath = lockPathFor(timelineDir, filename);
27966
+ function updateTrackedEntry(timelineDir, handle, update) {
27967
+ if (timelineDirectoryState(timelineDir) !== "safe")
27968
+ return { status: "rejected", reason: "unsafe" };
27969
+ if (!DATE_FILENAME_PATTERN.test(handle.filename) || basename(handle.filename) !== handle.filename) {
27970
+ return { status: "rejected", reason: "unsafe" };
27971
+ }
27972
+ const filePath = join10(timelineDir, handle.filename);
27973
+ const lockPath = lockPathFor(timelineDir, handle.filename);
27974
+ try {
27339
27975
  if (!acquireLock(lockPath))
27340
- return "rejected";
27341
- try {
27342
- const lines = scanTimelineFile(filePath);
27343
- if (!lines)
27344
- return "rejected";
27345
- if (lines.length === 0)
27346
- continue;
27347
- const latest = lines[lines.length - 1].entry;
27348
- if (latest.system)
27349
- return "missing";
27350
- const updated = {
27351
- ...latest,
27352
- messages: [...latest.messages],
27353
- agent_responses: [...latest.agent_responses]
27354
- };
27355
- try {
27356
- updater(updated);
27357
- } catch {
27358
- return "rejected";
27359
- }
27360
- const required2 = timelineLine(updated);
27361
- if (!required2)
27362
- return "rejected";
27363
- return writeRequiredTimeline(filePath, [...lines.slice(0, -1), required2], required2) ? "updated" : "rejected";
27364
- } catch {
27365
- return "rejected";
27366
- } finally {
27367
- releaseLock(lockPath);
27368
- }
27976
+ return { status: "rejected", reason: "lock" };
27977
+ } catch {
27978
+ return { status: "rejected", reason: "write" };
27979
+ }
27980
+ try {
27981
+ const existing = scanTimelineFile(filePath);
27982
+ if (!existing)
27983
+ return { status: "rejected", reason: "unsafe" };
27984
+ if (existing.length === 0)
27985
+ return { status: "rejected", reason: "missing" };
27986
+ const generation = timelineFileGeneration(filePath, existing);
27987
+ if (!generation || generation !== handle.fileGeneration) {
27988
+ return { status: "rejected", reason: "generation" };
27989
+ }
27990
+ const captured = existing[handle.rowOrdinal];
27991
+ if (!captured)
27992
+ return { status: "rejected", reason: "ordinal" };
27993
+ if (timelineRowHash(captured) !== handle.expectedHash) {
27994
+ return { status: "rejected", reason: "hash" };
27995
+ }
27996
+ if (captured.entry.system)
27997
+ return { status: "rejected", reason: "system" };
27998
+ const nextEntry = update({
27999
+ ...captured.entry,
28000
+ messages: [...captured.entry.messages],
28001
+ agent_responses: [...captured.entry.agent_responses]
28002
+ });
28003
+ const replacement = timelineLine(nextEntry);
28004
+ if (!replacement)
28005
+ return { status: "rejected", reason: "oversized" };
28006
+ const input = [...existing];
28007
+ input[handle.rowOrdinal] = replacement;
28008
+ return writeTrackedTimeline(filePath, handle.filename, existing, input, replacement, { oldOrdinal: handle.rowOrdinal, line: replacement });
28009
+ } catch {
28010
+ return { status: "rejected", reason: "write" };
28011
+ } finally {
28012
+ releaseLock(lockPath);
27369
28013
  }
27370
- return "missing";
27371
28014
  }
27372
28015
  function yieldToEventLoop() {
27373
28016
  return new Promise((resolve3) => setImmediate(resolve3));
@@ -27431,84 +28074,562 @@ function createTimelineEntry(fields) {
27431
28074
  provider: fields.provider ?? null
27432
28075
  };
27433
28076
  }
27434
- function createSystemEntry(type, time3) {
28077
+ function createSystemEntry(type, time3, backendSessionId) {
27435
28078
  return {
27436
28079
  session_id: null,
27437
28080
  messages: [],
27438
28081
  agent_responses: [],
27439
28082
  provider: null,
27440
- system: { type, time: time3 }
28083
+ system: {
28084
+ type,
28085
+ time: time3,
28086
+ ...backendSessionId ? { backend_session_id: backendSessionId } : {}
28087
+ }
27441
28088
  };
27442
28089
  }
27443
- function findResumableSession(rows, provider) {
28090
+ function resolveResumableSession(rows, provider) {
28091
+ let candidateSessionId = null;
28092
+ let recoveryMarkerSeen = false;
28093
+ let stalledSessionId = null;
28094
+ let fencedSessionId = null;
27444
28095
  for (let i = rows.length - 1;i >= 0; i--) {
27445
28096
  const e = rows[i];
27446
- if (e.system?.type === "reset_session" || e.system?.type === "nap")
27447
- return null;
28097
+ if (e.system) {
28098
+ if (e.system.type === "stall_recovery_attempt") {
28099
+ if (!recoveryMarkerSeen) {
28100
+ recoveryMarkerSeen = true;
28101
+ stalledSessionId = e.system.backend_session_id ?? null;
28102
+ }
28103
+ continue;
28104
+ }
28105
+ if (e.system.type === "stall_recovery_clear") {
28106
+ if (!recoveryMarkerSeen)
28107
+ recoveryMarkerSeen = true;
28108
+ continue;
28109
+ }
28110
+ if (e.system.type === "stall_recovery" && fencedSessionId === null) {
28111
+ fencedSessionId = e.system.backend_session_id ?? null;
28112
+ }
28113
+ if (candidateSessionId !== null) {
28114
+ return {
28115
+ kind: "session",
28116
+ sessionId: candidateSessionId,
28117
+ stalledSessionId: stalledSessionId === candidateSessionId ? stalledSessionId : null,
28118
+ fencedSessionId
28119
+ };
28120
+ }
28121
+ return {
28122
+ kind: "barrier",
28123
+ type: e.system.type,
28124
+ forgottenSessionId: e.system.backend_session_id ?? null,
28125
+ fencedSessionId
28126
+ };
28127
+ }
27448
28128
  if (!e.session_id)
27449
28129
  continue;
27450
28130
  if (provider && e.provider !== provider)
27451
28131
  continue;
27452
- return e.session_id;
28132
+ if (candidateSessionId === null) {
28133
+ candidateSessionId = e.session_id;
28134
+ continue;
28135
+ }
28136
+ if (candidateSessionId !== e.session_id)
28137
+ break;
27453
28138
  }
27454
- return null;
28139
+ if (candidateSessionId !== null) {
28140
+ return {
28141
+ kind: "session",
28142
+ sessionId: candidateSessionId,
28143
+ stalledSessionId: stalledSessionId === candidateSessionId ? stalledSessionId : null,
28144
+ fencedSessionId
28145
+ };
28146
+ }
28147
+ return { kind: "none", stalledSessionId, fencedSessionId };
27455
28148
  }
27456
28149
  // src/timeline/recorder.ts
27457
28150
  var MAX_AGENT_RESPONSES = 5;
27458
- function appendAgentResponse(entry, text2) {
27459
- entry.agent_responses.push(text2);
27460
- if (entry.agent_responses.length > MAX_AGENT_RESPONSES) {
27461
- entry.agent_responses.splice(0, entry.agent_responses.length - MAX_AGENT_RESPONSES);
28151
+ var MAX_AGENT_RESPONSE_BYTES = 65536;
28152
+ var MAX_PENDING_COMMITS_PER_AGENT = 8;
28153
+ var PENDING_COMMIT_TTL_MS = 15 * 60000;
28154
+ var TRUNCATION_MARKER = `
28155
+ … [truncated]`;
28156
+ function turnKey(agentId, owner) {
28157
+ return `${agentId}\x00${owner.sessionInstanceId}\x00${owner.rootTurnId}\x00${owner.barrierGeneration}`;
28158
+ }
28159
+ function sameOwner(left, right) {
28160
+ return left.sessionInstanceId === right.sessionInstanceId && left.rootTurnId === right.rootTurnId && left.barrierGeneration === right.barrierGeneration;
28161
+ }
28162
+ function utf8Prefix2(text2, maxBytes) {
28163
+ if (maxBytes <= 0)
28164
+ return "";
28165
+ if (Buffer.byteLength(text2, "utf8") <= maxBytes)
28166
+ return text2;
28167
+ let low = 0;
28168
+ let high = text2.length;
28169
+ while (low < high) {
28170
+ const mid = Math.ceil((low + high) / 2);
28171
+ let end2 = mid;
28172
+ const code2 = text2.charCodeAt(end2 - 1);
28173
+ if (code2 >= 55296 && code2 <= 56319)
28174
+ end2 -= 1;
28175
+ if (Buffer.byteLength(text2.slice(0, end2), "utf8") <= maxBytes)
28176
+ low = mid;
28177
+ else
28178
+ high = mid - 1;
28179
+ }
28180
+ let end = low;
28181
+ const code = text2.charCodeAt(end - 1);
28182
+ if (code >= 55296 && code <= 56319)
28183
+ end -= 1;
28184
+ while (end > 0 && Buffer.byteLength(text2.slice(0, end), "utf8") > maxBytes)
28185
+ end -= 1;
28186
+ return text2.slice(0, end);
28187
+ }
28188
+ function boundedResponse(text2, alreadyTruncated) {
28189
+ const markerBytes = Buffer.byteLength(TRUNCATION_MARKER, "utf8");
28190
+ const needsTruncation = alreadyTruncated || Buffer.byteLength(text2, "utf8") > MAX_AGENT_RESPONSE_BYTES;
28191
+ if (!needsTruncation)
28192
+ return text2;
28193
+ return utf8Prefix2(text2, MAX_AGENT_RESPONSE_BYTES - markerBytes) + TRUNCATION_MARKER;
28194
+ }
28195
+ function serializedTimelineBytes(entry) {
28196
+ return Buffer.byteLength(JSON.stringify(entry), "utf8") + 1;
28197
+ }
28198
+ function fitResponsesToRow(entry, responses) {
28199
+ let fitted = [...entry.agent_responses];
28200
+ for (const response of responses) {
28201
+ fitted = [...fitted, response].slice(-MAX_AGENT_RESPONSES);
28202
+ if (serializedTimelineBytes({ ...entry, agent_responses: fitted }) <= TIMELINE_MAX_BYTES)
28203
+ continue;
28204
+ const truncationBase = response.endsWith(TRUNCATION_MARKER) ? response.slice(0, -TRUNCATION_MARKER.length) : response;
28205
+ let low = 0;
28206
+ let high = truncationBase.length;
28207
+ let replacement = null;
28208
+ while (low <= high) {
28209
+ const mid = Math.floor((low + high) / 2);
28210
+ let end = mid;
28211
+ const code = truncationBase.charCodeAt(end - 1);
28212
+ if (code >= 55296 && code <= 56319)
28213
+ end -= 1;
28214
+ const candidateResponse = truncationBase.slice(0, end) + TRUNCATION_MARKER;
28215
+ const candidate = [...fitted.slice(0, -1), candidateResponse];
28216
+ if (serializedTimelineBytes({ ...entry, agent_responses: candidate }) <= TIMELINE_MAX_BYTES) {
28217
+ replacement = candidateResponse;
28218
+ low = mid + 1;
28219
+ } else {
28220
+ high = mid - 1;
28221
+ }
28222
+ }
28223
+ if (replacement === null)
28224
+ return null;
28225
+ fitted[fitted.length - 1] = replacement;
27462
28226
  }
28227
+ return fitted;
27463
28228
  }
27464
28229
  function createTimelineRecorder(opts) {
27465
28230
  const now = opts.now ?? (() => new Date);
27466
28231
  const dirFor = (agentId) => opts.timelineDirFor(agentId);
27467
28232
  const sessionByAgent = new Map;
28233
+ const resolveForAgent = (agentId, provider) => {
28234
+ const dir = dirFor(agentId);
28235
+ const recent = resolveResumableSession(readRecentEntries(dir, { now: now() }), provider ?? undefined);
28236
+ const control = readResumeControlState(dir);
28237
+ if (control.kind === "missing")
28238
+ return recent;
28239
+ if (control.kind === "invalid") {
28240
+ return {
28241
+ kind: "barrier",
28242
+ type: "reset_session",
28243
+ forgottenSessionId: null,
28244
+ fencedSessionId: null
28245
+ };
28246
+ }
28247
+ const { attemptedSessionId, fencedSessionId, fullBarrier } = control.state;
28248
+ if (fullBarrier !== null) {
28249
+ return {
28250
+ kind: "barrier",
28251
+ type: fullBarrier,
28252
+ forgottenSessionId: null,
28253
+ fencedSessionId
28254
+ };
28255
+ }
28256
+ if (recent.kind === "session") {
28257
+ return {
28258
+ kind: "session",
28259
+ sessionId: recent.sessionId,
28260
+ stalledSessionId: attemptedSessionId === recent.sessionId ? attemptedSessionId : null,
28261
+ fencedSessionId
28262
+ };
28263
+ }
28264
+ if (recent.kind === "barrier") {
28265
+ if (recent.type !== "stall_recovery" || recent.forgottenSessionId === fencedSessionId) {
28266
+ return { ...recent, fencedSessionId };
28267
+ }
28268
+ }
28269
+ return { kind: "none", stalledSessionId: attemptedSessionId, fencedSessionId };
28270
+ };
28271
+ const sessionByEpoch = new Map;
28272
+ const barrierByAgent = new Map;
28273
+ const turnsByAgent = new Map;
28274
+ const activeTurnByAgent = new Map;
28275
+ const epochKey = (agentId, sessionInstanceId) => `${agentId}\x00${sessionInstanceId}`;
28276
+ const currentBarrier = (agentId) => barrierByAgent.get(agentId) ?? 0;
28277
+ const statesFor = (agentId) => {
28278
+ let states = turnsByAgent.get(agentId);
28279
+ if (!states) {
28280
+ states = new Map;
28281
+ turnsByAgent.set(agentId, states);
28282
+ }
28283
+ return states;
28284
+ };
28285
+ const diagnostic = (agentId, code, reason) => {
28286
+ try {
28287
+ opts.onDiagnostic?.({ agentId, code, ...reason ? { reason } : {} });
28288
+ } catch {}
28289
+ };
28290
+ const applyRewrite = (agentId, rewrite) => {
28291
+ for (const state of statesFor(agentId).values()) {
28292
+ if (!state.handle)
28293
+ continue;
28294
+ const refreshed = refreshTimelineEntryHandle(state.handle, rewrite);
28295
+ if (refreshed) {
28296
+ state.handle = refreshed;
28297
+ } else if (state.handle.filename === rewrite.filename) {
28298
+ state.handle = undefined;
28299
+ state.rowFenced = true;
28300
+ diagnostic(agentId, "timeline_handle_fenced", "rewrite_remap");
28301
+ }
28302
+ }
28303
+ };
28304
+ const handleTrackedResult = (agentId, state, result) => {
28305
+ if (result.status === "written") {
28306
+ applyRewrite(agentId, result.rewrite);
28307
+ if (state && result.handle) {
28308
+ state.handle = result.handle;
28309
+ state.rowFenced = false;
28310
+ }
28311
+ return "written";
28312
+ }
28313
+ if (result.reason === "lock" || result.reason === "write")
28314
+ return "retryable";
28315
+ if (state && result.reason !== "oversized")
28316
+ state.rowFenced = true;
28317
+ diagnostic(agentId, "timeline_exact_write_rejected", result.reason);
28318
+ return "terminal";
28319
+ };
28320
+ const tryCommit = (agentId, state) => {
28321
+ if (state.responses.length === 0)
28322
+ return "written";
28323
+ const dir = dirFor(agentId);
28324
+ if (!prepareTimelineDirectory(dir)) {
28325
+ diagnostic(agentId, "timeline_directory_unavailable");
28326
+ return "terminal";
28327
+ }
28328
+ if (state.handle) {
28329
+ const result2 = updateTrackedEntry(dir, state.handle, (entry2) => {
28330
+ const fitted2 = fitResponsesToRow(entry2, state.responses);
28331
+ return { ...entry2, agent_responses: fitted2 ?? [...entry2.agent_responses, ...state.responses] };
28332
+ });
28333
+ return handleTrackedResult(agentId, state, result2);
28334
+ }
28335
+ if (state.rowFenced || state.pendingMode === "handle")
28336
+ return "terminal";
28337
+ if (state.owner.barrierGeneration !== currentBarrier(agentId))
28338
+ return "terminal";
28339
+ const entry = createTimelineEntry({
28340
+ messages: [],
28341
+ sessionId: state.backendSessionId,
28342
+ provider: state.provider
28343
+ });
28344
+ const fitted = fitResponsesToRow(entry, state.responses);
28345
+ if (!fitted) {
28346
+ diagnostic(agentId, "timeline_response_did_not_fit", "oversized");
28347
+ return "terminal";
28348
+ }
28349
+ entry.agent_responses = fitted;
28350
+ const result = appendTrackedEntry(dir, entry, now());
28351
+ return handleTrackedResult(agentId, state, result);
28352
+ };
28353
+ const deleteState = (agentId, key) => {
28354
+ const states = statesFor(agentId);
28355
+ states.delete(key);
28356
+ if (activeTurnByAgent.get(agentId) === key)
28357
+ activeTurnByAgent.delete(agentId);
28358
+ if (states.size === 0)
28359
+ turnsByAgent.delete(agentId);
28360
+ };
28361
+ const retryPending = (agentId) => {
28362
+ const states = turnsByAgent.get(agentId);
28363
+ if (!states)
28364
+ return;
28365
+ const nowMs = now().getTime();
28366
+ for (const [key, state] of [...states]) {
28367
+ if (state.finalized && state.pendingSinceMs === undefined && state.completedAtMs !== undefined && nowMs - state.completedAtMs >= PENDING_COMMIT_TTL_MS) {
28368
+ deleteState(agentId, key);
28369
+ continue;
28370
+ }
28371
+ if (!state.finalized || state.pendingSinceMs === undefined)
28372
+ continue;
28373
+ if (nowMs - state.pendingSinceMs >= PENDING_COMMIT_TTL_MS) {
28374
+ diagnostic(agentId, "timeline_pending_commit_expired");
28375
+ deleteState(agentId, key);
28376
+ continue;
28377
+ }
28378
+ const result = tryCommit(agentId, state);
28379
+ if (result === "written") {
28380
+ state.responses = [];
28381
+ state.pendingSinceMs = undefined;
28382
+ state.pendingMode = undefined;
28383
+ state.completedAtMs = nowMs;
28384
+ } else if (result === "terminal") {
28385
+ deleteState(agentId, key);
28386
+ }
28387
+ }
28388
+ const completed = [...states.entries()].filter(([, state]) => state.finalized && state.pendingSinceMs === undefined).sort((left, right) => (left[1].completedAtMs ?? 0) - (right[1].completedAtMs ?? 0));
28389
+ while (completed.length > MAX_PENDING_COMMITS_PER_AGENT) {
28390
+ const oldest = completed.shift();
28391
+ if (oldest)
28392
+ deleteState(agentId, oldest[0]);
28393
+ }
28394
+ };
28395
+ const retainPending = (agentId, key, state) => {
28396
+ const states = statesFor(agentId);
28397
+ const pending = [...states.values()].filter((candidate) => candidate.finalized && candidate.pendingSinceMs !== undefined);
28398
+ if (pending.length >= MAX_PENDING_COMMITS_PER_AGENT) {
28399
+ diagnostic(agentId, "timeline_pending_commit_overflow");
28400
+ deleteState(agentId, key);
28401
+ return;
28402
+ }
28403
+ state.pendingSinceMs ??= now().getTime();
28404
+ state.pendingMode = state.handle ? "handle" : "fallback";
28405
+ };
28406
+ const appendOwnerless = (agentId, messages) => {
28407
+ if (messages.length === 0)
28408
+ return;
28409
+ const dir = dirFor(agentId);
28410
+ if (!prepareTimelineDirectory(dir))
28411
+ return;
28412
+ const result = appendTrackedEntry(dir, createTimelineEntry({
28413
+ messages,
28414
+ sessionId: sessionByAgent.get(agentId) ?? null,
28415
+ provider: opts.providerFor?.(agentId) ?? null
28416
+ }), now());
28417
+ handleTrackedResult(agentId, null, result);
28418
+ };
27468
28419
  return {
27469
- setSession(agentId, sessionId) {
27470
- sessionByAgent.set(agentId, sessionId);
28420
+ barrierGeneration(agentId) {
28421
+ return currentBarrier(agentId);
27471
28422
  },
27472
- appendEntryForAgent(agentId, messages) {
28423
+ beginTurn(agentId, owner) {
28424
+ retryPending(agentId);
28425
+ const states = statesFor(agentId);
28426
+ if (owner.barrierGeneration !== currentBarrier(agentId)) {
28427
+ diagnostic(agentId, "timeline_turn_begin_fenced", "barrier_generation");
28428
+ return;
28429
+ }
28430
+ const key = turnKey(agentId, owner);
28431
+ if (!states.has(key)) {
28432
+ states.set(key, {
28433
+ owner: { ...owner },
28434
+ provider: opts.providerFor?.(agentId) ?? null,
28435
+ backendSessionId: sessionByEpoch.get(epochKey(agentId, owner.sessionInstanceId)) ?? sessionByAgent.get(agentId) ?? null,
28436
+ responses: [],
28437
+ rowFenced: false,
28438
+ finalized: false
28439
+ });
28440
+ }
28441
+ activeTurnByAgent.set(agentId, key);
28442
+ },
28443
+ recordInboxPull(agentId, owner, messages) {
28444
+ retryPending(agentId);
27473
28445
  if (messages.length === 0)
27474
28446
  return;
28447
+ if (!owner) {
28448
+ appendOwnerless(agentId, messages);
28449
+ return;
28450
+ }
28451
+ const key = turnKey(agentId, owner);
28452
+ const state = turnsByAgent.get(agentId)?.get(key);
28453
+ if (!state || state.rowFenced || !sameOwner(state.owner, owner)) {
28454
+ appendOwnerless(agentId, messages);
28455
+ return;
28456
+ }
27475
28457
  const dir = dirFor(agentId);
27476
28458
  if (!prepareTimelineDirectory(dir))
27477
28459
  return;
27478
- appendOrMergeEntry(dir, createTimelineEntry({
27479
- messages,
27480
- sessionId: sessionByAgent.get(agentId) ?? null,
27481
- provider: opts.providerFor?.(agentId) ?? null
27482
- }), now());
28460
+ const stamp = now();
28461
+ let result;
28462
+ if (state.handle && (state.handle.filename === filenameForDate(stamp) || state.owner.barrierGeneration !== currentBarrier(agentId))) {
28463
+ result = updateTrackedEntry(dir, state.handle, (entry) => ({
28464
+ ...entry,
28465
+ messages: [...entry.messages, ...messages],
28466
+ session_id: state.backendSessionId,
28467
+ provider: state.provider
28468
+ }));
28469
+ } else if (state.owner.barrierGeneration === currentBarrier(agentId)) {
28470
+ result = appendTrackedEntry(dir, createTimelineEntry({
28471
+ messages,
28472
+ sessionId: state.backendSessionId,
28473
+ provider: state.provider
28474
+ }), stamp);
28475
+ } else {
28476
+ appendOwnerless(agentId, messages);
28477
+ return;
28478
+ }
28479
+ handleTrackedResult(agentId, state, result);
27483
28480
  },
27484
- appendResponseToLatest(agentId, text2) {
27485
- const dir = dirFor(agentId);
27486
- if (!prepareTimelineDirectory(dir))
28481
+ recordAssistantMessage(agentId, owner, text2, truncated = false) {
28482
+ retryPending(agentId);
28483
+ const key = turnKey(agentId, owner);
28484
+ const state = turnsByAgent.get(agentId)?.get(key);
28485
+ if (!state || state.finalized || state.owner.barrierGeneration !== currentBarrier(agentId) || !sameOwner(state.owner, owner)) {
28486
+ diagnostic(agentId, "timeline_completed_message_rejected", "stale_owner");
27487
28487
  return;
27488
- const result = updateLatestEntryResult(dir, (entry2) => appendAgentResponse(entry2, text2), { now: now() });
27489
- if (result === "updated" || result === "rejected")
28488
+ }
28489
+ state.responses.push(boundedResponse(text2, truncated));
28490
+ if (state.responses.length > MAX_AGENT_RESPONSES) {
28491
+ state.responses.splice(0, state.responses.length - MAX_AGENT_RESPONSES);
28492
+ }
28493
+ },
28494
+ finalizeTurn(agentId, owner) {
28495
+ retryPending(agentId);
28496
+ const key = turnKey(agentId, owner);
28497
+ const state = turnsByAgent.get(agentId)?.get(key);
28498
+ if (!state || state.finalized || !sameOwner(state.owner, owner))
27490
28499
  return;
27491
- const entry = createTimelineEntry({
27492
- messages: [],
27493
- sessionId: sessionByAgent.get(agentId) ?? null,
27494
- provider: opts.providerFor?.(agentId) ?? null
27495
- });
27496
- appendAgentResponse(entry, text2);
27497
- appendEntry(dir, entry, now());
28500
+ const fallbackAuthorized = activeTurnByAgent.get(agentId) === key && owner.barrierGeneration === currentBarrier(agentId);
28501
+ state.finalized = true;
28502
+ activeTurnByAgent.delete(agentId);
28503
+ if (state.responses.length === 0) {
28504
+ state.completedAtMs = now().getTime();
28505
+ return;
28506
+ }
28507
+ if (!state.handle && !fallbackAuthorized) {
28508
+ diagnostic(agentId, "timeline_fallback_rejected", "fenced_owner");
28509
+ deleteState(agentId, key);
28510
+ return;
28511
+ }
28512
+ const result = tryCommit(agentId, state);
28513
+ if (result === "written") {
28514
+ state.responses = [];
28515
+ state.completedAtMs = now().getTime();
28516
+ } else if (result === "terminal") {
28517
+ deleteState(agentId, key);
28518
+ } else {
28519
+ retainPending(agentId, key, state);
28520
+ }
28521
+ },
28522
+ fenceSession(agentId) {
28523
+ retryPending(agentId);
28524
+ barrierByAgent.set(agentId, currentBarrier(agentId) + 1);
28525
+ activeTurnByAgent.delete(agentId);
28526
+ const states = turnsByAgent.get(agentId);
28527
+ if (!states)
28528
+ return;
28529
+ for (const [key, state] of [...states]) {
28530
+ if (!state.handle) {
28531
+ diagnostic(agentId, "timeline_fallback_rejected", "session_fence");
28532
+ deleteState(agentId, key);
28533
+ }
28534
+ }
28535
+ },
28536
+ setSession(agentId, sessionId, sessionInstanceId) {
28537
+ retryPending(agentId);
28538
+ const dir = dirFor(agentId);
28539
+ if (!prepareTimelineDirectory(dir))
28540
+ return false;
28541
+ const persisted = updateResumeControlState(dir, (state) => ({
28542
+ ...state,
28543
+ fullBarrier: null,
28544
+ attemptedSessionId: state.attemptedSessionId === sessionId ? state.attemptedSessionId : null
28545
+ }));
28546
+ if (!persisted)
28547
+ return false;
28548
+ sessionByAgent.set(agentId, sessionId);
28549
+ if (sessionInstanceId) {
28550
+ sessionByEpoch.set(epochKey(agentId, sessionInstanceId), sessionId);
28551
+ for (const state of statesFor(agentId).values()) {
28552
+ if (state.owner.sessionInstanceId === sessionInstanceId)
28553
+ state.backendSessionId = sessionId;
28554
+ }
28555
+ }
28556
+ return true;
27498
28557
  },
27499
28558
  resumeSessionId(agentId, provider) {
27500
- const rows = readRecentEntries(dirFor(agentId), { now: now() });
27501
- return findResumableSession(rows, provider ?? undefined);
28559
+ const resolution = resolveForAgent(agentId, provider);
28560
+ return resolution.kind === "session" ? resolution.sessionId : null;
28561
+ },
28562
+ resolveResumeSession(agentId, provider) {
28563
+ return resolveForAgent(agentId, provider);
27502
28564
  },
27503
- forgetSession(agentId, barrierType = "reset_session") {
28565
+ recordSessionStall(agentId, sessionId) {
28566
+ return appendStallMarker(agentId, "stall_recovery_attempt", sessionId);
28567
+ },
28568
+ clearSessionStall(agentId, sessionId) {
28569
+ return appendStallMarker(agentId, "stall_recovery_clear", sessionId);
28570
+ },
28571
+ forgetSession(agentId, barrierType = "reset_session", forgottenSessionId) {
28572
+ retryPending(agentId);
27504
28573
  const dir = dirFor(agentId);
27505
- sessionByAgent.delete(agentId);
27506
28574
  if (!prepareTimelineDirectory(dir))
27507
- return;
28575
+ return false;
27508
28576
  const stamp = now();
27509
- appendEntry(dir, createSystemEntry(barrierType, stamp.toISOString()), stamp);
28577
+ let persisted = false;
28578
+ if (barrierType === "reset_session" || barrierType === "nap") {
28579
+ persisted = updateResumeControlState(dir, (state) => ({
28580
+ ...state,
28581
+ attemptedSessionId: null,
28582
+ fencedSessionId: null,
28583
+ fullBarrier: barrierType
28584
+ }));
28585
+ } else if (barrierType === "stall_recovery") {
28586
+ persisted = updateResumeControlState(dir, (state) => ({
28587
+ ...state,
28588
+ attemptedSessionId: state.attemptedSessionId === forgottenSessionId ? null : state.attemptedSessionId,
28589
+ fencedSessionId: forgottenSessionId ?? null,
28590
+ fullBarrier: null
28591
+ }));
28592
+ }
28593
+ if (!persisted)
28594
+ return false;
28595
+ sessionByAgent.delete(agentId);
28596
+ for (const key of [...sessionByEpoch.keys()]) {
28597
+ if (key.startsWith(`${agentId}\x00`))
28598
+ sessionByEpoch.delete(key);
28599
+ }
28600
+ barrierByAgent.set(agentId, currentBarrier(agentId) + 1);
28601
+ activeTurnByAgent.delete(agentId);
28602
+ const states = turnsByAgent.get(agentId);
28603
+ if (states) {
28604
+ for (const [key, state] of [...states]) {
28605
+ if (!state.handle) {
28606
+ diagnostic(agentId, "timeline_fallback_rejected", "barrier");
28607
+ deleteState(agentId, key);
28608
+ }
28609
+ }
28610
+ }
28611
+ const result = appendTrackedEntry(dir, createSystemEntry(barrierType, stamp.toISOString(), forgottenSessionId), stamp);
28612
+ handleTrackedResult(agentId, null, result);
28613
+ return true;
27510
28614
  }
27511
28615
  };
28616
+ function appendStallMarker(agentId, type, sessionId) {
28617
+ retryPending(agentId);
28618
+ const dir = dirFor(agentId);
28619
+ if (!prepareTimelineDirectory(dir))
28620
+ return false;
28621
+ const stamp = now();
28622
+ const persisted = updateResumeControlState(dir, (state) => ({
28623
+ ...state,
28624
+ attemptedSessionId: type === "stall_recovery_attempt" ? sessionId : null,
28625
+ fullBarrier: null
28626
+ }));
28627
+ if (!persisted)
28628
+ return false;
28629
+ const result = appendTrackedEntry(dir, createSystemEntry(type, stamp.toISOString(), sessionId), stamp);
28630
+ handleTrackedResult(agentId, null, result);
28631
+ return true;
28632
+ }
27512
28633
  }
27513
28634
  // src/discovery.ts
27514
28635
  import * as path9 from "path";
@@ -28123,11 +29244,15 @@ async function createDaemon(opts) {
28123
29244
  const typingTracker = createTypingScopeTracker();
28124
29245
  const broker = new CredentialBroker({ upstreamBaseUrl: opts.serverUrl });
28125
29246
  const proxy = await startCredentialProxy(broker, {
28126
- onInboxPullStart: (agentId) => channelRef?.modelSeenGeneration(agentId),
29247
+ onInboxPullStart: (agentId) => ({
29248
+ modelSeenGeneration: channelRef?.modelSeenGeneration(agentId),
29249
+ owner: managerRef?.timelineTurnOwner(agentId) ?? null
29250
+ }),
28127
29251
  onInboxPullResponse: (agentId, messages, observationToken) => {
28128
- timeline2.appendEntryForAgent(agentId, messages);
28129
- if (typeof observationToken === "number") {
28130
- channelRef?.recordModelSeen(agentId, messages, observationToken);
29252
+ const token = observationToken && typeof observationToken === "object" ? observationToken : null;
29253
+ timeline2.recordInboxPull(agentId, token?.owner ?? null, messages);
29254
+ if (typeof token?.modelSeenGeneration === "number") {
29255
+ channelRef?.recordModelSeen(agentId, messages, token.modelSeenGeneration);
28131
29256
  }
28132
29257
  },
28133
29258
  onInboxPullObservationError: ({ agentId, reason, contentEncoding }) => {
@@ -28188,6 +29313,7 @@ async function createDaemon(opts) {
28188
29313
  typingTracker.clear(agentId);
28189
29314
  }
28190
29315
  const botsById = new Map;
29316
+ let botCacheReady = false;
28191
29317
  async function listMyBotsHttp() {
28192
29318
  const res = await fetch(`${opts.serverUrl}/api/community/daemon/bots`, {
28193
29319
  method: "GET",
@@ -28198,22 +29324,26 @@ async function createDaemon(opts) {
28198
29324
  const json2 = await res.json();
28199
29325
  return json2.bots ?? [];
28200
29326
  }
29327
+ function replaceBotCache(bots) {
29328
+ botsById.clear();
29329
+ for (const b of bots) {
29330
+ botsById.set(b.id, {
29331
+ name: b.name,
29332
+ discriminator: b.discriminator,
29333
+ description: b.description,
29334
+ ownerName: b.ownerName,
29335
+ ownerDiscriminator: b.ownerDiscriminator
29336
+ });
29337
+ }
29338
+ botCacheReady = true;
29339
+ }
28201
29340
  async function coldStartWarmup() {
28202
29341
  const start = Date.now();
28203
29342
  let attempt = 0;
28204
29343
  while (Date.now() - start < WARMUP_CEILING_MS) {
28205
29344
  try {
28206
29345
  const bots = await listMyBotsHttp();
28207
- botsById.clear();
28208
- for (const b of bots) {
28209
- botsById.set(b.id, {
28210
- name: b.name,
28211
- discriminator: b.discriminator,
28212
- description: b.description,
28213
- ownerName: b.ownerName,
28214
- ownerDiscriminator: b.ownerDiscriminator
28215
- });
28216
- }
29346
+ replaceBotCache(bots);
28217
29347
  log.info("cold-start bot-cache warmup succeeded", { bots: bots.length, attempt });
28218
29348
  return;
28219
29349
  } catch {
@@ -28434,7 +29564,14 @@ async function createDaemon(opts) {
28434
29564
  const statusPath = opts.statusFilePath;
28435
29565
  const writeStatus = () => {
28436
29566
  const nowMs = Date.now();
28437
- writeStatusFile(statusPath, { writtenAt: nowMs, agents: manager.statusProjection(nowMs) });
29567
+ writeStatusFile(statusPath, {
29568
+ writtenAt: nowMs,
29569
+ agentSummary: {
29570
+ total: botCacheReady ? botsById.size : null,
29571
+ running: manager.runningAgentCount()
29572
+ },
29573
+ agents: manager.statusProjection(nowMs)
29574
+ });
28438
29575
  };
28439
29576
  writeStatus();
28440
29577
  statusTimer = setInterval(writeStatus, STATUS_WRITE_INTERVAL_MS);
@@ -28459,15 +29596,7 @@ async function createDaemon(opts) {
28459
29596
  if (!botsById.has(agentId)) {
28460
29597
  try {
28461
29598
  const bots = await listMyBotsHttp();
28462
- for (const b of bots) {
28463
- botsById.set(b.id, {
28464
- name: b.name,
28465
- discriminator: b.discriminator,
28466
- description: b.description,
28467
- ownerName: b.ownerName,
28468
- ownerDiscriminator: b.ownerDiscriminator
28469
- });
28470
- }
29599
+ replaceBotCache(bots);
28471
29600
  } catch {}
28472
29601
  }
28473
29602
  if (!botsById.has(agentId)) {
@@ -28578,10 +29707,12 @@ export {
28578
29707
  RUNTIME_RAW_TRACE_AGENT_IDS_ENV,
28579
29708
  RUNTIME_AUTH_ACTION_REQUIRED_PATTERNS,
28580
29709
  LOCAL_MESSAGE_REMINDER_PATH,
29710
+ DEFAULT_TURN_SILENCE_POLICY,
28581
29711
  DEFAULT_STOPPING_STUCK_THRESHOLD_MS,
28582
29712
  DEFAULT_STALE_THRESHOLD_MS,
28583
29713
  DEFAULT_RESET_STUCK_THRESHOLD_MS,
28584
29714
  DEFAULT_IDLE_TIMEOUT_MS,
29715
+ DEFAULT_IDLE_RESET_TIMEOUT_MS,
28585
29716
  DEFAULT_HELD_CONTEXT_LIMIT,
28586
29717
  DEFAULT_CAPABILITY_RESOLVER,
28587
29718
  CredentialBroker,