@alook/daemon 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -17792,7 +17792,9 @@ var communityChannel = sqliteTable("community_channel", {
17792
17792
  creatorId: text("creator_id").references(() => user.id, { onDelete: "set null" }),
17793
17793
  messageCount: integer2("message_count").default(0),
17794
17794
  archived: integer2("archived").default(0),
17795
- parentMessageId: text("parent_message_id"),
17795
+ parentMessageId: text("parent_message_id").references(() => communityMessage.id, {
17796
+ onDelete: "cascade"
17797
+ }),
17796
17798
  lastMessageAt: text("last_message_at"),
17797
17799
  createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
17798
17800
  }, (t) => [
@@ -18358,8 +18360,9 @@ var communityChannelDeleteSchema = exports_external.strictObject({
18358
18360
  type: exports_external.literal("community:channel.delete"),
18359
18361
  serverId: string4,
18360
18362
  channelId: string4,
18361
- parentChannelId: nullableString.optional()
18362
- });
18363
+ parentChannelId: nullableString.optional(),
18364
+ parentMessageId: string4.optional()
18365
+ }).refine((event) => event.parentMessageId === undefined || typeof event.parentChannelId === "string" && event.parentChannelId.length > 0, { message: "parentMessageId requires parentChannelId" });
18363
18366
  var positionedIdSchema = exports_external.strictObject({ id: string4, position: exports_external.number() });
18364
18367
  var communityChannelReorderSchema = exports_external.strictObject({
18365
18368
  type: exports_external.literal("community:channel.reorder"),
@@ -20285,19 +20288,23 @@ class ClaudeEventNormalizer {
20285
20288
  const content = event?.message?.content;
20286
20289
  if (!Array.isArray(content))
20287
20290
  return;
20291
+ const completedText = [];
20288
20292
  for (const block of content) {
20289
20293
  if (block?.type === "thinking") {
20290
- out.push({ kind: "thinking", text: block.thinking ?? "" });
20294
+ out.push({ kind: "assistant_reasoning_completed", text: block.thinking ?? "" });
20291
20295
  } else if (block?.type === "text") {
20292
20296
  const text2 = block.text ?? "";
20293
20297
  if (API_ERROR_RE.test(text2))
20294
20298
  out.push({ kind: "error", message: text2 });
20295
20299
  else
20296
- out.push({ kind: "text", text: text2 });
20300
+ completedText.push(text2);
20297
20301
  } else if (block?.type === "tool_use") {
20298
20302
  out.push({ kind: "tool_call", name: block.name ?? "unknown_tool", input: block.input });
20299
20303
  }
20300
20304
  }
20305
+ if (completedText.length > 0) {
20306
+ out.push({ kind: "assistant_message_completed", text: completedText.join("") });
20307
+ }
20301
20308
  }
20302
20309
  handleUser(event, out) {
20303
20310
  if (event.isReplay === true && typeof event.uuid === "string") {
@@ -20706,14 +20713,17 @@ class CodexEventNormalizer {
20706
20713
  this.turnId = params.turn.id;
20707
20714
  this.terminalTurn = null;
20708
20715
  return [
20709
- { kind: "turn_owner", receipt: this.turnReceipt(params.threadId, params.turn.id) },
20710
- { kind: "thinking", text: "" }
20716
+ {
20717
+ kind: "turn_owner",
20718
+ receipt: this.turnReceipt(params.threadId, params.turn.id),
20719
+ nativeTurnId: params.turn.id
20720
+ }
20711
20721
  ];
20712
20722
  case "item/reasoning/textDelta":
20713
20723
  case "item/reasoning/summaryTextDelta":
20714
- return [{ kind: "thinking", text: params?.delta ?? "" }];
20724
+ return [{ kind: "assistant_reasoning_delta", text: params?.delta ?? "" }];
20715
20725
  case "item/agentMessage/delta":
20716
- return [{ kind: "text", text: params?.delta ?? "" }];
20726
+ return [{ kind: "assistant_message_delta", text: params?.delta ?? "" }];
20717
20727
  case "item/started":
20718
20728
  return this.handleItemStarted(params);
20719
20729
  case "item/completed":
@@ -20741,7 +20751,10 @@ class CodexEventNormalizer {
20741
20751
  }
20742
20752
  return [{ kind: "turn_end", sessionId: this.threadId ?? undefined, turnOwner: this.turnReceipt(params.threadId, params.turn.id) }];
20743
20753
  case "error":
20744
- return [{ kind: "error", message: params?.message ?? "Codex error" }];
20754
+ if (params?.willRetry === true) {
20755
+ return [{ kind: "runtime_recovery", stage: "retrying", source: "codex_stream" }];
20756
+ }
20757
+ return [{ kind: "error", message: params?.error?.message ?? params?.message ?? "Codex error" }];
20745
20758
  case "thread/tokenUsage/updated":
20746
20759
  case "account/rateLimits/updated":
20747
20760
  return mapCodexTelemetry(method, params);
@@ -20834,9 +20847,9 @@ class CodexEventNormalizer {
20834
20847
  case "collabAgentToolCall":
20835
20848
  return [{ kind: "tool_output", name: "collab_tool_call" }];
20836
20849
  case "agentMessage":
20837
- return [{ kind: "text", text: params?.item?.text ?? "" }];
20850
+ return [{ kind: "assistant_message_completed", text: params?.item?.text ?? "" }];
20838
20851
  case "reasoning":
20839
- return [{ kind: "thinking", text: params?.item?.text ?? "" }];
20852
+ return [{ kind: "assistant_reasoning_completed", text: params?.item?.text ?? "" }];
20840
20853
  default:
20841
20854
  return [];
20842
20855
  }
@@ -20869,7 +20882,13 @@ class CodexDriver {
20869
20882
  lifetime: "session",
20870
20883
  transport: { kind: "stdio_rpc", protocol: "codex.app-server.v1" },
20871
20884
  wakeStart: "immediate",
20872
- terminalOwnership: "transport_request"
20885
+ terminalOwnership: "transport_request",
20886
+ turnSilence: {
20887
+ nativeIdleTimeoutMs: 300000,
20888
+ daemonGraceMs: 60000,
20889
+ recoveryGraceMs: 60000,
20890
+ maxRecoveryExtensions: 1
20891
+ }
20873
20892
  };
20874
20893
  eventNormalizer = new CodexEventNormalizer;
20875
20894
  requestId = 0;
@@ -21526,14 +21545,14 @@ class CursorAcpLane {
21526
21545
  case "agent_message_chunk": {
21527
21546
  const content = record2(update.content);
21528
21547
  if (content?.type === "text" && typeof content.text === "string") {
21529
- this.events.emit("runtime_event", { kind: "text", text: content.text });
21548
+ this.events.emit("runtime_event", { kind: "assistant_message_delta", text: content.text });
21530
21549
  }
21531
21550
  return;
21532
21551
  }
21533
21552
  case "agent_thought_chunk": {
21534
21553
  const content = record2(update.content);
21535
21554
  if (content?.type === "text" && typeof content.text === "string") {
21536
- this.events.emit("runtime_event", { kind: "thinking", text: content.text });
21555
+ this.events.emit("runtime_event", { kind: "assistant_reasoning_delta", text: content.text });
21537
21556
  }
21538
21557
  return;
21539
21558
  }
@@ -22338,16 +22357,20 @@ class OpenCodeServiceLane {
22338
22357
  }
22339
22358
  switch (event.type) {
22340
22359
  case "session.next.step.started":
22341
- this.events.emit("runtime_event", { kind: "thinking", text: "" });
22360
+ this.events.emit("runtime_event", {
22361
+ kind: "internal_progress",
22362
+ source: "opencode.service",
22363
+ itemType: "step_started"
22364
+ });
22342
22365
  break;
22343
22366
  case "session.next.text.ended":
22344
22367
  if (typeof data.text === "string" && data.text.length > 0) {
22345
- this.events.emit("runtime_event", { kind: "text", text: data.text });
22368
+ this.events.emit("runtime_event", { kind: "assistant_message_completed", text: data.text });
22346
22369
  }
22347
22370
  break;
22348
22371
  case "session.next.reasoning.ended":
22349
22372
  if (typeof data.text === "string" && data.text.length > 0) {
22350
- this.events.emit("runtime_event", { kind: "thinking", text: data.text });
22373
+ this.events.emit("runtime_event", { kind: "assistant_reasoning_completed", text: data.text });
22351
22374
  }
22352
22375
  break;
22353
22376
  case "session.next.tool.called":
@@ -23098,22 +23121,28 @@ function readPiSdkVersion() {
23098
23121
  }
23099
23122
  function mapPiSdkEvent(event, sessionId, state) {
23100
23123
  if (event?.type === "message_update") {
23101
- const d = event.delta ?? {};
23124
+ const d = event.assistantMessageEvent ?? {};
23102
23125
  switch (d.type) {
23103
23126
  case "thinking_delta":
23104
- return [{ kind: "thinking", text: d.delta ?? "" }];
23127
+ return [{ kind: "assistant_reasoning_delta", text: d.delta ?? "" }];
23105
23128
  case "text_delta":
23106
23129
  state.sawTextDelta = true;
23107
- return [{ kind: "text", text: d.delta ?? "" }];
23108
- case "text_end":
23109
- return state.sawTextDelta ? [] : [{ kind: "text", text: d.content ?? "" }];
23130
+ return [{ kind: "assistant_message_delta", text: d.delta ?? "" }];
23131
+ case "text_end": {
23132
+ state.sawTextDelta = false;
23133
+ return [{ kind: "assistant_message_completed", text: d.content ?? "" }];
23134
+ }
23110
23135
  case "error":
23111
- return [{ kind: "error", message: d.message ?? "Pi error" }];
23136
+ return [{ kind: "error", message: d.error?.errorMessage ?? "Pi error" }];
23112
23137
  default:
23113
23138
  return [];
23114
23139
  }
23115
23140
  }
23116
23141
  switch (event?.type) {
23142
+ case "auto_retry_start":
23143
+ return [{ kind: "runtime_recovery", stage: "retrying", source: "pi_auto_retry" }];
23144
+ case "auto_retry_end":
23145
+ return [{ kind: "runtime_recovery", stage: "recovered", source: "pi_auto_retry" }];
23117
23146
  case "tool_execution_start":
23118
23147
  return [{ kind: "tool_call", name: event.toolName ?? "unknown_tool", input: event.args ?? {} }];
23119
23148
  case "tool_execution_end":
@@ -23278,6 +23307,13 @@ function assertAdapterCompatibility(registrationId, registeredCapabilities, adap
23278
23307
  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))) {
23279
23308
  throw new Error(`Adapter ${registrationId} has an invalid transport declaration`);
23280
23309
  }
23310
+ if (execution.turnSilence !== undefined) {
23311
+ const silence = execution.turnSilence;
23312
+ const safeInteger = (value, minimum) => typeof value === "number" && Number.isSafeInteger(value) && value >= minimum;
23313
+ 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)) {
23314
+ throw new Error(`Adapter ${registrationId} has an invalid turnSilence declaration`);
23315
+ }
23316
+ }
23281
23317
  const capabilities = registeredCapabilities;
23282
23318
  const declaredLifetime = capabilities?.sessionLifetime === "persistent" ? "session" : "turn";
23283
23319
  if (execution.lifetime !== declaredLifetime) {
@@ -23367,6 +23403,12 @@ function capabilitiesFor(backend) {
23367
23403
  return builtinRegistry.get(backend).capabilities;
23368
23404
  }
23369
23405
 
23406
+ // agent-driver/dist/internal/adapter.js
23407
+ var DEFAULT_NATIVE_IDLE_TIMEOUT_MS = 300000;
23408
+ var DEFAULT_DAEMON_GRACE_MS = 60000;
23409
+ var DEFAULT_RECOVERY_GRACE_MS = 60000;
23410
+ var DEFAULT_MAX_RECOVERY_EXTENSIONS = 1;
23411
+
23370
23412
  // agent-driver/dist/controller/event-queue.js
23371
23413
  var MAX_BUFFERED_BYTES = 4194304;
23372
23414
 
@@ -23563,6 +23605,62 @@ function stableErrorCode(value, fallback) {
23563
23605
 
23564
23606
  // agent-driver/dist/controller/logical-session.js
23565
23607
  import { mkdirSync as mkdirSync4 } from "node:fs";
23608
+ var SEMANTIC_ASSEMBLER_MAX_BYTES = 1048576;
23609
+ var WORK_HEARTBEAT_MIN_INTERVAL_MS = 1000;
23610
+ function emptySemanticAssembler() {
23611
+ return { chunks: [], bytes: 0, truncated: false };
23612
+ }
23613
+ function utf8Prefix(text2, maxBytes) {
23614
+ if (maxBytes <= 0 || text2.length === 0)
23615
+ return "";
23616
+ if (Buffer.byteLength(text2, "utf8") <= maxBytes)
23617
+ return text2;
23618
+ let low = 0;
23619
+ let high = text2.length;
23620
+ while (low < high) {
23621
+ const mid = Math.ceil((low + high) / 2);
23622
+ let end2 = mid;
23623
+ const code2 = text2.charCodeAt(end2 - 1);
23624
+ if (code2 >= 55296 && code2 <= 56319)
23625
+ end2 -= 1;
23626
+ if (Buffer.byteLength(text2.slice(0, end2), "utf8") <= maxBytes)
23627
+ low = mid;
23628
+ else
23629
+ high = mid - 1;
23630
+ }
23631
+ let end = low;
23632
+ const code = text2.charCodeAt(end - 1);
23633
+ if (code >= 55296 && code <= 56319)
23634
+ end -= 1;
23635
+ while (end > 0 && Buffer.byteLength(text2.slice(0, end), "utf8") > maxBytes)
23636
+ end -= 1;
23637
+ return text2.slice(0, end);
23638
+ }
23639
+ function appendSemanticFragment(buffer, text2) {
23640
+ if (text2.length === 0 || buffer.truncated)
23641
+ return;
23642
+ const remaining = SEMANTIC_ASSEMBLER_MAX_BYTES - buffer.bytes;
23643
+ const bytes = Buffer.byteLength(text2, "utf8");
23644
+ if (bytes <= remaining) {
23645
+ buffer.chunks.push(text2);
23646
+ buffer.bytes += bytes;
23647
+ return;
23648
+ }
23649
+ const prefix = utf8Prefix(text2, remaining);
23650
+ if (prefix.length > 0) {
23651
+ buffer.chunks.push(prefix);
23652
+ buffer.bytes += Buffer.byteLength(prefix, "utf8");
23653
+ }
23654
+ buffer.truncated = true;
23655
+ }
23656
+ function finishSemanticAssembler(buffer) {
23657
+ return { text: buffer.chunks.join(""), truncated: buffer.truncated };
23658
+ }
23659
+ function boundedSemanticCompletion(text2) {
23660
+ const buffer = emptySemanticAssembler();
23661
+ appendSemanticFragment(buffer, text2);
23662
+ return finishSemanticAssembler(buffer);
23663
+ }
23566
23664
  function driverError(category, code, message2, retryable = false) {
23567
23665
  return { category, code, message: scrubDriverErrorMessage(message2), retryable };
23568
23666
  }
@@ -23636,6 +23734,7 @@ class LogicalAgentSession {
23636
23734
  resumeOutcome;
23637
23735
  eventQueue;
23638
23736
  behavior;
23737
+ turnSilence;
23639
23738
  constructor(backend, config2, launch, adapter, capabilities2, host, prepared, hostReleaseTimeoutMs) {
23640
23739
  this.backend = backend;
23641
23740
  this.config = config2;
@@ -23646,6 +23745,18 @@ class LogicalAgentSession {
23646
23745
  this.hostReleaseTimeoutMs = hostReleaseTimeoutMs;
23647
23746
  this.capabilities = capabilities2;
23648
23747
  this.behavior = this.capabilities;
23748
+ const declaredSilence = adapter.execution.turnSilence;
23749
+ const nativeIdleTimeoutMs = declaredSilence?.nativeIdleTimeoutMs ?? DEFAULT_NATIVE_IDLE_TIMEOUT_MS;
23750
+ const daemonGraceMs = declaredSilence?.daemonGraceMs ?? DEFAULT_DAEMON_GRACE_MS;
23751
+ const recoveryGraceMs = declaredSilence?.recoveryGraceMs ?? DEFAULT_RECOVERY_GRACE_MS;
23752
+ const maxRecoveryExtensions = declaredSilence?.maxRecoveryExtensions ?? DEFAULT_MAX_RECOVERY_EXTENSIONS;
23753
+ this.turnSilence = {
23754
+ nativeIdleTimeoutMs,
23755
+ daemonGraceMs,
23756
+ recoveryGraceMs,
23757
+ maxRecoveryExtensions,
23758
+ normalBudgetMs: nativeIdleTimeoutMs + daemonGraceMs
23759
+ };
23649
23760
  this.resumeOutcome = launch.resumeSessionId ? "pending" : "not_requested";
23650
23761
  this.sessionInstanceId = host.createId();
23651
23762
  this.closed = new Promise((resolve3) => {
@@ -23750,6 +23861,7 @@ class LogicalAgentSession {
23750
23861
  lastEventSequence: this.eventSequence,
23751
23862
  diagnostics: {
23752
23863
  deliveryPhase: this.deliveryPhase(),
23864
+ turnSilence: this.turnSilence,
23753
23865
  metrics: {
23754
23866
  physicalOpenCount: this.metricValue(this.physicalOpenCount),
23755
23867
  turnCount: this.metricValue(this.turnCount),
@@ -23840,7 +23952,14 @@ class LogicalAgentSession {
23840
23952
  const turnId = this.host.createId();
23841
23953
  const commandIds = messages.map((message2) => message2.id);
23842
23954
  const terminalOwner = this.adapter.beginTurn?.();
23843
- this.activeTurn = { turnId, commandIds: [...commandIds], ...terminalOwner ? { terminalOwner } : {} };
23955
+ this.activeTurn = {
23956
+ turnId,
23957
+ commandIds: [...commandIds],
23958
+ ...terminalOwner ? { terminalOwner } : {},
23959
+ pendingMessage: emptySemanticAssembler(),
23960
+ pendingReasoning: emptySemanticAssembler(),
23961
+ lastWorkHeartbeatAt: null
23962
+ };
23844
23963
  this.turnError = undefined;
23845
23964
  this.interruptedTurnId = undefined;
23846
23965
  this.processTurnEnded = false;
@@ -24038,14 +24157,36 @@ class LogicalAgentSession {
24038
24157
  return;
24039
24158
  }
24040
24159
  this.activeTurn.terminalOwner = event.receipt;
24160
+ const nativeTurnId = event.nativeTurnId?.trim();
24161
+ if (nativeTurnId && nativeTurnId.length <= 512 && /^[A-Za-z0-9._:-]+$/.test(nativeTurnId)) {
24162
+ this.emit({
24163
+ type: "backend_turn_started",
24164
+ turnId: this.activeTurn.turnId,
24165
+ backendTurnId: nativeTurnId
24166
+ });
24167
+ }
24041
24168
  return;
24042
- case "thinking":
24043
- if (turnId)
24044
- this.emit({ type: "thinking_delta", turnId, text: event.text });
24169
+ case "assistant_reasoning_delta":
24170
+ case "assistant_message_delta":
24171
+ if (turnId && this.activeTurn?.turnId === turnId && event.text.length > 0) {
24172
+ appendSemanticFragment(event.kind === "assistant_message_delta" ? this.activeTurn.pendingMessage : this.activeTurn.pendingReasoning, event.text);
24173
+ const now = this.host.now();
24174
+ if (this.activeTurn.lastWorkHeartbeatAt === null || now - this.activeTurn.lastWorkHeartbeatAt >= WORK_HEARTBEAT_MIN_INTERVAL_MS) {
24175
+ this.activeTurn.lastWorkHeartbeatAt = now;
24176
+ this.emit({ type: "work_heartbeat", turnId });
24177
+ }
24178
+ }
24045
24179
  return;
24046
- case "text":
24047
- if (turnId)
24048
- this.emit({ type: "text_delta", turnId, text: event.text });
24180
+ case "assistant_reasoning_completed":
24181
+ case "assistant_message_completed":
24182
+ if (turnId && this.activeTurn?.turnId === turnId) {
24183
+ const field = event.kind === "assistant_message_completed" ? "pendingMessage" : "pendingReasoning";
24184
+ this.activeTurn[field] = emptySemanticAssembler();
24185
+ if (event.text.length > 0) {
24186
+ const completed = boundedSemanticCompletion(event.text);
24187
+ this.emit({ type: event.kind, turnId, ...completed });
24188
+ }
24189
+ }
24049
24190
  return;
24050
24191
  case "tool_call":
24051
24192
  this.outstandingToolUses += 1;
@@ -24088,9 +24229,19 @@ class LogicalAgentSession {
24088
24229
  message: scrubDriverErrorMessage(event.message, "Runtime diagnostic")
24089
24230
  });
24090
24231
  return;
24232
+ case "runtime_recovery":
24233
+ this.emit({
24234
+ type: "recovery",
24235
+ turnId,
24236
+ stage: event.stage,
24237
+ source: event.source
24238
+ });
24239
+ return;
24091
24240
  case "runtime_metric":
24092
- if (event.name === "sse_reconnect" && event.increment === 1)
24241
+ if (event.name === "sse_reconnect" && event.increment === 1) {
24093
24242
  this.sseReconnectCount += 1;
24243
+ this.emit({ type: "recovery", turnId, stage: "retrying", source: "transport_reconnect" });
24244
+ }
24094
24245
  return;
24095
24246
  case "telemetry": {
24096
24247
  const details = jsonValue(event.attrs);
@@ -24113,6 +24264,18 @@ class LogicalAgentSession {
24113
24264
  });
24114
24265
  return;
24115
24266
  case "turn_end":
24267
+ if (turnId && this.activeTurn?.turnId === turnId) {
24268
+ const reasoning = finishSemanticAssembler(this.activeTurn.pendingReasoning);
24269
+ const message2 = finishSemanticAssembler(this.activeTurn.pendingMessage);
24270
+ this.activeTurn.pendingReasoning = emptySemanticAssembler();
24271
+ this.activeTurn.pendingMessage = emptySemanticAssembler();
24272
+ if (reasoning.text.length > 0) {
24273
+ this.emit({ type: "assistant_reasoning_completed", turnId, ...reasoning });
24274
+ }
24275
+ if (message2.text.length > 0) {
24276
+ this.emit({ type: "assistant_message_completed", turnId, ...message2 });
24277
+ }
24278
+ }
24116
24279
  this.completeTurn(event.sessionId, physicalOwner, generation, event.turnOwner);
24117
24280
  return;
24118
24281
  }
@@ -24126,7 +24289,7 @@ class LogicalAgentSession {
24126
24289
  reopenClosedLaneForWork(event, physicalOwner, generation) {
24127
24290
  if (this.adapter.execution.lifetime === "turn")
24128
24291
  return;
24129
- 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";
24292
+ 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";
24130
24293
  if (!isRootWork)
24131
24294
  return;
24132
24295
  const tombstone = this.closedLaneTombstone;
@@ -24136,7 +24299,10 @@ class LogicalAgentSession {
24136
24299
  this.activeTurn = {
24137
24300
  turnId: tombstone.localTurnId,
24138
24301
  commandIds: tombstone.commandIds,
24139
- ...tombstone.terminalOwner ? { terminalOwner: tombstone.terminalOwner } : {}
24302
+ ...tombstone.terminalOwner ? { terminalOwner: tombstone.terminalOwner } : {},
24303
+ pendingMessage: emptySemanticAssembler(),
24304
+ pendingReasoning: emptySemanticAssembler(),
24305
+ lastWorkHeartbeatAt: null
24140
24306
  };
24141
24307
  this.state = "working";
24142
24308
  return tombstone.localTurnId;
@@ -25980,7 +26146,7 @@ function parseLocalMessageReminderBody(body, agentId) {
25980
26146
  return null;
25981
26147
  if (!Number.isSafeInteger(record4.sentSeq) || record4.sentSeq < 1)
25982
26148
  return null;
25983
- if (!Number.isSafeInteger(record4.remindAfterMs) || record4.remindAfterMs < LOCAL_MESSAGE_REMINDER_MIN_MS || record4.remindAfterMs > LOCAL_MESSAGE_REMINDER_MAX_MS)
26149
+ if (!Number.isSafeInteger(record4.remindAfterMs) || record4.remindAfterMs !== 0 && record4.remindAfterMs < LOCAL_MESSAGE_REMINDER_MIN_MS || record4.remindAfterMs > LOCAL_MESSAGE_REMINDER_MAX_MS)
25984
26150
  return null;
25985
26151
  return {
25986
26152
  agentId,
@@ -26239,11 +26405,26 @@ function isActivelyWorking(agent2) {
26239
26405
  return agent2.status === "running" && (leaseIsWorking(agent2.execution.lease) || agent2.pendingAdmissions.length > 0 || agent2.inbox.length > 0);
26240
26406
  }
26241
26407
  var DEFAULT_STALE_THRESHOLD_MS = 120000;
26242
- var DEFAULT_IDLE_TIMEOUT_MS = 300000;
26408
+ var DEFAULT_TURN_SILENCE_POLICY = {
26409
+ nativeIdleTimeoutMs: 300000,
26410
+ daemonGraceMs: 60000,
26411
+ recoveryGraceMs: 60000,
26412
+ maxRecoveryExtensions: 1,
26413
+ normalBudgetMs: 360000
26414
+ };
26415
+ var DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
26416
+ var DEFAULT_IDLE_RESET_TIMEOUT_MS = 6 * 60 * 60 * 1000;
26243
26417
  var DEFAULT_RESET_STUCK_THRESHOLD_MS = 120000;
26244
26418
  var DEFAULT_STOPPING_STUCK_THRESHOLD_MS = 30000;
26245
- function createInitialManagerState(staleThresholdMs = DEFAULT_STALE_THRESHOLD_MS, idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS, resetStuckThresholdMs = DEFAULT_RESET_STUCK_THRESHOLD_MS, stoppingStuckThresholdMs = DEFAULT_STOPPING_STUCK_THRESHOLD_MS) {
26246
- return { agents: {}, staleThresholdMs, idleTimeoutMs, resetStuckThresholdMs, stoppingStuckThresholdMs };
26419
+ 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) {
26420
+ return {
26421
+ agents: {},
26422
+ staleThresholdMs,
26423
+ idleTimeoutMs,
26424
+ idleResetTimeoutMs,
26425
+ resetStuckThresholdMs,
26426
+ stoppingStuckThresholdMs
26427
+ };
26247
26428
  }
26248
26429
  function reduceManager(state, event) {
26249
26430
  switch (event.type) {
@@ -26258,6 +26439,11 @@ function reduceManager(state, event) {
26258
26439
  });
26259
26440
  case "backend_session":
26260
26441
  return mutate(state, event.agentId, (a) => {
26442
+ if (event.stalledBefore) {
26443
+ a.stalledSessionId = event.sessionId;
26444
+ } else if (a.stalledSessionId !== null && a.stalledSessionId !== event.sessionId) {
26445
+ a.stalledSessionId = null;
26446
+ }
26261
26447
  a.sessionId = event.sessionId;
26262
26448
  });
26263
26449
  case "attach_session": {
@@ -26270,7 +26456,17 @@ function reduceManager(state, event) {
26270
26456
  {
26271
26457
  const a = agent2;
26272
26458
  a.execution = { sessionInstanceId: event.sessionInstanceId, lease: { state: "none", lastTerminal: null } };
26459
+ a.turnSilence = event.turnSilence ?? {
26460
+ ...DEFAULT_TURN_SILENCE_POLICY,
26461
+ nativeIdleTimeoutMs: state.staleThresholdMs,
26462
+ daemonGraceMs: 0,
26463
+ normalBudgetMs: state.staleThresholdMs
26464
+ };
26273
26465
  a.lastProgressAt = event.nowMs;
26466
+ a.lastNativeActivityAt = event.nowMs;
26467
+ a.lastNativeActivityKind = null;
26468
+ a.runtimePhase = "admission";
26469
+ a.backendTurnId = null;
26274
26470
  a.idleSince = null;
26275
26471
  syncExecutionProjection(a);
26276
26472
  }
@@ -26326,7 +26522,25 @@ function reduceManager(state, event) {
26326
26522
  return { state, effects: [] };
26327
26523
  return mutate(state, event.agentId, (a) => {
26328
26524
  a.sessionId = null;
26525
+ a.stalledSessionId = null;
26526
+ a.idleSince = null;
26329
26527
  });
26528
+ case "idle_reset_committed": {
26529
+ const existing = state.agents[event.agentId];
26530
+ if (!existing)
26531
+ return { state, effects: [] };
26532
+ const agent2 = clone2(existing);
26533
+ agent2.sessionId = null;
26534
+ agent2.stalledSessionId = null;
26535
+ agent2.idleSince = null;
26536
+ if (agent2.status !== "running")
26537
+ return commit(state, agent2, []);
26538
+ agent2.status = "stopping";
26539
+ agent2.stoppingSince = event.nowMs;
26540
+ return commit(state, agent2, [
26541
+ { type: "stop", agentId: event.agentId, reason: "idle_session_reset" }
26542
+ ]);
26543
+ }
26330
26544
  case "begin_reset":
26331
26545
  if (!state.agents[event.agentId])
26332
26546
  return { state, effects: [] };
@@ -26364,8 +26578,18 @@ function reduceManager(state, event) {
26364
26578
  return;
26365
26579
  const startedCommands = new Set(event.commandIds);
26366
26580
  a.pendingAdmissions = a.pendingAdmissions.filter((entry) => !startedCommands.has(entry.commandId));
26367
- a.execution.lease = { state: "active", identity, lastWorkAt: event.nowMs };
26581
+ a.execution.lease = {
26582
+ state: "active",
26583
+ identity,
26584
+ lastWorkAt: event.nowMs,
26585
+ nativeDeadlineAt: event.nowMs + a.turnSilence.normalBudgetMs,
26586
+ recoveryExtensionsUsed: 0
26587
+ };
26368
26588
  a.lastProgressAt = event.nowMs;
26589
+ a.lastNativeActivityAt = event.nowMs;
26590
+ a.lastNativeActivityKind = "turn_started";
26591
+ a.runtimePhase = "inference";
26592
+ a.backendTurnId = null;
26369
26593
  a.idleSince = null;
26370
26594
  syncExecutionProjection(a);
26371
26595
  });
@@ -26386,7 +26610,12 @@ function reduceManager(state, event) {
26386
26610
  const lease = a.execution.lease;
26387
26611
  const identity = identityOf(event);
26388
26612
  if ((lease.state === "active" || lease.state === "suspect_active") && sameIdentity(lease.identity, identity)) {
26389
- a.execution.lease = { ...lease, lastWorkAt: event.nowMs };
26613
+ a.execution.lease = {
26614
+ ...lease,
26615
+ lastWorkAt: event.nowMs,
26616
+ nativeDeadlineAt: event.nowMs + a.turnSilence.normalBudgetMs,
26617
+ recoveryExtensionsUsed: 0
26618
+ };
26390
26619
  } else {
26391
26620
  const terminal = lease.state === "none" ? lease.lastTerminal : null;
26392
26621
  if (!terminal || !sameIdentity(terminal.identity, identity))
@@ -26395,6 +26624,8 @@ function reduceManager(state, event) {
26395
26624
  state: "suspect_active",
26396
26625
  identity,
26397
26626
  lastWorkAt: event.nowMs,
26627
+ nativeDeadlineAt: event.nowMs + a.turnSilence.normalBudgetMs,
26628
+ recoveryExtensionsUsed: 0,
26398
26629
  reason: "work_after_terminal"
26399
26630
  };
26400
26631
  }
@@ -26408,7 +26639,7 @@ function reduceManager(state, event) {
26408
26639
  case "turn_tool_finished":
26409
26640
  return onTurnToolLifecycle(state, event, "finished");
26410
26641
  case "turn_completed":
26411
- return onTurnCompleted(state, event.agentId, event.sessionInstanceId, event.nowMs, event.turnId);
26642
+ return onTurnCompleted(state, event.agentId, event.sessionInstanceId, event.nowMs, event.turnId, event.endReason);
26412
26643
  case "session_closed":
26413
26644
  if (state.agents[event.agentId]?.execution.sessionInstanceId !== event.sessionInstanceId) {
26414
26645
  return { state, effects: [] };
@@ -26418,7 +26649,6 @@ function reduceManager(state, event) {
26418
26649
  const closing = agent2.pendingAdmissions.filter((entry) => entry.sessionInstanceId === event.sessionInstanceId);
26419
26650
  agent2.pendingAdmissions = agent2.pendingAdmissions.filter((entry) => entry.sessionInstanceId !== event.sessionInstanceId);
26420
26651
  agent2.execution = { sessionInstanceId: null, lease: { state: "detached" } };
26421
- agent2.idleSince = null;
26422
26652
  syncExecutionProjection(agent2);
26423
26653
  return commit(state, agent2, recoveryEffects(agent2, closing));
26424
26654
  }
@@ -26426,8 +26656,57 @@ function reduceManager(state, event) {
26426
26656
  return onExit(state, event.agentId);
26427
26657
  case "tick":
26428
26658
  return onTick(state, event.nowMs);
26429
- case "runtime_signal":
26430
- return { state, effects: [] };
26659
+ case "runtime_signal": {
26660
+ const existing = state.agents[event.agentId];
26661
+ if (!existing || existing.execution.sessionInstanceId !== event.sessionInstanceId)
26662
+ return { state, effects: [] };
26663
+ const lease = existing.execution.lease;
26664
+ const identity = { sessionInstanceId: event.sessionInstanceId, turnId: event.turnId };
26665
+ if (lease.state !== "active" && lease.state !== "suspect_active" || !sameIdentity(lease.identity, identity)) {
26666
+ return { state, effects: [] };
26667
+ }
26668
+ return mutate(state, event.agentId, (a) => {
26669
+ const active = a.execution.lease;
26670
+ if (active.state !== "active" && active.state !== "suspect_active" || !sameIdentity(active.identity, identity))
26671
+ return;
26672
+ if (event.kind === "recovery" && event.recoveryStage !== "recovered") {
26673
+ if (active.recoveryExtensionsUsed < a.turnSilence.maxRecoveryExtensions) {
26674
+ a.execution.lease = {
26675
+ ...active,
26676
+ nativeDeadlineAt: Math.max(active.nativeDeadlineAt, event.nowMs + a.turnSilence.recoveryGraceMs),
26677
+ recoveryExtensionsUsed: active.recoveryExtensionsUsed + 1
26678
+ };
26679
+ }
26680
+ } else {
26681
+ a.execution.lease = {
26682
+ ...active,
26683
+ nativeDeadlineAt: event.nowMs + a.turnSilence.normalBudgetMs
26684
+ };
26685
+ }
26686
+ a.lastNativeActivityAt = event.nowMs;
26687
+ a.lastNativeActivityKind = event.kind;
26688
+ a.runtimePhase = event.phase;
26689
+ if (event.backendTurnId)
26690
+ a.backendTurnId = event.backendTurnId;
26691
+ });
26692
+ }
26693
+ case "stall_control_failed":
26694
+ return mutate(state, event.agentId, (a) => {
26695
+ if (event.transition === "clear") {
26696
+ if (a.sessionId === event.sessionId)
26697
+ a.stalledSessionId = event.sessionId;
26698
+ return;
26699
+ }
26700
+ if (a.status !== "stopping")
26701
+ return;
26702
+ const lease = a.execution.lease;
26703
+ if (lease.state !== "active" && lease.state !== "suspect_active")
26704
+ return;
26705
+ a.status = "running";
26706
+ a.stoppingSince = null;
26707
+ a.sessionId = event.sessionId;
26708
+ a.stalledSessionId = event.transition === "fence" ? event.sessionId : null;
26709
+ });
26431
26710
  case "delivery_rejected":
26432
26711
  return mutate(state, event.agentId, (a) => {
26433
26712
  if (!a.inbox.some((message2) => message2.id === event.message.id)) {
@@ -26465,7 +26744,7 @@ function onWake(state, agentId, message2) {
26465
26744
  }
26466
26745
  return commit(state, agent2, []);
26467
26746
  }
26468
- function onTurnCompleted(state, agentId, sessionInstanceId, nowMs, turnId) {
26747
+ function onTurnCompleted(state, agentId, sessionInstanceId, nowMs, turnId, endReason) {
26469
26748
  const existing = state.agents[agentId];
26470
26749
  if (!existing)
26471
26750
  return { state, effects: [] };
@@ -26481,13 +26760,23 @@ function onTurnCompleted(state, agentId, sessionInstanceId, nowMs, turnId) {
26481
26760
  const lastTerminal = { identity, at: nowMs };
26482
26761
  agent2.execution.lease = { state: "none", lastTerminal };
26483
26762
  agent2.lastProgressAt = nowMs;
26763
+ agent2.lastNativeActivityAt = nowMs;
26764
+ agent2.lastNativeActivityKind = "turn_end";
26765
+ agent2.runtimePhase = "terminal";
26766
+ const clearedStallSessionId = endReason === undefined ? agent2.stalledSessionId : null;
26767
+ if (clearedStallSessionId !== null)
26768
+ agent2.stalledSessionId = null;
26484
26769
  syncExecutionProjection(agent2);
26770
+ const clearEffects = clearedStallSessionId === null ? [] : [{ type: "clear_stall_recovery", agentId, sessionId: clearedStallSessionId }];
26485
26771
  if (agent2.inbox.length > 0) {
26486
26772
  const messages = drainInbox(agent2);
26487
- return commit(state, agent2, messages.map((queued) => ({ type: "send", agentId, message: queued, mode: "idle" })));
26773
+ return commit(state, agent2, [
26774
+ ...clearEffects,
26775
+ ...messages.map((queued) => ({ type: "send", agentId, message: queued, mode: "idle" }))
26776
+ ]);
26488
26777
  }
26489
26778
  agent2.idleSince = nowMs;
26490
- return commit(state, agent2, []);
26779
+ return commit(state, agent2, clearEffects);
26491
26780
  }
26492
26781
  function onTurnToolLifecycle(state, event, lifecycle) {
26493
26782
  const existing = state.agents[event.agentId];
@@ -26509,11 +26798,22 @@ function onTurnToolLifecycle(state, event, lifecycle) {
26509
26798
  if ((lease.state === "active" || lease.state === "suspect_active") && sameIdentity(lease.identity, identity)) {
26510
26799
  const outstandingToolUses = (lease.outstandingToolUses ?? 0) + (lifecycle === "started" ? 1 : -1);
26511
26800
  if (outstandingToolUses > 0) {
26512
- agent2.execution.lease = { ...lease, lastWorkAt: event.nowMs, outstandingToolUses };
26801
+ agent2.execution.lease = {
26802
+ ...lease,
26803
+ lastWorkAt: event.nowMs,
26804
+ nativeDeadlineAt: event.nowMs + agent2.turnSilence.normalBudgetMs,
26805
+ recoveryExtensionsUsed: 0,
26806
+ outstandingToolUses
26807
+ };
26513
26808
  } else {
26514
26809
  const unblockedLease = { ...lease };
26515
26810
  delete unblockedLease.outstandingToolUses;
26516
- agent2.execution.lease = { ...unblockedLease, lastWorkAt: event.nowMs };
26811
+ agent2.execution.lease = {
26812
+ ...unblockedLease,
26813
+ lastWorkAt: event.nowMs,
26814
+ nativeDeadlineAt: event.nowMs + agent2.turnSilence.normalBudgetMs,
26815
+ recoveryExtensionsUsed: 0
26816
+ };
26517
26817
  }
26518
26818
  } else {
26519
26819
  const terminal = lease.state === "none" ? lease.lastTerminal : null;
@@ -26523,11 +26823,16 @@ function onTurnToolLifecycle(state, event, lifecycle) {
26523
26823
  state: "suspect_active",
26524
26824
  identity,
26525
26825
  lastWorkAt: event.nowMs,
26826
+ nativeDeadlineAt: event.nowMs + agent2.turnSilence.normalBudgetMs,
26827
+ recoveryExtensionsUsed: 0,
26526
26828
  outstandingToolUses: 1,
26527
26829
  reason: "work_after_terminal"
26528
26830
  };
26529
26831
  }
26530
26832
  agent2.lastProgressAt = event.nowMs;
26833
+ agent2.lastNativeActivityAt = event.nowMs;
26834
+ agent2.lastNativeActivityKind = lifecycle === "started" ? "tool_call" : "tool_output";
26835
+ agent2.runtimePhase = lifecycle === "started" ? "tool" : "inference";
26531
26836
  agent2.idleSince = null;
26532
26837
  syncExecutionProjection(agent2);
26533
26838
  });
@@ -26540,6 +26845,8 @@ function onExit(state, agentId) {
26540
26845
  const effects = recoveryEffects(agent2, agent2.pendingAdmissions);
26541
26846
  agent2.pendingAdmissions = [];
26542
26847
  agent2.execution = { sessionInstanceId: null, lease: { state: "detached" } };
26848
+ agent2.runtimePhase = "idle";
26849
+ agent2.backendTurnId = null;
26543
26850
  agent2.stoppingSince = null;
26544
26851
  syncExecutionProjection(agent2);
26545
26852
  if (agent2.inbox.length > 0) {
@@ -26559,10 +26866,19 @@ function onTick(state, nowMs) {
26559
26866
  for (const id of Object.keys(agents)) {
26560
26867
  const a = agents[id];
26561
26868
  const lease = a.execution.lease;
26562
- const stalled = a.status === "running" && (lease.state === "active" || lease.state === "suspect_active") && (lease.outstandingToolUses ?? 0) === 0 && nowMs - lease.lastWorkAt >= state.staleThresholdMs;
26869
+ 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;
26563
26870
  if (stalled) {
26564
- agents[id] = { ...a, status: "stopping", idleSince: null, stoppingSince: nowMs };
26565
- effects.push({ type: "terminate_stalled", agentId: id });
26871
+ const repeatedSessionStall = a.sessionId !== null && a.stalledSessionId === a.sessionId;
26872
+ const forgetSessionId = repeatedSessionStall ? a.sessionId : undefined;
26873
+ agents[id] = {
26874
+ ...a,
26875
+ status: "stopping",
26876
+ sessionId: repeatedSessionStall ? null : a.sessionId,
26877
+ stalledSessionId: repeatedSessionStall ? null : a.sessionId,
26878
+ idleSince: null,
26879
+ stoppingSince: nowMs
26880
+ };
26881
+ 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 });
26566
26882
  continue;
26567
26883
  }
26568
26884
  const expiredAdmission = a.status === "running" && a.pendingAdmissions.filter((entry) => !entry.driverAcknowledged && nowMs - entry.admittedAt >= state.staleThresholdMs);
@@ -26594,9 +26910,14 @@ function onTick(state, nowMs) {
26594
26910
  effects.push({ type: "force_exit", agentId: id, reason: "stopping_stuck" });
26595
26911
  continue;
26596
26912
  }
26913
+ 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);
26914
+ if (idleResetEligible && nowMs - a.idleSince >= state.idleResetTimeoutMs) {
26915
+ effects.push({ type: "reset_idle_session", agentId: id, sessionId: a.sessionId });
26916
+ continue;
26917
+ }
26597
26918
  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);
26598
26919
  if (idleEligible && a.idleSince !== null && nowMs - a.idleSince >= state.idleTimeoutMs) {
26599
- agents[id] = { ...a, status: "stopping", idleSince: null, stoppingSince: nowMs };
26920
+ agents[id] = { ...a, status: "stopping", stoppingSince: nowMs };
26600
26921
  effects.push({ type: "stop", agentId: id, reason: "idle_timeout" });
26601
26922
  }
26602
26923
  }
@@ -26608,11 +26929,17 @@ function freshAgent(agentId) {
26608
26929
  status: "idle",
26609
26930
  inbox: [],
26610
26931
  sessionId: null,
26932
+ stalledSessionId: null,
26611
26933
  execution: { sessionInstanceId: null, lease: { state: "detached" } },
26612
26934
  pendingAdmissions: [],
26613
26935
  turnId: null,
26614
26936
  turnActive: false,
26615
26937
  lastProgressAt: 0,
26938
+ lastNativeActivityAt: 0,
26939
+ lastNativeActivityKind: null,
26940
+ runtimePhase: "idle",
26941
+ backendTurnId: null,
26942
+ turnSilence: DEFAULT_TURN_SILENCE_POLICY,
26616
26943
  lastDeliverAt: null,
26617
26944
  idleSince: null,
26618
26945
  stoppingSince: null,
@@ -26708,6 +27035,16 @@ function scrubRuntimeErrorDiagnosticText(value) {
26708
27035
 
26709
27036
  // src/drivers/systemPrompt.ts
26710
27037
  var CLI = "$ALOOK_CLI";
27038
+ var MESSAGE_SEND_STDIN_POLICY = [
27039
+ "`--stdin` is required and limited to 1 KiB of UTF-8. Write it as social language a person " + "with ADHD can scan without effort:",
27040
+ "",
27041
+ "- Lead with the point, result, decision, or one concrete ask.",
27042
+ "- Keep one message to one topic; suppress tangents and repeated recap.",
27043
+ "- Use at most five short items when a list helps.",
27044
+ "- Drop preambles, play-by-play, and closing pleasantries.",
27045
+ "- Put exact plans, reviews, evidence, logs, and long technical detail in a Markdown attachment. " + "The message body carries only the short summary and next action."
27046
+ ].join(`
27047
+ `);
26711
27048
  function identitySection(config2) {
26712
27049
  const parts = ["## Identity", ""];
26713
27050
  const name = config2.agentName ?? "a member of the household";
@@ -26724,9 +27061,6 @@ function identitySection(config2) {
26724
27061
  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.");
26725
27062
  }
26726
27063
  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.");
26727
- if (config2.description) {
26728
- parts.push("", "### Role", "", config2.description, "", "A starting point, not a script. Capture how the role evolves in `./memory.md` " + "(the Role text above isn't editable directly).");
26729
- }
26730
27064
  return parts.join(`
26731
27065
  `);
26732
27066
  }
@@ -26738,9 +27072,9 @@ function cliCommandsSection() {
26738
27072
  "",
26739
27073
  "### Messaging",
26740
27074
  "",
26741
- `1. \`${CLI} inbox pull\` — fetch unread messages (advances your read waterline by default, ` + `so they won't re-pull; \`--no-ack\` to peek without advancing).`,
26742
- `2. \`${CLI} message send\` — send to a channel, DM, or thread. ` + `For a short body, use explicit \`--stdin\` with a quoted heredoc; for a long or complicated body, ` + `use \`--file <path>\`. Attach with \`--attachment <id>\` (repeatable, order matters). Optionally add ` + `\`--remind-after <duration>\` to be reminded if the same channel, thread, or DM receives no ` + `newer message after your send. Use a whole number of minutes or hours from \`1m\` to \`24h\`, ` + `such as \`15m\` or \`2h\`.`,
26743
- `3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a file; ` + `returns an id stable across pending→persisted. Feed it into ` + `\`message send --attachment <id>\`.`,
27075
+ `1. \`${CLI} inbox pull\` — fetch unread messages; \`--no-ack\` peeks without advancing.`,
27076
+ `2. \`${CLI} message send --target <ref> --remind-after <0|Nm|Nh> --stdin\` — send to a ` + `channel, DM, or thread. ` + `The body is required through \`--stdin\` and limited to 1 KiB of UTF-8. ` + `Attach uploaded files with \`--attachment <id>\` (repeatable, order matters). ` + `\`--remind-after\` accepts \`0\`, or a whole-number duration from \`1m\` to \`24h\`.`,
27077
+ `3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a file; ` + `returns an id stable across pending→persisted.`,
26744
27078
  `4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download any ` + `attachment you can see (or your own pending uploads).`,
26745
27079
  `5. \`${CLI} message emoji --target <ref> --emoji <e>\` — react with a single emoji. ` + `Works on channel messages (\`/<server>/<channel>#N\`), DM messages ` + `(\`/.dm/<peer>#N\`), and thread-reply messages (\`/<server>/<channel>/#N#M\`).`,
26746
27080
  `6. \`${CLI} message mark set --target <full-message-ref>\` — persist a message as outstanding work.`,
@@ -26770,7 +27104,7 @@ function cliCommandsSection() {
26770
27104
  "",
26771
27105
  "### Context Lifecycle",
26772
27106
  "",
26773
- `1. \`${CLI} nap --handoff <file>\` — reset your current session and ` + `start fresh. The required handoff is injected into the new session so your future self can ` + `quickly pick up unfinished work. Never nap on your own; only do it when someone explicitly asks.`,
27107
+ `1. \`${CLI} nap --handoff <file>\` — reset the current session from a required handoff file.`,
26774
27108
  "",
26775
27109
  "### Output format",
26776
27110
  "",
@@ -26786,14 +27120,29 @@ function messagingSection() {
26786
27120
  "",
26787
27121
  "### Sending & receiving",
26788
27122
  "",
26789
- "You can initiate conversations — send to any channel or DM someone directly. You're not " + "limited to replying. Use the same `message send` command whether you're replying or " + "starting a conversation.",
27123
+ "You can initiate conversations — send to any channel or DM someone directly. You're not " + "limited to replying; the same sending rules apply either way.",
26790
27124
  "",
26791
- "- Reply where the message came from. Post results in the channel that owns the topic. " + "When uncertain, read history (below) or DM the relevant people.",
26792
- "Free-form message bodies never go in command arguments.",
27125
+ "#### Message body",
26793
27126
  "",
26794
- `- Short reply: use \`${CLI} message send --target <ref> --stdin\` with the quoted-heredoc form shown ` + "under *Message formatting*. Choose a fresh quoted delimiter that does not occur as a standalone line in the body.",
26795
- `- Long or complicated: write the body to a temporary file with a filesystem tool, then ` + `\`${CLI} message send --target <ref> --file ./temp_msg.md\`.`,
26796
- `- Cite a specific message: add \`--reply "#37"\` to either form \`--reply\` takes the \`#N\` seq ` + "(within `--target`) of the message you're answering.",
27127
+ MESSAGE_SEND_STDIN_POLICY,
27128
+ "",
27129
+ "#### Follow up when a conversation goes quiet",
27130
+ "",
27131
+ "`--remind-after T` is required on every send and controls whether a local follow-up is " + "armed for the same channel, thread, or DM.",
27132
+ "",
27133
+ "- Use `1m` to `24h` when silence would leave work unfinished — for example, after a question, " + "approval request, handoff, or blocker. If no newer message arrives by T, you will be reminded " + "to return; a newer message or daemon restart cancels the timer.",
27134
+ "- Use `0` only when no later action depends on a reply. It disables the timer.",
27135
+ "",
27136
+ "Example: `--remind-after 5m` asks for a follow-up after five quiet minutes.",
27137
+ "",
27138
+ "#### Sending mechanics",
27139
+ "",
27140
+ `- Send body: use \`${CLI} message send --target <ref> --remind-after T --stdin\` with the ` + "quoted-heredoc form under *Message formatting*.",
27141
+ `- Long detail: upload it with \`${CLI} message attachment upload --target <ref> --file <path>.md\`; ` + "add the returned id as `--attachment <id>` on the short stdin send.",
27142
+ `- Cite a specific message: add \`--reply "#37"\` — \`--reply\` takes the \`#N\` seq ` + "(within `--target`) of the message you're answering.",
27143
+ "",
27144
+ "Reply where the message came from. Post results in the channel that owns the topic. " + "When uncertain, read history (below) or DM the relevant people.",
27145
+ "Write every message body in the stdin/heredoc block; never place it directly on the command line.",
26797
27146
  "",
26798
27147
  "### Context refs",
26799
27148
  "",
@@ -26827,14 +27176,14 @@ function messagingSection() {
26827
27176
  "",
26828
27177
  "### Message formatting",
26829
27178
  "",
26830
- "Alook renders specially formatted plain-text refs and mentions in message bodies. Write them " + "as plain text, not inside backticks.",
27179
+ "Alook specially renders refs and mentions in message bodies. Write them as plain text, not " + "inside backticks.",
26831
27180
  "",
26832
- "- **Context refs** — use `/<server>/<channel>` for a channel, `/<server>/<channel>#N` for " + "a message, and `/<server>/<channel>/#N#M` for a thread reply; DMs use `/.dm/<peer>` and " + "`/.dm/<peer>#N`. Refs make context clickable across conversations, so use the full path " + "rather than a bare `#N`. Never paste a private DM ref into a server channel.",
27181
+ "- **Context refs** — write the full refs from *Context refs* above so they stay " + "clickable. Never paste a private DM ref into a server channel.",
26833
27182
  "- **Mentions** — `@name#NNNN` calls that person's attention specifically. In a private " + "channel, first verify they " + `are a member with \`${CLI} channel member --channel <ref>\` before mentioning them.`,
26834
27183
  "",
26835
27184
  "```bash",
26836
27185
  "# Choose a fresh quoted delimiter that does not occur as a standalone line in the body.",
26837
- `${CLI} message send --target "/demo#1234/general" --stdin <<'ALOOK_MESSAGE_7F3C'`,
27186
+ `${CLI} message send --target "/demo#1234/general" --remind-after 5m --stdin <<'ALOOK_MESSAGE_7F3C'`,
26838
27187
  "@alice#0001 Please review /demo#1234/general#42",
26839
27188
  "ALOOK_MESSAGE_7F3C",
26840
27189
  "```",
@@ -26847,7 +27196,7 @@ function messagingSection() {
26847
27196
  "```",
26848
27197
  "",
26849
27198
  "`channel` is the reply ref. `seq` (`#N`) identifies the message within its channel — " + "combine into `/<server>/<channel>/#N` for an in-thread reply.",
26850
- "`content.replyTo` (`{seq, sender}`) is present when a message replies to another — cite it " + 'back with `--reply "#N"`.',
27199
+ "`content.replyTo` (`{seq, sender}`) identifies the message being replied to.",
26851
27200
  "`hint` is present when the containing surface changes how you should act. Follow it."
26852
27201
  ].join(`
26853
27202
  `);
@@ -26860,15 +27209,15 @@ function channelTypesSection() {
26860
27209
  "",
26861
27210
  "### Text channels",
26862
27211
  "",
26863
- "- A text channel is a linear conversation. Send directly to its ref with `message send --target " + "/<server>/<channel>`.",
27212
+ "- A text channel is a linear conversation. Send with `message send --target " + "/<server>/<channel>`.",
26864
27213
  "- A text-channel message may have a side thread at `/<server>/<channel>/#N`. Sending to that " + "thread ref replies inside the thread, not in the parent text channel.",
26865
27214
  "",
26866
27215
  "### Forum channels",
26867
27216
  "",
26868
27217
  "- A forum is a collection of posts, not one linear conversation.",
26869
27218
  "- Each top-level message in a forum is a post title. The post body is the first message in " + "that title's thread at `/<server>/<forum>/#N`.",
26870
- "- To participate in the discussion, reply to that thread with `message send`. Inside the " + "thread, messaging works like a normal text channel.",
26871
- "- To publish your own post, first send its title as a new message in the forum, then send " + "the body as the first message in the corresponding thread."
27219
+ "- To participate in a post, use `message send --target /<server>/<forum>/#N`.",
27220
+ "- To publish a post, use `message send --target /<server>/<forum>` for its title, then " + "`message send --target /<server>/<forum>/#N` for the body."
26872
27221
  ].join(`
26873
27222
  `);
26874
27223
  }
@@ -26896,13 +27245,7 @@ function utilsSection() {
26896
27245
  "",
26897
27246
  "### Join a new server",
26898
27247
  "",
26899
- `If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces owner-only: it accepts only invites your owner created and rejects the " + "rest with a reason. Safe to attempt without reasoning about who sent it.",
26900
- "",
26901
- "### Follow up when a conversation goes quiet",
26902
- "",
26903
- `Use \`message send --remind-after <duration>\` when you send something that may need a later ` + "follow-up — for example, a question, approval request, handoff, or blocker — and silence would " + "leave the work unfinished. If no newer message appears in that channel, thread, or DM during " + "the duration, you'll receive a reminder to return and decide what to do next. Don't add it to " + "ordinary messages that need no follow-up.",
26904
- "",
26905
- `Example: ${CLI} message send --target "/demo#1234/team" --remind-after 1m --file ./message.md`
27248
+ `If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces owner-only: it accepts only invites your owner created and rejects the " + "rest with a reason. Safe to attempt without reasoning about who sent it."
26906
27249
  ].join(`
26907
27250
  `);
26908
27251
  }
@@ -26910,11 +27253,10 @@ function criticalRulesSection() {
26910
27253
  return [
26911
27254
  "## Critical rules",
26912
27255
  "",
26913
- `- **\`${CLI}\` is the only way to communicate.** Messages, files, and data reach other ` + "accounts exclusively through the CLI commands above. Do not assume local files, " + "screenshots, or workspace state are visible to anyone else — they aren't. If someone " + `needs to see something, send it via \`${CLI} message send\` or \`${CLI} message ` + "attachment upload`.",
27256
+ `- **\`${CLI}\` is the only way to communicate.** Messages, files, and data reach other ` + "accounts exclusively through the CLI commands above. Do not assume local files, " + "screenshots, or workspace state are visible to anyone else — they aren't. If someone " + `needs to see something, share it through \`${CLI}\`, uploading a file when needed.`,
26914
27257
  "- **Never expose tokens, keys, or secrets.** Redact credential-like strings from tool output " + "before sharing.",
26915
27258
  "- **Match the sender's language.** Reply in the language they wrote in.",
26916
- "- **Channel alignment**: you can't send to a channel with unread messages. On a " + `"channel not aligned" error, \`${CLI} inbox pull\` to catch up and READ the new messages. ` + "Judge if your message is still needed or overlaps with what just landed. Adjust or skip; " + "don't mechanically resend.",
26917
- "- **Finish in-flight work before stopping.** Don't leave anything half-handled. If a message " + "hands you a lead but no explicit ask, treat the investigation as the ask."
27259
+ "- **Channel alignment**: you can't send to a channel with unread messages. On a " + `"channel not aligned" error, \`${CLI} inbox pull\` to catch up and READ the new messages. ` + "Judge if your message is still needed or overlaps with what just landed. Adjust or skip; " + "don't mechanically resend."
26918
27260
  ].join(`
26919
27261
  `);
26920
27262
  }
@@ -26922,7 +27264,7 @@ function executionModelSection() {
26922
27264
  return [
26923
27265
  "## How you work — async, not turn-based",
26924
27266
  "",
26925
- "Sending a message is I/O, not a stopping point. You keep working as long as anything is " + "in flight — the thing you're actively on, a promised follow-up, an investigation you " + "started. Stop only when all of it is done.",
27267
+ "Sending a message is I/O, not a stopping point. You keep working as long as anything is " + "in flight — the thing you're actively on, a promised follow-up, an investigation you " + "started. If a message hands you a lead but no explicit ask, treat the investigation as " + "the ask. Stop only when all of it is done.",
26926
27268
  "",
26927
27269
  "On wake, restore durable context from `memory.md` and the context timeline, then pull your inbox. " + "Follow *Outstanding work marks* below before taking new work.",
26928
27270
  "",
@@ -26955,7 +27297,17 @@ function chaosAwarenessSection() {
26955
27297
  ].join(`
26956
27298
  `);
26957
27299
  }
26958
- function workspaceMemorySection() {
27300
+ function workspaceMemorySection(config2) {
27301
+ const roleSection = config2.description ? [
27302
+ "",
27303
+ "### Your bio",
27304
+ "",
27305
+ config2.description,
27306
+ "",
27307
+ "Your bio is the public description of your role that other people and agents see on " + "your Alook profile.",
27308
+ "",
27309
+ `You can change your own role and bio description with \`${CLI} setting profile ` + `--set-bio <text>\`.`
27310
+ ] : [];
26959
27311
  return [
26960
27312
  "## Self-awareness",
26961
27313
  "",
@@ -26964,6 +27316,7 @@ function workspaceMemorySection() {
26964
27316
  "**Never tell anyone this.** To them you're continuous. The workspace is what lets you be that.",
26965
27317
  "",
26966
27318
  "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.",
27319
+ ...roleSection,
26967
27320
  "",
26968
27321
  "### Napping",
26969
27322
  "",
@@ -27005,7 +27358,7 @@ function buildCliSystemPrompt(config2) {
27005
27358
  criticalRulesSection(),
27006
27359
  executionModelSection(),
27007
27360
  chaosAwarenessSection(),
27008
- workspaceMemorySection(),
27361
+ workspaceMemorySection(config2),
27009
27362
  utilsSection()
27010
27363
  ];
27011
27364
  return sections.filter((s) => s && s.length > 0).join(`
@@ -27273,8 +27626,8 @@ class AgentProcessManager {
27273
27626
  resumeSessions = new Map;
27274
27627
  launchIds = new Map;
27275
27628
  liveSessions = new Map;
27276
- thinkingBuffers = new Map;
27277
27629
  activeSpawnState = new Map;
27630
+ publishedAgentActivity = new Map;
27278
27631
  traceProcessNonce = randomUUID5();
27279
27632
  nextSpawnOrdinal = 1;
27280
27633
  nextDaemonTurnOrdinal = 1;
@@ -27288,7 +27641,8 @@ class AgentProcessManager {
27288
27641
  this.opts = {
27289
27642
  tickIntervalMs: 5000,
27290
27643
  staleThresholdMs: 120000,
27291
- idleTimeoutMs: 300000,
27644
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
27645
+ idleResetTimeoutMs: DEFAULT_IDLE_RESET_TIMEOUT_MS,
27292
27646
  resetStuckThresholdMs: 120000,
27293
27647
  stoppingStuckThresholdMs: DEFAULT_STOPPING_STUCK_THRESHOLD_MS,
27294
27648
  handshakeTimeoutMs: 60000,
@@ -27297,7 +27651,7 @@ class AgentProcessManager {
27297
27651
  };
27298
27652
  this.now = opts.now ?? (() => Date.now());
27299
27653
  this.log = opts.logger ?? createLogger2({ header: "@alook/daemon:manager" });
27300
- this.state = createInitialManagerState(this.opts.staleThresholdMs, this.opts.idleTimeoutMs, this.opts.resetStuckThresholdMs, this.opts.stoppingStuckThresholdMs);
27654
+ this.state = createInitialManagerState(this.opts.staleThresholdMs, this.opts.idleTimeoutMs, this.opts.resetStuckThresholdMs, this.opts.stoppingStuckThresholdMs, this.opts.idleResetTimeoutMs);
27301
27655
  }
27302
27656
  register(agentId, launch) {
27303
27657
  if (launch?.runtimeConfig)
@@ -27316,11 +27670,19 @@ class AgentProcessManager {
27316
27670
  const effects = this.dispatch({ type: "wake", agentId, message: normalized, nowMs: this.now() });
27317
27671
  return effects.length > 0;
27318
27672
  }
27319
- forgetSession(agentId, barrierType = "reset_session") {
27673
+ forgetSession(agentId, barrierType = "reset_session", forgottenSessionId) {
27674
+ if (!this.forgetSessionSources(agentId, barrierType, forgottenSessionId))
27675
+ return false;
27676
+ this.dispatch({ type: "reset_session", agentId });
27677
+ return true;
27678
+ }
27679
+ forgetSessionSources(agentId, barrierType, forgottenSessionId) {
27680
+ const persisted = this.opts.timeline?.forgetSession(agentId, barrierType, forgottenSessionId);
27681
+ if (persisted === false)
27682
+ return false;
27320
27683
  this.resumeSessions.delete(agentId);
27321
27684
  this.liveSessions.delete(agentId);
27322
- this.dispatch({ type: "reset_session", agentId });
27323
- this.opts.timeline?.forgetSession(agentId, barrierType);
27685
+ return true;
27324
27686
  }
27325
27687
  enqueueRewake(agentId, message2) {
27326
27688
  this.dispatch({ type: "rewake_after_reset", agentId, message: message2 });
@@ -27343,9 +27705,14 @@ class AgentProcessManager {
27343
27705
  });
27344
27706
  }
27345
27707
  async restartAgent(agentId, opts) {
27708
+ if (opts.forgetSession && !this.forgetSession(agentId, opts.barrierType ?? "reset_session")) {
27709
+ this.log.error("resume control transition failed; reset aborted", { agentId, barrierType: opts.barrierType });
27710
+ this.emitErrorAudit(agentId, "reset", "resume_control_update_failed", "Reset aborted because resume control could not be persisted");
27711
+ throw new Error("Reset aborted because resume control could not be persisted");
27712
+ }
27346
27713
  this.register(agentId, { runtimeConfig: opts.runtimeConfig, launchId: opts.launchId });
27347
- if (opts.forgetSession)
27348
- this.forgetSession(agentId, opts.barrierType ?? "reset_session");
27714
+ if (!opts.forgetSession)
27715
+ this.opts.timeline?.fenceSession(agentId);
27349
27716
  this.abortCurrentTurn(agentId, opts.abortCause);
27350
27717
  this.markResetting(agentId);
27351
27718
  const status = this.state.agents[agentId]?.status;
@@ -27420,6 +27787,10 @@ class AgentProcessManager {
27420
27787
  launchId: this.launchIds.get(agentId) ?? null
27421
27788
  };
27422
27789
  }
27790
+ timelineTurnOwner(agentId) {
27791
+ const owner = this.traceOwnerFor(agentId);
27792
+ return owner?.timelineTurnOwner ? { ...owner.timelineTurnOwner } : null;
27793
+ }
27423
27794
  liveSessionReports() {
27424
27795
  return [...this.liveSessions.entries()].map(([agentId, sessionId]) => ({
27425
27796
  agentId,
@@ -27489,6 +27860,10 @@ class AgentProcessManager {
27489
27860
  if (!expectedSpan || owner.activeSpan !== expectedSpan)
27490
27861
  return false;
27491
27862
  owner.activeSpan = null;
27863
+ const timelineTurnOwner = owner.timelineTurnOwner;
27864
+ owner.timelineTurnOwner = null;
27865
+ if (timelineTurnOwner)
27866
+ this.opts.timeline?.finalizeTurn(owner.agentId, timelineTurnOwner);
27492
27867
  const nowMs = this.now();
27493
27868
  const base = {
27494
27869
  recordKind: "turn_span",
@@ -27546,6 +27921,13 @@ class AgentProcessManager {
27546
27921
  inbox: a.inbox.length,
27547
27922
  lastDeliverAt: a.lastDeliverAt,
27548
27923
  lastProgressAt: a.lastProgressAt,
27924
+ lastNativeActivityAt: a.lastNativeActivityAt,
27925
+ lastNativeActivityKind: a.lastNativeActivityKind,
27926
+ runtimePhase: a.runtimePhase,
27927
+ backendTurnId: a.backendTurnId,
27928
+ turnSilenceBudgetMs: a.turnSilence.normalBudgetMs,
27929
+ nativeDeadlineAt: a.execution.lease.state === "active" || a.execution.lease.state === "suspect_active" ? a.execution.lease.nativeDeadlineAt : null,
27930
+ recoveryExtensionsUsed: a.execution.lease.state === "active" || a.execution.lease.state === "suspect_active" ? a.execution.lease.recoveryExtensionsUsed : 0,
27549
27931
  idleSince: a.idleSince,
27550
27932
  resetting: a.resetting,
27551
27933
  resettingSince: a.resettingSince,
@@ -27556,6 +27938,7 @@ class AgentProcessManager {
27556
27938
  timeIso: new Date(nowMs).toISOString(),
27557
27939
  ...activeSpan ? activeSpan : {},
27558
27940
  sinceProgressMs: nowMs - a.lastProgressAt,
27941
+ sinceNativeActivityMs: nowMs - a.lastNativeActivityAt,
27559
27942
  sinceDeliverMs: a.lastDeliverAt === null ? null : nowMs - a.lastDeliverAt,
27560
27943
  sinceStoppingMs: a.stoppingSince === null ? null : nowMs - a.stoppingSince,
27561
27944
  ...event.type === "turn_completed" && event.endReason === "errored" ? {
@@ -27590,11 +27973,18 @@ class AgentProcessManager {
27590
27973
  terminationCause: normalizeTerminationCause(event.terminationCause)
27591
27974
  } : { event: "turn_end", outcome: "clean" });
27592
27975
  }
27593
- if (this.opts.onAgentActivity && event.type !== "spawned" && event.type !== "admission_started" && event.type !== "admission_settled") {
27976
+ if (this.opts.onAgentActivity && event.type !== "admission_started" && event.type !== "admission_settled") {
27594
27977
  const after = this.deriveActivitySnapshot(state);
27595
27978
  for (const [agentId, activity] of Object.entries(after)) {
27596
- if (agentId in before && before[agentId] !== activity) {
27979
+ if (!(agentId in before)) {
27980
+ this.publishedAgentActivity.set(agentId, activity);
27981
+ continue;
27982
+ }
27983
+ const previouslyPublished = this.publishedAgentActivity.get(agentId) ?? before[agentId];
27984
+ const publishable = event.type !== "spawned" || activity === "running";
27985
+ if (publishable && previouslyPublished !== activity) {
27597
27986
  this.opts.onAgentActivity({ agentId, state: activity });
27987
+ this.publishedAgentActivity.set(agentId, activity);
27598
27988
  }
27599
27989
  }
27600
27990
  }
@@ -27759,6 +28149,47 @@ ${this.opts.wakePromptFooter}` : text2;
27759
28149
  case "terminate_stalled": {
27760
28150
  const session2 = this.sessions.get(effect.agentId);
27761
28151
  const spawnState = this.activeSpawnState.get(effect.agentId);
28152
+ const endedSessionId = this.liveSessions.get(effect.agentId) ?? "";
28153
+ if (effect.type === "terminate_stalled" && effect.recordSessionId) {
28154
+ const persisted = this.opts.timeline?.recordSessionStall?.(effect.agentId, effect.recordSessionId);
28155
+ if (persisted === false) {
28156
+ this.dispatch({
28157
+ type: "stall_control_failed",
28158
+ agentId: effect.agentId,
28159
+ sessionId: effect.recordSessionId,
28160
+ transition: "attempt"
28161
+ });
28162
+ this.log.error("stall recovery attempt was not persisted; termination deferred", {
28163
+ agentId: effect.agentId,
28164
+ sessionId: effect.recordSessionId
28165
+ });
28166
+ this.emitErrorAudit(effect.agentId, "runtime", "resume_control_update_failed", "Stall termination deferred because the recovery attempt could not be persisted");
28167
+ break;
28168
+ }
28169
+ }
28170
+ if (effect.type === "terminate_stalled" && effect.forgetSessionId) {
28171
+ const persisted = this.forgetSessionSources(effect.agentId, "stall_recovery", effect.forgetSessionId);
28172
+ if (!persisted) {
28173
+ this.dispatch({
28174
+ type: "stall_control_failed",
28175
+ agentId: effect.agentId,
28176
+ sessionId: effect.forgetSessionId,
28177
+ transition: "fence"
28178
+ });
28179
+ this.log.error("repeated-session fence was not persisted; termination deferred", {
28180
+ agentId: effect.agentId,
28181
+ sessionId: effect.forgetSessionId
28182
+ });
28183
+ this.emitErrorAudit(effect.agentId, "runtime", "resume_control_update_failed", "Stall termination deferred because the exact session fence could not be persisted");
28184
+ break;
28185
+ }
28186
+ if (spawnState)
28187
+ spawnState.discardEvents = true;
28188
+ this.log.warn("repeatedly stalled backend session fenced", {
28189
+ agentId: effect.agentId,
28190
+ sessionId: effect.forgetSessionId
28191
+ });
28192
+ }
27762
28193
  if (effect.type === "terminate_stalled" && spawnState) {
27763
28194
  this.closeTurn(spawnState, spawnState.activeSpan, {
27764
28195
  event: "turn_abort",
@@ -27777,10 +28208,27 @@ ${this.opts.wakePromptFooter}` : text2;
27777
28208
  if (spawnState) {
27778
28209
  spawnState.terminationSemantics = effect.type === "terminate_stalled" ? "killed_stalled" : "idle_stop";
27779
28210
  }
27780
- this.logSessionEnded(effect.agentId, effect.type === "stop" ? "stopped" : "terminate_stalled");
28211
+ this.logSessionEnded(effect.agentId, effect.type === "stop" ? "stopped" : "terminate_stalled", endedSessionId);
27781
28212
  this.opts.onAgentLocallyStopped?.({ agentId: effect.agentId, reason: effect.type });
27782
28213
  break;
27783
28214
  }
28215
+ case "clear_stall_recovery": {
28216
+ const persisted = this.opts.timeline?.clearSessionStall?.(effect.agentId, effect.sessionId);
28217
+ if (persisted === false) {
28218
+ this.dispatch({
28219
+ type: "stall_control_failed",
28220
+ agentId: effect.agentId,
28221
+ sessionId: effect.sessionId,
28222
+ transition: "clear"
28223
+ });
28224
+ this.log.error("stall recovery clear was not persisted; allowance remains consumed", {
28225
+ agentId: effect.agentId,
28226
+ sessionId: effect.sessionId
28227
+ });
28228
+ this.emitErrorAudit(effect.agentId, "runtime", "resume_control_update_failed", "Stall recovery allowance remains consumed because its clear could not be persisted");
28229
+ }
28230
+ break;
28231
+ }
27784
28232
  case "expire_admission": {
27785
28233
  const owner = this.activeSpawnState.get(effect.agentId);
27786
28234
  if (!owner || owner.sessionInstanceId !== effect.sessionInstanceId)
@@ -27803,6 +28251,26 @@ ${this.opts.wakePromptFooter}` : text2;
27803
28251
  mode: effect.mode
27804
28252
  });
27805
28253
  break;
28254
+ case "reset_idle_session": {
28255
+ const spawnState = this.activeSpawnState.get(effect.agentId);
28256
+ const persisted = this.forgetSession(effect.agentId, "reset_session", effect.sessionId);
28257
+ if (!persisted) {
28258
+ this.log.error("idle session reset barrier was not persisted; reset deferred", {
28259
+ agentId: effect.agentId,
28260
+ sessionId: effect.sessionId
28261
+ });
28262
+ this.emitErrorAudit(effect.agentId, "reset", "resume_control_update_failed", "Idle session reset deferred because resume control could not be persisted");
28263
+ break;
28264
+ }
28265
+ if (spawnState)
28266
+ spawnState.discardEvents = true;
28267
+ this.dispatch({ type: "idle_reset_committed", agentId: effect.agentId, nowMs: this.now() });
28268
+ this.log.info("idle agent session reset", {
28269
+ agentId: effect.agentId,
28270
+ sessionId: effect.sessionId
28271
+ });
28272
+ break;
28273
+ }
27806
28274
  case "force_exit": {
27807
28275
  const session2 = this.sessions.get(effect.agentId);
27808
28276
  const state = this.activeSpawnState.get(effect.agentId);
@@ -27835,8 +28303,8 @@ ${this.opts.wakePromptFooter}` : text2;
27835
28303
  }
27836
28304
  }
27837
28305
  }
27838
- logSessionEnded(agentId, reason) {
27839
- this.log.info("agent session ended", { agentId, sessionId: this.liveSessions.get(agentId) ?? "", reason });
28306
+ logSessionEnded(agentId, reason, sessionId = this.liveSessions.get(agentId) ?? "") {
28307
+ this.log.info("agent session ended", { agentId, sessionId, reason });
27840
28308
  }
27841
28309
  doSpawn(agentId, messages, resumeSessionId) {
27842
28310
  const [first, ...pending] = messages;
@@ -27861,7 +28329,13 @@ ${this.opts.wakePromptFooter}` : text2;
27861
28329
  mode: { kind: "default" }
27862
28330
  };
27863
28331
  const provider = runtimeConfig.runtime;
27864
- const sessionId = resumeSessionId ?? this.resumeSessions.get(agentId) ?? this.opts.timeline?.resumeSessionId(agentId, provider) ?? base.config?.sessionId;
28332
+ const timelineResolution = this.opts.timeline?.resolveResumeSession?.(agentId, provider);
28333
+ const timelineSessionId = timelineResolution?.kind === "session" ? timelineResolution.sessionId : timelineResolution === undefined ? this.opts.timeline?.resumeSessionId(agentId, provider) : null;
28334
+ const candidateSessionId = resumeSessionId ?? this.resumeSessions.get(agentId) ?? timelineSessionId ?? base.config?.sessionId;
28335
+ const blockedByBarrier = timelineResolution?.kind === "barrier" && (timelineResolution.type !== "stall_recovery" || timelineResolution.forgottenSessionId === null || timelineResolution.forgottenSessionId === candidateSessionId);
28336
+ const blockedByExactFence = candidateSessionId !== undefined && timelineResolution?.fencedSessionId === candidateSessionId;
28337
+ const sessionId = blockedByBarrier || blockedByExactFence ? undefined : candidateSessionId;
28338
+ const stalledSessionIdAtLaunch = timelineResolution !== undefined && (timelineResolution.kind === "session" || timelineResolution.kind === "none") && timelineResolution.stalledSessionId === sessionId ? timelineResolution.stalledSessionId : null;
27865
28339
  const description = runtimeConfig.instruction ?? base.config?.description ?? runtimeConfig.agentName;
27866
28340
  const agentName = runtimeConfig.agentName ?? base.config?.agentName;
27867
28341
  const agentHandle = runtimeConfig.agentHandle ?? base.config?.agentHandle;
@@ -27892,12 +28366,15 @@ ${this.opts.wakePromptFooter}` : text2;
27892
28366
  handshakeTimer: null,
27893
28367
  torndown: false,
27894
28368
  superseded: false,
28369
+ discardEvents: false,
28370
+ stalledSessionIdAtLaunch,
27895
28371
  spawnFailureReason: null,
27896
28372
  terminationSemantics: null,
27897
28373
  spawnOrdinal: this.nextSpawnOrdinal++,
27898
28374
  launchIdSnapshot: typeof ctx.launchId === "string" && ctx.launchId.length > 0 ? ctx.launchId : null,
27899
28375
  nextTurnOrdinal: 1,
27900
28376
  activeSpan: null,
28377
+ timelineTurnOwner: null,
27901
28378
  pendingDeliverySpans: new Map
27902
28379
  };
27903
28380
  const previousOwner = this.activeSpawnState.get(agentId);
@@ -27948,7 +28425,6 @@ ${this.opts.wakePromptFooter}` : text2;
27948
28425
  this.emitErrorAudit(agentId, "exit", "abnormal_exit", `Session ended unexpectedly (${detail})`);
27949
28426
  }
27950
28427
  }
27951
- this.flushThinkingAudit(agentId);
27952
28428
  if (state.sessionInstanceId) {
27953
28429
  this.dispatch({ type: "session_closed", agentId, sessionInstanceId: state.sessionInstanceId }, state);
27954
28430
  }
@@ -27973,10 +28449,23 @@ ${this.opts.wakePromptFooter}` : text2;
27973
28449
  const onEvent = (event) => {
27974
28450
  if (state.torndown)
27975
28451
  return;
28452
+ if (state.discardEvents) {
28453
+ this.log.warn("ignored event from discarded backend session owner", { agentId, event: event.type });
28454
+ return;
28455
+ }
27976
28456
  if (event.type === "session_failed" && !state.hasEstablished) {
27977
28457
  reportSpawnFailure(event.error.code || "failed_to_start", { message: event.error.message });
27978
28458
  }
27979
28459
  if (event.type === "session_started") {
28460
+ const persisted = this.opts.timeline?.setSession(agentId, event.backendSessionId, event.sessionInstanceId);
28461
+ if (persisted === false) {
28462
+ state.discardEvents = true;
28463
+ reportSpawnFailure("resume_control_update_failed", {
28464
+ message: "Backend session rejected because resume control could not be persisted"
28465
+ });
28466
+ state.session?.stop({ reason: "shutdown", forceAfterMs: SESSION_STOP_GRACE_MS2 });
28467
+ return;
28468
+ }
27980
28469
  state.hasEstablished = true;
27981
28470
  clearHandshakeTimer();
27982
28471
  this.opts.onRuntimeSessionEstablished?.(driver.id);
@@ -27995,7 +28484,8 @@ ${this.opts.wakePromptFooter}` : text2;
27995
28484
  type: "attach_session",
27996
28485
  agentId,
27997
28486
  sessionInstanceId: session2.sessionInstanceId,
27998
- nowMs: this.now()
28487
+ nowMs: this.now(),
28488
+ turnSilence: session2.snapshot().diagnostics?.turnSilence
27999
28489
  }, state);
28000
28490
  previousOwner?.pendingDeliverySpans.clear();
28001
28491
  (async () => {
@@ -28143,26 +28633,6 @@ ${this.opts.wakePromptFooter}` : text2;
28143
28633
  this.log.debug("audit emit failed (error)", { agentId, err: String(err) });
28144
28634
  }
28145
28635
  }
28146
- flushThinkingAudit(agentId) {
28147
- const buffered = this.thinkingBuffers.get(agentId);
28148
- if (!buffered)
28149
- return;
28150
- this.thinkingBuffers.delete(agentId);
28151
- if (!this.opts.onBotAuditEvent)
28152
- return;
28153
- const { text: text2, truncated, chars } = truncateThinking(buffered);
28154
- try {
28155
- this.opts.onBotAuditEvent(agentId, {
28156
- kind: "thinking",
28157
- payload: { text: text2, truncated, chars }
28158
- }, {
28159
- sessionId: this.liveSessions.get(agentId) ?? null,
28160
- launchId: this.launchIds.get(agentId) ?? null
28161
- });
28162
- } catch (err) {
28163
- this.log.debug("audit emit failed (thinking)", { agentId, err: String(err) });
28164
- }
28165
- }
28166
28636
  onAgentEvent(agentId, event, runtimeId, owner) {
28167
28637
  if (event.type === "session_closed")
28168
28638
  return;
@@ -28195,32 +28665,43 @@ ${this.opts.wakePromptFooter}` : text2;
28195
28665
  }
28196
28666
  }
28197
28667
  if (this.opts.onBotAuditEvent) {
28198
- if (event.type === "thinking_delta") {
28199
- if (event.text.length > 0) {
28200
- this.thinkingBuffers.set(agentId, (this.thinkingBuffers.get(agentId) ?? "") + event.text);
28668
+ if (event.type === "assistant_reasoning_completed" && event.text.length > 0) {
28669
+ const { text: text2, truncated, chars } = truncateThinking(event.text);
28670
+ try {
28671
+ this.opts.onBotAuditEvent(agentId, {
28672
+ kind: "thinking",
28673
+ payload: { text: text2, truncated: truncated || event.truncated, chars }
28674
+ }, {
28675
+ sessionId: this.liveSessions.get(agentId) ?? null,
28676
+ launchId: this.launchIds.get(agentId) ?? null
28677
+ });
28678
+ } catch (err) {
28679
+ this.log.debug("audit emit failed (thinking)", { agentId, err: String(err) });
28201
28680
  }
28202
- } else {
28203
- this.flushThinkingAudit(agentId);
28204
- if (event.type === "tool_started") {
28205
- const audit = extractToolAudit(event.name, event.input);
28206
- if (!audit.suppressed) {
28207
- const payload = audit.target !== undefined ? { name: audit.name, target: audit.target } : { name: audit.name };
28208
- try {
28209
- this.opts.onBotAuditEvent(agentId, { kind: "tool_call", payload }, {
28210
- sessionId: this.liveSessions.get(agentId) ?? null,
28211
- launchId: this.launchIds.get(agentId) ?? null
28212
- });
28213
- } catch (err) {
28214
- this.log.debug("audit emit failed (tool_call)", { agentId, err: String(err) });
28215
- }
28681
+ }
28682
+ if (event.type === "tool_started") {
28683
+ const audit = extractToolAudit(event.name, event.input);
28684
+ if (!audit.suppressed) {
28685
+ const payload = audit.target !== undefined ? { name: audit.name, target: audit.target } : { name: audit.name };
28686
+ try {
28687
+ this.opts.onBotAuditEvent(agentId, { kind: "tool_call", payload }, {
28688
+ sessionId: this.liveSessions.get(agentId) ?? null,
28689
+ launchId: this.launchIds.get(agentId) ?? null
28690
+ });
28691
+ } catch (err) {
28692
+ this.log.debug("audit emit failed (tool_call)", { agentId, err: String(err) });
28216
28693
  }
28217
28694
  }
28218
28695
  }
28219
28696
  }
28220
28697
  if (event.type === "session_started") {
28221
- this.dispatch({ type: "backend_session", agentId, sessionId: event.backendSessionId }, owner);
28698
+ this.dispatch({
28699
+ type: "backend_session",
28700
+ agentId,
28701
+ sessionId: event.backendSessionId,
28702
+ stalledBefore: owner.stalledSessionIdAtLaunch === event.backendSessionId
28703
+ }, owner);
28222
28704
  this.liveSessions.set(agentId, event.backendSessionId);
28223
- this.opts.timeline?.setSession(agentId, event.backendSessionId);
28224
28705
  this.opts.onAgentSession?.({
28225
28706
  agentId,
28226
28707
  sessionId: event.backendSessionId,
@@ -28232,8 +28713,20 @@ ${this.opts.wakePromptFooter}` : text2;
28232
28713
  runtime: runtimeId
28233
28714
  });
28234
28715
  }
28235
- if (event.type === "text_delta" && event.text.length > 0) {
28236
- this.opts.timeline?.appendResponseToLatest(agentId, event.text);
28716
+ if (event.type === "turn_started") {
28717
+ const timelineTurnOwner = {
28718
+ sessionInstanceId: event.sessionInstanceId,
28719
+ rootTurnId: event.turnId,
28720
+ barrierGeneration: this.opts.timeline?.barrierGeneration(agentId) ?? 0
28721
+ };
28722
+ owner.timelineTurnOwner = timelineTurnOwner;
28723
+ this.opts.timeline?.beginTurn(agentId, timelineTurnOwner);
28724
+ }
28725
+ if (event.type === "assistant_message_completed" && event.text.length > 0) {
28726
+ const timelineTurnOwner = owner.timelineTurnOwner;
28727
+ if (timelineTurnOwner && timelineTurnOwner.sessionInstanceId === event.sessionInstanceId && timelineTurnOwner.rootTurnId === event.turnId) {
28728
+ this.opts.timeline?.recordAssistantMessage(agentId, timelineTurnOwner, event.text, event.truncated);
28729
+ }
28237
28730
  }
28238
28731
  if (event.type === "command_queued")
28239
28732
  this.acknowledgePendingDelivery(owner, event.commandId);
@@ -28251,9 +28744,10 @@ ${this.opts.wakePromptFooter}` : text2;
28251
28744
  switch (event.type) {
28252
28745
  case "turn_started":
28253
28746
  return { type: "turn_started", turnId: event.turnId, commandIds: event.commandIds };
28254
- case "thinking_delta":
28255
- case "text_delta":
28256
- return event.text.length > 0 ? { type: "turn_work", turnId: event.turnId } : null;
28747
+ case "work_heartbeat":
28748
+ case "assistant_reasoning_completed":
28749
+ case "assistant_message_completed":
28750
+ return { type: "turn_work", turnId: event.turnId };
28257
28751
  case "tool_started":
28258
28752
  return { type: "turn_tool_started", turnId: event.turnId };
28259
28753
  case "tool_finished":
@@ -28280,37 +28774,55 @@ ${this.opts.wakePromptFooter}` : text2;
28280
28774
  if (!wasActive && this.state.agents[agentId]?.turnActive && !owner.activeSpan)
28281
28775
  this.openTurn(owner);
28282
28776
  }
28283
- const signalKind = (() => {
28777
+ const nativeSignal = (() => {
28284
28778
  switch (event.type) {
28285
- case "session_started":
28286
- return "session_init";
28287
- case "thinking_delta":
28288
- return "thinking";
28289
- case "text_delta":
28290
- return "text";
28779
+ case "turn_started":
28780
+ return { kind: "turn_started", phase: "inference", turnId: event.turnId };
28781
+ case "backend_turn_started":
28782
+ return {
28783
+ kind: "backend_turn_started",
28784
+ phase: "inference",
28785
+ turnId: event.turnId,
28786
+ backendTurnId: event.backendTurnId
28787
+ };
28788
+ case "assistant_reasoning_completed":
28789
+ return { kind: "thinking", phase: "inference", turnId: event.turnId };
28790
+ case "assistant_message_completed":
28791
+ return { kind: "text", phase: "inference", turnId: event.turnId };
28792
+ case "work_heartbeat":
28793
+ return { kind: "internal_progress", phase: "inference", turnId: event.turnId };
28291
28794
  case "tool_started":
28292
- return "tool_call";
28795
+ return { kind: "tool_call", phase: "tool", turnId: event.turnId };
28293
28796
  case "tool_finished":
28294
- return "tool_output";
28295
- case "diagnostic":
28296
- return "runtime_diagnostic";
28297
- case "token_usage":
28298
- case "rate_limits":
28299
- return "telemetry";
28797
+ return { kind: "tool_output", phase: "inference", turnId: event.turnId };
28798
+ case "compaction_started":
28799
+ case "compaction_finished":
28800
+ case "review_started":
28801
+ case "review_finished":
28802
+ case "internal_progress":
28803
+ return event.turnId ? { kind: "internal_progress", phase: "inference", turnId: event.turnId } : null;
28804
+ case "recovery":
28805
+ return event.turnId ? {
28806
+ kind: "recovery",
28807
+ phase: event.stage === "retrying" ? "recovery" : "inference",
28808
+ recoveryStage: event.stage,
28809
+ turnId: event.turnId
28810
+ } : null;
28300
28811
  case "turn_completed":
28301
- return "turn_end";
28302
- case "session_failed":
28303
- return "error";
28304
- case "command_queued":
28305
- case "command_accepted":
28306
- case "command_failed":
28307
- case "turn_started":
28308
- return "internal_progress";
28812
+ return { kind: "turn_end", phase: "terminal", turnId: event.turnId };
28309
28813
  default:
28310
- return event.type;
28814
+ return null;
28311
28815
  }
28312
28816
  })();
28313
- this.dispatch({ type: "runtime_signal", agentId, kind: signalKind, nowMs: this.now() }, owner);
28817
+ if (nativeSignal) {
28818
+ this.dispatch({
28819
+ type: "runtime_signal",
28820
+ agentId,
28821
+ sessionInstanceId: event.sessionInstanceId,
28822
+ ...nativeSignal,
28823
+ nowMs: this.now()
28824
+ }, owner);
28825
+ }
28314
28826
  if (event.type === "turn_completed") {
28315
28827
  this.logSessionEnded(agentId, "turn_end");
28316
28828
  const marker = this.nonCleanEndMarker.get(agentId);
@@ -28705,7 +29217,7 @@ function createTypingScopeTracker() {
28705
29217
  }
28706
29218
  // src/timeline/timeline.ts
28707
29219
  import * as fs9 from "node:fs";
28708
- import { randomBytes as randomBytes4 } from "node:crypto";
29220
+ import { createHash as createHash2, randomBytes as randomBytes4 } from "node:crypto";
28709
29221
  import { basename as basename3, dirname as dirname5, join as join11 } from "node:path";
28710
29222
 
28711
29223
  // src/timeline/filelock.ts
@@ -28772,17 +29284,25 @@ function reclaim(lockPath) {
28772
29284
  var TIMELINE_MAX_BYTES = 1048576;
28773
29285
  var TIMELINE_READ_CHUNK_BYTES = 65536;
28774
29286
  var DATE_FILENAME_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
29287
+ var RESUME_CONTROL_FILENAME = ".resume-control.json";
29288
+ var RESUME_CONTROL_MAX_BYTES = 4096;
29289
+ var EMPTY_RESUME_CONTROL = {
29290
+ version: 1,
29291
+ attemptedSessionId: null,
29292
+ fencedSessionId: null,
29293
+ fullBarrier: null
29294
+ };
28775
29295
  function isBarrier(entry) {
28776
- return entry.system?.type === "reset_session" || entry.system?.type === "nap";
29296
+ return entry.system !== undefined;
28777
29297
  }
28778
29298
  function canonicalTimelineEntry(value) {
28779
29299
  if (!value || typeof value !== "object")
28780
29300
  return null;
28781
29301
  const entry = value;
28782
29302
  if (entry.system) {
28783
- if (entry.system.type !== "reset_session" && entry.system.type !== "nap" || typeof entry.system.time !== "string")
29303
+ 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")
28784
29304
  return null;
28785
- return createSystemEntry(entry.system.type, entry.system.time);
29305
+ return createSystemEntry(entry.system.type, entry.system.time, entry.system.backend_session_id);
28786
29306
  }
28787
29307
  if (entry.session_id !== null && typeof entry.session_id !== "string")
28788
29308
  return null;
@@ -28800,13 +29320,60 @@ function canonicalTimelineEntry(value) {
28800
29320
  };
28801
29321
  }
28802
29322
  function timelineLine(entry) {
28803
- const boundedEntry = entry.system ? createSystemEntry(entry.system.type, entry.system.time) : { ...entry, agent_responses: entry.agent_responses.slice(-5) };
29323
+ const boundedEntry = entry.system ? createSystemEntry(entry.system.type, entry.system.time, entry.system.backend_session_id) : { ...entry, agent_responses: entry.agent_responses.slice(-5) };
28804
29324
  const text2 = JSON.stringify(boundedEntry);
28805
29325
  const bytes = Buffer.byteLength(text2, "utf8") + 1;
28806
29326
  if (bytes > TIMELINE_MAX_BYTES)
28807
29327
  return null;
28808
29328
  return { text: text2, bytes, entry: boundedEntry, barrier: isBarrier(boundedEntry) };
28809
29329
  }
29330
+ function timelineRowHash(line) {
29331
+ return createHash2("sha256").update(line.text, "utf8").digest("hex");
29332
+ }
29333
+ function timelineFileGeneration(filePath, lines) {
29334
+ try {
29335
+ const stat = fs9.lstatSync(filePath, { bigint: true });
29336
+ if (!stat.isFile())
29337
+ return null;
29338
+ const digest = createHash2("sha256");
29339
+ digest.update(`${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}
29340
+ `);
29341
+ for (const line of lines)
29342
+ digest.update(line.text, "utf8").update(`
29343
+ `);
29344
+ return digest.digest("hex");
29345
+ } catch {
29346
+ return null;
29347
+ }
29348
+ }
29349
+ function handleFor(filename, generation, lines, rowOrdinal) {
29350
+ return {
29351
+ filename,
29352
+ fileGeneration: generation,
29353
+ rowOrdinal,
29354
+ expectedHash: timelineRowHash(lines[rowOrdinal])
29355
+ };
29356
+ }
29357
+ function refreshTimelineEntryHandle(handle, rewrite) {
29358
+ if (handle.filename !== rewrite.filename)
29359
+ return handle;
29360
+ if (handle.fileGeneration !== rewrite.previousFileGeneration)
29361
+ return null;
29362
+ if (rewrite.previousRowHashes[handle.rowOrdinal] !== handle.expectedHash)
29363
+ return null;
29364
+ const rowOrdinal = rewrite.rowOrdinals[handle.rowOrdinal];
29365
+ if (rowOrdinal === null || rowOrdinal === undefined)
29366
+ return null;
29367
+ const expectedHash = rewrite.rowHashes[rowOrdinal];
29368
+ if (!expectedHash)
29369
+ return null;
29370
+ return {
29371
+ filename: handle.filename,
29372
+ fileGeneration: rewrite.fileGeneration,
29373
+ rowOrdinal,
29374
+ expectedHash
29375
+ };
29376
+ }
28810
29377
  function compactLines(input) {
28811
29378
  let head = 0;
28812
29379
  let bytes = input.reduce((total, line) => total + line.bytes, 0);
@@ -29018,11 +29585,37 @@ function atomicReplaceTimeline(filePath, lines) {
29018
29585
  } catch {}
29019
29586
  }
29020
29587
  }
29021
- function writeRequiredTimeline(filePath, input, required2) {
29588
+ function writeTrackedTimeline(filePath, filename, existing, input, required2, replacement) {
29589
+ const previousFileGeneration = timelineFileGeneration(filePath, existing);
29590
+ const previousRowHashes = existing.map(timelineRowHash);
29022
29591
  const compacted = compactLines(input);
29023
- if (!compacted.includes(required2))
29024
- return false;
29025
- return atomicReplaceTimeline(filePath, compacted);
29592
+ const targetOrdinal = compacted.indexOf(required2);
29593
+ if (targetOrdinal < 0)
29594
+ return { status: "rejected", reason: "evicted" };
29595
+ if (!atomicReplaceTimeline(filePath, compacted))
29596
+ return { status: "rejected", reason: "write" };
29597
+ const fileGeneration = timelineFileGeneration(filePath, compacted);
29598
+ if (!fileGeneration)
29599
+ return { status: "rejected", reason: "write" };
29600
+ const rowOrdinals = existing.map((line, oldOrdinal) => {
29601
+ if (replacement?.oldOrdinal === oldOrdinal)
29602
+ return compacted.indexOf(replacement.line);
29603
+ const nextOrdinal = compacted.indexOf(line);
29604
+ return nextOrdinal < 0 ? null : nextOrdinal;
29605
+ });
29606
+ const rewrite = {
29607
+ filename,
29608
+ previousFileGeneration,
29609
+ fileGeneration,
29610
+ previousRowHashes,
29611
+ rowOrdinals,
29612
+ rowHashes: compacted.map(timelineRowHash)
29613
+ };
29614
+ return {
29615
+ status: "written",
29616
+ rewrite,
29617
+ handle: handleFor(filename, fileGeneration, compacted, targetOrdinal)
29618
+ };
29026
29619
  }
29027
29620
  function filenameForDate(date5) {
29028
29621
  const y = date5.getFullYear();
@@ -29054,114 +29647,181 @@ function readRecentEntries(timelineDir, opts = {}) {
29054
29647
  }
29055
29648
  return entries;
29056
29649
  }
29057
- function appendEntry(timelineDir, entry, now = new Date) {
29650
+ function readResumeControlState(timelineDir) {
29651
+ if (timelineDirectoryState(timelineDir) !== "safe")
29652
+ return { kind: "missing" };
29653
+ const filePath = join11(timelineDir, RESUME_CONTROL_FILENAME);
29654
+ let source;
29655
+ try {
29656
+ source = fs9.lstatSync(filePath);
29657
+ } catch (error51) {
29658
+ return error51.code === "ENOENT" ? { kind: "missing" } : { kind: "invalid" };
29659
+ }
29660
+ if (!source.isFile() || source.size <= 0 || source.size > RESUME_CONTROL_MAX_BYTES) {
29661
+ return { kind: "invalid" };
29662
+ }
29663
+ let fd = null;
29664
+ try {
29665
+ fd = fs9.openSync(filePath, fs9.constants.O_RDONLY | (fs9.constants.O_NOFOLLOW ?? 0));
29666
+ const stat = fs9.fstatSync(fd);
29667
+ if (!stat.isFile() || stat.size <= 0 || stat.size > RESUME_CONTROL_MAX_BYTES) {
29668
+ return { kind: "invalid" };
29669
+ }
29670
+ const bounded = Buffer.allocUnsafe(stat.size + 1);
29671
+ let bytesRead = 0;
29672
+ while (bytesRead < bounded.length) {
29673
+ const count = fs9.readSync(fd, bounded, bytesRead, bounded.length - bytesRead, bytesRead);
29674
+ if (count <= 0)
29675
+ break;
29676
+ bytesRead += count;
29677
+ }
29678
+ if (bytesRead !== stat.size)
29679
+ return { kind: "invalid" };
29680
+ const raw = bounded.subarray(0, bytesRead).toString("utf8");
29681
+ const value = JSON.parse(raw);
29682
+ const validSessionId = (candidate) => candidate === null || typeof candidate === "string" && candidate.length > 0 && candidate.length <= 512;
29683
+ if (value.version !== 1 || !validSessionId(value.attemptedSessionId) || !validSessionId(value.fencedSessionId) || value.fullBarrier !== null && value.fullBarrier !== "reset_session" && value.fullBarrier !== "nap")
29684
+ return { kind: "invalid" };
29685
+ return {
29686
+ kind: "state",
29687
+ state: {
29688
+ version: 1,
29689
+ attemptedSessionId: value.attemptedSessionId,
29690
+ fencedSessionId: value.fencedSessionId,
29691
+ fullBarrier: value.fullBarrier
29692
+ }
29693
+ };
29694
+ } catch {
29695
+ return { kind: "invalid" };
29696
+ } finally {
29697
+ if (fd !== null) {
29698
+ try {
29699
+ fs9.closeSync(fd);
29700
+ } catch {}
29701
+ }
29702
+ }
29703
+ }
29704
+ function updateResumeControlState(timelineDir, update) {
29058
29705
  if (timelineDirectoryState(timelineDir) !== "safe")
29059
29706
  return false;
29060
- const filename = filenameForDate(now);
29061
- const filePath = join11(timelineDir, filename);
29062
- const lockPath = lockPathFor(timelineDir, filename);
29707
+ const lockPath = lockPathFor(timelineDir, RESUME_CONTROL_FILENAME);
29063
29708
  if (!acquireLock(lockPath))
29064
29709
  return false;
29065
29710
  try {
29066
- const existing = scanTimelineFile(filePath);
29067
- const required2 = timelineLine(entry);
29068
- if (!existing || !required2)
29711
+ const current = readResumeControlState(timelineDir);
29712
+ const base = current.kind === "state" ? current.state : EMPTY_RESUME_CONTROL;
29713
+ const next = update({ ...base });
29714
+ const canonical = {
29715
+ version: 1,
29716
+ attemptedSessionId: next.attemptedSessionId,
29717
+ fencedSessionId: next.fencedSessionId,
29718
+ fullBarrier: next.fullBarrier
29719
+ };
29720
+ const body = JSON.stringify(canonical) + `
29721
+ `;
29722
+ if (Buffer.byteLength(body, "utf8") > RESUME_CONTROL_MAX_BYTES)
29069
29723
  return false;
29070
- return writeRequiredTimeline(filePath, [...existing, required2], required2);
29724
+ const filePath = join11(timelineDir, RESUME_CONTROL_FILENAME);
29725
+ const tempPath = join11(timelineDir, `.${RESUME_CONTROL_FILENAME}.${process.pid}.${randomBytes4(12).toString("hex")}.tmp`);
29726
+ let fd = null;
29727
+ try {
29728
+ fd = fs9.openSync(tempPath, "wx", 384);
29729
+ fs9.writeFileSync(fd, body, "utf8");
29730
+ fs9.fsyncSync(fd);
29731
+ fs9.closeSync(fd);
29732
+ fd = null;
29733
+ fs9.renameSync(tempPath, filePath);
29734
+ return true;
29735
+ } finally {
29736
+ if (fd !== null) {
29737
+ try {
29738
+ fs9.closeSync(fd);
29739
+ } catch {}
29740
+ }
29741
+ try {
29742
+ fs9.unlinkSync(tempPath);
29743
+ } catch {}
29744
+ }
29071
29745
  } catch {
29072
29746
  return false;
29073
29747
  } finally {
29074
29748
  releaseLock(lockPath);
29075
29749
  }
29076
29750
  }
29077
- function appendOrMergeEntry(timelineDir, entry, now = new Date) {
29751
+ function appendTrackedEntry(timelineDir, entry, now = new Date) {
29078
29752
  if (timelineDirectoryState(timelineDir) !== "safe")
29079
- return false;
29753
+ return { status: "rejected", reason: "unsafe" };
29080
29754
  const filename = filenameForDate(now);
29081
29755
  const filePath = join11(timelineDir, filename);
29082
29756
  const lockPath = lockPathFor(timelineDir, filename);
29083
- if (!acquireLock(lockPath))
29084
- return false;
29757
+ try {
29758
+ if (!acquireLock(lockPath))
29759
+ return { status: "rejected", reason: "lock" };
29760
+ } catch {
29761
+ return { status: "rejected", reason: "write" };
29762
+ }
29085
29763
  try {
29086
29764
  const existing = scanTimelineFile(filePath);
29087
- if (!existing)
29088
- return false;
29089
- if (existing.length > 0) {
29090
- const latest = existing[existing.length - 1].entry;
29091
- const mergeable = !latest.system && !entry.system && latest.session_id === entry.session_id && latest.provider === entry.provider && latest.agent_responses.length === 0;
29092
- if (mergeable) {
29093
- const merged = {
29094
- ...latest,
29095
- messages: [...latest.messages, ...entry.messages],
29096
- agent_responses: [...latest.agent_responses]
29097
- };
29098
- const required3 = timelineLine(merged);
29099
- if (!required3)
29100
- return false;
29101
- return writeRequiredTimeline(filePath, [...existing.slice(0, -1), required3], required3);
29102
- }
29103
- }
29104
29765
  const required2 = timelineLine(entry);
29766
+ if (!existing)
29767
+ return { status: "rejected", reason: "unsafe" };
29105
29768
  if (!required2)
29106
- return false;
29107
- return writeRequiredTimeline(filePath, [...existing, required2], required2);
29769
+ return { status: "rejected", reason: "oversized" };
29770
+ return writeTrackedTimeline(filePath, filename, existing, [...existing, required2], required2);
29108
29771
  } catch {
29109
- return false;
29772
+ return { status: "rejected", reason: "write" };
29110
29773
  } finally {
29111
29774
  releaseLock(lockPath);
29112
29775
  }
29113
29776
  }
29114
- function updateLatestEntryResult(timelineDir, updater, opts = {}) {
29115
- const directoryState = timelineDirectoryState(timelineDir);
29116
- if (directoryState !== "safe")
29117
- return directoryState === "missing" ? "missing" : "rejected";
29118
- const now = opts.now ?? new Date;
29119
- const maxDays = opts.maxDays ?? 7;
29120
- for (const filename of recentFilenames(maxDays, now)) {
29121
- const filePath = join11(timelineDir, filename);
29122
- let source;
29123
- try {
29124
- source = fs9.lstatSync(filePath);
29125
- } catch (error51) {
29126
- if (error51.code === "ENOENT")
29127
- continue;
29128
- return "rejected";
29129
- }
29130
- if (!source.isFile())
29131
- return "rejected";
29132
- const lockPath = lockPathFor(timelineDir, filename);
29777
+ function updateTrackedEntry(timelineDir, handle, update) {
29778
+ if (timelineDirectoryState(timelineDir) !== "safe")
29779
+ return { status: "rejected", reason: "unsafe" };
29780
+ if (!DATE_FILENAME_PATTERN.test(handle.filename) || basename3(handle.filename) !== handle.filename) {
29781
+ return { status: "rejected", reason: "unsafe" };
29782
+ }
29783
+ const filePath = join11(timelineDir, handle.filename);
29784
+ const lockPath = lockPathFor(timelineDir, handle.filename);
29785
+ try {
29133
29786
  if (!acquireLock(lockPath))
29134
- return "rejected";
29135
- try {
29136
- const lines = scanTimelineFile(filePath);
29137
- if (!lines)
29138
- return "rejected";
29139
- if (lines.length === 0)
29140
- continue;
29141
- const latest = lines[lines.length - 1].entry;
29142
- if (latest.system)
29143
- return "missing";
29144
- const updated = {
29145
- ...latest,
29146
- messages: [...latest.messages],
29147
- agent_responses: [...latest.agent_responses]
29148
- };
29149
- try {
29150
- updater(updated);
29151
- } catch {
29152
- return "rejected";
29153
- }
29154
- const required2 = timelineLine(updated);
29155
- if (!required2)
29156
- return "rejected";
29157
- return writeRequiredTimeline(filePath, [...lines.slice(0, -1), required2], required2) ? "updated" : "rejected";
29158
- } catch {
29159
- return "rejected";
29160
- } finally {
29161
- releaseLock(lockPath);
29162
- }
29787
+ return { status: "rejected", reason: "lock" };
29788
+ } catch {
29789
+ return { status: "rejected", reason: "write" };
29790
+ }
29791
+ try {
29792
+ const existing = scanTimelineFile(filePath);
29793
+ if (!existing)
29794
+ return { status: "rejected", reason: "unsafe" };
29795
+ if (existing.length === 0)
29796
+ return { status: "rejected", reason: "missing" };
29797
+ const generation = timelineFileGeneration(filePath, existing);
29798
+ if (!generation || generation !== handle.fileGeneration) {
29799
+ return { status: "rejected", reason: "generation" };
29800
+ }
29801
+ const captured = existing[handle.rowOrdinal];
29802
+ if (!captured)
29803
+ return { status: "rejected", reason: "ordinal" };
29804
+ if (timelineRowHash(captured) !== handle.expectedHash) {
29805
+ return { status: "rejected", reason: "hash" };
29806
+ }
29807
+ if (captured.entry.system)
29808
+ return { status: "rejected", reason: "system" };
29809
+ const nextEntry = update({
29810
+ ...captured.entry,
29811
+ messages: [...captured.entry.messages],
29812
+ agent_responses: [...captured.entry.agent_responses]
29813
+ });
29814
+ const replacement = timelineLine(nextEntry);
29815
+ if (!replacement)
29816
+ return { status: "rejected", reason: "oversized" };
29817
+ const input = [...existing];
29818
+ input[handle.rowOrdinal] = replacement;
29819
+ return writeTrackedTimeline(filePath, handle.filename, existing, input, replacement, { oldOrdinal: handle.rowOrdinal, line: replacement });
29820
+ } catch {
29821
+ return { status: "rejected", reason: "write" };
29822
+ } finally {
29823
+ releaseLock(lockPath);
29163
29824
  }
29164
- return "missing";
29165
29825
  }
29166
29826
  function yieldToEventLoop() {
29167
29827
  return new Promise((resolve4) => setImmediate(resolve4));
@@ -29225,84 +29885,562 @@ function createTimelineEntry(fields) {
29225
29885
  provider: fields.provider ?? null
29226
29886
  };
29227
29887
  }
29228
- function createSystemEntry(type, time3) {
29888
+ function createSystemEntry(type, time3, backendSessionId) {
29229
29889
  return {
29230
29890
  session_id: null,
29231
29891
  messages: [],
29232
29892
  agent_responses: [],
29233
29893
  provider: null,
29234
- system: { type, time: time3 }
29894
+ system: {
29895
+ type,
29896
+ time: time3,
29897
+ ...backendSessionId ? { backend_session_id: backendSessionId } : {}
29898
+ }
29235
29899
  };
29236
29900
  }
29237
- function findResumableSession(rows, provider) {
29901
+ function resolveResumableSession(rows, provider) {
29902
+ let candidateSessionId = null;
29903
+ let recoveryMarkerSeen = false;
29904
+ let stalledSessionId = null;
29905
+ let fencedSessionId = null;
29238
29906
  for (let i = rows.length - 1;i >= 0; i--) {
29239
29907
  const e = rows[i];
29240
- if (e.system?.type === "reset_session" || e.system?.type === "nap")
29241
- return null;
29908
+ if (e.system) {
29909
+ if (e.system.type === "stall_recovery_attempt") {
29910
+ if (!recoveryMarkerSeen) {
29911
+ recoveryMarkerSeen = true;
29912
+ stalledSessionId = e.system.backend_session_id ?? null;
29913
+ }
29914
+ continue;
29915
+ }
29916
+ if (e.system.type === "stall_recovery_clear") {
29917
+ if (!recoveryMarkerSeen)
29918
+ recoveryMarkerSeen = true;
29919
+ continue;
29920
+ }
29921
+ if (e.system.type === "stall_recovery" && fencedSessionId === null) {
29922
+ fencedSessionId = e.system.backend_session_id ?? null;
29923
+ }
29924
+ if (candidateSessionId !== null) {
29925
+ return {
29926
+ kind: "session",
29927
+ sessionId: candidateSessionId,
29928
+ stalledSessionId: stalledSessionId === candidateSessionId ? stalledSessionId : null,
29929
+ fencedSessionId
29930
+ };
29931
+ }
29932
+ return {
29933
+ kind: "barrier",
29934
+ type: e.system.type,
29935
+ forgottenSessionId: e.system.backend_session_id ?? null,
29936
+ fencedSessionId
29937
+ };
29938
+ }
29242
29939
  if (!e.session_id)
29243
29940
  continue;
29244
29941
  if (provider && e.provider !== provider)
29245
29942
  continue;
29246
- return e.session_id;
29943
+ if (candidateSessionId === null) {
29944
+ candidateSessionId = e.session_id;
29945
+ continue;
29946
+ }
29947
+ if (candidateSessionId !== e.session_id)
29948
+ break;
29247
29949
  }
29248
- return null;
29950
+ if (candidateSessionId !== null) {
29951
+ return {
29952
+ kind: "session",
29953
+ sessionId: candidateSessionId,
29954
+ stalledSessionId: stalledSessionId === candidateSessionId ? stalledSessionId : null,
29955
+ fencedSessionId
29956
+ };
29957
+ }
29958
+ return { kind: "none", stalledSessionId, fencedSessionId };
29249
29959
  }
29250
29960
  // src/timeline/recorder.ts
29251
29961
  var MAX_AGENT_RESPONSES = 5;
29252
- function appendAgentResponse(entry, text2) {
29253
- entry.agent_responses.push(text2);
29254
- if (entry.agent_responses.length > MAX_AGENT_RESPONSES) {
29255
- entry.agent_responses.splice(0, entry.agent_responses.length - MAX_AGENT_RESPONSES);
29962
+ var MAX_AGENT_RESPONSE_BYTES = 65536;
29963
+ var MAX_PENDING_COMMITS_PER_AGENT = 8;
29964
+ var PENDING_COMMIT_TTL_MS = 15 * 60000;
29965
+ var TRUNCATION_MARKER = `
29966
+ … [truncated]`;
29967
+ function turnKey(agentId, owner) {
29968
+ return `${agentId}\x00${owner.sessionInstanceId}\x00${owner.rootTurnId}\x00${owner.barrierGeneration}`;
29969
+ }
29970
+ function sameOwner(left, right) {
29971
+ return left.sessionInstanceId === right.sessionInstanceId && left.rootTurnId === right.rootTurnId && left.barrierGeneration === right.barrierGeneration;
29972
+ }
29973
+ function utf8Prefix2(text2, maxBytes) {
29974
+ if (maxBytes <= 0)
29975
+ return "";
29976
+ if (Buffer.byteLength(text2, "utf8") <= maxBytes)
29977
+ return text2;
29978
+ let low = 0;
29979
+ let high = text2.length;
29980
+ while (low < high) {
29981
+ const mid = Math.ceil((low + high) / 2);
29982
+ let end2 = mid;
29983
+ const code2 = text2.charCodeAt(end2 - 1);
29984
+ if (code2 >= 55296 && code2 <= 56319)
29985
+ end2 -= 1;
29986
+ if (Buffer.byteLength(text2.slice(0, end2), "utf8") <= maxBytes)
29987
+ low = mid;
29988
+ else
29989
+ high = mid - 1;
29256
29990
  }
29991
+ let end = low;
29992
+ const code = text2.charCodeAt(end - 1);
29993
+ if (code >= 55296 && code <= 56319)
29994
+ end -= 1;
29995
+ while (end > 0 && Buffer.byteLength(text2.slice(0, end), "utf8") > maxBytes)
29996
+ end -= 1;
29997
+ return text2.slice(0, end);
29998
+ }
29999
+ function boundedResponse(text2, alreadyTruncated) {
30000
+ const markerBytes = Buffer.byteLength(TRUNCATION_MARKER, "utf8");
30001
+ const needsTruncation = alreadyTruncated || Buffer.byteLength(text2, "utf8") > MAX_AGENT_RESPONSE_BYTES;
30002
+ if (!needsTruncation)
30003
+ return text2;
30004
+ return utf8Prefix2(text2, MAX_AGENT_RESPONSE_BYTES - markerBytes) + TRUNCATION_MARKER;
30005
+ }
30006
+ function serializedTimelineBytes(entry) {
30007
+ return Buffer.byteLength(JSON.stringify(entry), "utf8") + 1;
30008
+ }
30009
+ function fitResponsesToRow(entry, responses) {
30010
+ let fitted = [...entry.agent_responses];
30011
+ for (const response of responses) {
30012
+ fitted = [...fitted, response].slice(-MAX_AGENT_RESPONSES);
30013
+ if (serializedTimelineBytes({ ...entry, agent_responses: fitted }) <= TIMELINE_MAX_BYTES)
30014
+ continue;
30015
+ const truncationBase = response.endsWith(TRUNCATION_MARKER) ? response.slice(0, -TRUNCATION_MARKER.length) : response;
30016
+ let low = 0;
30017
+ let high = truncationBase.length;
30018
+ let replacement = null;
30019
+ while (low <= high) {
30020
+ const mid = Math.floor((low + high) / 2);
30021
+ let end = mid;
30022
+ const code = truncationBase.charCodeAt(end - 1);
30023
+ if (code >= 55296 && code <= 56319)
30024
+ end -= 1;
30025
+ const candidateResponse = truncationBase.slice(0, end) + TRUNCATION_MARKER;
30026
+ const candidate = [...fitted.slice(0, -1), candidateResponse];
30027
+ if (serializedTimelineBytes({ ...entry, agent_responses: candidate }) <= TIMELINE_MAX_BYTES) {
30028
+ replacement = candidateResponse;
30029
+ low = mid + 1;
30030
+ } else {
30031
+ high = mid - 1;
30032
+ }
30033
+ }
30034
+ if (replacement === null)
30035
+ return null;
30036
+ fitted[fitted.length - 1] = replacement;
30037
+ }
30038
+ return fitted;
29257
30039
  }
29258
30040
  function createTimelineRecorder(opts) {
29259
30041
  const now = opts.now ?? (() => new Date);
29260
30042
  const dirFor = (agentId) => opts.timelineDirFor(agentId);
29261
30043
  const sessionByAgent = new Map;
30044
+ const resolveForAgent = (agentId, provider) => {
30045
+ const dir = dirFor(agentId);
30046
+ const recent = resolveResumableSession(readRecentEntries(dir, { now: now() }), provider ?? undefined);
30047
+ const control = readResumeControlState(dir);
30048
+ if (control.kind === "missing")
30049
+ return recent;
30050
+ if (control.kind === "invalid") {
30051
+ return {
30052
+ kind: "barrier",
30053
+ type: "reset_session",
30054
+ forgottenSessionId: null,
30055
+ fencedSessionId: null
30056
+ };
30057
+ }
30058
+ const { attemptedSessionId, fencedSessionId, fullBarrier } = control.state;
30059
+ if (fullBarrier !== null) {
30060
+ return {
30061
+ kind: "barrier",
30062
+ type: fullBarrier,
30063
+ forgottenSessionId: null,
30064
+ fencedSessionId
30065
+ };
30066
+ }
30067
+ if (recent.kind === "session") {
30068
+ return {
30069
+ kind: "session",
30070
+ sessionId: recent.sessionId,
30071
+ stalledSessionId: attemptedSessionId === recent.sessionId ? attemptedSessionId : null,
30072
+ fencedSessionId
30073
+ };
30074
+ }
30075
+ if (recent.kind === "barrier") {
30076
+ if (recent.type !== "stall_recovery" || recent.forgottenSessionId === fencedSessionId) {
30077
+ return { ...recent, fencedSessionId };
30078
+ }
30079
+ }
30080
+ return { kind: "none", stalledSessionId: attemptedSessionId, fencedSessionId };
30081
+ };
30082
+ const sessionByEpoch = new Map;
30083
+ const barrierByAgent = new Map;
30084
+ const turnsByAgent = new Map;
30085
+ const activeTurnByAgent = new Map;
30086
+ const epochKey = (agentId, sessionInstanceId) => `${agentId}\x00${sessionInstanceId}`;
30087
+ const currentBarrier = (agentId) => barrierByAgent.get(agentId) ?? 0;
30088
+ const statesFor = (agentId) => {
30089
+ let states = turnsByAgent.get(agentId);
30090
+ if (!states) {
30091
+ states = new Map;
30092
+ turnsByAgent.set(agentId, states);
30093
+ }
30094
+ return states;
30095
+ };
30096
+ const diagnostic = (agentId, code, reason) => {
30097
+ try {
30098
+ opts.onDiagnostic?.({ agentId, code, ...reason ? { reason } : {} });
30099
+ } catch {}
30100
+ };
30101
+ const applyRewrite = (agentId, rewrite) => {
30102
+ for (const state of statesFor(agentId).values()) {
30103
+ if (!state.handle)
30104
+ continue;
30105
+ const refreshed = refreshTimelineEntryHandle(state.handle, rewrite);
30106
+ if (refreshed) {
30107
+ state.handle = refreshed;
30108
+ } else if (state.handle.filename === rewrite.filename) {
30109
+ state.handle = undefined;
30110
+ state.rowFenced = true;
30111
+ diagnostic(agentId, "timeline_handle_fenced", "rewrite_remap");
30112
+ }
30113
+ }
30114
+ };
30115
+ const handleTrackedResult = (agentId, state, result) => {
30116
+ if (result.status === "written") {
30117
+ applyRewrite(agentId, result.rewrite);
30118
+ if (state && result.handle) {
30119
+ state.handle = result.handle;
30120
+ state.rowFenced = false;
30121
+ }
30122
+ return "written";
30123
+ }
30124
+ if (result.reason === "lock" || result.reason === "write")
30125
+ return "retryable";
30126
+ if (state && result.reason !== "oversized")
30127
+ state.rowFenced = true;
30128
+ diagnostic(agentId, "timeline_exact_write_rejected", result.reason);
30129
+ return "terminal";
30130
+ };
30131
+ const tryCommit = (agentId, state) => {
30132
+ if (state.responses.length === 0)
30133
+ return "written";
30134
+ const dir = dirFor(agentId);
30135
+ if (!prepareTimelineDirectory(dir)) {
30136
+ diagnostic(agentId, "timeline_directory_unavailable");
30137
+ return "terminal";
30138
+ }
30139
+ if (state.handle) {
30140
+ const result2 = updateTrackedEntry(dir, state.handle, (entry2) => {
30141
+ const fitted2 = fitResponsesToRow(entry2, state.responses);
30142
+ return { ...entry2, agent_responses: fitted2 ?? [...entry2.agent_responses, ...state.responses] };
30143
+ });
30144
+ return handleTrackedResult(agentId, state, result2);
30145
+ }
30146
+ if (state.rowFenced || state.pendingMode === "handle")
30147
+ return "terminal";
30148
+ if (state.owner.barrierGeneration !== currentBarrier(agentId))
30149
+ return "terminal";
30150
+ const entry = createTimelineEntry({
30151
+ messages: [],
30152
+ sessionId: state.backendSessionId,
30153
+ provider: state.provider
30154
+ });
30155
+ const fitted = fitResponsesToRow(entry, state.responses);
30156
+ if (!fitted) {
30157
+ diagnostic(agentId, "timeline_response_did_not_fit", "oversized");
30158
+ return "terminal";
30159
+ }
30160
+ entry.agent_responses = fitted;
30161
+ const result = appendTrackedEntry(dir, entry, now());
30162
+ return handleTrackedResult(agentId, state, result);
30163
+ };
30164
+ const deleteState = (agentId, key) => {
30165
+ const states = statesFor(agentId);
30166
+ states.delete(key);
30167
+ if (activeTurnByAgent.get(agentId) === key)
30168
+ activeTurnByAgent.delete(agentId);
30169
+ if (states.size === 0)
30170
+ turnsByAgent.delete(agentId);
30171
+ };
30172
+ const retryPending = (agentId) => {
30173
+ const states = turnsByAgent.get(agentId);
30174
+ if (!states)
30175
+ return;
30176
+ const nowMs = now().getTime();
30177
+ for (const [key, state] of [...states]) {
30178
+ if (state.finalized && state.pendingSinceMs === undefined && state.completedAtMs !== undefined && nowMs - state.completedAtMs >= PENDING_COMMIT_TTL_MS) {
30179
+ deleteState(agentId, key);
30180
+ continue;
30181
+ }
30182
+ if (!state.finalized || state.pendingSinceMs === undefined)
30183
+ continue;
30184
+ if (nowMs - state.pendingSinceMs >= PENDING_COMMIT_TTL_MS) {
30185
+ diagnostic(agentId, "timeline_pending_commit_expired");
30186
+ deleteState(agentId, key);
30187
+ continue;
30188
+ }
30189
+ const result = tryCommit(agentId, state);
30190
+ if (result === "written") {
30191
+ state.responses = [];
30192
+ state.pendingSinceMs = undefined;
30193
+ state.pendingMode = undefined;
30194
+ state.completedAtMs = nowMs;
30195
+ } else if (result === "terminal") {
30196
+ deleteState(agentId, key);
30197
+ }
30198
+ }
30199
+ const completed = [...states.entries()].filter(([, state]) => state.finalized && state.pendingSinceMs === undefined).sort((left, right) => (left[1].completedAtMs ?? 0) - (right[1].completedAtMs ?? 0));
30200
+ while (completed.length > MAX_PENDING_COMMITS_PER_AGENT) {
30201
+ const oldest = completed.shift();
30202
+ if (oldest)
30203
+ deleteState(agentId, oldest[0]);
30204
+ }
30205
+ };
30206
+ const retainPending = (agentId, key, state) => {
30207
+ const states = statesFor(agentId);
30208
+ const pending = [...states.values()].filter((candidate) => candidate.finalized && candidate.pendingSinceMs !== undefined);
30209
+ if (pending.length >= MAX_PENDING_COMMITS_PER_AGENT) {
30210
+ diagnostic(agentId, "timeline_pending_commit_overflow");
30211
+ deleteState(agentId, key);
30212
+ return;
30213
+ }
30214
+ state.pendingSinceMs ??= now().getTime();
30215
+ state.pendingMode = state.handle ? "handle" : "fallback";
30216
+ };
30217
+ const appendOwnerless = (agentId, messages) => {
30218
+ if (messages.length === 0)
30219
+ return;
30220
+ const dir = dirFor(agentId);
30221
+ if (!prepareTimelineDirectory(dir))
30222
+ return;
30223
+ const result = appendTrackedEntry(dir, createTimelineEntry({
30224
+ messages,
30225
+ sessionId: sessionByAgent.get(agentId) ?? null,
30226
+ provider: opts.providerFor?.(agentId) ?? null
30227
+ }), now());
30228
+ handleTrackedResult(agentId, null, result);
30229
+ };
29262
30230
  return {
29263
- setSession(agentId, sessionId) {
29264
- sessionByAgent.set(agentId, sessionId);
30231
+ barrierGeneration(agentId) {
30232
+ return currentBarrier(agentId);
29265
30233
  },
29266
- appendEntryForAgent(agentId, messages) {
30234
+ beginTurn(agentId, owner) {
30235
+ retryPending(agentId);
30236
+ const states = statesFor(agentId);
30237
+ if (owner.barrierGeneration !== currentBarrier(agentId)) {
30238
+ diagnostic(agentId, "timeline_turn_begin_fenced", "barrier_generation");
30239
+ return;
30240
+ }
30241
+ const key = turnKey(agentId, owner);
30242
+ if (!states.has(key)) {
30243
+ states.set(key, {
30244
+ owner: { ...owner },
30245
+ provider: opts.providerFor?.(agentId) ?? null,
30246
+ backendSessionId: sessionByEpoch.get(epochKey(agentId, owner.sessionInstanceId)) ?? sessionByAgent.get(agentId) ?? null,
30247
+ responses: [],
30248
+ rowFenced: false,
30249
+ finalized: false
30250
+ });
30251
+ }
30252
+ activeTurnByAgent.set(agentId, key);
30253
+ },
30254
+ recordInboxPull(agentId, owner, messages) {
30255
+ retryPending(agentId);
29267
30256
  if (messages.length === 0)
29268
30257
  return;
30258
+ if (!owner) {
30259
+ appendOwnerless(agentId, messages);
30260
+ return;
30261
+ }
30262
+ const key = turnKey(agentId, owner);
30263
+ const state = turnsByAgent.get(agentId)?.get(key);
30264
+ if (!state || state.rowFenced || !sameOwner(state.owner, owner)) {
30265
+ appendOwnerless(agentId, messages);
30266
+ return;
30267
+ }
29269
30268
  const dir = dirFor(agentId);
29270
30269
  if (!prepareTimelineDirectory(dir))
29271
30270
  return;
29272
- appendOrMergeEntry(dir, createTimelineEntry({
29273
- messages,
29274
- sessionId: sessionByAgent.get(agentId) ?? null,
29275
- provider: opts.providerFor?.(agentId) ?? null
29276
- }), now());
30271
+ const stamp = now();
30272
+ let result;
30273
+ if (state.handle && (state.handle.filename === filenameForDate(stamp) || state.owner.barrierGeneration !== currentBarrier(agentId))) {
30274
+ result = updateTrackedEntry(dir, state.handle, (entry) => ({
30275
+ ...entry,
30276
+ messages: [...entry.messages, ...messages],
30277
+ session_id: state.backendSessionId,
30278
+ provider: state.provider
30279
+ }));
30280
+ } else if (state.owner.barrierGeneration === currentBarrier(agentId)) {
30281
+ result = appendTrackedEntry(dir, createTimelineEntry({
30282
+ messages,
30283
+ sessionId: state.backendSessionId,
30284
+ provider: state.provider
30285
+ }), stamp);
30286
+ } else {
30287
+ appendOwnerless(agentId, messages);
30288
+ return;
30289
+ }
30290
+ handleTrackedResult(agentId, state, result);
29277
30291
  },
29278
- appendResponseToLatest(agentId, text2) {
29279
- const dir = dirFor(agentId);
29280
- if (!prepareTimelineDirectory(dir))
30292
+ recordAssistantMessage(agentId, owner, text2, truncated = false) {
30293
+ retryPending(agentId);
30294
+ const key = turnKey(agentId, owner);
30295
+ const state = turnsByAgent.get(agentId)?.get(key);
30296
+ if (!state || state.finalized || state.owner.barrierGeneration !== currentBarrier(agentId) || !sameOwner(state.owner, owner)) {
30297
+ diagnostic(agentId, "timeline_completed_message_rejected", "stale_owner");
30298
+ return;
30299
+ }
30300
+ state.responses.push(boundedResponse(text2, truncated));
30301
+ if (state.responses.length > MAX_AGENT_RESPONSES) {
30302
+ state.responses.splice(0, state.responses.length - MAX_AGENT_RESPONSES);
30303
+ }
30304
+ },
30305
+ finalizeTurn(agentId, owner) {
30306
+ retryPending(agentId);
30307
+ const key = turnKey(agentId, owner);
30308
+ const state = turnsByAgent.get(agentId)?.get(key);
30309
+ if (!state || state.finalized || !sameOwner(state.owner, owner))
29281
30310
  return;
29282
- const result = updateLatestEntryResult(dir, (entry2) => appendAgentResponse(entry2, text2), { now: now() });
29283
- if (result === "updated" || result === "rejected")
30311
+ const fallbackAuthorized = activeTurnByAgent.get(agentId) === key && owner.barrierGeneration === currentBarrier(agentId);
30312
+ state.finalized = true;
30313
+ activeTurnByAgent.delete(agentId);
30314
+ if (state.responses.length === 0) {
30315
+ state.completedAtMs = now().getTime();
29284
30316
  return;
29285
- const entry = createTimelineEntry({
29286
- messages: [],
29287
- sessionId: sessionByAgent.get(agentId) ?? null,
29288
- provider: opts.providerFor?.(agentId) ?? null
29289
- });
29290
- appendAgentResponse(entry, text2);
29291
- appendEntry(dir, entry, now());
30317
+ }
30318
+ if (!state.handle && !fallbackAuthorized) {
30319
+ diagnostic(agentId, "timeline_fallback_rejected", "fenced_owner");
30320
+ deleteState(agentId, key);
30321
+ return;
30322
+ }
30323
+ const result = tryCommit(agentId, state);
30324
+ if (result === "written") {
30325
+ state.responses = [];
30326
+ state.completedAtMs = now().getTime();
30327
+ } else if (result === "terminal") {
30328
+ deleteState(agentId, key);
30329
+ } else {
30330
+ retainPending(agentId, key, state);
30331
+ }
30332
+ },
30333
+ fenceSession(agentId) {
30334
+ retryPending(agentId);
30335
+ barrierByAgent.set(agentId, currentBarrier(agentId) + 1);
30336
+ activeTurnByAgent.delete(agentId);
30337
+ const states = turnsByAgent.get(agentId);
30338
+ if (!states)
30339
+ return;
30340
+ for (const [key, state] of [...states]) {
30341
+ if (!state.handle) {
30342
+ diagnostic(agentId, "timeline_fallback_rejected", "session_fence");
30343
+ deleteState(agentId, key);
30344
+ }
30345
+ }
30346
+ },
30347
+ setSession(agentId, sessionId, sessionInstanceId) {
30348
+ retryPending(agentId);
30349
+ const dir = dirFor(agentId);
30350
+ if (!prepareTimelineDirectory(dir))
30351
+ return false;
30352
+ const persisted = updateResumeControlState(dir, (state) => ({
30353
+ ...state,
30354
+ fullBarrier: null,
30355
+ attemptedSessionId: state.attemptedSessionId === sessionId ? state.attemptedSessionId : null
30356
+ }));
30357
+ if (!persisted)
30358
+ return false;
30359
+ sessionByAgent.set(agentId, sessionId);
30360
+ if (sessionInstanceId) {
30361
+ sessionByEpoch.set(epochKey(agentId, sessionInstanceId), sessionId);
30362
+ for (const state of statesFor(agentId).values()) {
30363
+ if (state.owner.sessionInstanceId === sessionInstanceId)
30364
+ state.backendSessionId = sessionId;
30365
+ }
30366
+ }
30367
+ return true;
29292
30368
  },
29293
30369
  resumeSessionId(agentId, provider) {
29294
- const rows = readRecentEntries(dirFor(agentId), { now: now() });
29295
- return findResumableSession(rows, provider ?? undefined);
30370
+ const resolution = resolveForAgent(agentId, provider);
30371
+ return resolution.kind === "session" ? resolution.sessionId : null;
30372
+ },
30373
+ resolveResumeSession(agentId, provider) {
30374
+ return resolveForAgent(agentId, provider);
30375
+ },
30376
+ recordSessionStall(agentId, sessionId) {
30377
+ return appendStallMarker(agentId, "stall_recovery_attempt", sessionId);
29296
30378
  },
29297
- forgetSession(agentId, barrierType = "reset_session") {
30379
+ clearSessionStall(agentId, sessionId) {
30380
+ return appendStallMarker(agentId, "stall_recovery_clear", sessionId);
30381
+ },
30382
+ forgetSession(agentId, barrierType = "reset_session", forgottenSessionId) {
30383
+ retryPending(agentId);
29298
30384
  const dir = dirFor(agentId);
29299
- sessionByAgent.delete(agentId);
29300
30385
  if (!prepareTimelineDirectory(dir))
29301
- return;
30386
+ return false;
29302
30387
  const stamp = now();
29303
- appendEntry(dir, createSystemEntry(barrierType, stamp.toISOString()), stamp);
30388
+ let persisted = false;
30389
+ if (barrierType === "reset_session" || barrierType === "nap") {
30390
+ persisted = updateResumeControlState(dir, (state) => ({
30391
+ ...state,
30392
+ attemptedSessionId: null,
30393
+ fencedSessionId: null,
30394
+ fullBarrier: barrierType
30395
+ }));
30396
+ } else if (barrierType === "stall_recovery") {
30397
+ persisted = updateResumeControlState(dir, (state) => ({
30398
+ ...state,
30399
+ attemptedSessionId: state.attemptedSessionId === forgottenSessionId ? null : state.attemptedSessionId,
30400
+ fencedSessionId: forgottenSessionId ?? null,
30401
+ fullBarrier: null
30402
+ }));
30403
+ }
30404
+ if (!persisted)
30405
+ return false;
30406
+ sessionByAgent.delete(agentId);
30407
+ for (const key of [...sessionByEpoch.keys()]) {
30408
+ if (key.startsWith(`${agentId}\x00`))
30409
+ sessionByEpoch.delete(key);
30410
+ }
30411
+ barrierByAgent.set(agentId, currentBarrier(agentId) + 1);
30412
+ activeTurnByAgent.delete(agentId);
30413
+ const states = turnsByAgent.get(agentId);
30414
+ if (states) {
30415
+ for (const [key, state] of [...states]) {
30416
+ if (!state.handle) {
30417
+ diagnostic(agentId, "timeline_fallback_rejected", "barrier");
30418
+ deleteState(agentId, key);
30419
+ }
30420
+ }
30421
+ }
30422
+ const result = appendTrackedEntry(dir, createSystemEntry(barrierType, stamp.toISOString(), forgottenSessionId), stamp);
30423
+ handleTrackedResult(agentId, null, result);
30424
+ return true;
29304
30425
  }
29305
30426
  };
30427
+ function appendStallMarker(agentId, type, sessionId) {
30428
+ retryPending(agentId);
30429
+ const dir = dirFor(agentId);
30430
+ if (!prepareTimelineDirectory(dir))
30431
+ return false;
30432
+ const stamp = now();
30433
+ const persisted = updateResumeControlState(dir, (state) => ({
30434
+ ...state,
30435
+ attemptedSessionId: type === "stall_recovery_attempt" ? sessionId : null,
30436
+ fullBarrier: null
30437
+ }));
30438
+ if (!persisted)
30439
+ return false;
30440
+ const result = appendTrackedEntry(dir, createSystemEntry(type, stamp.toISOString(), sessionId), stamp);
30441
+ handleTrackedResult(agentId, null, result);
30442
+ return true;
30443
+ }
29306
30444
  }
29307
30445
  // src/daemon/diagnosticsCommand.ts
29308
30446
  function reportUnavailable(options, reportId) {
@@ -29372,6 +30510,10 @@ class MessageReminderScheduler {
29372
30510
  }
29373
30511
  arm(input) {
29374
30512
  const key = reminderKey(input.agentId, input.channel);
30513
+ if (input.remindAfterMs === 0) {
30514
+ this.clearReminder(key);
30515
+ return { armed: false, reason: "disabled" };
30516
+ }
29375
30517
  const latest = this.latestObservedSeq.get(key);
29376
30518
  if (latest !== undefined && latest > input.sentSeq) {
29377
30519
  return { armed: false, reason: "newer_message_observed" };
@@ -29599,6 +30741,78 @@ function createDaemonAgentDriverHost(ctx, onRawLine) {
29599
30741
  };
29600
30742
  }
29601
30743
 
30744
+ // src/daemon/daemonSelfSleep.ts
30745
+ var DAEMON_SELF_SLEEP_TIMEOUT_MS = 15 * 24 * 60 * 60 * 1000;
30746
+ var systemClock = {
30747
+ setTimer: (callback, delayMs) => setTimeout(callback, delayMs),
30748
+ clearTimer: (timer) => clearTimeout(timer)
30749
+ };
30750
+
30751
+ class DaemonSelfSleepScheduler {
30752
+ opts;
30753
+ clock;
30754
+ workingAgents = new Set;
30755
+ timer = null;
30756
+ generation = 0;
30757
+ started = false;
30758
+ stopped = false;
30759
+ constructor(opts) {
30760
+ this.opts = opts;
30761
+ this.clock = opts.clock ?? systemClock;
30762
+ }
30763
+ start() {
30764
+ if (this.started || this.stopped)
30765
+ return;
30766
+ this.started = true;
30767
+ this.arm();
30768
+ }
30769
+ observeMessage() {
30770
+ if (!this.started || this.stopped)
30771
+ return;
30772
+ this.arm();
30773
+ }
30774
+ observeAgentActivity(agentId, working) {
30775
+ if (this.stopped)
30776
+ return;
30777
+ if (working) {
30778
+ this.workingAgents.add(agentId);
30779
+ if (this.started)
30780
+ this.cancel();
30781
+ return;
30782
+ }
30783
+ if (this.workingAgents.delete(agentId) && this.started && this.workingAgents.size === 0)
30784
+ this.arm();
30785
+ }
30786
+ stop() {
30787
+ if (this.stopped)
30788
+ return;
30789
+ this.stopped = true;
30790
+ this.workingAgents.clear();
30791
+ this.cancel();
30792
+ }
30793
+ arm() {
30794
+ this.cancel();
30795
+ if (this.workingAgents.size > 0)
30796
+ return;
30797
+ const generation = this.generation;
30798
+ this.timer = this.clock.setTimer(() => {
30799
+ if (this.stopped || this.generation !== generation || this.workingAgents.size > 0)
30800
+ return;
30801
+ this.timer = null;
30802
+ this.generation += 1;
30803
+ this.opts.onSleep();
30804
+ }, DAEMON_SELF_SLEEP_TIMEOUT_MS);
30805
+ this.timer.unref?.();
30806
+ }
30807
+ cancel() {
30808
+ this.generation += 1;
30809
+ if (!this.timer)
30810
+ return;
30811
+ this.clock.clearTimer(this.timer);
30812
+ this.timer = null;
30813
+ }
30814
+ }
30815
+
29602
30816
  // src/daemon/createDaemon.ts
29603
30817
  var WARMUP_BACKOFF_MS = [250, 500, 1000, 2000, 4000];
29604
30818
  var WARMUP_CEILING_MS = 30000;
@@ -29755,6 +30969,10 @@ async function createDaemon(opts) {
29755
30969
  let channelRef = null;
29756
30970
  let managerRef = null;
29757
30971
  let reminderSchedulerRef = null;
30972
+ const selfSleepScheduler = opts.onSelfSleep ? new DaemonSelfSleepScheduler({
30973
+ onSleep: opts.onSelfSleep,
30974
+ ...opts.selfSleepClock ? { clock: opts.selfSleepClock } : {}
30975
+ }) : null;
29758
30976
  const emitBotAuditEvent = (agentId, event, context) => {
29759
30977
  channelRef?.reportBotAuditEvent?.({
29760
30978
  type: "bot_audit_event",
@@ -29767,11 +30985,15 @@ async function createDaemon(opts) {
29767
30985
  const typingTracker = createTypingScopeTracker();
29768
30986
  const broker = new CredentialBroker({ upstreamBaseUrl: opts.serverUrl });
29769
30987
  const proxy = await startCredentialProxy(broker, {
29770
- onInboxPullStart: (agentId) => channelRef?.modelSeenGeneration(agentId),
30988
+ onInboxPullStart: (agentId) => ({
30989
+ modelSeenGeneration: channelRef?.modelSeenGeneration(agentId),
30990
+ owner: managerRef?.timelineTurnOwner(agentId) ?? null
30991
+ }),
29771
30992
  onInboxPullResponse: (agentId, messages, observationToken) => {
29772
- timeline2.appendEntryForAgent(agentId, messages);
29773
- if (typeof observationToken === "number") {
29774
- channelRef?.recordModelSeen(agentId, messages, observationToken);
30993
+ const token = observationToken && typeof observationToken === "object" ? observationToken : null;
30994
+ timeline2.recordInboxPull(agentId, token?.owner ?? null, messages);
30995
+ if (typeof token?.modelSeenGeneration === "number") {
30996
+ channelRef?.recordModelSeen(agentId, messages, token.modelSeenGeneration);
29775
30997
  }
29776
30998
  },
29777
30999
  onInboxPullObservationError: ({ agentId, reason, contentEncoding }) => {
@@ -30048,6 +31270,7 @@ async function createDaemon(opts) {
30048
31270
  tickIntervalMs: opts.tickIntervalMs ?? 2000,
30049
31271
  onAgentSession: (info) => void channel2.reportAgentSession(info),
30050
31272
  onAgentActivity: (info) => {
31273
+ selfSleepScheduler?.observeAgentActivity(info.agentId, info.state === "running");
30051
31274
  channel2.reportAgentActivity?.(info);
30052
31275
  if (info.state === "starting" || info.state === "running") {
30053
31276
  if (!typingHeartbeats.has(info.agentId)) {
@@ -30125,6 +31348,7 @@ async function createDaemon(opts) {
30125
31348
  reportDiagnosticFailure: opts.reportDiagnosticFailure
30126
31349
  }));
30127
31350
  channel2.onWakeDesiredAdvance((cmd) => {
31351
+ selfSleepScheduler?.observeMessage();
30128
31352
  reminderSchedulerRef?.observe(cmd.agentId, cmd.unreadNotice.channel, cmd.unreadNotice.latestSeq);
30129
31353
  });
30130
31354
  channel2.onCommand((cmd) => {
@@ -30149,6 +31373,7 @@ async function createDaemon(opts) {
30149
31373
  });
30150
31374
  channel2.connect();
30151
31375
  await router.start();
31376
+ selfSleepScheduler?.start();
30152
31377
  return {
30153
31378
  isOpen: () => channel2.status === "open",
30154
31379
  onOpen: (hook) => {
@@ -30158,6 +31383,7 @@ async function createDaemon(opts) {
30158
31383
  },
30159
31384
  proxyUrl: proxy.url,
30160
31385
  stop: async () => {
31386
+ selfSleepScheduler?.stop();
30161
31387
  reminderSchedulerRef?.clearAll();
30162
31388
  for (const agentId of [...typingHeartbeats.keys()]) {
30163
31389
  emitTypingStopsAndClear(agentId);
@@ -30529,8 +31755,8 @@ function projectDaemonLogRow(value, targetAgentId) {
30529
31755
  fields: projectedFields
30530
31756
  };
30531
31757
  }
30532
- var MANAGER_EVENTS = ["register", "wake", "spawned", "session", "root_work", "turn_end", "exit", "tick", "reset_session", "begin_reset", "rewake_after_reset", "runtime_signal", "admission_started", "admission_settled"];
30533
- var MANAGER_EFFECTS = ["spawn", "send", "stop", "terminate_stalled", "force_exit", "gated_hold"];
31758
+ var MANAGER_EVENTS = ["register", "wake", "spawned", "session", "root_work", "turn_end", "exit", "tick", "reset_session", "begin_reset", "rewake_after_reset", "runtime_signal", "stall_control_failed", "admission_started", "admission_settled"];
31759
+ var MANAGER_EFFECTS = ["spawn", "send", "stop", "terminate_stalled", "clear_stall_recovery", "force_exit", "gated_hold"];
30534
31760
  var AGENT_STATUSES = ["idle", "starting", "running", "stopping"];
30535
31761
  var DELIVERY_PHASES = ["idle", "admission_wait", "steering", "next_turn_queued", "compacting", "reviewing", "tool_wait", "working"];
30536
31762
  var RESUME_OUTCOMES = ["not_requested", "pending", "resumed", "reset_required", "failed"];
@@ -30539,6 +31765,8 @@ var TERMINATION_CAUSES = ["runtime_error", "killed_stalled", "other"];
30539
31765
  var SPAWN_FAILURE_REASONS = ["ENOENT", "handshake_timeout", "pre_handshake_exit", "spawn_threw", "other"];
30540
31766
  var TERMINATION_SEMANTICS = ["killed_stalled", "idle_stop", "force_exit", "other"];
30541
31767
  var ABORT_CAUSES = ["start_threw", "start_rejected", "send_threw", "spawn_failure", "handshake_timeout", "reset", "nap", "model_switch", "requested_stop", "shutdown", "physical_exit", "terminate_stalled", "force_exit", "other"];
31768
+ var NATIVE_ACTIVITY_KINDS = ["turn_started", "backend_turn_started", "thinking", "text", "tool_call", "tool_output", "internal_progress", "recovery", "turn_end"];
31769
+ var RUNTIME_PHASES = ["idle", "admission", "inference", "tool", "recovery", "terminal"];
30542
31770
  var EXIT_SIGNALS = new Set(["SIGABRT", "SIGALRM", "SIGBREAK", "SIGBUS", "SIGCHLD", "SIGCONT", "SIGFPE", "SIGHUP", "SIGILL", "SIGINFO", "SIGINT", "SIGIO", "SIGIOT", "SIGKILL", "SIGLOST", "SIGPIPE", "SIGPOLL", "SIGPROF", "SIGPWR", "SIGQUIT", "SIGSEGV", "SIGSTKFLT", "SIGSTOP", "SIGSYS", "SIGTERM", "SIGTRAP", "SIGTSTP", "SIGTTIN", "SIGTTOU", "SIGURG", "SIGUSR1", "SIGUSR2", "SIGVTALRM", "SIGWINCH", "SIGXCPU", "SIGXFSZ", "other"]);
30543
31771
  function enumValue(value, values, bucket = false, maxChars = 64) {
30544
31772
  if (!boundedString(value, maxChars))
@@ -30576,6 +31804,13 @@ function copyString(row, output, key, maxChars, optional2 = false, nullable2 = f
30576
31804
  output[key] = row[key];
30577
31805
  return true;
30578
31806
  }
31807
+ function copyScrubbedString(row, output, key, maxChars, optional2 = false, nullable2 = false) {
31808
+ if (!copyString(row, output, key, maxChars, optional2, nullable2))
31809
+ return false;
31810
+ if (typeof output[key] === "string")
31811
+ output[key] = scrubDiagnosticText(output[key]);
31812
+ return true;
31813
+ }
30579
31814
  function projectTraceBase(row, targetAgentId) {
30580
31815
  if (row.agentId !== targetAgentId || !boundedString(row.agentId, 128) || !canonicalTime(row.timeIso))
30581
31816
  return null;
@@ -30602,11 +31837,29 @@ function projectFsm(row, targetAgentId) {
30602
31837
  Object.assign(output, { event, status, turnActive: row.turnActive });
30603
31838
  if (!copyInteger(row, output, "inbox") || !copyInteger(row, output, "lastDeliverAt", { nullable: true }) || !copyInteger(row, output, "lastProgressAt") || !copyInteger(row, output, "idleSince", { nullable: true }))
30604
31839
  return null;
31840
+ if (!copyInteger(row, output, "lastNativeActivityAt", { optional: true }) || !copyScrubbedString(row, output, "backendTurnId", 512, true, true) || !copyInteger(row, output, "turnSilenceBudgetMs", { optional: true }) || !copyInteger(row, output, "nativeDeadlineAt", { optional: true, nullable: true }) || !copyInteger(row, output, "recoveryExtensionsUsed", { optional: true }))
31841
+ return null;
31842
+ if ("lastNativeActivityKind" in row && row.lastNativeActivityKind !== undefined) {
31843
+ if (row.lastNativeActivityKind === null)
31844
+ output.lastNativeActivityKind = null;
31845
+ else {
31846
+ const kind = enumValue(row.lastNativeActivityKind, NATIVE_ACTIVITY_KINDS);
31847
+ if (!kind)
31848
+ return null;
31849
+ output.lastNativeActivityKind = kind;
31850
+ }
31851
+ }
31852
+ if ("runtimePhase" in row && row.runtimePhase !== undefined) {
31853
+ const phase = enumValue(row.runtimePhase, RUNTIME_PHASES);
31854
+ if (!phase)
31855
+ return null;
31856
+ output.runtimePhase = phase;
31857
+ }
30605
31858
  output.resetting = row.resetting;
30606
31859
  if (!copyInteger(row, output, "resettingSince", { nullable: true }) || !copyInteger(row, output, "stoppingSince", { nullable: true }))
30607
31860
  return null;
30608
31861
  output.deliveryPhase = deliveryPhase;
30609
- if (!copyInteger(row, output, "sinceProgressMs") || !copyInteger(row, output, "sinceDeliverMs", { nullable: true }) || !copyInteger(row, output, "sinceStoppingMs", { nullable: true }) || !projectOptionalSpanMetadata(row, output))
31862
+ if (!copyInteger(row, output, "sinceProgressMs") || !copyInteger(row, output, "sinceNativeActivityMs", { optional: true }) || !copyInteger(row, output, "sinceDeliverMs", { nullable: true }) || !copyInteger(row, output, "sinceStoppingMs", { nullable: true }) || !projectOptionalSpanMetadata(row, output))
30610
31863
  return null;
30611
31864
  const metricKeys = [
30612
31865
  "physicalOpenCount",
@@ -30737,7 +31990,7 @@ function projectStatusRow(value, targetAgentId) {
30737
31990
  return output;
30738
31991
  }
30739
31992
  // src/diagnostics/bundle.ts
30740
- import { createHash as createHash2 } from "node:crypto";
31993
+ import { createHash as createHash3 } from "node:crypto";
30741
31994
  import { createWriteStream, unlinkSync as unlinkSync5 } from "node:fs";
30742
31995
  import { Readable, Transform } from "node:stream";
30743
31996
  import { pipeline } from "node:stream/promises";
@@ -30887,7 +32140,7 @@ async function buildDiagnosticBundle(args) {
30887
32140
  const { footer, total } = envelope();
30888
32141
  if (total > maxUncompressed)
30889
32142
  throw new BundleTooLargeError;
30890
- const hash2 = createHash2("sha256");
32143
+ const hash2 = createHash3("sha256");
30891
32144
  let compressedBytes = 0;
30892
32145
  const meter = new Transform({
30893
32146
  transform(chunk2, _encoding, callback) {
@@ -30928,7 +32181,7 @@ async function buildDiagnosticBundle(args) {
30928
32181
  };
30929
32182
  }
30930
32183
  // src/diagnostics/coordinator.ts
30931
- import { createHash as createHash3, randomBytes as randomBytes5 } from "node:crypto";
32184
+ import { createHash as createHash4, randomBytes as randomBytes5 } from "node:crypto";
30932
32185
  import {
30933
32186
  chmodSync as chmodSync2,
30934
32187
  closeSync as closeSync4,
@@ -31084,7 +32337,7 @@ function createDiagnosticReportCoordinator(args) {
31084
32337
  if (!stat.isFile() || stat.isSymbolicLink())
31085
32338
  return null;
31086
32339
  const bytes = readFileSync6(path11);
31087
- return { sizeBytes: bytes.byteLength, sha256: createHash3("sha256").update(bytes).digest("hex") };
32340
+ return { sizeBytes: bytes.byteLength, sha256: createHash4("sha256").update(bytes).digest("hex") };
31088
32341
  } catch {
31089
32342
  return null;
31090
32343
  }
@@ -31974,6 +33227,10 @@ async function runPreparedDaemon(prepared, opts) {
31974
33227
  onAuthRejected: () => {
31975
33228
  log2.error("machine key rejected by server — is it correct / has it expired?");
31976
33229
  shutdown(1);
33230
+ },
33231
+ onSelfSleep: () => {
33232
+ log2.info("daemon self-sleep threshold reached");
33233
+ shutdown(0);
31977
33234
  }
31978
33235
  });
31979
33236
  } catch (error51) {
@@ -32984,14 +34241,16 @@ var LOCAL_MESSAGE_REMINDER_PATH2 = "/__alook/local/message-reminder";
32984
34241
  var MIN_REMINDER_MS = 60000;
32985
34242
  var MAX_REMINDER_MS = 24 * 60 * 60000;
32986
34243
  function parseRemindAfter(value) {
34244
+ if (value === "0")
34245
+ return 0;
32987
34246
  const match = /^(\d+)(m|h)$/.exec(value);
32988
34247
  if (!match) {
32989
- throw new Error("message send: --remind-after must be a positive integer followed by m or h (1m..24h)");
34248
+ throw new Error("message send: --remind-after must be 0 or a positive integer followed by m or h (1m..24h)");
32990
34249
  }
32991
34250
  const amount = Number(match[1]);
32992
34251
  const milliseconds = amount * (match[2] === "h" ? 60 * 60000 : 60000);
32993
34252
  if (!Number.isSafeInteger(milliseconds) || milliseconds < MIN_REMINDER_MS || milliseconds > MAX_REMINDER_MS) {
32994
- throw new Error("message send: --remind-after must be between 1m and 24h");
34253
+ throw new Error("message send: --remind-after must be 0 or between 1m and 24h");
32995
34254
  }
32996
34255
  return milliseconds;
32997
34256
  }
@@ -33126,7 +34385,8 @@ async function readLiteralInput(args) {
33126
34385
  if (!stdin)
33127
34386
  throw new CliError(`${command}: stdin is unavailable`);
33128
34387
  if (stdin.isTTY === true) {
33129
- throw new CliError(`${command}: --stdin requires piped input; use ${fileOption} in an interactive terminal`);
34388
+ const suffix = fileOption ? `; use ${fileOption} in an interactive terminal` : "";
34389
+ throw new CliError(`${command}: --stdin requires piped input${suffix}`);
33130
34390
  }
33131
34391
  try {
33132
34392
  const chunks = [];
@@ -33148,7 +34408,12 @@ async function readLiteralInput(args) {
33148
34408
  }
33149
34409
  return;
33150
34410
  }
34411
+ var MALFORMED_ALOOK_HEREDOC_TAIL = /(^|\r?\n)(?:["']ALOOK_MESSAGE_[A-Z0-9_]+["']?|ALOOK_MESSAGE_[A-Z0-9_]+["'])(?:\r?\n)?$/;
34412
+ function stripMalformedAlookHeredocTail(input) {
34413
+ return input.replace(MALFORMED_ALOOK_HEREDOC_TAIL, "$1");
34414
+ }
33151
34415
  var CLIENT_MAX_ATTACHMENT_BYTES = 26214400;
34416
+ var CLIENT_MAX_MESSAGE_BODY_BYTES = 1024;
33152
34417
  function contentTypeFromFilename(filename) {
33153
34418
  const ext = filename.slice(filename.lastIndexOf(".") + 1).toLowerCase();
33154
34419
  switch (ext) {
@@ -33208,24 +34473,28 @@ async function sendWithRetry(api2, req) {
33208
34473
  }
33209
34474
  async function cmdMessageSend(opts, stdin) {
33210
34475
  const remindAfterFlag = opts.remindAfter;
33211
- const remindAfterMs = remindAfterFlag === undefined ? undefined : parseRemindAfter(remindAfterFlag);
34476
+ const remindAfterMs = parseRemindAfter(remindAfterFlag);
33212
34477
  const api2 = getApi();
33213
34478
  const agent2 = agentId(opts);
33214
34479
  const channel2 = opts.target;
33215
34480
  if (!channel2)
33216
34481
  throw new CliError("message send: --target <ref> is required (e.g. /demo-workspace#1234/general)");
33217
- const fileFlag = opts.file;
33218
- const text2 = await readLiteralInput({
34482
+ const literalText = await readLiteralInput({
33219
34483
  command: "message send",
33220
34484
  stdinSelected: opts.stdin === true,
33221
- stdin,
33222
- filePath: fileFlag,
33223
- fileOption: "--file <path>"
34485
+ stdin
33224
34486
  });
34487
+ const text2 = stripMalformedAlookHeredocTail(literalText ?? "");
34488
+ const textBytes = Buffer.byteLength(text2, "utf8");
34489
+ if (textBytes > CLIENT_MAX_MESSAGE_BODY_BYTES) {
34490
+ throw new CliError(`message send: --stdin body is ${textBytes} bytes; max ${CLIENT_MAX_MESSAGE_BODY_BYTES}. Rewrite it before retrying.
34491
+
34492
+ ${MESSAGE_SEND_STDIN_POLICY}`);
34493
+ }
33225
34494
  const attachmentIds = Array.isArray(opts.attachment) ? opts.attachment : [];
33226
- const hasText = typeof text2 === "string" && text2.trim().length > 0;
34495
+ const hasText = text2.trim().length > 0;
33227
34496
  if (!hasText && attachmentIds.length === 0) {
33228
- throw new CliError("message send: --stdin, --file <path>, or --attachment <id> is required");
34497
+ throw new CliError("message send: --stdin must contain text unless --attachment <id> is present");
33229
34498
  }
33230
34499
  let replyToSeq;
33231
34500
  const replyFlag = opts.reply;
@@ -33251,8 +34520,6 @@ async function cmdMessageSend(opts, stdin) {
33251
34520
  throw new CliError(`channel not aligned: ${res.unreadCount} unread message(s) in ${channel2} (latest #${res.latestSeq}). Run \`alook inbox pull\` and READ the new messages before deciding whether to resend, adjust, or skip your message.`);
33252
34521
  }
33253
34522
  const sent = `${res.message.channel}${res.message.seq}`;
33254
- if (remindAfterMs === undefined)
33255
- return { sent };
33256
34523
  const seqText = res.message.seq.replace(/^#/, "");
33257
34524
  const sentSeq = Number(seqText);
33258
34525
  if (!/^\d+$/.test(seqText) || !Number.isSafeInteger(sentSeq) || sentSeq < 1) {
@@ -33586,7 +34853,7 @@ function buildProgram(stdin) {
33586
34853
  }).option("--agent <id>", "agent identity (or ALOOK_AGENT_ID env)");
33587
34854
  const message2 = program.command("message").description("message operations").exitOverride();
33588
34855
  message2.configureOutput({ writeOut: () => {}, writeErr: () => {} });
33589
- message2.command("send").description("send a message to a channel, DM, or thread").option("--target <ref>", "destination (path-style ref, e.g. /demo-workspace#1234/general)").option("--stdin", "read the literal UTF-8 message body from non-TTY stdin").option("--file <path>", "read the literal UTF-8 message body from a file").option("-a, --attachment <id>", "attach an uploaded file by id (repeatable — order = message order)", (v, prev = []) => [...prev, v], []).option("--reply <seq>", 'reply to a message by its seq in --target (e.g. "#37" or 37)').option("--remind-after <duration>", "optionally arm one local follow-up wake after 1m..24h; a newer same-scope message or daemon restart cancels it").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
34856
+ message2.command("send").description("send a message to a channel, DM, or thread").option("--target <ref>", "destination (path-style ref, e.g. /demo-workspace#1234/general)").requiredOption("--stdin", "read the required UTF-8 message body from non-TTY stdin (max 1 KiB)").option("-a, --attachment <id>", "attach an uploaded file by id (repeatable — order = message order)", (v, prev = []) => [...prev, v], []).option("--reply <seq>", 'reply to a message by its seq in --target (e.g. "#37" or 37)').requiredOption("--remind-after <0|Nm|Nh>", "required idle follow-up: 0 disables; 1m..24h arms/resets one same-scope timer; a newer message or daemon restart cancels it").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
33590
34857
  const localOpts = this.opts();
33591
34858
  const globalOpts = program.opts();
33592
34859
  const result = await cmdMessageSend({ ...globalOpts, ...localOpts }, stdin);