@alook/daemon 0.1.28 → 0.1.30

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,11 +1235,40 @@ 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"]);
1245
+ var CLAUDE_USAGE_COMPONENTS = [
1246
+ "inputTokens",
1247
+ "outputTokens",
1248
+ "cacheReadInputTokens",
1249
+ "cacheCreationInputTokens"
1250
+ ];
1251
+ function safeCount(value) {
1252
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
1253
+ }
1254
+ function safeMoney(value) {
1255
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
1256
+ }
1257
+ function safeAdd(left, right) {
1258
+ const total = left + right;
1259
+ return Number.isSafeInteger(total) ? total : null;
1260
+ }
1261
+ function equalCost(left, right) {
1262
+ return Math.abs(left - right) <= Math.max(0.000000000001, Math.abs(right) * 0.000000001);
1263
+ }
1203
1264
 
1204
1265
  class ClaudeEventNormalizer {
1205
1266
  turnProtocol;
1206
1267
  currentSession = null;
1207
1268
  usageProjector = new SettledUsageProjector;
1269
+ claudeUsageSession = null;
1270
+ claudeModelUsageHighWater = new Map;
1271
+ claudeUsageMode = "unknown";
1208
1272
  constructor(turnProtocol) {
1209
1273
  this.turnProtocol = turnProtocol;
1210
1274
  }
@@ -1271,9 +1335,9 @@ class ClaudeEventNormalizer {
1271
1335
  out.push({ kind: "assistant_reasoning_completed", text: block.thinking ?? "" });
1272
1336
  } else if (block?.type === "text") {
1273
1337
  const text = block.text ?? "";
1274
- if (API_ERROR_RE.test(text))
1275
- out.push({ kind: "error", message: text });
1276
- else
1338
+ if (API_ERROR_RE.test(text)) {
1339
+ out.push({ kind: "error", code: "claude.api_error", message: text });
1340
+ } else
1277
1341
  completedText.push(text);
1278
1342
  } else if (block?.type === "tool_use") {
1279
1343
  out.push({ kind: "tool_call", name: block.name ?? "unknown_tool", input: block.input });
@@ -1299,14 +1363,19 @@ class ClaudeEventNormalizer {
1299
1363
  }
1300
1364
  handleResult(event, out) {
1301
1365
  const rawOwner = typeof event.user_message_uuid === "string" && event.user_message_uuid.length > 0 ? event.user_message_uuid : null;
1302
- 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;
1303
1368
  if (this.turnProtocol && !turnOwner)
1304
1369
  return;
1305
1370
  const usage = this.buildUsageTelemetry(event, rawOwner);
1306
1371
  if (usage)
1307
1372
  out.push(usage);
1308
- if (event.is_error || event.subtype === "error_during_execution") {
1309
- 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
+ });
1310
1379
  }
1311
1380
  out.push({
1312
1381
  kind: "turn_end",
@@ -1318,14 +1387,39 @@ class ClaudeEventNormalizer {
1318
1387
  return this.turnProtocol?.acceptsTurnWork() ?? true;
1319
1388
  }
1320
1389
  buildUsageTelemetry(event, rootRequestId) {
1321
- const u = event?.usage;
1322
- if (!u)
1323
- return null;
1324
1390
  const backendSessionId = event.session_id ?? this.currentSession;
1325
1391
  if (typeof backendSessionId !== "string" || !backendSessionId)
1326
1392
  return null;
1327
1393
  const providerRecordId = rootRequestId ?? (typeof event.request_id === "string" ? event.request_id : "invocation-result");
1328
- return this.usageProjector.project({
1394
+ const cumulative = this.prepareClaudeModelUsage(event, backendSessionId);
1395
+ if (cumulative === null)
1396
+ return null;
1397
+ if (cumulative) {
1398
+ const hasDelta = cumulative.input > 0 || cumulative.output > 0 || cumulative.cacheRead > 0 || cumulative.cacheWrite > 0;
1399
+ if (!hasDelta) {
1400
+ this.commitClaudeModelUsage(backendSessionId, cumulative);
1401
+ return null;
1402
+ }
1403
+ const projected2 = this.usageProjector.project({
1404
+ runtime: "claude",
1405
+ backendSessionId,
1406
+ providerRecordId,
1407
+ source: "claude_result_model_usage",
1408
+ input: cumulative.input,
1409
+ output: cumulative.output,
1410
+ cacheRead: cumulative.cacheRead,
1411
+ cacheWrite: cumulative.cacheWrite,
1412
+ inputIncludesCache: false,
1413
+ outputIncludesReasoning: true
1414
+ });
1415
+ if (projected2)
1416
+ this.commitClaudeModelUsage(backendSessionId, cumulative);
1417
+ return projected2;
1418
+ }
1419
+ const u = event?.usage;
1420
+ if (!u)
1421
+ return null;
1422
+ const projected = this.usageProjector.project({
1329
1423
  runtime: "claude",
1330
1424
  backendSessionId,
1331
1425
  providerRecordId,
@@ -1337,6 +1431,102 @@ class ClaudeEventNormalizer {
1337
1431
  inputIncludesCache: false,
1338
1432
  outputIncludesReasoning: true
1339
1433
  });
1434
+ if (projected)
1435
+ this.claudeUsageMode = "legacy";
1436
+ return projected;
1437
+ }
1438
+ prepareClaudeModelUsage(event, backendSessionId) {
1439
+ if (this.claudeUsageMode === "invalid")
1440
+ return null;
1441
+ if (!Object.hasOwn(event ?? {}, "modelUsage")) {
1442
+ if (this.claudeUsageMode !== "model_usage")
1443
+ return;
1444
+ return event?.usage ? this.lockClaudeUsageMode() : null;
1445
+ }
1446
+ if (this.claudeUsageMode === "legacy")
1447
+ return this.lockClaudeUsageMode();
1448
+ if (this.claudeUsageMode === "unknown")
1449
+ this.claudeUsageMode = "model_usage";
1450
+ this.claudeUsageSession ??= backendSessionId;
1451
+ if (this.claudeUsageSession !== null && this.claudeUsageSession !== backendSessionId) {
1452
+ return this.lockClaudeUsageMode();
1453
+ }
1454
+ if (!event.modelUsage || typeof event.modelUsage !== "object" || Array.isArray(event.modelUsage)) {
1455
+ return null;
1456
+ }
1457
+ const entries = Object.entries(event.modelUsage);
1458
+ if (entries.length === 0)
1459
+ return null;
1460
+ const highWater = new Map;
1461
+ let input = 0;
1462
+ let output = 0;
1463
+ let cacheRead = 0;
1464
+ let cacheWrite = 0;
1465
+ let snapshotCost = 0;
1466
+ for (const [model, rawValue] of entries) {
1467
+ if (!model.trim() || !rawValue || typeof rawValue !== "object" || Array.isArray(rawValue)) {
1468
+ return null;
1469
+ }
1470
+ const raw = rawValue;
1471
+ const current = {
1472
+ inputTokens: safeCount(raw.inputTokens),
1473
+ outputTokens: safeCount(raw.outputTokens),
1474
+ cacheReadInputTokens: safeCount(raw.cacheReadInputTokens),
1475
+ cacheCreationInputTokens: safeCount(raw.cacheCreationInputTokens),
1476
+ costUSD: safeMoney(raw.costUSD)
1477
+ };
1478
+ if (Object.values(current).some((value) => value === null)) {
1479
+ return null;
1480
+ }
1481
+ const snapshot = current;
1482
+ const prior = this.claudeModelUsageHighWater.get(model) ?? {
1483
+ inputTokens: 0,
1484
+ outputTokens: 0,
1485
+ cacheReadInputTokens: 0,
1486
+ cacheCreationInputTokens: 0,
1487
+ costUSD: 0
1488
+ };
1489
+ for (const component of CLAUDE_USAGE_COMPONENTS) {
1490
+ if (snapshot[component] < prior[component])
1491
+ return null;
1492
+ }
1493
+ if (snapshot.costUSD < prior.costUSD)
1494
+ return null;
1495
+ const nextInput = safeAdd(input, snapshot.inputTokens - prior.inputTokens);
1496
+ const nextOutput = safeAdd(output, snapshot.outputTokens - prior.outputTokens);
1497
+ const nextCacheRead = safeAdd(cacheRead, snapshot.cacheReadInputTokens - prior.cacheReadInputTokens);
1498
+ const nextCacheWrite = safeAdd(cacheWrite, snapshot.cacheCreationInputTokens - prior.cacheCreationInputTokens);
1499
+ if (nextInput === null || nextOutput === null || nextCacheRead === null || nextCacheWrite === null) {
1500
+ return null;
1501
+ }
1502
+ input = nextInput;
1503
+ output = nextOutput;
1504
+ cacheRead = nextCacheRead;
1505
+ cacheWrite = nextCacheWrite;
1506
+ snapshotCost += snapshot.costUSD;
1507
+ if (!Number.isFinite(snapshotCost))
1508
+ return null;
1509
+ highWater.set(model, snapshot);
1510
+ }
1511
+ for (const model of this.claudeModelUsageHighWater.keys()) {
1512
+ if (!highWater.has(model))
1513
+ return null;
1514
+ }
1515
+ if (Object.hasOwn(event, "total_cost_usd")) {
1516
+ const totalCost = safeMoney(event.total_cost_usd);
1517
+ if (totalCost === null || !equalCost(snapshotCost, totalCost))
1518
+ return null;
1519
+ }
1520
+ return { highWater, input, output, cacheRead, cacheWrite };
1521
+ }
1522
+ commitClaudeModelUsage(backendSessionId, projection) {
1523
+ this.claudeUsageSession ??= backendSessionId;
1524
+ this.claudeModelUsageHighWater = projection.highWater;
1525
+ this.claudeUsageMode = "model_usage";
1526
+ }
1527
+ lockClaudeUsageMode() {
1528
+ this.claudeUsageMode = "invalid";
1529
+ return null;
1340
1530
  }
1341
1531
  }
1342
1532
 
@@ -1463,6 +1653,7 @@ class ClaudeTurnProtocol {
1463
1653
  segmentOwnerUuid = null;
1464
1654
  delivered = new Map;
1465
1655
  awaitingFollowOn = false;
1656
+ interruptRequested = false;
1466
1657
  finalized = false;
1467
1658
  beginTurn() {
1468
1659
  const uuid = randomUUID4();
@@ -1471,6 +1662,7 @@ class ClaudeTurnProtocol {
1471
1662
  this.delivered.clear();
1472
1663
  this.delivered.set(uuid, { acknowledged: false, followOn: false });
1473
1664
  this.awaitingFollowOn = false;
1665
+ this.interruptRequested = false;
1474
1666
  this.finalized = false;
1475
1667
  return this.receipt(uuid);
1476
1668
  }
@@ -1497,6 +1689,13 @@ class ClaudeTurnProtocol {
1497
1689
  return false;
1498
1690
  return this.delivered.get(this.rootUuid)?.acknowledged === true;
1499
1691
  }
1692
+ hasActiveTurn() {
1693
+ return this.rootUuid !== null && !this.finalized;
1694
+ }
1695
+ noteInterruptRequested() {
1696
+ if (this.hasActiveTurn())
1697
+ this.interruptRequested = true;
1698
+ }
1500
1699
  claimResult(userMessageUuid) {
1501
1700
  if (this.finalized || !this.rootUuid || userMessageUuid !== this.segmentOwnerUuid)
1502
1701
  return null;
@@ -1513,6 +1712,19 @@ class ClaudeTurnProtocol {
1513
1712
  this.finalized = true;
1514
1713
  return this.receipt(this.rootUuid);
1515
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
+ }
1516
1728
  receipt(uuid) {
1517
1729
  return `claude:${uuid}`;
1518
1730
  }
@@ -1538,7 +1750,9 @@ class ClaudeDriver {
1538
1750
  };
1539
1751
  turnProtocol = new ClaudeTurnProtocol;
1540
1752
  eventNormalizer = new ClaudeEventNormalizer(this.turnProtocol);
1753
+ pendingInterrupt;
1541
1754
  beginTurn() {
1755
+ this.pendingInterrupt = undefined;
1542
1756
  const receipt = this.turnProtocol.beginTurn();
1543
1757
  this.eventNormalizer.beginTurn();
1544
1758
  return receipt;
@@ -1577,11 +1791,45 @@ class ClaudeDriver {
1577
1791
  return { process: proc };
1578
1792
  }
1579
1793
  normalizeLine(line) {
1580
- return this.eventNormalizer.normalizeLine(line);
1794
+ const events = this.eventNormalizer.normalizeLine(line);
1795
+ this.flushPendingInterrupt();
1796
+ return events;
1581
1797
  }
1582
1798
  get currentSessionId() {
1583
1799
  return this.eventNormalizer.currentSessionId;
1584
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
+ }
1585
1833
  encodeMessage(text, sessionId, opts) {
1586
1834
  const uuid = opts?.mode === "idle" ? this.turnProtocol.rootInputUuid() : this.turnProtocol.steeringInputUuid();
1587
1835
  return JSON.stringify({
@@ -1713,6 +1961,35 @@ import { randomBytes } from "node:crypto";
1713
1961
  var codexQuotaSourceEpoch = randomBytes(16).toString("base64url");
1714
1962
  var codexQuotaSourceGeneration = 0;
1715
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
+ }
1716
1993
  function rotateCodexQuotaSource() {
1717
1994
  codexQuotaSourceEpoch = randomBytes(16).toString("base64url");
1718
1995
  codexQuotaSourceGeneration += 1;
@@ -1870,7 +2147,11 @@ class CodexEventNormalizer {
1870
2147
  return this.replaceQuotaSnapshots(msg.result ?? {});
1871
2148
  }
1872
2149
  if (msg?.error && msg.id !== undefined) {
1873
- 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
+ }];
1874
2155
  }
1875
2156
  if (msg?.result?.thread?.id) {
1876
2157
  return this.adoptAndInit(msg.result.thread.id);
@@ -1939,20 +2220,26 @@ class CodexEventNormalizer {
1939
2220
  return [];
1940
2221
  this.releaseUsageForTurn(params.threadId, params.turn.id);
1941
2222
  if (params.turn.status === "failed") {
2223
+ const error = params.turn.error;
1942
2224
  return [
1943
- { 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
+ },
1944
2230
  { kind: "turn_end", sessionId: this.threadId ?? undefined, turnOwner: this.turnReceipt(params.threadId, params.turn.id) }
1945
2231
  ];
1946
2232
  }
1947
- if (params.turn.status === "interrupted") {
1948
- return [{ kind: "error", message: "Codex turn interrupted" }, { kind: "turn_end", sessionId: this.threadId ?? undefined, turnOwner: this.turnReceipt(params.threadId, params.turn.id) }];
1949
- }
1950
2233
  return [{ kind: "turn_end", sessionId: this.threadId ?? undefined, turnOwner: this.turnReceipt(params.threadId, params.turn.id) }];
1951
2234
  case "error":
1952
2235
  if (params?.willRetry === true) {
1953
2236
  return [{ kind: "runtime_recovery", stage: "retrying", source: "codex_stream" }];
1954
2237
  }
1955
- 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
+ }];
1956
2243
  case "thread/tokenUsage/updated":
1957
2244
  return [];
1958
2245
  case "account/rateLimits/updated":
@@ -2049,42 +2336,46 @@ class CodexEventNormalizer {
2049
2336
  }
2050
2337
  handleItemStarted(params) {
2051
2338
  const t = params?.item?.type ?? params?.type;
2339
+ const callId = this.itemCallId(params);
2340
+ const identity = callId ? { callId } : {};
2052
2341
  switch (t) {
2053
2342
  case "commandExecution":
2054
- return [{ kind: "tool_call", name: "shell", input: params?.item }];
2343
+ return [{ kind: "tool_call", ...identity, name: "shell", input: params?.item }];
2055
2344
  case "contextCompaction":
2056
2345
  return [{ kind: "compaction_started" }];
2057
2346
  case "enteredReviewMode":
2058
2347
  return [{ kind: "review_started" }];
2059
2348
  case "fileChange":
2060
- return [{ kind: "tool_call", name: "file_change", input: normalizeFileChangeInput(params?.item) }];
2349
+ return [{ kind: "tool_call", ...identity, name: "file_change", input: normalizeFileChangeInput(params?.item) }];
2061
2350
  case "mcpToolCall":
2062
- 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 }];
2063
2352
  case "webSearch":
2064
- return [{ kind: "tool_call", name: "web_search", input: params?.item }];
2353
+ return [{ kind: "tool_call", ...identity, name: "web_search", input: params?.item }];
2065
2354
  case "collabAgentToolCall":
2066
- return [{ kind: "tool_call", name: "collab_tool_call", input: params?.item }];
2355
+ return [{ kind: "tool_call", ...identity, name: "collab_tool_call", input: params?.item }];
2067
2356
  default:
2068
2357
  return [];
2069
2358
  }
2070
2359
  }
2071
2360
  handleItemCompleted(params) {
2072
2361
  const t = params?.item?.type ?? params?.type;
2362
+ const callId = this.itemCallId(params);
2363
+ const identity = callId ? { callId } : {};
2073
2364
  switch (t) {
2074
2365
  case "commandExecution":
2075
- return [{ kind: "tool_output", name: "shell" }];
2366
+ return [{ kind: "tool_output", ...identity, name: "shell" }];
2076
2367
  case "contextCompaction":
2077
2368
  return [{ kind: "compaction_finished" }];
2078
2369
  case "exitedReviewMode":
2079
2370
  return [{ kind: "review_finished" }];
2080
2371
  case "fileChange":
2081
- return [{ kind: "tool_output", name: "file_change" }];
2372
+ return [{ kind: "tool_output", ...identity, name: "file_change" }];
2082
2373
  case "mcpToolCall":
2083
- 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"}` }];
2084
2375
  case "webSearch":
2085
- return [{ kind: "tool_output", name: "web_search" }];
2376
+ return [{ kind: "tool_output", ...identity, name: "web_search" }];
2086
2377
  case "collabAgentToolCall":
2087
- return [{ kind: "tool_output", name: "collab_tool_call" }];
2378
+ return [{ kind: "tool_output", ...identity, name: "collab_tool_call" }];
2088
2379
  case "agentMessage":
2089
2380
  return [{ kind: "assistant_message_completed", text: params?.item?.text ?? "" }];
2090
2381
  case "reasoning":
@@ -2093,6 +2384,10 @@ class CodexEventNormalizer {
2093
2384
  return [];
2094
2385
  }
2095
2386
  }
2387
+ itemCallId(params) {
2388
+ const value = params?.item?.id ?? params?.itemId;
2389
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
2390
+ }
2096
2391
  }
2097
2392
 
2098
2393
  // agent-driver/dist/adapters/codex/home.js
@@ -2563,6 +2858,16 @@ class CodexDriver {
2563
2858
  get currentSessionId() {
2564
2859
  return this.eventNormalizer.currentSessionId;
2565
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
+ }
2566
2871
  buildTurnStart(threadId, text) {
2567
2872
  return jsonRpcRequest("turn/start", { threadId, input: [{ type: "text", text }] }, this.nextRequestId());
2568
2873
  }
@@ -2818,7 +3123,8 @@ class CursorAcpLane {
2818
3123
  started = false;
2819
3124
  ready = false;
2820
3125
  sessionId = null;
2821
- activePrompt = null;
3126
+ currentPromptRequestId = null;
3127
+ terminalOwner = null;
2822
3128
  requestedStopReason;
2823
3129
  spawnPromise;
2824
3130
  stopPromise;
@@ -2890,16 +3196,19 @@ class CursorAcpLane {
2890
3196
  async send(input) {
2891
3197
  if (!this.ready || !this.process || this.isClosed())
2892
3198
  return { ok: false, reason: "closed" };
2893
- if (input.mode !== "idle") {
2894
- return { ok: false, reason: "unsupported", error: "Cursor ACP does not support mid-turn steering" };
3199
+ if (input.mode === "busy") {
3200
+ if (this.currentPromptRequestId === null) {
3201
+ return { ok: false, reason: "runtime_busy", error: "Cursor ACP has no active prompt to steer" };
3202
+ }
3203
+ return this.admitPrompt(input.text, "steer");
2895
3204
  }
2896
- if (this.activePrompt) {
3205
+ if (this.currentPromptRequestId !== null) {
2897
3206
  return { ok: false, reason: "runtime_busy", error: "Cursor ACP prompt is still active" };
2898
3207
  }
2899
3208
  return this.admitPrompt(input.text);
2900
3209
  }
2901
3210
  async interrupt(_input) {
2902
- if (!this.ready || !this.sessionId || !this.activePrompt || this.isClosed())
3211
+ if (!this.ready || !this.sessionId || this.currentPromptRequestId === null || this.isClosed())
2903
3212
  return false;
2904
3213
  this.write({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId: this.sessionId } });
2905
3214
  return true;
@@ -2907,7 +3216,8 @@ class CursorAcpLane {
2907
3216
  stop(input = {}) {
2908
3217
  this.requestedStopReason ??= input.reason ?? "requested_stop";
2909
3218
  this.ready = false;
2910
- this.activePrompt = null;
3219
+ this.currentPromptRequestId = null;
3220
+ this.terminalOwner = null;
2911
3221
  this.openToolCalls.clear();
2912
3222
  this.rejectAllPending(new Error("Cursor ACP lane stopped"));
2913
3223
  this.stopPromise ??= this.stopPhysicalOnce(input);
@@ -3013,19 +3323,22 @@ class CursorAcpLane {
3013
3323
  throw new CursorAcpIncompatibleError("Cursor ACP did not confirm the exact configured model");
3014
3324
  }
3015
3325
  }
3016
- admitPrompt(text) {
3326
+ admitPrompt(text, delivery = "prompt") {
3017
3327
  if (!this.sessionId)
3018
3328
  return { ok: false, reason: "not_ready", error: "Cursor ACP session is not ready" };
3019
- if (this.activePrompt)
3020
- return { ok: false, reason: "runtime_busy", error: "Cursor ACP prompt is still active" };
3021
3329
  const requestId = ++this.requestSequence;
3022
- const active = {
3330
+ const prompt = {
3023
3331
  requestId,
3024
3332
  receipt: `cursor:acp:${requestId}`
3025
3333
  };
3334
+ const previousPromptRequestId = this.currentPromptRequestId;
3335
+ const previousTerminalOwner = this.terminalOwner;
3336
+ const previousOpenToolCalls = new Set(this.openToolCalls);
3337
+ if (delivery === "prompt")
3338
+ this.terminalOwner = prompt.receipt;
3339
+ this.currentPromptRequestId = requestId;
3026
3340
  this.openToolCalls.clear();
3027
- this.activePrompt = active;
3028
- this.pending.set(requestId, { kind: "prompt", method: "session/prompt", active });
3341
+ this.pending.set(requestId, { kind: "prompt", method: "session/prompt", prompt });
3029
3342
  try {
3030
3343
  this.write({
3031
3344
  jsonrpc: "2.0",
@@ -3038,41 +3351,51 @@ class CursorAcpLane {
3038
3351
  });
3039
3352
  } catch (error) {
3040
3353
  this.pending.delete(requestId);
3041
- if (this.activePrompt?.requestId === requestId)
3042
- this.activePrompt = null;
3354
+ if (this.currentPromptRequestId === requestId) {
3355
+ this.currentPromptRequestId = previousPromptRequestId;
3356
+ this.terminalOwner = previousTerminalOwner;
3357
+ this.openToolCalls.clear();
3358
+ for (const toolCallId of previousOpenToolCalls)
3359
+ this.openToolCalls.add(toolCallId);
3360
+ }
3043
3361
  throw error;
3044
3362
  }
3045
- return { ok: true, acceptedAs: "prompt", receipt: active.receipt };
3363
+ return { ok: true, acceptedAs: delivery, receipt: prompt.receipt };
3046
3364
  }
3047
- completePrompt(active, value) {
3048
- if (this.activePrompt?.requestId !== active.requestId)
3365
+ completePrompt(prompt, value) {
3366
+ if (this.currentPromptRequestId !== prompt.requestId)
3049
3367
  return;
3050
3368
  const result = record2(value);
3051
3369
  if (!result || typeof result.stopReason !== "string" || !PROMPT_STOP_REASONS.has(result.stopReason)) {
3052
- this.failPrompt(active, 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");
3053
3371
  return;
3054
3372
  }
3055
- this.activePrompt = null;
3373
+ const terminalOwner = this.terminalOwner ?? prompt.receipt;
3374
+ this.currentPromptRequestId = null;
3375
+ this.terminalOwner = null;
3056
3376
  this.openToolCalls.clear();
3057
3377
  this.events.emit("runtime_event", {
3058
3378
  kind: "turn_end",
3059
3379
  sessionId: this.sessionId ?? undefined,
3060
- turnOwner: active.receipt
3380
+ turnOwner: terminalOwner
3061
3381
  });
3062
3382
  }
3063
- failPrompt(active, error) {
3064
- if (this.activePrompt?.requestId !== active.requestId)
3383
+ failPrompt(prompt, error, code = "cursor.prompt_failed") {
3384
+ if (this.currentPromptRequestId !== prompt.requestId)
3065
3385
  return;
3066
- this.activePrompt = null;
3386
+ const terminalOwner = this.terminalOwner ?? prompt.receipt;
3387
+ this.currentPromptRequestId = null;
3388
+ this.terminalOwner = null;
3067
3389
  this.openToolCalls.clear();
3068
3390
  this.events.emit("runtime_event", {
3069
3391
  kind: "error",
3392
+ code,
3070
3393
  message: error instanceof Error ? error.message : String(error)
3071
3394
  });
3072
3395
  this.events.emit("runtime_event", {
3073
3396
  kind: "turn_end",
3074
3397
  sessionId: this.sessionId ?? undefined,
3075
- turnOwner: active.receipt
3398
+ turnOwner: terminalOwner
3076
3399
  });
3077
3400
  }
3078
3401
  call(method, params) {
@@ -3158,7 +3481,8 @@ class CursorAcpLane {
3158
3481
  }
3159
3482
  this.processTerminal = true;
3160
3483
  this.ready = false;
3161
- this.activePrompt = null;
3484
+ this.currentPromptRequestId = null;
3485
+ this.terminalOwner = null;
3162
3486
  this.openToolCalls.clear();
3163
3487
  this.stopPromise ??= this.stopPhysicalOnce({ reason: "runtime_error", forceAfterMs: 0 });
3164
3488
  this.stopPromise.catch(() => {});
@@ -3176,7 +3500,8 @@ class CursorAcpLane {
3176
3500
  return;
3177
3501
  this.processTerminal = true;
3178
3502
  this.ready = false;
3179
- this.activePrompt = null;
3503
+ this.currentPromptRequestId = null;
3504
+ this.terminalOwner = null;
3180
3505
  this.openToolCalls.clear();
3181
3506
  this.rejectAllPending(new Error("Cursor ACP process exited"));
3182
3507
  if (this.suppressExit || !this.processActivated)
@@ -3218,11 +3543,12 @@ class CursorAcpLane {
3218
3543
  this.pending.delete(id);
3219
3544
  if (message.error !== undefined) {
3220
3545
  const payload = record2(message.error);
3221
- this.failPrompt(pending.active, 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}`);
3222
3548
  } else if (!("result" in message)) {
3223
- this.failPrompt(pending.active, new Error("Cursor ACP response omitted result"));
3549
+ this.failPrompt(pending.prompt, new Error("Cursor ACP response omitted result"), "cursor.invalid_response");
3224
3550
  } else {
3225
- this.completePrompt(pending.active, message.result);
3551
+ this.completePrompt(pending.prompt, message.result);
3226
3552
  }
3227
3553
  return;
3228
3554
  }
@@ -3263,7 +3589,7 @@ class CursorAcpLane {
3263
3589
  const sameSession = payload?.sessionId === this.sessionId;
3264
3590
  const options = Array.isArray(payload?.options) ? payload.options.map(record2).filter(Boolean) : [];
3265
3591
  const allowOnce = options.find((option) => option.kind === "allow_once" && typeof option.optionId === "string" && option.optionId.trim().length > 0);
3266
- if (!this.ready || !this.activePrompt || !sameSession || !allowOnce) {
3592
+ if (!this.ready || this.currentPromptRequestId === null || !sameSession || !allowOnce) {
3267
3593
  this.write({ jsonrpc: "2.0", id, result: { outcome: { outcome: "cancelled" } } });
3268
3594
  this.diagnostic("error", "Cursor ACP permission request was not allowed for the active prompt");
3269
3595
  return;
@@ -3287,7 +3613,7 @@ class CursorAcpLane {
3287
3613
  this.diagnostic("warning", "Cursor ACP emitted an update for a different session");
3288
3614
  return;
3289
3615
  }
3290
- if (!this.ready || !this.activePrompt) {
3616
+ if (!this.ready || this.currentPromptRequestId === null) {
3291
3617
  this.diagnostic("warning", "Cursor ACP emitted a session update without an active prompt");
3292
3618
  return;
3293
3619
  }
@@ -3505,7 +3831,11 @@ function parseModelRef(model) {
3505
3831
  function messageFromError(value) {
3506
3832
  const payload = record3(value);
3507
3833
  const message = typeof payload?.message === "string" && payload.message.trim() ? payload.message : undefined;
3508
- 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";
3509
3839
  }
3510
3840
 
3511
3841
  class OpenCodeServiceLane {
@@ -4173,7 +4503,10 @@ class OpenCodeServiceLane {
4173
4503
  root.outcomes.push({
4174
4504
  seq,
4175
4505
  ok: successful,
4176
- ...!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
+ } : {}
4177
4510
  });
4178
4511
  }
4179
4512
  const tokens = record3(data.tokens);
@@ -4202,7 +4535,12 @@ class OpenCodeServiceLane {
4202
4535
  break;
4203
4536
  }
4204
4537
  case "session.next.step.failed":
4205
- 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
+ });
4206
4544
  break;
4207
4545
  default:
4208
4546
  break;
@@ -4382,11 +4720,11 @@ class OpenCodeServiceLane {
4382
4720
  return;
4383
4721
  const outcome = root.outcomes.filter((item) => item.seq > maxAdmission).sort((left, right) => left.seq - right.seq).at(-1);
4384
4722
  if (!root.interrupted && !outcome) {
4385
- 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");
4386
4724
  return;
4387
4725
  }
4388
4726
  if (outcome && !outcome.ok) {
4389
- this.settleRoot(root, false, outcome.message ?? "OpenCode turn failed");
4727
+ this.settleRoot(root, false, outcome.message ?? "OpenCode turn failed", outcome.code ?? "opencode.turn_failed");
4390
4728
  return;
4391
4729
  }
4392
4730
  this.settleRoot(root, true);
@@ -4412,7 +4750,7 @@ class OpenCodeServiceLane {
4412
4750
  barrierStillCurrent(root, identity, generation) {
4413
4751
  return this.activeRoot === root && root.generation === generation && root.pendingAdmissions === 0 && !root.interruptPending && this.isIdentity(identity) && this.identityMatches();
4414
4752
  }
4415
- settleRoot(root, success, message) {
4753
+ settleRoot(root, success, message, code = "opencode.turn_failed") {
4416
4754
  if (this.activeRoot !== root)
4417
4755
  return;
4418
4756
  this.activeRoot = null;
@@ -4420,6 +4758,7 @@ class OpenCodeServiceLane {
4420
4758
  if (!success) {
4421
4759
  this.events.emit("runtime_event", {
4422
4760
  kind: "error",
4761
+ code,
4423
4762
  message: message ?? "OpenCode turn failed"
4424
4763
  });
4425
4764
  }
@@ -4935,7 +5274,11 @@ function mapPiSdkEvent(event, sessionId, state) {
4935
5274
  return [{ kind: "assistant_message_completed", text: d.content ?? "" }];
4936
5275
  }
4937
5276
  case "error":
4938
- 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
+ }];
4939
5282
  default:
4940
5283
  return [];
4941
5284
  }
@@ -5215,7 +5558,7 @@ var capabilities = {
5215
5558
  commandOverride: true,
5216
5559
  resume: "by_id",
5217
5560
  sessionLifetime: "persistent",
5218
- midTurnDelivery: "next_turn_queue",
5561
+ midTurnDelivery: "steer",
5219
5562
  interrupt: true
5220
5563
  },
5221
5564
  opencode: {
@@ -5523,7 +5866,9 @@ class LogicalAgentSession {
5523
5866
  interruptedTurnId;
5524
5867
  processTurnEnded = false;
5525
5868
  stopRequestId;
5526
- outstandingToolUses = 0;
5869
+ anonymousOutstandingToolUses = 0;
5870
+ outstandingToolCallIds = new Set;
5871
+ toolLifecycleMismatchCount = 0;
5527
5872
  compacting = false;
5528
5873
  reviewing = false;
5529
5874
  steeringDeliveries = new Set;
@@ -5713,6 +6058,9 @@ class LogicalAgentSession {
5713
6058
  queueDwellCount: this.metricValue(this.queueDwellCount),
5714
6059
  queueDwellTotalMs: this.metricValue(this.queueDwellTotalMs),
5715
6060
  sseReconnectCount: this.metricValue(this.sseReconnectCount),
6061
+ outstandingToolUses: this.outstandingToolUseCount(),
6062
+ anonymousOutstandingToolUses: this.metricValue(this.anonymousOutstandingToolUses),
6063
+ toolLifecycleMismatchCount: this.metricValue(this.toolLifecycleMismatchCount),
5716
6064
  resumeOutcome: this.resumeOutcome,
5717
6065
  terminalOwnerKind: this.adapter.execution.terminalOwnership
5718
6066
  }
@@ -6032,15 +6380,36 @@ class LogicalAgentSession {
6032
6380
  }
6033
6381
  return;
6034
6382
  case "tool_call":
6035
- this.outstandingToolUses += 1;
6036
- if (turnId)
6037
- 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
+ });
6038
6397
  return;
6039
6398
  case "tool_output":
6040
- this.outstandingToolUses = Math.max(0, this.outstandingToolUses - 1);
6041
- if (turnId)
6042
- this.emit({ type: "tool_finished", turnId, name: event.name });
6043
- 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)
6044
6413
  this.flushSafeBoundaryQueue();
6045
6414
  return;
6046
6415
  case "compaction_started":
@@ -6096,7 +6465,7 @@ class LogicalAgentSession {
6096
6465
  }
6097
6466
  case "error":
6098
6467
  this.toolBoundaryFlushDisabled = true;
6099
- this.turnError = driverError("process", "runtime_error", event.message, true);
6468
+ this.turnError = driverError("process", stableErrorCode(event.code, "runtime_error"), event.message, true);
6100
6469
  this.emit({
6101
6470
  type: "diagnostic",
6102
6471
  turnId,
@@ -6183,7 +6552,7 @@ class LogicalAgentSession {
6183
6552
  this.activeTurn = undefined;
6184
6553
  this.turnError = undefined;
6185
6554
  this.interruptedTurnId = undefined;
6186
- this.outstandingToolUses = 0;
6555
+ this.clearOutstandingToolUses();
6187
6556
  this.compacting = false;
6188
6557
  this.reviewing = false;
6189
6558
  this.toolBoundaryFlushDisabled = false;
@@ -6207,7 +6576,7 @@ class LogicalAgentSession {
6207
6576
  return this.safeBoundaryFlush;
6208
6577
  }
6209
6578
  canFlushSafeBoundaryQueue() {
6210
- 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;
6211
6580
  }
6212
6581
  async drainSafeBoundaryQueue() {
6213
6582
  while (this.canFlushSafeBoundaryQueue()) {
@@ -6388,7 +6757,7 @@ class LogicalAgentSession {
6388
6757
  return "compacting";
6389
6758
  if (this.reviewing)
6390
6759
  return "reviewing";
6391
- if (this.outstandingToolUses > 0)
6760
+ if (this.outstandingToolUseCount() > 0)
6392
6761
  return "tool_wait";
6393
6762
  if (this.activeTurn)
6394
6763
  return "working";
@@ -6398,6 +6767,46 @@ class LogicalAgentSession {
6398
6767
  const now = this.host.now();
6399
6768
  return Number.isFinite(now) && now >= 0 ? now : 0;
6400
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
+ }
6401
6810
  metricValue(value) {
6402
6811
  return Number.isFinite(value) && value >= 0 ? value : 0;
6403
6812
  }
@@ -7892,6 +8301,7 @@ function reduceManager(state, event) {
7892
8301
  if ((lease.state === "active" || lease.state === "suspect_active") && lease.outstandingToolUses !== undefined) {
7893
8302
  const unblockedLease = { ...lease };
7894
8303
  delete unblockedLease.outstandingToolUses;
8304
+ delete unblockedLease.outstandingToolCallIds;
7895
8305
  a.execution.lease = unblockedLease;
7896
8306
  }
7897
8307
  });
@@ -8152,26 +8562,40 @@ function onTurnToolLifecycle(state, event, lifecycle) {
8152
8562
  const matchesTerminal = lifecycle === "started" && current.state === "none" && current.lastTerminal !== null && sameIdentity(current.lastTerminal.identity, identity);
8153
8563
  if (!matchesActive && !matchesTerminal)
8154
8564
  return { state, effects: [] };
8155
- if (lifecycle === "finished" && matchesActive && (current.outstandingToolUses ?? 0) === 0) {
8156
- return { state, effects: [] };
8157
- }
8158
8565
  return mutate(state, event.agentId, (agent) => {
8159
8566
  if (!matchesSession(agent, event.sessionInstanceId))
8160
8567
  return;
8161
8568
  const lease = agent.execution.lease;
8569
+ const callId = normalizedToolCallId(event.callId);
8162
8570
  if ((lease.state === "active" || lease.state === "suspect_active") && sameIdentity(lease.identity, identity)) {
8163
- 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);
8164
8586
  if (outstandingToolUses > 0) {
8165
8587
  agent.execution.lease = {
8166
8588
  ...lease,
8167
8589
  lastWorkAt: event.nowMs,
8168
8590
  nativeDeadlineAt: event.nowMs + agent.turnSilence.normalBudgetMs,
8169
8591
  recoveryExtensionsUsed: 0,
8170
- outstandingToolUses
8592
+ outstandingToolUses,
8593
+ ...identified.size > 0 ? { outstandingToolCallIds: [...identified] } : {}
8171
8594
  };
8172
8595
  } else {
8173
8596
  const unblockedLease = { ...lease };
8174
8597
  delete unblockedLease.outstandingToolUses;
8598
+ delete unblockedLease.outstandingToolCallIds;
8175
8599
  agent.execution.lease = {
8176
8600
  ...unblockedLease,
8177
8601
  lastWorkAt: event.nowMs,
@@ -8190,6 +8614,7 @@ function onTurnToolLifecycle(state, event, lifecycle) {
8190
8614
  nativeDeadlineAt: event.nowMs + agent.turnSilence.normalBudgetMs,
8191
8615
  recoveryExtensionsUsed: 0,
8192
8616
  outstandingToolUses: 1,
8617
+ ...callId ? { outstandingToolCallIds: [callId] } : {},
8193
8618
  reason: "work_after_terminal"
8194
8619
  };
8195
8620
  }
@@ -8353,6 +8778,10 @@ function identityOf(event) {
8353
8778
  function sameIdentity(left, right) {
8354
8779
  return left.sessionInstanceId === right.sessionInstanceId && left.turnId === right.turnId;
8355
8780
  }
8781
+ function normalizedToolCallId(rawCallId) {
8782
+ const callId = rawCallId?.trim();
8783
+ return callId && callId.length <= 1024 ? callId : undefined;
8784
+ }
8356
8785
  function leaseIsWorking(lease) {
8357
8786
  return lease.state === "active" || lease.state === "suspect_active";
8358
8787
  }
@@ -8432,13 +8861,13 @@ function cliCommandsSection() {
8432
8861
  "",
8433
8862
  "### Messaging",
8434
8863
  "",
8435
- `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.`,
8436
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\`.`,
8437
8866
  `3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a file; ` + `returns an id stable across pending→persisted.`,
8438
8867
  `4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download any ` + `attachment you can see (or your own pending uploads).`,
8439
- `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\`).`,
8440
- `6. \`${CLI} message mark set --target <full-message-ref>\` — persist a message as outstanding work.`,
8441
- `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.`,
8442
8871
  `8. \`${CLI} message mark list\` — list every currently visible marked message with its full content.`,
8443
8872
  "",
8444
8873
  "### Servers",
@@ -8446,12 +8875,14 @@ function cliCommandsSection() {
8446
8875
  `1. \`${CLI} server list\` — list your servers.`,
8447
8876
  `2. \`${CLI} server member --server <name#discriminator>\` — list a server's members.`,
8448
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.`,
8449
8879
  "",
8450
8880
  "### Channels",
8451
8881
  "",
8452
8882
  `1. \`${CLI} channel list --server <name#discriminator>\` — list top-level channels, grouped by ` + `category; each is marked \`public\` or \`private\`.`,
8453
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.",
8454
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.`,
8455
8886
  "",
8456
8887
  "### Friends",
8457
8888
  "",
@@ -8504,6 +8935,15 @@ function messagingSection() {
8504
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.",
8505
8936
  "Write every message body in the stdin/heredoc block; never place it directly on the command line.",
8506
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
+ "",
8507
8947
  "### Context refs",
8508
8948
  "",
8509
8949
  "Path-style refs:",
@@ -8616,6 +9056,7 @@ function criticalRulesSection() {
8616
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.`,
8617
9057
  "- **Never expose tokens, keys, or secrets.** Redact credential-like strings from tool output " + "before sharing.",
8618
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.`,
8619
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."
8620
9061
  ].join(`
8621
9062
  `);
@@ -8628,7 +9069,7 @@ function executionModelSection() {
8628
9069
  "",
8629
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.",
8630
9071
  "",
8631
- "`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."
8632
9073
  ].join(`
8633
9074
  `);
8634
9075
  }
@@ -8700,9 +9141,9 @@ function workspaceMemorySection(config) {
8700
9141
  "",
8701
9142
  "### Outstanding work marks",
8702
9143
  "",
8703
- "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.",
8704
9145
  "",
8705
- "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.",
8706
9147
  "",
8707
9148
  "If `inbox pull` returns a `markedReminder`, run " + `\`${CLI} message mark list\` before taking new work. Do not copy marked tasks into local files.`
8708
9149
  ].join(`
@@ -8819,7 +9260,7 @@ function nowLocalISO() {
8819
9260
  }
8820
9261
 
8821
9262
  // src/manager/managerRuntime.ts
8822
- import { randomUUID as randomUUID5 } from "node:crypto";
9263
+ import { randomUUID as randomUUID6 } from "node:crypto";
8823
9264
  var SESSION_STOP_GRACE_MS2 = 2000;
8824
9265
  function normalizeTerminationCause(value) {
8825
9266
  return value === "runtime_error" || value === "killed_stalled" ? value : "other";
@@ -9056,7 +9497,7 @@ class AgentProcessManager {
9056
9497
  liveBackendIds = new Map;
9057
9498
  activeSpawnState = new Map;
9058
9499
  publishedAgentActivity = new Map;
9059
- traceProcessNonce = randomUUID5();
9500
+ traceProcessNonce = randomUUID6();
9060
9501
  nextSpawnOrdinal = 1;
9061
9502
  nextDaemonTurnOrdinal = 1;
9062
9503
  nextDeliveryOrdinal = 1;
@@ -9332,6 +9773,12 @@ class AgentProcessManager {
9332
9773
  if (this.sessions.get(agentId) === session)
9333
9774
  this.sessions.delete(agentId);
9334
9775
  }
9776
+ async interrupt(agentId) {
9777
+ const session = this.sessions.get(agentId);
9778
+ if (!session)
9779
+ return;
9780
+ await session.interrupt({ requestId: randomUUID6(), reason: "owner_request" });
9781
+ }
9335
9782
  async stopAll() {
9336
9783
  if (this.tickTimer) {
9337
9784
  clearInterval(this.tickTimer);
@@ -9829,7 +10276,7 @@ ${this.opts.wakePromptFooter}` : text;
9829
10276
  case "reset_idle_session": {
9830
10277
  const spawnState = this.activeSpawnState.get(effect.agentId);
9831
10278
  const completion = {
9832
- eventId: `bae_${randomUUID5()}`,
10279
+ eventId: `bae_${randomUUID6()}`,
9833
10280
  occurredAt: new Date(this.now()).toISOString()
9834
10281
  };
9835
10282
  const persisted = this.forgetSession(effect.agentId, "reset_session", effect.sessionId, completion);
@@ -10224,11 +10671,12 @@ ${this.opts.wakePromptFooter}` : text;
10224
10671
  emitErrorAudit(agentId, scope, code, rawMessage) {
10225
10672
  if (!this.opts.onBotAuditEvent)
10226
10673
  return;
10674
+ const stableCode = /^[A-Za-z0-9_.-]{1,100}$/.test(code) ? code : "runtime_error";
10227
10675
  const message = scrubRuntimeErrorDiagnosticText(rawMessage).slice(0, AUDIT_ERROR_MESSAGE_MAX_LEN);
10228
10676
  try {
10229
10677
  this.opts.onBotAuditEvent(agentId, {
10230
10678
  kind: "error",
10231
- payload: { scope, code, message, model: this.currentModelFor(agentId) }
10679
+ payload: { scope, code: stableCode, message, model: this.currentModelFor(agentId) }
10232
10680
  }, {
10233
10681
  sessionId: this.liveSessions.get(agentId) ?? null,
10234
10682
  launchId: this.launchIds.get(agentId) ?? null
@@ -10253,18 +10701,18 @@ ${this.opts.wakePromptFooter}` : text;
10253
10701
  return;
10254
10702
  }
10255
10703
  const sessionSuperseded = owner.superseded;
10256
- const errorMessage2 = event.type === "session_failed" ? event.error.message : event.type === "turn_completed" && event.result.outcome === "failed" ? event.result.error.message : undefined;
10257
- if (errorMessage2 !== undefined && !sessionSuperseded) {
10258
- this.emitErrorAudit(agentId, "runtime", "runtime_error", errorMessage2);
10704
+ const runtimeError = event.type === "session_failed" ? event.error : event.type === "turn_completed" && event.result.outcome === "failed" ? event.result.error : undefined;
10705
+ if (runtimeError !== undefined && !sessionSuperseded) {
10706
+ this.emitErrorAudit(agentId, "runtime", runtimeError.code, runtimeError.message);
10259
10707
  if (this.nonCleanEndMarker.get(agentId)?.cause !== "killed_stalled") {
10260
- this.nonCleanEndMarker.set(agentId, { cause: "runtime_error", detail: errorMessage2 });
10708
+ this.nonCleanEndMarker.set(agentId, { cause: "runtime_error", detail: runtimeError.message });
10261
10709
  }
10262
10710
  const agent = this.state.agents[agentId];
10263
10711
  if (agent?.resetting && agent.resettingSince !== null && this.now() - agent.resettingSince >= this.state.resetStuckThresholdMs) {
10264
10712
  this.log.warn("runtime error during a stuck reset window", {
10265
10713
  agentId,
10266
10714
  resettingForMs: this.now() - agent.resettingSince,
10267
- message: errorMessage2
10715
+ message: runtimeError.message
10268
10716
  });
10269
10717
  }
10270
10718
  }
@@ -10359,9 +10807,9 @@ ${this.opts.wakePromptFooter}` : text;
10359
10807
  case "assistant_message_completed":
10360
10808
  return { type: "turn_work", turnId: event.turnId };
10361
10809
  case "tool_started":
10362
- return { type: "turn_tool_started", turnId: event.turnId };
10810
+ return { type: "turn_tool_started", turnId: event.turnId, callId: event.callId };
10363
10811
  case "tool_finished":
10364
- return { type: "turn_tool_finished", turnId: event.turnId };
10812
+ return { type: "turn_tool_finished", turnId: event.turnId, callId: event.callId };
10365
10813
  case "compaction_started":
10366
10814
  case "compaction_finished":
10367
10815
  case "review_started":
@@ -27136,6 +27584,10 @@ var OwnerDiagnosticReportSchema = exports_external.discriminatedUnion("status",
27136
27584
  // ../shared/src/community-cli-contract.ts
27137
27585
  var DM_SERVER = ".dm";
27138
27586
  var CONTROL_HEARTBEAT_CAPABILITY = "control-heartbeat-v1";
27587
+ var AgentInterruptRequestSchema = exports_external.strictObject({
27588
+ type: exports_external.literal("agent:interrupt"),
27589
+ agentId: exports_external.string().min(1).max(128)
27590
+ });
27139
27591
  function parseRef(ref) {
27140
27592
  if (!ref.startsWith("/"))
27141
27593
  throw new Error(`ref must start with "/": ${ref}`);
@@ -27227,6 +27679,7 @@ var HostCommandSchema = exports_external.discriminatedUnion("type", [
27227
27679
  type: exports_external.literal("agent:stop"),
27228
27680
  agentId: exports_external.string().min(1)
27229
27681
  }),
27682
+ AgentInterruptRequestSchema,
27230
27683
  exports_external.object({
27231
27684
  type: exports_external.literal("agent:reset"),
27232
27685
  agentId: exports_external.string().min(1),
@@ -27625,6 +28078,10 @@ class AgentRouter {
27625
28078
  }
27626
28079
  break;
27627
28080
  }
28081
+ case "agent:interrupt": {
28082
+ await this.opts.manager.interrupt(cmd.agentId);
28083
+ break;
28084
+ }
27628
28085
  case "agent:stop":
27629
28086
  this.log.info("agent:stop received", { agentId: cmd.agentId });
27630
28087
  try {
@@ -27794,10 +28251,11 @@ var DEFAULT_CAPABILITY_RESOLVER = (method, pathname) => {
27794
28251
  const isMessagesDoor = /\/channels\/[^/]+\/messages(\/|$|\?)/.test(pathname);
27795
28252
  if (isMessagesDoor)
27796
28253
  return method === "GET" ? "read" : "send";
28254
+ if (/\/messages\/[^/]+\/properties(\/|$|\?)/.test(pathname)) {
28255
+ return method === "GET" ? "read" : "send";
28256
+ }
27797
28257
  if (/\/messages\/[^/]+\/reactions\//.test(pathname))
27798
28258
  return "send";
27799
- if ((method === "PUT" || method === "DELETE") && /\/messages\/[^/]+\/marks(\/|$|\?)/.test(pathname))
27800
- return "send";
27801
28259
  if (method === "GET" && /\/users\/me\/marks(\/|$|\?)/.test(pathname))
27802
28260
  return "read";
27803
28261
  if (method === "GET" && /\/messages\/[^/]+(\?|$)/.test(pathname))
@@ -27838,6 +28296,12 @@ function isCanonicalChannelScope(channel2) {
27838
28296
  return false;
27839
28297
  }
27840
28298
  }
28299
+ function propertyAuditMetadata(method, pathname, requestUrl) {
28300
+ if (method !== "PUT" && method !== "DELETE" || !/^\/api\/community\/messages\/[^/]+\/properties$/.test(pathname))
28301
+ return;
28302
+ const type = new URL2(requestUrl, "http://placeholder").searchParams.get("type");
28303
+ return type === "emoji" || type === "tag" || type === "mark" ? { propertyType: type } : undefined;
28304
+ }
27841
28305
  function parseLocalMessageReminderBody(body, agentId) {
27842
28306
  let value;
27843
28307
  try {
@@ -27976,7 +28440,12 @@ async function startCredentialProxy(broker, options = {}) {
27976
28440
  }
27977
28441
  if (onProxyRequest) {
27978
28442
  try {
27979
- onProxyRequest(reg.agentId, req.method ?? "GET", pathname);
28443
+ const method = req.method ?? "GET";
28444
+ const metadata = propertyAuditMetadata(method, pathname, req.url ?? pathname);
28445
+ if (metadata)
28446
+ onProxyRequest(reg.agentId, method, pathname, metadata);
28447
+ else
28448
+ onProxyRequest(reg.agentId, method, pathname);
27980
28449
  } catch {}
27981
28450
  }
27982
28451
  const outHeaders = { ...req.headers };
@@ -28394,6 +28863,7 @@ var ALLOWED_ICON_MIME_TYPES = [
28394
28863
  ];
28395
28864
  var MAX_ICON_SOURCE_FILE_SIZE_BYTES = 15 * 1024 * 1024;
28396
28865
  // ../shared/src/provider-telemetry.ts
28866
+ var DAILY_TOKEN_USAGE_WINDOW_DAYS = 30;
28397
28867
  var safeToken = exports_external.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
28398
28868
  var boundedText = exports_external.string().min(1).refine((value) => new TextEncoder().encode(value).length <= 64, { message: "must be at most 64 UTF-8 bytes" });
28399
28869
  var DailyUsageMetricSchema = safeToken.nullable();
@@ -29125,7 +29595,7 @@ var AgentActivityMessageSchema = exports_external.object({
29125
29595
  state: exports_external.enum(["idle", "starting", "running", "stopping"]),
29126
29596
  usageTimeZone: exports_external.string().min(1).max(128).optional(),
29127
29597
  usageDay: exports_external.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
29128
- dailyUsage: exports_external.array(DailyUsageSnapshotSchema).max(7).optional(),
29598
+ dailyUsage: exports_external.array(DailyUsageSnapshotSchema).max(DAILY_TOKEN_USAGE_WINDOW_DAYS).optional(),
29129
29599
  quota: ProviderQuotaSnapshotSchema.optional()
29130
29600
  });
29131
29601
  var AgentTypingMessageSchema = exports_external.object({
@@ -29289,17 +29759,18 @@ var CommunityAgentNapRequestSchema = exports_external.object({
29289
29759
  message: "handoff is required"
29290
29760
  })
29291
29761
  });
29292
- var CommunityAgentReactAddRequestSchema = exports_external.object({
29762
+ var CommunityAgentMessagePropertyRequestSchema = exports_external.object({
29293
29763
  channel: exports_external.string().min(1),
29294
29764
  seq: CommunityAgentPositiveSeqSchema,
29295
- emoji: exports_external.string().min(1)
29765
+ property: exports_external.unknown()
29296
29766
  });
29297
29767
  var CommunityAgentFriendRequestSchema = exports_external.object({
29298
29768
  username: exports_external.string().min(1)
29299
29769
  });
29300
29770
  var CommunityAgentListFriendsSchema = exports_external.object({});
29301
29771
  var AuditLogCliInvocationPayloadSchema = exports_external.object({
29302
- subcommand: exports_external.string().min(1)
29772
+ subcommand: exports_external.string().min(1),
29773
+ propertyType: exports_external.enum(["emoji", "tag", "mark"]).optional()
29303
29774
  });
29304
29775
  var AuditLogToolCallPayloadSchema = exports_external.object({
29305
29776
  name: exports_external.string().min(1),
@@ -32678,7 +33149,7 @@ class MessageReminderScheduler {
32678
33149
  }
32679
33150
 
32680
33151
  // src/manager/agentDriverHost.ts
32681
- import { randomUUID as randomUUID6 } from "node:crypto";
33152
+ import { randomUUID as randomUUID7 } from "node:crypto";
32682
33153
  import { homedir as homedir4 } from "node:os";
32683
33154
  import { join as join13 } from "node:path";
32684
33155
 
@@ -32834,7 +33305,7 @@ function createDaemonAgentDriverHost(ctx, onRawLine) {
32834
33305
  onRawLine?.(event.text);
32835
33306
  },
32836
33307
  now: () => Date.now(),
32837
- createId: () => randomUUID6()
33308
+ createId: () => randomUUID7()
32838
33309
  };
32839
33310
  }
32840
33311
 
@@ -32913,13 +33384,16 @@ class DaemonSelfSleepScheduler {
32913
33384
  // src/telemetry/dailyTokenUsage.ts
32914
33385
  import { chmod, mkdir, open, readFile as readFile2, rename, rm } from "node:fs/promises";
32915
33386
  import { dirname as dirname5, join as join14 } from "node:path";
32916
- import { randomUUID as randomUUID7 } from "node:crypto";
33387
+ import { randomUUID as randomUUID8 } from "node:crypto";
33388
+ var TOKEN_USAGE_WINDOW_DAYS = 30;
33389
+ var TOKEN_USAGE_RECOVERY_DAYS = 2;
33390
+ var TOKEN_USAGE_RETENTION_DAYS = TOKEN_USAGE_WINDOW_DAYS + TOKEN_USAGE_RECOVERY_DAYS;
32917
33391
  function oldestRetainedDay(at, timeZone) {
32918
33392
  const today = dayKeyInTimeZone(at, timeZone);
32919
- return calendarDayKeyDaysAgo(today, 8);
33393
+ return calendarDayKeyDaysAgo(today, TOKEN_USAGE_RETENTION_DAYS - 1);
32920
33394
  }
32921
33395
  function oldestVisibleDay(today) {
32922
- return calendarDayKeyDaysAgo(today, 6);
33396
+ return calendarDayKeyDaysAgo(today, TOKEN_USAGE_WINDOW_DAYS - 1);
32923
33397
  }
32924
33398
  function isMetric(value) {
32925
33399
  return value === null || typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
@@ -33081,7 +33555,7 @@ class DailyTokenUsageStore {
33081
33555
  async persist() {
33082
33556
  const directory = dirname5(this.filePath);
33083
33557
  await mkdir(directory, { recursive: true, mode: 448 });
33084
- const temporary = `${this.filePath}.${randomUUID7()}.tmp`;
33558
+ const temporary = `${this.filePath}.${randomUUID8()}.tmp`;
33085
33559
  try {
33086
33560
  const file2 = await open(temporary, "wx", 384);
33087
33561
  try {
@@ -33163,15 +33637,17 @@ function createRuntimeRawLineTap(args) {
33163
33637
  }
33164
33638
  function deriveAuditLogSubcommand(pathname, method) {
33165
33639
  const canonical = pathname.split("?")[0] ?? pathname;
33166
- if (/^\/api\/community\/messages\/[^/]+\/reactions\//.test(canonical))
33167
- return "reactAdd";
33168
- if (/^\/api\/community\/messages\/[^/]+\/marks$/.test(canonical)) {
33640
+ if (/^\/api\/community\/messages\/[^/]+\/properties$/.test(canonical)) {
33641
+ if (method === "GET")
33642
+ return "messagePropertyList";
33169
33643
  if (method === "PUT")
33170
- return "markSet";
33644
+ return "messagePropertySet";
33171
33645
  if (method === "DELETE")
33172
- return "markRemove";
33646
+ return "messagePropertyRemove";
33173
33647
  return null;
33174
33648
  }
33649
+ if (/^\/api\/community\/messages\/[^/]+\/reactions\//.test(canonical))
33650
+ return "reactAdd";
33175
33651
  if (method === "GET" && /^\/api\/community\/users\/me\/marks$/.test(canonical))
33176
33652
  return "markList";
33177
33653
  if (/^\/api\/community\/channels\/[^/]+\/messages\/seq\//.test(canonical))
@@ -33337,14 +33813,14 @@ async function createDaemon(opts) {
33337
33813
  log2.warn("inbox pull timeline observation failed", { agentId, reason, contentEncoding });
33338
33814
  },
33339
33815
  onMessageReminderArm: (input) => reminderSchedulerRef?.arm(input) ?? { armed: false, reason: "reminder_scheduler_unavailable" },
33340
- onProxyRequest: (agentId, method, pathname) => {
33816
+ onProxyRequest: (agentId, method, pathname, metadata) => {
33341
33817
  const subcommand = deriveAuditLogSubcommand(pathname, method);
33342
33818
  if (!subcommand)
33343
33819
  return;
33344
33820
  const context = managerRef?.auditContext(agentId);
33345
33821
  emitBotAuditEvent(agentId, {
33346
33822
  kind: "cli_invocation",
33347
- payload: { subcommand }
33823
+ payload: { subcommand, ...metadata ? { propertyType: metadata.propertyType } : {} }
33348
33824
  }, context);
33349
33825
  emitImplicitTypingStopOnSend({
33350
33826
  subcommand,