@alook/daemon 0.1.29 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -119,6 +119,9 @@ function createDefaultAgentDriverHost(options = {}) {
119
119
  };
120
120
  }
121
121
 
122
+ // agent-driver/dist/adapters/claude/index.js
123
+ import { randomUUID as randomUUID5 } from "node:crypto";
124
+
122
125
  // agent-driver/dist/controller/process-host.js
123
126
  import { EventEmitter } from "events";
124
127
 
@@ -698,6 +701,7 @@ class ProcessLane {
698
701
  admissionSequence = 0;
699
702
  activeTerminalReceipt;
700
703
  stopPromise;
704
+ pendingInterrupt;
701
705
  pendingPromptAdmission;
702
706
  constructor(driver, ctx, opts = {}) {
703
707
  this.driver = driver;
@@ -737,12 +741,14 @@ class ProcessLane {
737
741
  try {
738
742
  spawned = await this.driver.spawn(launchCtx);
739
743
  } catch (error) {
744
+ this.settlePendingInterrupt(false);
740
745
  this.settlePendingPrompt({ ok: false, reason: "runtime_error", error: String(error) });
741
746
  throw error;
742
747
  }
743
748
  const { process: proc } = spawned;
744
749
  this.process = proc;
745
750
  this.attachProcess(proc);
751
+ this.dispatchPendingInterrupt(proc);
746
752
  this.armPendingPromptTimeout();
747
753
  if (this.driver.execution.lifetime === "session" && !this.writableStdin(proc)) {
748
754
  const rejection = this.stdinUnavailableAdmission();
@@ -841,6 +847,7 @@ class ProcessLane {
841
847
  pending.resolve(admission);
842
848
  }
843
849
  stop(opts = {}) {
850
+ this.settlePendingInterrupt(false);
844
851
  this.settlePendingPrompt({ ok: false, reason: "closed", error: "runtime lane stopped before command admission" });
845
852
  if (!this.process)
846
853
  return Promise.resolve();
@@ -859,11 +866,39 @@ class ProcessLane {
859
866
  proc.kill(opts?.signal ?? "SIGTERM");
860
867
  }
861
868
  }
862
- async interrupt() {
869
+ async interrupt(input) {
863
870
  const proc = this.process;
864
- if (!proc || this.closed)
871
+ if (!proc) {
872
+ if (!this.started)
873
+ return false;
874
+ if (this.pendingInterrupt)
875
+ return this.pendingInterrupt.promise;
876
+ let resolve;
877
+ let reject;
878
+ const promise = new Promise((res, rej) => {
879
+ resolve = res;
880
+ reject = rej;
881
+ });
882
+ this.pendingInterrupt = { input, promise, resolve, reject };
883
+ return promise;
884
+ }
885
+ if (this.closed)
865
886
  return false;
866
- return proc.kill("SIGINT");
887
+ return this.driver.interrupt(input, proc);
888
+ }
889
+ dispatchPendingInterrupt(proc) {
890
+ const pending = this.pendingInterrupt;
891
+ if (!pending)
892
+ return;
893
+ this.pendingInterrupt = undefined;
894
+ this.driver.interrupt(pending.input, proc).then(pending.resolve, pending.reject);
895
+ }
896
+ settlePendingInterrupt(accepted) {
897
+ const pending = this.pendingInterrupt;
898
+ if (!pending)
899
+ return;
900
+ this.pendingInterrupt = undefined;
901
+ pending.resolve(accepted);
867
902
  }
868
903
  updateSettings(input) {
869
904
  return this.driver.updateSettings?.(input) ?? Promise.resolve({ status: "unsupported" });
@@ -1200,6 +1235,13 @@ function tryParseJsonLine(line) {
1200
1235
 
1201
1236
  // agent-driver/dist/adapters/claude/normalizer.js
1202
1237
  var API_ERROR_RE = /API Error:.*(?:Connection error|\b[45]\d{2}\b)/i;
1238
+ var CLAUDE_RESULT_ERROR_CODES = new Map([
1239
+ ["error_max_turns", "claude.error_max_turns"],
1240
+ ["error_during_execution", "claude.error_during_execution"],
1241
+ ["error_max_budget_usd", "claude.error_max_budget_usd"],
1242
+ ["error_max_structured_output_retries", "claude.error_max_structured_output_retries"]
1243
+ ]);
1244
+ var CLAUDE_INTERRUPT_TERMINAL_REASONS = new Set(["aborted_streaming", "aborted_tools"]);
1203
1245
  var CLAUDE_USAGE_COMPONENTS = [
1204
1246
  "inputTokens",
1205
1247
  "outputTokens",
@@ -1293,9 +1335,9 @@ class ClaudeEventNormalizer {
1293
1335
  out.push({ kind: "assistant_reasoning_completed", text: block.thinking ?? "" });
1294
1336
  } else if (block?.type === "text") {
1295
1337
  const text = block.text ?? "";
1296
- if (API_ERROR_RE.test(text))
1297
- out.push({ kind: "error", message: text });
1298
- else
1338
+ if (API_ERROR_RE.test(text)) {
1339
+ out.push({ kind: "error", code: "claude.api_error", message: text });
1340
+ } else
1299
1341
  completedText.push(text);
1300
1342
  } else if (block?.type === "tool_use") {
1301
1343
  out.push({ kind: "tool_call", name: block.name ?? "unknown_tool", input: block.input });
@@ -1321,14 +1363,19 @@ class ClaudeEventNormalizer {
1321
1363
  }
1322
1364
  handleResult(event, out) {
1323
1365
  const rawOwner = typeof event.user_message_uuid === "string" && event.user_message_uuid.length > 0 ? event.user_message_uuid : null;
1324
- const turnOwner = rawOwner ? this.turnProtocol?.claimResult(rawOwner) ?? (this.turnProtocol ? null : `claude:${rawOwner}`) : null;
1366
+ const interruptedTerminal = Boolean(this.turnProtocol && !rawOwner && event.subtype === "error_during_execution" && CLAUDE_INTERRUPT_TERMINAL_REASONS.has(event.terminal_reason));
1367
+ const turnOwner = rawOwner ? this.turnProtocol?.claimResult(rawOwner) ?? (this.turnProtocol ? null : `claude:${rawOwner}`) : interruptedTerminal ? this.turnProtocol.claimInterruptedResult() : null;
1325
1368
  if (this.turnProtocol && !turnOwner)
1326
1369
  return;
1327
1370
  const usage = this.buildUsageTelemetry(event, rawOwner);
1328
1371
  if (usage)
1329
1372
  out.push(usage);
1330
- if (event.is_error || event.subtype === "error_during_execution") {
1331
- out.push({ kind: "error", message: String(event.result ?? "Claude runtime error") });
1373
+ if (!interruptedTerminal && (event.is_error || event.subtype === "error_during_execution")) {
1374
+ out.push({
1375
+ kind: "error",
1376
+ code: CLAUDE_RESULT_ERROR_CODES.get(event.subtype) ?? "claude.result_error",
1377
+ message: String(event.result ?? "Claude runtime error")
1378
+ });
1332
1379
  }
1333
1380
  out.push({
1334
1381
  kind: "turn_end",
@@ -1606,6 +1653,7 @@ class ClaudeTurnProtocol {
1606
1653
  segmentOwnerUuid = null;
1607
1654
  delivered = new Map;
1608
1655
  awaitingFollowOn = false;
1656
+ interruptRequested = false;
1609
1657
  finalized = false;
1610
1658
  beginTurn() {
1611
1659
  const uuid = randomUUID4();
@@ -1614,6 +1662,7 @@ class ClaudeTurnProtocol {
1614
1662
  this.delivered.clear();
1615
1663
  this.delivered.set(uuid, { acknowledged: false, followOn: false });
1616
1664
  this.awaitingFollowOn = false;
1665
+ this.interruptRequested = false;
1617
1666
  this.finalized = false;
1618
1667
  return this.receipt(uuid);
1619
1668
  }
@@ -1640,6 +1689,13 @@ class ClaudeTurnProtocol {
1640
1689
  return false;
1641
1690
  return this.delivered.get(this.rootUuid)?.acknowledged === true;
1642
1691
  }
1692
+ hasActiveTurn() {
1693
+ return this.rootUuid !== null && !this.finalized;
1694
+ }
1695
+ noteInterruptRequested() {
1696
+ if (this.hasActiveTurn())
1697
+ this.interruptRequested = true;
1698
+ }
1643
1699
  claimResult(userMessageUuid) {
1644
1700
  if (this.finalized || !this.rootUuid || userMessageUuid !== this.segmentOwnerUuid)
1645
1701
  return null;
@@ -1656,6 +1712,19 @@ class ClaudeTurnProtocol {
1656
1712
  this.finalized = true;
1657
1713
  return this.receipt(this.rootUuid);
1658
1714
  }
1715
+ claimInterruptedResult() {
1716
+ if (this.finalized || !this.rootUuid || !this.interruptRequested)
1717
+ return null;
1718
+ const unacknowledged = [...this.delivered.values()].filter((input) => !input.acknowledged);
1719
+ if (unacknowledged.length > 0) {
1720
+ this.awaitingFollowOn = true;
1721
+ for (const input of unacknowledged)
1722
+ input.followOn = true;
1723
+ return null;
1724
+ }
1725
+ this.finalized = true;
1726
+ return this.receipt(this.rootUuid);
1727
+ }
1659
1728
  receipt(uuid) {
1660
1729
  return `claude:${uuid}`;
1661
1730
  }
@@ -1681,7 +1750,9 @@ class ClaudeDriver {
1681
1750
  };
1682
1751
  turnProtocol = new ClaudeTurnProtocol;
1683
1752
  eventNormalizer = new ClaudeEventNormalizer(this.turnProtocol);
1753
+ pendingInterrupt;
1684
1754
  beginTurn() {
1755
+ this.pendingInterrupt = undefined;
1685
1756
  const receipt = this.turnProtocol.beginTurn();
1686
1757
  this.eventNormalizer.beginTurn();
1687
1758
  return receipt;
@@ -1720,11 +1791,45 @@ class ClaudeDriver {
1720
1791
  return { process: proc };
1721
1792
  }
1722
1793
  normalizeLine(line) {
1723
- return this.eventNormalizer.normalizeLine(line);
1794
+ const events = this.eventNormalizer.normalizeLine(line);
1795
+ this.flushPendingInterrupt();
1796
+ return events;
1724
1797
  }
1725
1798
  get currentSessionId() {
1726
1799
  return this.eventNormalizer.currentSessionId;
1727
1800
  }
1801
+ async interrupt(input, process2) {
1802
+ const stdin = process2.stdin;
1803
+ if (!this.turnProtocol.hasActiveTurn() || !stdin || stdin.destroyed || stdin.writableEnded || stdin.writable === false)
1804
+ return false;
1805
+ this.turnProtocol.noteInterruptRequested();
1806
+ if (!this.turnProtocol.acceptsTurnWork()) {
1807
+ this.pendingInterrupt ??= { input, process: process2 };
1808
+ return true;
1809
+ }
1810
+ this.writeInterrupt(input, process2);
1811
+ return true;
1812
+ }
1813
+ writeInterrupt(input, process2) {
1814
+ process2.stdin.write(JSON.stringify({
1815
+ type: "control_request",
1816
+ request_id: input.requestId ?? randomUUID5(),
1817
+ request: { subtype: "interrupt" }
1818
+ }) + `
1819
+ `);
1820
+ }
1821
+ flushPendingInterrupt() {
1822
+ const pending = this.pendingInterrupt;
1823
+ if (!pending || !this.turnProtocol.acceptsTurnWork())
1824
+ return;
1825
+ this.pendingInterrupt = undefined;
1826
+ const stdin = pending.process.stdin;
1827
+ if (!stdin || stdin.destroyed || stdin.writableEnded || stdin.writable === false)
1828
+ return;
1829
+ try {
1830
+ this.writeInterrupt(pending.input, pending.process);
1831
+ } catch {}
1832
+ }
1728
1833
  encodeMessage(text, sessionId, opts) {
1729
1834
  const uuid = opts?.mode === "idle" ? this.turnProtocol.rootInputUuid() : this.turnProtocol.steeringInputUuid();
1730
1835
  return JSON.stringify({
@@ -1856,6 +1961,35 @@ import { randomBytes } from "node:crypto";
1856
1961
  var codexQuotaSourceEpoch = randomBytes(16).toString("base64url");
1857
1962
  var codexQuotaSourceGeneration = 0;
1858
1963
  var codexAccountFingerprint = null;
1964
+ var CODEX_ERROR_INFO_CODES = new Set([
1965
+ "contextWindowExceeded",
1966
+ "sessionBudgetExceeded",
1967
+ "usageLimitExceeded",
1968
+ "serverOverloaded",
1969
+ "cyberPolicy",
1970
+ "misalignmentPolicyViolation",
1971
+ "internalServerError",
1972
+ "unauthorized",
1973
+ "badRequest",
1974
+ "threadRollbackFailed",
1975
+ "sandboxError",
1976
+ "other",
1977
+ "httpConnectionFailed",
1978
+ "responseStreamConnectionFailed",
1979
+ "responseStreamDisconnected",
1980
+ "responseTooManyFailedAttempts",
1981
+ "activeTurnNotSteerable"
1982
+ ]);
1983
+ function snakeCase(value) {
1984
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
1985
+ }
1986
+ function codexErrorInfoCode(value, fallback) {
1987
+ const label = typeof value === "string" ? value : value && typeof value === "object" && !Array.isArray(value) ? Object.keys(value)[0] : undefined;
1988
+ return label && CODEX_ERROR_INFO_CODES.has(label) ? `codex.${snakeCase(label)}` : fallback;
1989
+ }
1990
+ function codexRpcErrorCode(value) {
1991
+ return typeof value === "number" && Number.isSafeInteger(value) ? `codex.rpc.${value}` : "codex.rpc_error";
1992
+ }
1859
1993
  function rotateCodexQuotaSource() {
1860
1994
  codexQuotaSourceEpoch = randomBytes(16).toString("base64url");
1861
1995
  codexQuotaSourceGeneration += 1;
@@ -2013,7 +2147,11 @@ class CodexEventNormalizer {
2013
2147
  return this.replaceQuotaSnapshots(msg.result ?? {});
2014
2148
  }
2015
2149
  if (msg?.error && msg.id !== undefined) {
2016
- return [{ kind: "error", message: msg.error?.message ?? "Codex RPC error" }];
2150
+ return [{
2151
+ kind: "error",
2152
+ code: codexRpcErrorCode(msg.error?.code),
2153
+ message: msg.error?.message ?? "Codex RPC error"
2154
+ }];
2017
2155
  }
2018
2156
  if (msg?.result?.thread?.id) {
2019
2157
  return this.adoptAndInit(msg.result.thread.id);
@@ -2082,20 +2220,26 @@ class CodexEventNormalizer {
2082
2220
  return [];
2083
2221
  this.releaseUsageForTurn(params.threadId, params.turn.id);
2084
2222
  if (params.turn.status === "failed") {
2223
+ const error = params.turn.error;
2085
2224
  return [
2086
- { kind: "error", message: "Codex turn failed" },
2225
+ {
2226
+ kind: "error",
2227
+ code: codexErrorInfoCode(error?.codexErrorInfo, "codex.turn_failed"),
2228
+ message: error?.message ?? "Codex turn failed"
2229
+ },
2087
2230
  { kind: "turn_end", sessionId: this.threadId ?? undefined, turnOwner: this.turnReceipt(params.threadId, params.turn.id) }
2088
2231
  ];
2089
2232
  }
2090
- if (params.turn.status === "interrupted") {
2091
- return [{ kind: "error", message: "Codex turn interrupted" }, { kind: "turn_end", sessionId: this.threadId ?? undefined, turnOwner: this.turnReceipt(params.threadId, params.turn.id) }];
2092
- }
2093
2233
  return [{ kind: "turn_end", sessionId: this.threadId ?? undefined, turnOwner: this.turnReceipt(params.threadId, params.turn.id) }];
2094
2234
  case "error":
2095
2235
  if (params?.willRetry === true) {
2096
2236
  return [{ kind: "runtime_recovery", stage: "retrying", source: "codex_stream" }];
2097
2237
  }
2098
- return [{ kind: "error", message: params?.error?.message ?? params?.message ?? "Codex error" }];
2238
+ return [{
2239
+ kind: "error",
2240
+ code: codexErrorInfoCode(params?.error?.codexErrorInfo, "codex.error"),
2241
+ message: params?.error?.message ?? params?.message ?? "Codex error"
2242
+ }];
2099
2243
  case "thread/tokenUsage/updated":
2100
2244
  return [];
2101
2245
  case "account/rateLimits/updated":
@@ -2192,42 +2336,46 @@ class CodexEventNormalizer {
2192
2336
  }
2193
2337
  handleItemStarted(params) {
2194
2338
  const t = params?.item?.type ?? params?.type;
2339
+ const callId = this.itemCallId(params);
2340
+ const identity = callId ? { callId } : {};
2195
2341
  switch (t) {
2196
2342
  case "commandExecution":
2197
- return [{ kind: "tool_call", name: "shell", input: params?.item }];
2343
+ return [{ kind: "tool_call", ...identity, name: "shell", input: params?.item }];
2198
2344
  case "contextCompaction":
2199
2345
  return [{ kind: "compaction_started" }];
2200
2346
  case "enteredReviewMode":
2201
2347
  return [{ kind: "review_started" }];
2202
2348
  case "fileChange":
2203
- return [{ kind: "tool_call", name: "file_change", input: normalizeFileChangeInput(params?.item) }];
2349
+ return [{ kind: "tool_call", ...identity, name: "file_change", input: normalizeFileChangeInput(params?.item) }];
2204
2350
  case "mcpToolCall":
2205
- return [{ kind: "tool_call", name: `mcp_${params?.item?.name ?? "tool"}`, input: params?.item }];
2351
+ return [{ kind: "tool_call", ...identity, name: `mcp_${params?.item?.tool ?? params?.item?.name ?? "tool"}`, input: params?.item }];
2206
2352
  case "webSearch":
2207
- return [{ kind: "tool_call", name: "web_search", input: params?.item }];
2353
+ return [{ kind: "tool_call", ...identity, name: "web_search", input: params?.item }];
2208
2354
  case "collabAgentToolCall":
2209
- return [{ kind: "tool_call", name: "collab_tool_call", input: params?.item }];
2355
+ return [{ kind: "tool_call", ...identity, name: "collab_tool_call", input: params?.item }];
2210
2356
  default:
2211
2357
  return [];
2212
2358
  }
2213
2359
  }
2214
2360
  handleItemCompleted(params) {
2215
2361
  const t = params?.item?.type ?? params?.type;
2362
+ const callId = this.itemCallId(params);
2363
+ const identity = callId ? { callId } : {};
2216
2364
  switch (t) {
2217
2365
  case "commandExecution":
2218
- return [{ kind: "tool_output", name: "shell" }];
2366
+ return [{ kind: "tool_output", ...identity, name: "shell" }];
2219
2367
  case "contextCompaction":
2220
2368
  return [{ kind: "compaction_finished" }];
2221
2369
  case "exitedReviewMode":
2222
2370
  return [{ kind: "review_finished" }];
2223
2371
  case "fileChange":
2224
- return [{ kind: "tool_output", name: "file_change" }];
2372
+ return [{ kind: "tool_output", ...identity, name: "file_change" }];
2225
2373
  case "mcpToolCall":
2226
- return [{ kind: "tool_output", name: `mcp_${params?.item?.name ?? "tool"}` }];
2374
+ return [{ kind: "tool_output", ...identity, name: `mcp_${params?.item?.tool ?? params?.item?.name ?? "tool"}` }];
2227
2375
  case "webSearch":
2228
- return [{ kind: "tool_output", name: "web_search" }];
2376
+ return [{ kind: "tool_output", ...identity, name: "web_search" }];
2229
2377
  case "collabAgentToolCall":
2230
- return [{ kind: "tool_output", name: "collab_tool_call" }];
2378
+ return [{ kind: "tool_output", ...identity, name: "collab_tool_call" }];
2231
2379
  case "agentMessage":
2232
2380
  return [{ kind: "assistant_message_completed", text: params?.item?.text ?? "" }];
2233
2381
  case "reasoning":
@@ -2236,6 +2384,10 @@ class CodexEventNormalizer {
2236
2384
  return [];
2237
2385
  }
2238
2386
  }
2387
+ itemCallId(params) {
2388
+ const value = params?.item?.id ?? params?.itemId;
2389
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
2390
+ }
2239
2391
  }
2240
2392
 
2241
2393
  // agent-driver/dist/adapters/codex/home.js
@@ -2706,6 +2858,16 @@ class CodexDriver {
2706
2858
  get currentSessionId() {
2707
2859
  return this.eventNormalizer.currentSessionId;
2708
2860
  }
2861
+ async interrupt(_input, process2) {
2862
+ const threadId = this.eventNormalizer.currentSessionId;
2863
+ const turnId = this.eventNormalizer.currentTurnId;
2864
+ const stdin = process2.stdin;
2865
+ if (!threadId || !turnId || !stdin || stdin.destroyed || stdin.writableEnded || stdin.writable === false)
2866
+ return false;
2867
+ stdin.write(jsonRpcRequest("turn/interrupt", { threadId, turnId }, this.nextRequestId()) + `
2868
+ `);
2869
+ return true;
2870
+ }
2709
2871
  buildTurnStart(threadId, text) {
2710
2872
  return jsonRpcRequest("turn/start", { threadId, input: [{ type: "text", text }] }, this.nextRequestId());
2711
2873
  }
@@ -3205,7 +3367,7 @@ class CursorAcpLane {
3205
3367
  return;
3206
3368
  const result = record2(value);
3207
3369
  if (!result || typeof result.stopReason !== "string" || !PROMPT_STOP_REASONS.has(result.stopReason)) {
3208
- this.failPrompt(prompt, new Error("Cursor ACP prompt response did not contain a supported stopReason"));
3370
+ this.failPrompt(prompt, new Error("Cursor ACP prompt response did not contain a supported stopReason"), "cursor.invalid_stop_reason");
3209
3371
  return;
3210
3372
  }
3211
3373
  const terminalOwner = this.terminalOwner ?? prompt.receipt;
@@ -3218,7 +3380,7 @@ class CursorAcpLane {
3218
3380
  turnOwner: terminalOwner
3219
3381
  });
3220
3382
  }
3221
- failPrompt(prompt, error) {
3383
+ failPrompt(prompt, error, code = "cursor.prompt_failed") {
3222
3384
  if (this.currentPromptRequestId !== prompt.requestId)
3223
3385
  return;
3224
3386
  const terminalOwner = this.terminalOwner ?? prompt.receipt;
@@ -3227,6 +3389,7 @@ class CursorAcpLane {
3227
3389
  this.openToolCalls.clear();
3228
3390
  this.events.emit("runtime_event", {
3229
3391
  kind: "error",
3392
+ code,
3230
3393
  message: error instanceof Error ? error.message : String(error)
3231
3394
  });
3232
3395
  this.events.emit("runtime_event", {
@@ -3380,9 +3543,10 @@ class CursorAcpLane {
3380
3543
  this.pending.delete(id);
3381
3544
  if (message.error !== undefined) {
3382
3545
  const payload = record2(message.error);
3383
- this.failPrompt(pending.prompt, new CursorAcpRpcError(pending.method, typeof payload?.code === "number" ? payload.code : undefined, rpcErrorMessage(message.error)));
3546
+ const rpcCode = typeof payload?.code === "number" && Number.isSafeInteger(payload.code) ? payload.code : undefined;
3547
+ this.failPrompt(pending.prompt, new CursorAcpRpcError(pending.method, rpcCode, rpcErrorMessage(message.error)), rpcCode === undefined ? "cursor.rpc_error" : `cursor.rpc.${rpcCode}`);
3384
3548
  } else if (!("result" in message)) {
3385
- this.failPrompt(pending.prompt, new Error("Cursor ACP response omitted result"));
3549
+ this.failPrompt(pending.prompt, new Error("Cursor ACP response omitted result"), "cursor.invalid_response");
3386
3550
  } else {
3387
3551
  this.completePrompt(pending.prompt, message.result);
3388
3552
  }
@@ -3667,7 +3831,11 @@ function parseModelRef(model) {
3667
3831
  function messageFromError(value) {
3668
3832
  const payload = record3(value);
3669
3833
  const message = typeof payload?.message === "string" && payload.message.trim() ? payload.message : undefined;
3670
- return message ? "OpenCode turn failed" : "OpenCode reported an inconsistent turn outcome";
3834
+ return message ?? "OpenCode reported an inconsistent turn outcome";
3835
+ }
3836
+ function codeFromError(value) {
3837
+ const payload = record3(value);
3838
+ return payload?.type === "unknown" ? "opencode.unknown" : "opencode.step_failed";
3671
3839
  }
3672
3840
 
3673
3841
  class OpenCodeServiceLane {
@@ -4335,7 +4503,10 @@ class OpenCodeServiceLane {
4335
4503
  root.outcomes.push({
4336
4504
  seq,
4337
4505
  ok: successful,
4338
- ...!successful ? { message: "OpenCode reported an unsupported final step outcome" } : {}
4506
+ ...!successful ? {
4507
+ code: "opencode.unsupported_finish",
4508
+ message: "OpenCode reported an unsupported final step outcome"
4509
+ } : {}
4339
4510
  });
4340
4511
  }
4341
4512
  const tokens = record3(data.tokens);
@@ -4364,7 +4535,12 @@ class OpenCodeServiceLane {
4364
4535
  break;
4365
4536
  }
4366
4537
  case "session.next.step.failed":
4367
- root.outcomes.push({ seq, ok: false, message: messageFromError(data.error) });
4538
+ root.outcomes.push({
4539
+ seq,
4540
+ ok: false,
4541
+ code: codeFromError(data.error),
4542
+ message: messageFromError(data.error)
4543
+ });
4368
4544
  break;
4369
4545
  default:
4370
4546
  break;
@@ -4544,11 +4720,11 @@ class OpenCodeServiceLane {
4544
4720
  return;
4545
4721
  const outcome = root.outcomes.filter((item) => item.seq > maxAdmission).sort((left, right) => left.seq - right.seq).at(-1);
4546
4722
  if (!root.interrupted && !outcome) {
4547
- this.settleRoot(root, false, "OpenCode drain settled without a durable turn outcome");
4723
+ this.settleRoot(root, false, "OpenCode drain settled without a durable turn outcome", "opencode.missing_outcome");
4548
4724
  return;
4549
4725
  }
4550
4726
  if (outcome && !outcome.ok) {
4551
- this.settleRoot(root, false, outcome.message ?? "OpenCode turn failed");
4727
+ this.settleRoot(root, false, outcome.message ?? "OpenCode turn failed", outcome.code ?? "opencode.turn_failed");
4552
4728
  return;
4553
4729
  }
4554
4730
  this.settleRoot(root, true);
@@ -4574,7 +4750,7 @@ class OpenCodeServiceLane {
4574
4750
  barrierStillCurrent(root, identity, generation) {
4575
4751
  return this.activeRoot === root && root.generation === generation && root.pendingAdmissions === 0 && !root.interruptPending && this.isIdentity(identity) && this.identityMatches();
4576
4752
  }
4577
- settleRoot(root, success, message) {
4753
+ settleRoot(root, success, message, code = "opencode.turn_failed") {
4578
4754
  if (this.activeRoot !== root)
4579
4755
  return;
4580
4756
  this.activeRoot = null;
@@ -4582,6 +4758,7 @@ class OpenCodeServiceLane {
4582
4758
  if (!success) {
4583
4759
  this.events.emit("runtime_event", {
4584
4760
  kind: "error",
4761
+ code,
4585
4762
  message: message ?? "OpenCode turn failed"
4586
4763
  });
4587
4764
  }
@@ -5097,7 +5274,11 @@ function mapPiSdkEvent(event, sessionId, state) {
5097
5274
  return [{ kind: "assistant_message_completed", text: d.content ?? "" }];
5098
5275
  }
5099
5276
  case "error":
5100
- return [{ kind: "error", message: d.error?.errorMessage ?? "Pi error" }];
5277
+ return [{
5278
+ kind: "error",
5279
+ code: "pi.message_error",
5280
+ message: d.error?.errorMessage ?? "Pi error"
5281
+ }];
5101
5282
  default:
5102
5283
  return [];
5103
5284
  }
@@ -5685,7 +5866,9 @@ class LogicalAgentSession {
5685
5866
  interruptedTurnId;
5686
5867
  processTurnEnded = false;
5687
5868
  stopRequestId;
5688
- outstandingToolUses = 0;
5869
+ anonymousOutstandingToolUses = 0;
5870
+ outstandingToolCallIds = new Set;
5871
+ toolLifecycleMismatchCount = 0;
5689
5872
  compacting = false;
5690
5873
  reviewing = false;
5691
5874
  steeringDeliveries = new Set;
@@ -5875,6 +6058,9 @@ class LogicalAgentSession {
5875
6058
  queueDwellCount: this.metricValue(this.queueDwellCount),
5876
6059
  queueDwellTotalMs: this.metricValue(this.queueDwellTotalMs),
5877
6060
  sseReconnectCount: this.metricValue(this.sseReconnectCount),
6061
+ outstandingToolUses: this.outstandingToolUseCount(),
6062
+ anonymousOutstandingToolUses: this.metricValue(this.anonymousOutstandingToolUses),
6063
+ toolLifecycleMismatchCount: this.metricValue(this.toolLifecycleMismatchCount),
5878
6064
  resumeOutcome: this.resumeOutcome,
5879
6065
  terminalOwnerKind: this.adapter.execution.terminalOwnership
5880
6066
  }
@@ -6194,15 +6380,36 @@ class LogicalAgentSession {
6194
6380
  }
6195
6381
  return;
6196
6382
  case "tool_call":
6197
- this.outstandingToolUses += 1;
6198
- if (turnId)
6199
- this.emit({ type: "tool_started", turnId, name: event.name, input: jsonValue(event.input) });
6383
+ if (!turnId) {
6384
+ this.toolLifecycleMismatchCount += 1;
6385
+ return;
6386
+ }
6387
+ if (!this.startToolUse(event.callId))
6388
+ return;
6389
+ const startedCallId = this.toolCallId(event.callId);
6390
+ this.emit({
6391
+ type: "tool_started",
6392
+ turnId,
6393
+ ...startedCallId ? { callId: startedCallId } : {},
6394
+ name: event.name,
6395
+ input: jsonValue(event.input)
6396
+ });
6200
6397
  return;
6201
6398
  case "tool_output":
6202
- this.outstandingToolUses = Math.max(0, this.outstandingToolUses - 1);
6203
- if (turnId)
6204
- this.emit({ type: "tool_finished", turnId, name: event.name });
6205
- if (this.outstandingToolUses === 0 && !this.toolBoundaryFlushDisabled)
6399
+ if (!turnId) {
6400
+ this.toolLifecycleMismatchCount += 1;
6401
+ return;
6402
+ }
6403
+ if (!this.finishToolUse(event.callId))
6404
+ return;
6405
+ const finishedCallId = this.toolCallId(event.callId);
6406
+ this.emit({
6407
+ type: "tool_finished",
6408
+ turnId,
6409
+ ...finishedCallId ? { callId: finishedCallId } : {},
6410
+ name: event.name
6411
+ });
6412
+ if (this.outstandingToolUseCount() === 0 && !this.toolBoundaryFlushDisabled)
6206
6413
  this.flushSafeBoundaryQueue();
6207
6414
  return;
6208
6415
  case "compaction_started":
@@ -6258,7 +6465,7 @@ class LogicalAgentSession {
6258
6465
  }
6259
6466
  case "error":
6260
6467
  this.toolBoundaryFlushDisabled = true;
6261
- this.turnError = driverError("process", "runtime_error", event.message, true);
6468
+ this.turnError = driverError("process", stableErrorCode(event.code, "runtime_error"), event.message, true);
6262
6469
  this.emit({
6263
6470
  type: "diagnostic",
6264
6471
  turnId,
@@ -6345,7 +6552,7 @@ class LogicalAgentSession {
6345
6552
  this.activeTurn = undefined;
6346
6553
  this.turnError = undefined;
6347
6554
  this.interruptedTurnId = undefined;
6348
- this.outstandingToolUses = 0;
6555
+ this.clearOutstandingToolUses();
6349
6556
  this.compacting = false;
6350
6557
  this.reviewing = false;
6351
6558
  this.toolBoundaryFlushDisabled = false;
@@ -6369,7 +6576,7 @@ class LogicalAgentSession {
6369
6576
  return this.safeBoundaryFlush;
6370
6577
  }
6371
6578
  canFlushSafeBoundaryQueue() {
6372
- return this.state === "working" && this.behavior.midTurnDelivery === "safe_boundary_queue" && this.outstandingToolUses === 0 && !this.compacting && !this.reviewing && this.activeTurn !== undefined && this.lane !== null && this.queued.length > 0;
6579
+ return this.state === "working" && this.behavior.midTurnDelivery === "safe_boundary_queue" && this.outstandingToolUseCount() === 0 && !this.compacting && !this.reviewing && this.activeTurn !== undefined && this.lane !== null && this.queued.length > 0;
6373
6580
  }
6374
6581
  async drainSafeBoundaryQueue() {
6375
6582
  while (this.canFlushSafeBoundaryQueue()) {
@@ -6550,7 +6757,7 @@ class LogicalAgentSession {
6550
6757
  return "compacting";
6551
6758
  if (this.reviewing)
6552
6759
  return "reviewing";
6553
- if (this.outstandingToolUses > 0)
6760
+ if (this.outstandingToolUseCount() > 0)
6554
6761
  return "tool_wait";
6555
6762
  if (this.activeTurn)
6556
6763
  return "working";
@@ -6560,6 +6767,46 @@ class LogicalAgentSession {
6560
6767
  const now = this.host.now();
6561
6768
  return Number.isFinite(now) && now >= 0 ? now : 0;
6562
6769
  }
6770
+ outstandingToolUseCount() {
6771
+ return this.anonymousOutstandingToolUses + this.outstandingToolCallIds.size;
6772
+ }
6773
+ startToolUse(rawCallId) {
6774
+ const callId = this.toolCallId(rawCallId);
6775
+ if (!callId) {
6776
+ this.anonymousOutstandingToolUses += 1;
6777
+ return true;
6778
+ }
6779
+ if (this.outstandingToolCallIds.has(callId)) {
6780
+ this.toolLifecycleMismatchCount += 1;
6781
+ return false;
6782
+ }
6783
+ this.outstandingToolCallIds.add(callId);
6784
+ return true;
6785
+ }
6786
+ finishToolUse(rawCallId) {
6787
+ const callId = this.toolCallId(rawCallId);
6788
+ if (!callId) {
6789
+ if (this.anonymousOutstandingToolUses === 0) {
6790
+ this.toolLifecycleMismatchCount += 1;
6791
+ return false;
6792
+ }
6793
+ this.anonymousOutstandingToolUses -= 1;
6794
+ return true;
6795
+ }
6796
+ if (!this.outstandingToolCallIds.delete(callId)) {
6797
+ this.toolLifecycleMismatchCount += 1;
6798
+ return false;
6799
+ }
6800
+ return true;
6801
+ }
6802
+ clearOutstandingToolUses() {
6803
+ this.anonymousOutstandingToolUses = 0;
6804
+ this.outstandingToolCallIds.clear();
6805
+ }
6806
+ toolCallId(rawCallId) {
6807
+ const callId = rawCallId?.trim();
6808
+ return callId && callId.length <= 1024 ? callId : undefined;
6809
+ }
6563
6810
  metricValue(value) {
6564
6811
  return Number.isFinite(value) && value >= 0 ? value : 0;
6565
6812
  }
@@ -8054,6 +8301,7 @@ function reduceManager(state, event) {
8054
8301
  if ((lease.state === "active" || lease.state === "suspect_active") && lease.outstandingToolUses !== undefined) {
8055
8302
  const unblockedLease = { ...lease };
8056
8303
  delete unblockedLease.outstandingToolUses;
8304
+ delete unblockedLease.outstandingToolCallIds;
8057
8305
  a.execution.lease = unblockedLease;
8058
8306
  }
8059
8307
  });
@@ -8314,26 +8562,40 @@ function onTurnToolLifecycle(state, event, lifecycle) {
8314
8562
  const matchesTerminal = lifecycle === "started" && current.state === "none" && current.lastTerminal !== null && sameIdentity(current.lastTerminal.identity, identity);
8315
8563
  if (!matchesActive && !matchesTerminal)
8316
8564
  return { state, effects: [] };
8317
- if (lifecycle === "finished" && matchesActive && (current.outstandingToolUses ?? 0) === 0) {
8318
- return { state, effects: [] };
8319
- }
8320
8565
  return mutate(state, event.agentId, (agent) => {
8321
8566
  if (!matchesSession(agent, event.sessionInstanceId))
8322
8567
  return;
8323
8568
  const lease = agent.execution.lease;
8569
+ const callId = normalizedToolCallId(event.callId);
8324
8570
  if ((lease.state === "active" || lease.state === "suspect_active") && sameIdentity(lease.identity, identity)) {
8325
- const outstandingToolUses = (lease.outstandingToolUses ?? 0) + (lifecycle === "started" ? 1 : -1);
8571
+ const identified = new Set(lease.outstandingToolCallIds ?? []);
8572
+ const currentCount = lease.outstandingToolUses ?? 0;
8573
+ const anonymousCount = Math.max(0, currentCount - identified.size);
8574
+ if (lifecycle === "started") {
8575
+ if (callId && identified.has(callId))
8576
+ return;
8577
+ if (callId)
8578
+ identified.add(callId);
8579
+ } else if (callId) {
8580
+ if (!identified.delete(callId))
8581
+ return;
8582
+ } else if (anonymousCount === 0) {
8583
+ return;
8584
+ }
8585
+ const outstandingToolUses = currentCount + (lifecycle === "started" ? 1 : -1);
8326
8586
  if (outstandingToolUses > 0) {
8327
8587
  agent.execution.lease = {
8328
8588
  ...lease,
8329
8589
  lastWorkAt: event.nowMs,
8330
8590
  nativeDeadlineAt: event.nowMs + agent.turnSilence.normalBudgetMs,
8331
8591
  recoveryExtensionsUsed: 0,
8332
- outstandingToolUses
8592
+ outstandingToolUses,
8593
+ ...identified.size > 0 ? { outstandingToolCallIds: [...identified] } : {}
8333
8594
  };
8334
8595
  } else {
8335
8596
  const unblockedLease = { ...lease };
8336
8597
  delete unblockedLease.outstandingToolUses;
8598
+ delete unblockedLease.outstandingToolCallIds;
8337
8599
  agent.execution.lease = {
8338
8600
  ...unblockedLease,
8339
8601
  lastWorkAt: event.nowMs,
@@ -8352,6 +8614,7 @@ function onTurnToolLifecycle(state, event, lifecycle) {
8352
8614
  nativeDeadlineAt: event.nowMs + agent.turnSilence.normalBudgetMs,
8353
8615
  recoveryExtensionsUsed: 0,
8354
8616
  outstandingToolUses: 1,
8617
+ ...callId ? { outstandingToolCallIds: [callId] } : {},
8355
8618
  reason: "work_after_terminal"
8356
8619
  };
8357
8620
  }
@@ -8515,6 +8778,10 @@ function identityOf(event) {
8515
8778
  function sameIdentity(left, right) {
8516
8779
  return left.sessionInstanceId === right.sessionInstanceId && left.turnId === right.turnId;
8517
8780
  }
8781
+ function normalizedToolCallId(rawCallId) {
8782
+ const callId = rawCallId?.trim();
8783
+ return callId && callId.length <= 1024 ? callId : undefined;
8784
+ }
8518
8785
  function leaseIsWorking(lease) {
8519
8786
  return lease.state === "active" || lease.state === "suspect_active";
8520
8787
  }
@@ -8594,13 +8861,13 @@ function cliCommandsSection() {
8594
8861
  "",
8595
8862
  "### Messaging",
8596
8863
  "",
8597
- `1. \`${CLI} inbox pull\` — fetch unread messages; \`--no-ack\` peeks without advancing.`,
8864
+ `1. \`${CLI} inbox pull\` — fetch unread messages and advance their read waterlines.`,
8598
8865
  `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\`.`,
8599
8866
  `3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a file; ` + `returns an id stable across pending→persisted.`,
8600
8867
  `4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download any ` + `attachment you can see (or your own pending uploads).`,
8601
- `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\`).`,
8602
- `6. \`${CLI} message mark set --target <full-message-ref>\` — persist a message as outstanding work.`,
8603
- `7. \`${CLI} message mark remove --target <full-message-ref>\` — clear a completed message mark.`,
8868
+ `5. \`${CLI} message property set --target <full-message-ref> --json <json>\` — add a supported property value to a message.`,
8869
+ `6. \`${CLI} message property list --target <full-message-ref>\` — list the message's current properties and supported capabilities.`,
8870
+ `7. \`${CLI} message property remove --target <full-message-ref> --json <json>\` — remove a supported property value from a message.`,
8604
8871
  `8. \`${CLI} message mark list\` — list every currently visible marked message with its full content.`,
8605
8872
  "",
8606
8873
  "### Servers",
@@ -8608,12 +8875,14 @@ function cliCommandsSection() {
8608
8875
  `1. \`${CLI} server list\` — list your servers.`,
8609
8876
  `2. \`${CLI} server member --server <name#discriminator>\` — list a server's members.`,
8610
8877
  `3. \`${CLI} server join --invite <link>\` — join via invite link or token.`,
8878
+ `4. \`${CLI} server leave --server <name#NNNN>\` — leave a server you belong to. A server owner cannot leave.`,
8611
8879
  "",
8612
8880
  "### Channels",
8613
8881
  "",
8614
8882
  `1. \`${CLI} channel list --server <name#discriminator>\` — list top-level channels, grouped by ` + `category; each is marked \`public\` or \`private\`.`,
8615
8883
  `2. \`${CLI} channel history --channel <ref>\` — read a channel's or thread's past messages ` + `(the context you weren't awake for). Page with \`--before N\` / \`--after N\` (seq N as ` + "anchor), `--around N` to center on a message, `--limit N` for page size.",
8616
8884
  `3. \`${CLI} channel member --channel <ref>\` — roster of a channel or thread. A private ` + `channel/forum returns its concrete member list; a public one returns a hint pointing at ` + `\`${CLI} server member\` (its audience is the whole server).`,
8885
+ `4. \`${CLI} channel leave --channel <ref>\` — leave a thread or private channel/forum. Public channels require leaving their server; DMs cannot be left.`,
8617
8886
  "",
8618
8887
  "### Friends",
8619
8888
  "",
@@ -8666,6 +8935,15 @@ function messagingSection() {
8666
8935
  "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.",
8667
8936
  "Write every message body in the stdin/heredoc block; never place it directly on the command line.",
8668
8937
  "",
8938
+ "### Message properties",
8939
+ "",
8940
+ `Message properties are capabilities determined by the target message's channel type. \`${CLI} ` + "message property list --target <full-message-ref>` returns its current values and supported `capabilities`.",
8941
+ "",
8942
+ `- \`emoji\` works on text-channel, DM, and thread messages. Set or remove your own reaction with ` + `\`{"type":"emoji","value":"\uD83D\uDC4D"}\`; \`list\` shows everyone who reacted. Add: ` + `\`${CLI} message property set --target <full-message-ref> --json '{"type":"emoji","value":"\uD83D\uDC4D"}'\`.`,
8943
+ `- \`tag\` works on forum opener messages. Set or remove named tags with ` + `\`{"type":"tag","value":["bug","ready"]}\`. A forum reply is a thread message, so it ` + `supports \`emoji\`, not \`tag\`. Add: \`${CLI} message property set --target ` + `<full-message-ref> --json '{"type":"tag","value":["bug"]}'\`.`,
8944
+ `- \`mark\` works on every message. Mark outstanding work with \`{"type":"mark","value":true}\`: ` + `\`${CLI} message property set --target <full-message-ref> --json ` + `'{"type":"mark","value":true}'\`. Clear it with the same JSON through ` + `\`message property remove\`. Use \`${CLI} message mark list\` for the one-shot global view ` + `of all currently visible marked messages.`,
8945
+ "- `archived` is a reserved tag meaning the forum post is complete. Setting it hides the " + "post from the normal forum feed; removing it reopens the post.",
8946
+ "",
8669
8947
  "### Context refs",
8670
8948
  "",
8671
8949
  "Path-style refs:",
@@ -8778,6 +9056,7 @@ function criticalRulesSection() {
8778
9056
  `- **\`${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.`,
8779
9057
  "- **Never expose tokens, keys, or secrets.** Redact credential-like strings from tool output " + "before sharing.",
8780
9058
  "- **Match the sender's language.** Reply in the language they wrote in.",
9059
+ `- **Leaving a scope is destructive.** Use \`${CLI} channel leave\` or \`${CLI} server leave\` only after your owner explicitly asks you to leave that exact scope.`,
8781
9060
  "- **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."
8782
9061
  ].join(`
8783
9062
  `);
@@ -8790,7 +9069,7 @@ function executionModelSection() {
8790
9069
  "",
8791
9070
  "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.",
8792
9071
  "",
8793
- "`inbox pull` advances your read waterline by default — pulled messages won't come back in a " + "future pull. A task-bearing message stays durable through its server mark even after the " + "read waterline advances."
9072
+ "`inbox pull` advances your read waterline — pulled messages won't come back in a " + "future pull. A task-bearing message stays durable through its server mark even after the " + "read waterline advances."
8794
9073
  ].join(`
8795
9074
  `);
8796
9075
  }
@@ -8862,9 +9141,9 @@ function workspaceMemorySection(config) {
8862
9141
  "",
8863
9142
  "### Outstanding work marks",
8864
9143
  "",
8865
- "Marks are the durable work queue. If a message requires work beyond an immediate reply, " + `acknowledge it, then run \`${CLI} message mark set --target <full-message-ref>\` before ` + "starting or queuing. Do not mark a message you answer immediately.",
9144
+ "Marks are the durable work queue. If a message requires work beyond an immediate reply, " + `acknowledge it, then run \`${CLI} message property set --target <full-message-ref> ` + `--json '{"type":"mark","value":true}'\` before ` + "starting or queuing. Do not mark a message you answer immediately.",
8866
9145
  "",
8867
- "Keep it marked while the work is active, queued, or blocked; report blockers where the task " + `came from. Run \`${CLI} message mark remove --target <full-message-ref>\` only after ` + "sending the result, or when the request is cancelled or superseded and nothing remains.",
9146
+ "Keep it marked while the work is active, queued, or blocked; report blockers where the task " + `came from. Run \`${CLI} message property remove --target <full-message-ref> ` + `--json '{"type":"mark","value":true}'\` only after ` + "sending the result, or when the request is cancelled or superseded and nothing remains.",
8868
9147
  "",
8869
9148
  "If `inbox pull` returns a `markedReminder`, run " + `\`${CLI} message mark list\` before taking new work. Do not copy marked tasks into local files.`
8870
9149
  ].join(`
@@ -8981,7 +9260,7 @@ function nowLocalISO() {
8981
9260
  }
8982
9261
 
8983
9262
  // src/manager/managerRuntime.ts
8984
- import { randomUUID as randomUUID5 } from "node:crypto";
9263
+ import { randomUUID as randomUUID6 } from "node:crypto";
8985
9264
  var SESSION_STOP_GRACE_MS2 = 2000;
8986
9265
  function normalizeTerminationCause(value) {
8987
9266
  return value === "runtime_error" || value === "killed_stalled" ? value : "other";
@@ -9218,7 +9497,7 @@ class AgentProcessManager {
9218
9497
  liveBackendIds = new Map;
9219
9498
  activeSpawnState = new Map;
9220
9499
  publishedAgentActivity = new Map;
9221
- traceProcessNonce = randomUUID5();
9500
+ traceProcessNonce = randomUUID6();
9222
9501
  nextSpawnOrdinal = 1;
9223
9502
  nextDaemonTurnOrdinal = 1;
9224
9503
  nextDeliveryOrdinal = 1;
@@ -9494,6 +9773,24 @@ class AgentProcessManager {
9494
9773
  if (this.sessions.get(agentId) === session)
9495
9774
  this.sessions.delete(agentId);
9496
9775
  }
9776
+ async interrupt(agentId) {
9777
+ const session = this.sessions.get(agentId);
9778
+ if (!session)
9779
+ return;
9780
+ const result = await session.interrupt({ requestId: randomUUID6(), reason: "owner_request" });
9781
+ if (result.status !== "accepted")
9782
+ return;
9783
+ this.log.info("agent turn interrupt accepted", {
9784
+ agentId,
9785
+ sessionInstanceId: session.sessionInstanceId,
9786
+ turnId: result.turnId
9787
+ });
9788
+ try {
9789
+ this.opts.onBotAuditEvent?.(agentId, { kind: "turn_interrupt", payload: { status: "accepted" } }, this.auditContext(agentId));
9790
+ } catch (err) {
9791
+ this.log.debug("audit emit failed (turn interrupt)", { agentId, err: String(err) });
9792
+ }
9793
+ }
9497
9794
  async stopAll() {
9498
9795
  if (this.tickTimer) {
9499
9796
  clearInterval(this.tickTimer);
@@ -9991,7 +10288,7 @@ ${this.opts.wakePromptFooter}` : text;
9991
10288
  case "reset_idle_session": {
9992
10289
  const spawnState = this.activeSpawnState.get(effect.agentId);
9993
10290
  const completion = {
9994
- eventId: `bae_${randomUUID5()}`,
10291
+ eventId: `bae_${randomUUID6()}`,
9995
10292
  occurredAt: new Date(this.now()).toISOString()
9996
10293
  };
9997
10294
  const persisted = this.forgetSession(effect.agentId, "reset_session", effect.sessionId, completion);
@@ -10386,11 +10683,12 @@ ${this.opts.wakePromptFooter}` : text;
10386
10683
  emitErrorAudit(agentId, scope, code, rawMessage) {
10387
10684
  if (!this.opts.onBotAuditEvent)
10388
10685
  return;
10686
+ const stableCode = /^[A-Za-z0-9_.-]{1,100}$/.test(code) ? code : "runtime_error";
10389
10687
  const message = scrubRuntimeErrorDiagnosticText(rawMessage).slice(0, AUDIT_ERROR_MESSAGE_MAX_LEN);
10390
10688
  try {
10391
10689
  this.opts.onBotAuditEvent(agentId, {
10392
10690
  kind: "error",
10393
- payload: { scope, code, message, model: this.currentModelFor(agentId) }
10691
+ payload: { scope, code: stableCode, message, model: this.currentModelFor(agentId) }
10394
10692
  }, {
10395
10693
  sessionId: this.liveSessions.get(agentId) ?? null,
10396
10694
  launchId: this.launchIds.get(agentId) ?? null
@@ -10415,18 +10713,18 @@ ${this.opts.wakePromptFooter}` : text;
10415
10713
  return;
10416
10714
  }
10417
10715
  const sessionSuperseded = owner.superseded;
10418
- const errorMessage2 = event.type === "session_failed" ? event.error.message : event.type === "turn_completed" && event.result.outcome === "failed" ? event.result.error.message : undefined;
10419
- if (errorMessage2 !== undefined && !sessionSuperseded) {
10420
- this.emitErrorAudit(agentId, "runtime", "runtime_error", errorMessage2);
10716
+ const runtimeError = event.type === "session_failed" ? event.error : event.type === "turn_completed" && event.result.outcome === "failed" ? event.result.error : undefined;
10717
+ if (runtimeError !== undefined && !sessionSuperseded) {
10718
+ this.emitErrorAudit(agentId, "runtime", runtimeError.code, runtimeError.message);
10421
10719
  if (this.nonCleanEndMarker.get(agentId)?.cause !== "killed_stalled") {
10422
- this.nonCleanEndMarker.set(agentId, { cause: "runtime_error", detail: errorMessage2 });
10720
+ this.nonCleanEndMarker.set(agentId, { cause: "runtime_error", detail: runtimeError.message });
10423
10721
  }
10424
10722
  const agent = this.state.agents[agentId];
10425
10723
  if (agent?.resetting && agent.resettingSince !== null && this.now() - agent.resettingSince >= this.state.resetStuckThresholdMs) {
10426
10724
  this.log.warn("runtime error during a stuck reset window", {
10427
10725
  agentId,
10428
10726
  resettingForMs: this.now() - agent.resettingSince,
10429
- message: errorMessage2
10727
+ message: runtimeError.message
10430
10728
  });
10431
10729
  }
10432
10730
  }
@@ -10521,9 +10819,9 @@ ${this.opts.wakePromptFooter}` : text;
10521
10819
  case "assistant_message_completed":
10522
10820
  return { type: "turn_work", turnId: event.turnId };
10523
10821
  case "tool_started":
10524
- return { type: "turn_tool_started", turnId: event.turnId };
10822
+ return { type: "turn_tool_started", turnId: event.turnId, callId: event.callId };
10525
10823
  case "tool_finished":
10526
- return { type: "turn_tool_finished", turnId: event.turnId };
10824
+ return { type: "turn_tool_finished", turnId: event.turnId, callId: event.callId };
10527
10825
  case "compaction_started":
10528
10826
  case "compaction_finished":
10529
10827
  case "review_started":
@@ -27298,6 +27596,10 @@ var OwnerDiagnosticReportSchema = exports_external.discriminatedUnion("status",
27298
27596
  // ../shared/src/community-cli-contract.ts
27299
27597
  var DM_SERVER = ".dm";
27300
27598
  var CONTROL_HEARTBEAT_CAPABILITY = "control-heartbeat-v1";
27599
+ var AgentInterruptRequestSchema = exports_external.strictObject({
27600
+ type: exports_external.literal("agent:interrupt"),
27601
+ agentId: exports_external.string().min(1).max(128)
27602
+ });
27301
27603
  function parseRef(ref) {
27302
27604
  if (!ref.startsWith("/"))
27303
27605
  throw new Error(`ref must start with "/": ${ref}`);
@@ -27389,6 +27691,7 @@ var HostCommandSchema = exports_external.discriminatedUnion("type", [
27389
27691
  type: exports_external.literal("agent:stop"),
27390
27692
  agentId: exports_external.string().min(1)
27391
27693
  }),
27694
+ AgentInterruptRequestSchema,
27392
27695
  exports_external.object({
27393
27696
  type: exports_external.literal("agent:reset"),
27394
27697
  agentId: exports_external.string().min(1),
@@ -27787,6 +28090,10 @@ class AgentRouter {
27787
28090
  }
27788
28091
  break;
27789
28092
  }
28093
+ case "agent:interrupt": {
28094
+ await this.opts.manager.interrupt(cmd.agentId);
28095
+ break;
28096
+ }
27790
28097
  case "agent:stop":
27791
28098
  this.log.info("agent:stop received", { agentId: cmd.agentId });
27792
28099
  try {
@@ -27956,10 +28263,11 @@ var DEFAULT_CAPABILITY_RESOLVER = (method, pathname) => {
27956
28263
  const isMessagesDoor = /\/channels\/[^/]+\/messages(\/|$|\?)/.test(pathname);
27957
28264
  if (isMessagesDoor)
27958
28265
  return method === "GET" ? "read" : "send";
28266
+ if (/\/messages\/[^/]+\/properties(\/|$|\?)/.test(pathname)) {
28267
+ return method === "GET" ? "read" : "send";
28268
+ }
27959
28269
  if (/\/messages\/[^/]+\/reactions\//.test(pathname))
27960
28270
  return "send";
27961
- if ((method === "PUT" || method === "DELETE") && /\/messages\/[^/]+\/marks(\/|$|\?)/.test(pathname))
27962
- return "send";
27963
28271
  if (method === "GET" && /\/users\/me\/marks(\/|$|\?)/.test(pathname))
27964
28272
  return "read";
27965
28273
  if (method === "GET" && /\/messages\/[^/]+(\?|$)/.test(pathname))
@@ -28000,6 +28308,12 @@ function isCanonicalChannelScope(channel2) {
28000
28308
  return false;
28001
28309
  }
28002
28310
  }
28311
+ function propertyAuditMetadata(method, pathname, requestUrl) {
28312
+ if (method !== "PUT" && method !== "DELETE" || !/^\/api\/community\/messages\/[^/]+\/properties$/.test(pathname))
28313
+ return;
28314
+ const type = new URL2(requestUrl, "http://placeholder").searchParams.get("type");
28315
+ return type === "emoji" || type === "tag" || type === "mark" ? { propertyType: type } : undefined;
28316
+ }
28003
28317
  function parseLocalMessageReminderBody(body, agentId) {
28004
28318
  let value;
28005
28319
  try {
@@ -28138,7 +28452,12 @@ async function startCredentialProxy(broker, options = {}) {
28138
28452
  }
28139
28453
  if (onProxyRequest) {
28140
28454
  try {
28141
- onProxyRequest(reg.agentId, req.method ?? "GET", pathname);
28455
+ const method = req.method ?? "GET";
28456
+ const metadata = propertyAuditMetadata(method, pathname, req.url ?? pathname);
28457
+ if (metadata)
28458
+ onProxyRequest(reg.agentId, method, pathname, metadata);
28459
+ else
28460
+ onProxyRequest(reg.agentId, method, pathname);
28142
28461
  } catch {}
28143
28462
  }
28144
28463
  const outHeaders = { ...req.headers };
@@ -29452,17 +29771,18 @@ var CommunityAgentNapRequestSchema = exports_external.object({
29452
29771
  message: "handoff is required"
29453
29772
  })
29454
29773
  });
29455
- var CommunityAgentReactAddRequestSchema = exports_external.object({
29774
+ var CommunityAgentMessagePropertyRequestSchema = exports_external.object({
29456
29775
  channel: exports_external.string().min(1),
29457
29776
  seq: CommunityAgentPositiveSeqSchema,
29458
- emoji: exports_external.string().min(1)
29777
+ property: exports_external.unknown()
29459
29778
  });
29460
29779
  var CommunityAgentFriendRequestSchema = exports_external.object({
29461
29780
  username: exports_external.string().min(1)
29462
29781
  });
29463
29782
  var CommunityAgentListFriendsSchema = exports_external.object({});
29464
29783
  var AuditLogCliInvocationPayloadSchema = exports_external.object({
29465
- subcommand: exports_external.string().min(1)
29784
+ subcommand: exports_external.string().min(1),
29785
+ propertyType: exports_external.enum(["emoji", "tag", "mark"]).optional()
29466
29786
  });
29467
29787
  var AuditLogToolCallPayloadSchema = exports_external.object({
29468
29788
  name: exports_external.string().min(1),
@@ -29473,6 +29793,9 @@ var AuditLogThinkingPayloadSchema = exports_external.object({
29473
29793
  truncated: exports_external.boolean(),
29474
29794
  chars: exports_external.number().int().nonnegative()
29475
29795
  });
29796
+ var AuditLogTurnInterruptPayloadSchema = exports_external.object({
29797
+ status: exports_external.literal("accepted")
29798
+ });
29476
29799
  var AuditLogWakeTriggerPayloadSchema = exports_external.object({
29477
29800
  messageId: exports_external.string().min(1),
29478
29801
  channel: exports_external.string().min(1),
@@ -29505,6 +29828,7 @@ var BotAuditEventSchema = exports_external.discriminatedUnion("kind", [
29505
29828
  exports_external.object({ kind: exports_external.literal("cli_invocation"), payload: AuditLogCliInvocationPayloadSchema }),
29506
29829
  exports_external.object({ kind: exports_external.literal("tool_call"), payload: AuditLogToolCallPayloadSchema }),
29507
29830
  exports_external.object({ kind: exports_external.literal("thinking"), payload: AuditLogThinkingPayloadSchema }),
29831
+ exports_external.object({ kind: exports_external.literal("turn_interrupt"), payload: AuditLogTurnInterruptPayloadSchema }),
29508
29832
  exports_external.object({ kind: exports_external.literal("wake_trigger"), payload: AuditLogWakeTriggerPayloadSchema }),
29509
29833
  exports_external.object({ kind: exports_external.literal("session_reset"), payload: AuditLogSessionResetPayloadSchema }),
29510
29834
  exports_external.object({ kind: exports_external.literal("nap"), payload: AuditLogNapPayloadSchema }),
@@ -29516,6 +29840,7 @@ var BotAuditEventKindSchema = exports_external.enum([
29516
29840
  "cli_invocation",
29517
29841
  "tool_call",
29518
29842
  "thinking",
29843
+ "turn_interrupt",
29519
29844
  "wake_trigger",
29520
29845
  "session_reset",
29521
29846
  "nap",
@@ -30472,7 +30797,7 @@ var communityBotAuditEventSchema = exports_external.strictObject({
30472
30797
  type: exports_external.literal("community:bot.audit_event"),
30473
30798
  botId: string4,
30474
30799
  id: string4,
30475
- kind: exports_external.enum(["cli_invocation", "tool_call", "thinking", "wake_trigger", "session_reset", "nap", "model_changed", "provider_changed", "error"]),
30800
+ kind: exports_external.enum(["cli_invocation", "tool_call", "thinking", "turn_interrupt", "wake_trigger", "session_reset", "nap", "model_changed", "provider_changed", "error"]),
30476
30801
  payload: exports_external.unknown(),
30477
30802
  sessionId: nullableString.optional(),
30478
30803
  launchId: nullableString.optional(),
@@ -32841,7 +33166,7 @@ class MessageReminderScheduler {
32841
33166
  }
32842
33167
 
32843
33168
  // src/manager/agentDriverHost.ts
32844
- import { randomUUID as randomUUID6 } from "node:crypto";
33169
+ import { randomUUID as randomUUID7 } from "node:crypto";
32845
33170
  import { homedir as homedir4 } from "node:os";
32846
33171
  import { join as join13 } from "node:path";
32847
33172
 
@@ -32997,7 +33322,7 @@ function createDaemonAgentDriverHost(ctx, onRawLine) {
32997
33322
  onRawLine?.(event.text);
32998
33323
  },
32999
33324
  now: () => Date.now(),
33000
- createId: () => randomUUID6()
33325
+ createId: () => randomUUID7()
33001
33326
  };
33002
33327
  }
33003
33328
 
@@ -33076,7 +33401,7 @@ class DaemonSelfSleepScheduler {
33076
33401
  // src/telemetry/dailyTokenUsage.ts
33077
33402
  import { chmod, mkdir, open, readFile as readFile2, rename, rm } from "node:fs/promises";
33078
33403
  import { dirname as dirname5, join as join14 } from "node:path";
33079
- import { randomUUID as randomUUID7 } from "node:crypto";
33404
+ import { randomUUID as randomUUID8 } from "node:crypto";
33080
33405
  var TOKEN_USAGE_WINDOW_DAYS = 30;
33081
33406
  var TOKEN_USAGE_RECOVERY_DAYS = 2;
33082
33407
  var TOKEN_USAGE_RETENTION_DAYS = TOKEN_USAGE_WINDOW_DAYS + TOKEN_USAGE_RECOVERY_DAYS;
@@ -33247,7 +33572,7 @@ class DailyTokenUsageStore {
33247
33572
  async persist() {
33248
33573
  const directory = dirname5(this.filePath);
33249
33574
  await mkdir(directory, { recursive: true, mode: 448 });
33250
- const temporary = `${this.filePath}.${randomUUID7()}.tmp`;
33575
+ const temporary = `${this.filePath}.${randomUUID8()}.tmp`;
33251
33576
  try {
33252
33577
  const file2 = await open(temporary, "wx", 384);
33253
33578
  try {
@@ -33329,15 +33654,17 @@ function createRuntimeRawLineTap(args) {
33329
33654
  }
33330
33655
  function deriveAuditLogSubcommand(pathname, method) {
33331
33656
  const canonical = pathname.split("?")[0] ?? pathname;
33332
- if (/^\/api\/community\/messages\/[^/]+\/reactions\//.test(canonical))
33333
- return "reactAdd";
33334
- if (/^\/api\/community\/messages\/[^/]+\/marks$/.test(canonical)) {
33657
+ if (/^\/api\/community\/messages\/[^/]+\/properties$/.test(canonical)) {
33658
+ if (method === "GET")
33659
+ return "messagePropertyList";
33335
33660
  if (method === "PUT")
33336
- return "markSet";
33661
+ return "messagePropertySet";
33337
33662
  if (method === "DELETE")
33338
- return "markRemove";
33663
+ return "messagePropertyRemove";
33339
33664
  return null;
33340
33665
  }
33666
+ if (/^\/api\/community\/messages\/[^/]+\/reactions\//.test(canonical))
33667
+ return "reactAdd";
33341
33668
  if (method === "GET" && /^\/api\/community\/users\/me\/marks$/.test(canonical))
33342
33669
  return "markList";
33343
33670
  if (/^\/api\/community\/channels\/[^/]+\/messages\/seq\//.test(canonical))
@@ -33503,14 +33830,14 @@ async function createDaemon(opts) {
33503
33830
  log2.warn("inbox pull timeline observation failed", { agentId, reason, contentEncoding });
33504
33831
  },
33505
33832
  onMessageReminderArm: (input) => reminderSchedulerRef?.arm(input) ?? { armed: false, reason: "reminder_scheduler_unavailable" },
33506
- onProxyRequest: (agentId, method, pathname) => {
33833
+ onProxyRequest: (agentId, method, pathname, metadata) => {
33507
33834
  const subcommand = deriveAuditLogSubcommand(pathname, method);
33508
33835
  if (!subcommand)
33509
33836
  return;
33510
33837
  const context = managerRef?.auditContext(agentId);
33511
33838
  emitBotAuditEvent(agentId, {
33512
33839
  kind: "cli_invocation",
33513
- payload: { subcommand }
33840
+ payload: { subcommand, ...metadata ? { propertyType: metadata.propertyType } : {} }
33514
33841
  }, context);
33515
33842
  emitImplicitTypingStopOnSend({
33516
33843
  subcommand,