@agentproto/runtime 5.13.0 → 5.15.0

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.mjs CHANGED
@@ -7,7 +7,7 @@ import { ensureChromeDevtoolsMcp, resolveChrome, buildHeadlessBrowserMcpEntry, h
7
7
  import { selectPricingTier, getModelProvider, resolveContextWindow, formatTokens, splitContextWindowHint, resolvePricing, LLM_PRICING_CATALOG, resolvePricingExact, MODEL_ALIASES, isKnownLlmId } from '@agentproto/model-catalog/llm';
8
8
  import { resolveCustomRoute, registerCustomRoute, tryParseModelRef, resolveLlmModelRoute, formatModelRef, stripRouteSuffix, stripFixedNativeVendor } from '@agentproto/model-catalog/route-identity';
9
9
  import { randomBytes, createHmac, randomUUID, timingSafeEqual, createHash } from 'crypto';
10
- import { createReadStream, promises, readFileSync, mkdirSync, writeFileSync, renameSync, readdirSync, openSync, fstatSync, readSync, closeSync, existsSync, statSync, mkdtempSync, chmodSync, realpathSync, cpSync, appendFileSync, createWriteStream } from 'fs';
10
+ import { createReadStream, promises, readFileSync, mkdirSync, writeFileSync, renameSync, readdirSync, openSync, fstatSync, readSync, closeSync, existsSync, statSync, mkdtempSync, chmodSync, writeSync, fsyncSync, unlinkSync, realpathSync, cpSync, appendFileSync, createWriteStream } from 'fs';
11
11
  import { createInterface } from 'readline';
12
12
  import * as providers_store_star from '@agentproto/providers-store';
13
13
  import { findAnthropicGatewayPreset, anthropicGatewayPresetList, getAnthropicGatewayPreset } from '@agentproto/provider-presets';
@@ -16,7 +16,7 @@ import { z, ZodError } from 'zod';
16
16
  import { listAuthProfiles, eligibleProfiles, getAuthProfile, createAuthProfile, AuthProfileValidationError, deleteAuthProfile, updateAuthProfile, KeychainStore, removeAuthProfile, addAuthProfile, setAuthProfileEnabled, setAuthProfileModels, refreshAuthProfileModels, credentialIdentity } from '@agentproto/auth';
17
17
  import { CatalogProviderSchema, getModelsByProvider, getStaticModelProvider } from '@agentproto/model-catalog';
18
18
  import { resolvePairPageUrl, DEFAULT_PAIR_PAGE } from '@agentproto/secrets/pairing/browser';
19
- import { ListToolsRequestSchema, CallToolRequestSchema, SUPPORTED_PROTOCOL_VERSIONS, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
19
+ import { ListToolsRequestSchema, CallToolRequestSchema, McpError, ErrorCode, SUPPORTED_PROTOCOL_VERSIONS, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
20
20
  import { makeCredsStore, makeAdapterResolver, makeAdapterLister, discoverAdapterPackages, makeSetupLedger, makeListTool, makeSetupTool } from '@agentproto/provider-kit';
21
21
  import { syncPiModels, getConfiguredEndpoints, checkHarnessFit, connectorById } from '@agentproto/llm-endpoint';
22
22
  import { createServer, request as request$1 } from 'http';
@@ -38,6 +38,7 @@ import { createBrainManager, parseKnowledgeConfig } from '@agentproto/workspace-
38
38
  import { browserAdapters, getBrowserAdapter } from '@agentproto/adapter-browser';
39
39
  import { defineHttpDriver } from '@agentproto/driver-http';
40
40
  import matter2 from 'gray-matter';
41
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
41
42
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
42
43
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
43
44
  import { WebSocketServer } from 'ws';
@@ -48,8 +49,8 @@ import { createReconnectLogGate } from '@agentproto/pairing-host';
48
49
  export { createReconnectLogGate } from '@agentproto/pairing-host';
49
50
  import { loadAppBundledTools, loadAppHandle, peekAppUi, unpackApp, safeId, versionSatisfies } from '@agentproto/app-kit';
50
51
  import { loadAgent } from '@agentproto/agent';
51
- import { normalizeToolId } from '@agentproto/driver';
52
- import { compileWorkflow, runWorkflow, isCompilableJsonSchema, validateAgainstJsonSchema, validateWorkflowInput, buildAgentStep, StepOutcomeError, MissingArtifactError } from '@agentproto/workflow-runtime';
52
+ import { normalizeToolId, runTool } from '@agentproto/driver';
53
+ import { compileWorkflow, runWorkflow, isCompilableJsonSchema, validateAgainstJsonSchema, validateWorkflowInput, buildAgentStep, AgentSessionLostError, StepOutcomeError, MissingArtifactError } from '@agentproto/workflow-runtime';
53
54
  import { loadWorkflowHandle } from '@agentproto/workflow-loader';
54
55
  import { Cron } from 'croner';
55
56
  import { brotliCompressSync, constants, gzipSync } from 'zlib';
@@ -145,15 +146,15 @@ function hasMarker(command, marker) {
145
146
  return false;
146
147
  }
147
148
  function listProcesses() {
148
- return new Promise((resolve45) => {
149
+ return new Promise((resolve47) => {
149
150
  execFile("ps", ["-axo", "pid=,command="], { maxBuffer: 32 * 1024 * 1024 }, (err, stdout) => {
150
- if (err) return resolve45([]);
151
+ if (err) return resolve47([]);
151
152
  const out = [];
152
153
  for (const line of stdout.split("\n")) {
153
154
  const m = line.match(/^\s*(\d+)\s+(.*)$/);
154
155
  if (m) out.push({ pid: Number(m[1]), command: m[2] ?? "" });
155
156
  }
156
- resolve45(out);
157
+ resolve47(out);
157
158
  });
158
159
  });
159
160
  }
@@ -523,7 +524,7 @@ function resolveSessionPricing(model) {
523
524
  function tokenCost(tokens, pricePer1M) {
524
525
  return tokens === void 0 ? 0 : tokens * pricePer1M / 1e6;
525
526
  }
526
- function deriveSessionUsage(input, resolve45 = defaultResolver) {
527
+ function deriveSessionUsage(input, resolve47 = defaultResolver) {
527
528
  const contextUsed = plausibleContextUsed(input.contextSize, input.contextUsed);
528
529
  const base = {
529
530
  ...input.model !== void 0 ? { model: input.model } : {},
@@ -538,7 +539,7 @@ function deriveSessionUsage(input, resolve45 = defaultResolver) {
538
539
  }
539
540
  const hasTokens = input.tokensIn !== void 0 || input.tokensOut !== void 0 || input.cacheReadTokens !== void 0 || input.cacheWriteTokens !== void 0;
540
541
  if (hasTokens) {
541
- const basePricing = input.model !== void 0 ? resolve45(input.model) : void 0;
542
+ const basePricing = input.model !== void 0 ? resolve47(input.model) : void 0;
542
543
  if (!basePricing) {
543
544
  return { ...base, source: "no-pricing" };
544
545
  }
@@ -1045,8 +1046,8 @@ function createTranscriptWriter(opts) {
1045
1046
  const state = states.get(sessionId);
1046
1047
  if (!state) return Promise.resolve();
1047
1048
  flushBuffers(sessionId, state);
1048
- return new Promise((resolve45) => {
1049
- state.stream.write("", () => resolve45());
1049
+ return new Promise((resolve47) => {
1050
+ state.stream.write("", () => resolve47());
1050
1051
  });
1051
1052
  },
1052
1053
  close(sessionId) {
@@ -1054,8 +1055,8 @@ function createTranscriptWriter(opts) {
1054
1055
  if (!state) return Promise.resolve();
1055
1056
  flushBuffers(sessionId, state);
1056
1057
  states.delete(sessionId);
1057
- return new Promise((resolve45) => {
1058
- state.stream.end(() => resolve45());
1058
+ return new Promise((resolve47) => {
1059
+ state.stream.end(() => resolve47());
1059
1060
  });
1060
1061
  },
1061
1062
  closeAll() {
@@ -1064,7 +1065,7 @@ function createTranscriptWriter(opts) {
1064
1065
  const state = states.get(sessionId);
1065
1066
  if (!state) continue;
1066
1067
  flushBuffers(sessionId, state);
1067
- closings.push(new Promise((resolve45) => state.stream.end(() => resolve45())));
1068
+ closings.push(new Promise((resolve47) => state.stream.end(() => resolve47())));
1068
1069
  }
1069
1070
  states.clear();
1070
1071
  return Promise.all(closings).then(() => void 0);
@@ -1225,9 +1226,9 @@ async function exportClaudeCodeSession(adapterSessionId, cwd, configDir) {
1225
1226
  let stream;
1226
1227
  try {
1227
1228
  stream = createReadStream(filePath, { encoding: "utf8" });
1228
- await new Promise((resolve45, reject) => {
1229
+ await new Promise((resolve47, reject) => {
1229
1230
  stream.once("error", reject);
1230
- stream.once("open", resolve45);
1231
+ stream.once("open", resolve47);
1231
1232
  });
1232
1233
  } catch (err) {
1233
1234
  const code = err.code;
@@ -1501,7 +1502,7 @@ async function discoverHermesSessions(cwd, since, expectedId) {
1501
1502
  }
1502
1503
  async function defaultHermesRunner(adapterSessionId) {
1503
1504
  const { spawn: spawn8 } = await import('child_process');
1504
- return new Promise((resolve45, reject) => {
1505
+ return new Promise((resolve47, reject) => {
1505
1506
  const chunks = [];
1506
1507
  const errChunks = [];
1507
1508
  const proc = spawn8(
@@ -1520,7 +1521,7 @@ async function defaultHermesRunner(adapterSessionId) {
1520
1521
  )
1521
1522
  );
1522
1523
  } else {
1523
- resolve45(Buffer.concat(chunks).toString("utf8"));
1524
+ resolve47(Buffer.concat(chunks).toString("utf8"));
1524
1525
  }
1525
1526
  });
1526
1527
  });
@@ -1557,9 +1558,9 @@ async function exportDaemonEventsSession(sessionId, desc, baseDir) {
1557
1558
  let stream;
1558
1559
  try {
1559
1560
  stream = createReadStream(filePath, { encoding: "utf8" });
1560
- await new Promise((resolve45, reject) => {
1561
+ await new Promise((resolve47, reject) => {
1561
1562
  stream.once("error", reject);
1562
- stream.once("open", resolve45);
1563
+ stream.once("open", resolve47);
1563
1564
  });
1564
1565
  } catch (err) {
1565
1566
  const code = err.code;
@@ -1781,7 +1782,11 @@ async function readFirstJsonLine(path) {
1781
1782
  function codexHome() {
1782
1783
  return process.env.CODEX_HOME ?? join(homedir(), ".codex");
1783
1784
  }
1784
- function codexSessionsDir() {
1785
+ function codexSessionsDir(configDir) {
1786
+ if (configDir) {
1787
+ const isolated = join(configDir, "sessions");
1788
+ if (existsSync(isolated)) return isolated;
1789
+ }
1785
1790
  return join(codexHome(), "sessions");
1786
1791
  }
1787
1792
  async function walkCodexRollouts(dir) {
@@ -1816,8 +1821,8 @@ function codexBlocksText(content) {
1816
1821
  }
1817
1822
  return acc;
1818
1823
  }
1819
- async function findCodexRolloutFile(conversationId) {
1820
- const files = await walkCodexRollouts(codexSessionsDir());
1824
+ async function findCodexRolloutFile(conversationId, configDir) {
1825
+ const files = await walkCodexRollouts(codexSessionsDir(configDir));
1821
1826
  const byName = files.find((f) => f.endsWith(`-${conversationId}.jsonl`));
1822
1827
  if (byName) return byName;
1823
1828
  for (const f of files) {
@@ -1834,11 +1839,11 @@ async function findCodexRolloutFile(conversationId) {
1834
1839
  }
1835
1840
  return void 0;
1836
1841
  }
1837
- async function exportCodexSession(conversationId) {
1838
- const file = await findCodexRolloutFile(conversationId);
1842
+ async function exportCodexSession(conversationId, configDir) {
1843
+ const file = await findCodexRolloutFile(conversationId, configDir);
1839
1844
  if (!file) {
1840
1845
  throw new Error(
1841
- `codex: no rollout file for conversation "${conversationId}" under ${codexSessionsDir()}.
1846
+ `codex: no rollout file for conversation "${conversationId}" under ${codexSessionsDir(configDir)}.
1842
1847
  The session may predate persistence, or CODEX_HOME points elsewhere.`
1843
1848
  );
1844
1849
  }
@@ -2488,7 +2493,7 @@ var init_transcript_export = __esm({
2488
2493
  exportSession: (id) => exportHermesSession(id)
2489
2494
  },
2490
2495
  codex: {
2491
- exportSession: (id) => exportCodexSession(id)
2496
+ exportSession: (id, _cwd, configDir) => exportCodexSession(id, configDir)
2492
2497
  },
2493
2498
  opencode: {
2494
2499
  exportSession: (id) => exportOpenCodeSession(id)
@@ -3907,8 +3912,8 @@ async function enrichWithRemainingQuota(rollup, opts) {
3907
3912
  })
3908
3913
  );
3909
3914
  const capMs = opts.timeoutMs ?? DEFAULT_ENRICH_CAP_MS;
3910
- const cap = new Promise((resolve45) => {
3911
- setTimeout(() => resolve45(null), capMs);
3915
+ const cap = new Promise((resolve47) => {
3916
+ setTimeout(() => resolve47(null), capMs);
3912
3917
  });
3913
3918
  const settled = await Promise.race([work, cap]);
3914
3919
  if (settled === null) return rollup;
@@ -3972,8 +3977,8 @@ async function enrichWithAccountCredits(rollup, opts) {
3972
3977
  })
3973
3978
  );
3974
3979
  const capMs = opts.timeoutMs ?? DEFAULT_ENRICH_CAP_MS;
3975
- const cap = new Promise((resolve45) => {
3976
- setTimeout(() => resolve45(null), capMs);
3980
+ const cap = new Promise((resolve47) => {
3981
+ setTimeout(() => resolve47(null), capMs);
3977
3982
  });
3978
3983
  const settled = await Promise.race([work, cap]);
3979
3984
  if (settled === null) return rollup;
@@ -4228,14 +4233,14 @@ function parseProcStat(statLine, cmdline, uid, ctx) {
4228
4233
  };
4229
4234
  }
4230
4235
  function execPs(platform, timeoutMs) {
4231
- return new Promise((resolve45, reject) => {
4236
+ return new Promise((resolve47, reject) => {
4232
4237
  execFile(
4233
4238
  "ps",
4234
4239
  psArgs(platform),
4235
4240
  // LC_ALL=C pins the decimal separator of `pcpu` to `.` regardless of the
4236
4241
  // operator's locale.
4237
4242
  { maxBuffer: 32 * 1024 * 1024, timeout: timeoutMs, env: { ...process.env, LC_ALL: "C" } },
4238
- (err, stdout) => err ? reject(new Error(`ps failed${err.killed ? " (timed out, host overloaded?)" : ""}: ${err.message.trim()}`)) : resolve45(stdout)
4243
+ (err, stdout) => err ? reject(new Error(`ps failed${err.killed ? " (timed out, host overloaded?)" : ""}: ${err.message.trim()}`)) : resolve47(stdout)
4239
4244
  );
4240
4245
  });
4241
4246
  }
@@ -4539,8 +4544,8 @@ function parseVmStatAvailable(text13) {
4539
4544
  return total * Number(size[1]);
4540
4545
  }
4541
4546
  function execText(cmd, args) {
4542
- return new Promise((resolve45, reject) => {
4543
- execFile(cmd, args, { timeout: 3e3 }, (err, stdout) => err ? reject(err) : resolve45(stdout));
4547
+ return new Promise((resolve47, reject) => {
4548
+ execFile(cmd, args, { timeout: 3e3 }, (err, stdout) => err ? reject(err) : resolve47(stdout));
4544
4549
  });
4545
4550
  }
4546
4551
  function createProcessStatsService(opts = {}) {
@@ -4566,14 +4571,14 @@ function createProcessStatsService(opts = {}) {
4566
4571
  nowMs: now()
4567
4572
  }).claimedPids;
4568
4573
  const byPid = new Map(rows.map((r) => [r.pid, r]));
4569
- const keyOf2 = (pid) => `${pid}:${Math.floor((now() - (byPid.get(pid)?.elapsedSec ?? 0) * 1e3) / 2e3)}`;
4574
+ const keyOf3 = (pid) => `${pid}:${Math.floor((now() - (byPid.get(pid)?.elapsedSec ?? 0) * 1e3) / 2e3)}`;
4570
4575
  const candidates = strayEnvCandidates(rows, claimed, uid);
4571
- const unread = candidates.filter((p) => !envMemo.has(keyOf2(p)));
4576
+ const unread = candidates.filter((p) => !envMemo.has(keyOf3(p)));
4572
4577
  const read = await readEnv(unread);
4573
- for (const p of unread) envMemo.set(keyOf2(p), read.get(p) ?? null);
4578
+ for (const p of unread) envMemo.set(keyOf3(p), read.get(p) ?? null);
4574
4579
  const envHints = /* @__PURE__ */ new Map();
4575
4580
  for (const p of candidates) {
4576
- const h = envMemo.get(keyOf2(p));
4581
+ const h = envMemo.get(keyOf3(p));
4577
4582
  if (h) envHints.set(p, h);
4578
4583
  }
4579
4584
  if (envMemo.size > 4096) envMemo.clear();
@@ -5419,9 +5424,9 @@ async function readFirstTurn(sessionId) {
5419
5424
  let stream;
5420
5425
  try {
5421
5426
  stream = createReadStream(sessionEventsPath(sessionId), { encoding: "utf8" });
5422
- await new Promise((resolve45, reject) => {
5427
+ await new Promise((resolve47, reject) => {
5423
5428
  stream.once("error", reject);
5424
- stream.once("open", resolve45);
5429
+ stream.once("open", resolve47);
5425
5430
  });
5426
5431
  } catch {
5427
5432
  return null;
@@ -8935,7 +8940,7 @@ function boundaryMeta(b) {
8935
8940
  return { [APP_BOUNDARY_META_KEY]: encodeAppBoundary(b) };
8936
8941
  }
8937
8942
  function inAny(canon, zones) {
8938
- return zones.some((z61) => isPathWithin(canon, canonicalizePath(z61)));
8943
+ return zones.some((z62) => isPathWithin(canon, canonicalizePath(z62)));
8939
8944
  }
8940
8945
  function resolveBoundaryPath(b, input, mode) {
8941
8946
  if (typeof input !== "string" || input.length === 0) {
@@ -9271,7 +9276,7 @@ async function* pollAgentOutputFallback(host, remoteSessionId, since) {
9271
9276
  `sandbox proxy: ${consecutivePollFailures} consecutive poll failures against the box daemon (session "${remoteSessionId}") \u2014 giving up. Last error: ${pollErr instanceof Error ? pollErr.message : String(pollErr)}`
9272
9277
  );
9273
9278
  }
9274
- await new Promise((resolve45) => setTimeout(resolve45, POLL_RETRY_DELAY_MS));
9279
+ await new Promise((resolve47) => setTimeout(resolve47, POLL_RETRY_DELAY_MS));
9275
9280
  continue;
9276
9281
  }
9277
9282
  if (result.timedOut) continue;
@@ -9506,19 +9511,19 @@ async function startDeviceSandboxBridge(opts) {
9506
9511
  }
9507
9512
  }
9508
9513
  const port = await getFreePort();
9509
- await new Promise((resolve45, reject) => {
9514
+ await new Promise((resolve47, reject) => {
9510
9515
  server.once("error", reject);
9511
- server.listen(port, "127.0.0.1", () => resolve45());
9516
+ server.listen(port, "127.0.0.1", () => resolve47());
9512
9517
  });
9513
9518
  return {
9514
9519
  mcpUrl: `http://127.0.0.1:${port}/mcp`,
9515
9520
  async close() {
9516
- await new Promise((resolve45) => server.close(() => resolve45()));
9521
+ await new Promise((resolve47) => server.close(() => resolve47()));
9517
9522
  }
9518
9523
  };
9519
9524
  }
9520
9525
  async function getFreePort() {
9521
- return new Promise((resolve45, reject) => {
9526
+ return new Promise((resolve47, reject) => {
9522
9527
  const srv = createServer$1();
9523
9528
  srv.once("error", reject);
9524
9529
  srv.listen(0, "127.0.0.1", () => {
@@ -9528,7 +9533,7 @@ async function getFreePort() {
9528
9533
  return;
9529
9534
  }
9530
9535
  const { port } = address;
9531
- srv.close(() => resolve45(port));
9536
+ srv.close(() => resolve47(port));
9532
9537
  });
9533
9538
  });
9534
9539
  }
@@ -9673,7 +9678,7 @@ async function pollServeReady(url, opts) {
9673
9678
  for (; ; ) {
9674
9679
  if (await probe(url)) return true;
9675
9680
  if (Date.now() >= deadline) return false;
9676
- await new Promise((resolve45) => setTimeout(resolve45, intervalMs));
9681
+ await new Promise((resolve47) => setTimeout(resolve47, intervalMs));
9677
9682
  }
9678
9683
  }
9679
9684
  async function seedAllowlist(client) {
@@ -9798,7 +9803,7 @@ var init_sandbox_app_serve = __esm({
9798
9803
  }
9799
9804
  });
9800
9805
  async function getFreePort2() {
9801
- return new Promise((resolve45, reject) => {
9806
+ return new Promise((resolve47, reject) => {
9802
9807
  const srv = createServer$1();
9803
9808
  srv.once("error", reject);
9804
9809
  srv.listen(0, "127.0.0.1", () => {
@@ -9808,7 +9813,7 @@ async function getFreePort2() {
9808
9813
  return;
9809
9814
  }
9810
9815
  const { port } = address;
9811
- srv.close(() => resolve45(port));
9816
+ srv.close(() => resolve47(port));
9812
9817
  });
9813
9818
  });
9814
9819
  }
@@ -9821,7 +9826,7 @@ async function probeHealth(url, timeoutMs) {
9821
9826
  } catch {
9822
9827
  }
9823
9828
  if (Date.now() >= deadline) return false;
9824
- await new Promise((resolve45) => setTimeout(resolve45, POLL_INTERVAL_MS));
9829
+ await new Promise((resolve47) => setTimeout(resolve47, POLL_INTERVAL_MS));
9825
9830
  }
9826
9831
  }
9827
9832
  var HEALTH_TIMEOUT_MS, POLL_INTERVAL_MS, localSandboxProvider;
@@ -11398,8 +11403,8 @@ async function spawnAgentSession(deps2, input) {
11398
11403
  }
11399
11404
  let resolveClaim;
11400
11405
  claims.set(key, {
11401
- result: new Promise((resolve45) => {
11402
- resolveClaim = resolve45;
11406
+ result: new Promise((resolve47) => {
11407
+ resolveClaim = resolve47;
11403
11408
  }),
11404
11409
  // An implicit claim expires sooner than an explicit one — see
11405
11410
  // `IMPLICIT_SPAWN_CLAIM_WINDOW_MS`'s docblock.
@@ -11958,13 +11963,13 @@ async function isSharedDirtyCwd(cwd) {
11958
11963
  const identity = resolveWorktreeIdentity(cwd);
11959
11964
  if (identity?.worktreeId !== void 0) return false;
11960
11965
  const { spawn: spawn8 } = await import('child_process');
11961
- return await new Promise((resolve45) => {
11966
+ return await new Promise((resolve47) => {
11962
11967
  let stdout = "";
11963
11968
  let settled = false;
11964
11969
  const done = (v) => {
11965
11970
  if (!settled) {
11966
11971
  settled = true;
11967
- resolve45(v);
11972
+ resolve47(v);
11968
11973
  }
11969
11974
  };
11970
11975
  const child = spawn8("git", ["-C", cwd, "status", "--porcelain"], {
@@ -11979,15 +11984,15 @@ async function isSharedDirtyCwd(cwd) {
11979
11984
  }
11980
11985
  async function resolveMcpCredentialHeaders(mcpServers) {
11981
11986
  if (!mcpServers || mcpServers.length === 0) return mcpServers;
11982
- const { resolveMcpCredentialHeaders: resolve45 } = getMcpCredentialDeps();
11983
- if (!resolve45) return mcpServers;
11987
+ const { resolveMcpCredentialHeaders: resolve47 } = getMcpCredentialDeps();
11988
+ if (!resolve47) return mcpServers;
11984
11989
  return Promise.all(
11985
11990
  mcpServers.map(async (entry) => {
11986
11991
  const ref = entry.credentialRef;
11987
11992
  if (!ref) return entry;
11988
11993
  let brokered;
11989
11994
  try {
11990
- brokered = await resolve45({ credentialRef: ref });
11995
+ brokered = await resolve47({ credentialRef: ref });
11991
11996
  } catch (err) {
11992
11997
  console.warn(
11993
11998
  `[agent_start] credentialRef resolution failed for "${entry.name}" (${ref}): ${err instanceof Error ? err.message : String(err)}`
@@ -12083,7 +12088,7 @@ async function startDeviceSessionWithRetry(host, args, target) {
12083
12088
  lastErr = err;
12084
12089
  if (!isDeviceTransportError(err)) throw err;
12085
12090
  if (attempt < attempts) {
12086
- await new Promise((resolve45) => setTimeout(resolve45, DEVICE_SPAWN_RETRY_DELAY_MS));
12091
+ await new Promise((resolve47) => setTimeout(resolve47, DEVICE_SPAWN_RETRY_DELAY_MS));
12087
12092
  }
12088
12093
  }
12089
12094
  }
@@ -12335,10 +12340,10 @@ function markFailedBootLedger(opts) {
12335
12340
  }
12336
12341
  }
12337
12342
  async function resolveSandboxSecret(slug) {
12338
- const { resolveSandboxSecret: resolve45 } = getMcpCredentialDeps();
12339
- if (!resolve45) return null;
12343
+ const { resolveSandboxSecret: resolve47 } = getMcpCredentialDeps();
12344
+ if (!resolve47) return null;
12340
12345
  try {
12341
- return await resolve45(slug);
12346
+ return await resolve47(slug);
12342
12347
  } catch (err) {
12343
12348
  console.warn(
12344
12349
  `[agent_start] sandbox secret resolution failed for "${slug}": ${err instanceof Error ? err.message : String(err)}`
@@ -13044,8 +13049,8 @@ async function readLogTailSafe(dir, n) {
13044
13049
  async function resolveAppUiBuildState(input) {
13045
13050
  const { dir, uiPath } = input;
13046
13051
  const pending = peekInFlightBuild(uiPath) ?? ensureAppUiBuilt(input);
13047
- const timeout = new Promise((resolve45) => {
13048
- const timer = setTimeout(() => resolve45(RESOLVE_TIMEOUT), RESOLVE_FAST_PATH_MS);
13052
+ const timeout = new Promise((resolve47) => {
13053
+ const timer = setTimeout(() => resolve47(RESOLVE_TIMEOUT), RESOLVE_FAST_PATH_MS);
13049
13054
  timer.unref();
13050
13055
  });
13051
13056
  const raced = await Promise.race([pending, timeout]);
@@ -16402,8 +16407,8 @@ function createTerminalTranscriptWriter(opts) {
16402
16407
  const stream = streams.get(sessionId);
16403
16408
  if (!stream) return Promise.resolve();
16404
16409
  streams.delete(sessionId);
16405
- return new Promise((resolve45) => {
16406
- stream.end(() => resolve45());
16410
+ return new Promise((resolve47) => {
16411
+ stream.end(() => resolve47());
16407
16412
  });
16408
16413
  },
16409
16414
  closeAll() {
@@ -16412,8 +16417,8 @@ function createTerminalTranscriptWriter(opts) {
16412
16417
  const stream = streams.get(sessionId);
16413
16418
  if (!stream) continue;
16414
16419
  closings.push(
16415
- new Promise((resolve45) => {
16416
- stream.end(() => resolve45());
16420
+ new Promise((resolve47) => {
16421
+ stream.end(() => resolve47());
16417
16422
  })
16418
16423
  );
16419
16424
  }
@@ -16860,8 +16865,8 @@ function createRegistryHandoffAsker(registry, desc, baseDir) {
16860
16865
  const turn = registry.sendPrompt(sessionId, prompt, { source: HANDOFF_PROMPT_SOURCE });
16861
16866
  let timer;
16862
16867
  const timedOut = /* @__PURE__ */ Symbol("timeout");
16863
- const deadline = new Promise((resolve45) => {
16864
- timer = setTimeout(() => resolve45(timedOut), timeoutMs);
16868
+ const deadline = new Promise((resolve47) => {
16869
+ timer = setTimeout(() => resolve47(timedOut), timeoutMs);
16865
16870
  });
16866
16871
  try {
16867
16872
  const outcome = await Promise.race([turn, deadline]);
@@ -16876,7 +16881,7 @@ function createRegistryHandoffAsker(registry, desc, baseDir) {
16876
16881
  for (let attempt = 0; attempt < REPLY_POLL_ATTEMPTS; attempt++) {
16877
16882
  const reply = readLastAssistantTextSync(eventsPath2);
16878
16883
  if (reply && reply !== before) return reply;
16879
- await new Promise((resolve45) => setTimeout(resolve45, REPLY_POLL_MS));
16884
+ await new Promise((resolve47) => setTimeout(resolve47, REPLY_POLL_MS));
16880
16885
  }
16881
16886
  throw new Error("source session produced no reply");
16882
16887
  };
@@ -16980,14 +16985,14 @@ function buildRecentDigest(messages) {
16980
16985
  }
16981
16986
  async function captureGitStatus(cwd) {
16982
16987
  if (!cwd) return void 0;
16983
- return new Promise((resolve45) => {
16988
+ return new Promise((resolve47) => {
16984
16989
  execFile("git", ["status", "--porcelain"], { cwd }, (err, stdout) => {
16985
16990
  if (err) {
16986
- resolve45(void 0);
16991
+ resolve47(void 0);
16987
16992
  return;
16988
16993
  }
16989
16994
  const trimmed = stdout.trim();
16990
- resolve45(trimmed || "(working tree clean)");
16995
+ resolve47(trimmed || "(working tree clean)");
16991
16996
  });
16992
16997
  });
16993
16998
  }
@@ -19135,11 +19140,11 @@ function createSessionsRegistry(opts) {
19135
19140
  };
19136
19141
  const waitForTurnSettled = (rt, id, caller) => {
19137
19142
  if (!rt.busy) return Promise.resolve();
19138
- return new Promise((resolve45, reject) => {
19143
+ return new Promise((resolve47, reject) => {
19139
19144
  const onBusy = (busy) => {
19140
19145
  if (busy) return;
19141
19146
  cleanup();
19142
- resolve45();
19147
+ resolve47();
19143
19148
  };
19144
19149
  const cleanup = () => {
19145
19150
  clearTimeout(timer);
@@ -19217,9 +19222,9 @@ function createSessionsRegistry(opts) {
19217
19222
  );
19218
19223
  return;
19219
19224
  }
19220
- const answer = isChildPromptSource(next.source) ? void 0 : matchStructuredQuestionAnswer(liveRt, next.message);
19221
- if (answer) {
19222
- await answerStructuredQuestion(liveRt, answer);
19225
+ const answer2 = isChildPromptSource(next.source) ? void 0 : matchStructuredQuestionAnswer(liveRt, next.message);
19226
+ if (answer2) {
19227
+ await answerStructuredQuestion(liveRt, answer2);
19223
19228
  return;
19224
19229
  }
19225
19230
  await runAgentTurn(
@@ -23034,14 +23039,14 @@ var init_pr_provenance_stamp = __esm({
23034
23039
  init_pr_provenance();
23035
23040
  defaultGhRunner = async (args, cwd) => {
23036
23041
  const { spawn: spawn8 } = await import('child_process');
23037
- return await new Promise((resolve45) => {
23042
+ return await new Promise((resolve47) => {
23038
23043
  let stdout = "";
23039
23044
  const child = spawn8("gh", [...args], { cwd, shell: false });
23040
23045
  child.stdout?.on("data", (d) => {
23041
23046
  stdout += d.toString("utf8");
23042
23047
  });
23043
- child.on("error", () => resolve45({ exitCode: 1, stdout }));
23044
- child.on("close", (code) => resolve45({ exitCode: code ?? 1, stdout }));
23048
+ child.on("error", () => resolve47({ exitCode: 1, stdout }));
23049
+ child.on("close", (code) => resolve47({ exitCode: code ?? 1, stdout }));
23045
23050
  });
23046
23051
  };
23047
23052
  }
@@ -23575,6 +23580,19 @@ var init_command_tools = __esm({
23575
23580
  }
23576
23581
  });
23577
23582
 
23583
+ // src/liveness-tick-guard.ts
23584
+ function createLivenessTickGuard(intervalMs, graceMs, now = Date.now) {
23585
+ let last = now();
23586
+ return {
23587
+ shouldSkip() {
23588
+ const t = now();
23589
+ const late = t - last - intervalMs;
23590
+ last = t;
23591
+ return late > graceMs;
23592
+ }
23593
+ };
23594
+ }
23595
+
23578
23596
  // src/index.ts
23579
23597
  init_browser_mount();
23580
23598
  async function writeRuntimeMeta(workspace, meta) {
@@ -25319,10 +25337,10 @@ function sandboxAuthForBox(auth) {
25319
25337
  };
25320
25338
  }
25321
25339
  async function resolveSandboxSecretFromBroker(slug) {
25322
- const { resolveSandboxSecret: resolve45 } = getMcpCredentialDeps();
25323
- if (!resolve45) return null;
25340
+ const { resolveSandboxSecret: resolve47 } = getMcpCredentialDeps();
25341
+ if (!resolve47) return null;
25324
25342
  try {
25325
- return await resolve45(slug);
25343
+ return await resolve47(slug);
25326
25344
  } catch (err) {
25327
25345
  console.warn(
25328
25346
  `[restartAgentSession] sandbox secret resolution failed for "${slug}": ${err instanceof Error ? err.message : String(err)}`
@@ -25916,6 +25934,12 @@ function createMcpObservationStore(opts = {}) {
25916
25934
  const obs = touch(sessionId);
25917
25935
  obs.httpError = { at: obs.lastSeenAt, status };
25918
25936
  },
25937
+ recordResult(sessionId, method, message, clientProtocolVersion) {
25938
+ try {
25939
+ record2(sessionId, method, message, clientProtocolVersion);
25940
+ } catch {
25941
+ }
25942
+ },
25919
25943
  get: (sessionId) => bySession.get(sessionId),
25920
25944
  delete: (sessionId) => {
25921
25945
  bySession.delete(sessionId);
@@ -26240,7 +26264,7 @@ async function runContinueOnBootPass(opts) {
26240
26264
 
26241
26265
  // src/session-tools.ts
26242
26266
  init_session_outcome();
26243
- var defaultPsExecutor = () => new Promise((resolve45, reject) => {
26267
+ var defaultPsExecutor = () => new Promise((resolve47, reject) => {
26244
26268
  if (process.platform === "win32") {
26245
26269
  reject(new Error("processTreeRss: ps is not available on win32"));
26246
26270
  return;
@@ -26251,7 +26275,7 @@ var defaultPsExecutor = () => new Promise((resolve45, reject) => {
26251
26275
  { maxBuffer: 16 * 1024 * 1024 },
26252
26276
  (err, stdout) => {
26253
26277
  if (err) reject(err);
26254
- else resolve45(stdout);
26278
+ else resolve47(stdout);
26255
26279
  }
26256
26280
  );
26257
26281
  });
@@ -26304,6 +26328,163 @@ async function processTreeRss(rootPids, exec = defaultPsExecutor) {
26304
26328
  }
26305
26329
  return result;
26306
26330
  }
26331
+ var SessionListFilterError = class extends Error {
26332
+ constructor(message) {
26333
+ super(message);
26334
+ this.name = "SessionListFilterError";
26335
+ }
26336
+ };
26337
+ var SESSION_LIST_FILTER_KEYS = [
26338
+ "q",
26339
+ "excludeLabelPrefix",
26340
+ "excludeLabels",
26341
+ "excludeKinds",
26342
+ "rootOnly",
26343
+ "parentSessionId",
26344
+ "updatedSince",
26345
+ "startedSince",
26346
+ "excludeNoise"
26347
+ ];
26348
+ function pickSessionListFilters(input) {
26349
+ const out = {};
26350
+ for (const key of SESSION_LIST_FILTER_KEYS) {
26351
+ if (input[key] !== void 0) out[key] = input[key];
26352
+ }
26353
+ return out;
26354
+ }
26355
+ var TIME_UNIT_MS = {
26356
+ s: 1e3,
26357
+ m: 6e4,
26358
+ h: 36e5,
26359
+ d: 864e5,
26360
+ w: 6048e5
26361
+ };
26362
+ var RELATIVE_RE = /^(\d+(?:\.\d+)?)\s*([smhdw])$/i;
26363
+ function parseTimeBound(raw, field, now = Date.now()) {
26364
+ const text13 = raw.trim();
26365
+ const rel = RELATIVE_RE.exec(text13);
26366
+ if (rel) {
26367
+ const n = Number(rel[1]);
26368
+ const unit = TIME_UNIT_MS[rel[2].toLowerCase()];
26369
+ return now - n * unit;
26370
+ }
26371
+ const abs = text13 === "" ? Number.NaN : Date.parse(text13);
26372
+ if (Number.isNaN(abs)) {
26373
+ throw new SessionListFilterError(
26374
+ `${field} must be an ISO-8601 time or a relative age like "30m", "24h", "7d", "2w"; got ${JSON.stringify(raw)}.`
26375
+ );
26376
+ }
26377
+ return abs;
26378
+ }
26379
+ function toList(v) {
26380
+ if (v === void 0) return [];
26381
+ const parts = typeof v === "string" ? [v] : [...v];
26382
+ return parts.flatMap((p) => p.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
26383
+ }
26384
+ var REVIEW_LABEL_PREFIX = "review:";
26385
+ var WORKFLOW_LABEL_PREFIX = "wf:";
26386
+ var NOISE_ORIGINS = /* @__PURE__ */ new Set(["review", "workflow"]);
26387
+ function isNoiseSession(s) {
26388
+ const label = s.label ?? "";
26389
+ if (label.startsWith(REVIEW_LABEL_PREFIX) || label.startsWith(WORKFLOW_LABEL_PREFIX)) return true;
26390
+ if (s.origin !== void 0 && NOISE_ORIGINS.has(s.origin)) return true;
26391
+ const ended = s.status === "exited" || s.status === "killed";
26392
+ if (ended && (s.kind === "command" || s.kind === "terminal" && !s.adapterSlug)) return true;
26393
+ return false;
26394
+ }
26395
+ function sessionActivityMs(s) {
26396
+ const ms = Date.parse(s.lastActivityAt ?? s.startedAt);
26397
+ return Number.isNaN(ms) ? 0 : ms;
26398
+ }
26399
+ function sortNewestActivityFirst(rows) {
26400
+ return [...rows].sort((a, b) => sessionActivityMs(b) - sessionActivityMs(a) || a.id.localeCompare(b.id));
26401
+ }
26402
+ function hasSessionListFilters(input) {
26403
+ return input.q !== void 0 && input.q.trim() !== "" || toList(input.excludeLabelPrefix).length > 0 || toList(input.excludeLabels).length > 0 || toList(input.excludeKinds).length > 0 || input.rootOnly === true || input.parentSessionId !== void 0 && input.parentSessionId !== "" || input.updatedSince !== void 0 && input.updatedSince.trim() !== "" || input.startedSince !== void 0 && input.startedSince.trim() !== "" || input.excludeNoise === true;
26404
+ }
26405
+ function compileSessionListFilters(input, now = Date.now()) {
26406
+ const q = input.q?.trim().toLowerCase() ?? "";
26407
+ const prefixes = toList(input.excludeLabelPrefix);
26408
+ const labels = new Set(toList(input.excludeLabels));
26409
+ const kinds = new Set(toList(input.excludeKinds));
26410
+ const parentId = input.parentSessionId && input.parentSessionId !== "" ? input.parentSessionId : void 0;
26411
+ const updatedSince = input.updatedSince && input.updatedSince.trim() !== "" ? parseTimeBound(input.updatedSince, "updatedSince", now) : void 0;
26412
+ const startedSince = input.startedSince && input.startedSince.trim() !== "" ? parseTimeBound(input.startedSince, "startedSince", now) : void 0;
26413
+ return (s) => {
26414
+ if (q) {
26415
+ const hit = [s.id, s.name, s.label, s.title, s.cwd].some((f) => f !== void 0 && f.toLowerCase().includes(q));
26416
+ if (!hit) return false;
26417
+ }
26418
+ if (kinds.has(s.kind)) return false;
26419
+ const label = s.label;
26420
+ if (label !== void 0) {
26421
+ if (labels.has(label)) return false;
26422
+ if (prefixes.some((p) => label.startsWith(p))) return false;
26423
+ }
26424
+ if (input.rootOnly === true && s.parentSessionId) return false;
26425
+ if (parentId !== void 0 && s.parentSessionId !== parentId) return false;
26426
+ if (updatedSince !== void 0 && sessionActivityMs(s) < updatedSince) return false;
26427
+ if (startedSince !== void 0) {
26428
+ const started = Date.parse(s.startedAt);
26429
+ if (Number.isNaN(started) || started < startedSince) return false;
26430
+ }
26431
+ if (input.excludeNoise === true && isNoiseSession(s)) return false;
26432
+ return true;
26433
+ };
26434
+ }
26435
+ function applySessionListFilters(rows, input, now = Date.now()) {
26436
+ if (!hasSessionListFilters(input)) return [...rows];
26437
+ return rows.filter(compileSessionListFilters(input, now));
26438
+ }
26439
+ function parseSessionListFilterParams(params) {
26440
+ const flag = (k) => {
26441
+ const v = params.get(k);
26442
+ return v === null ? void 0 : v === "true" || v === "1";
26443
+ };
26444
+ const list = (k) => {
26445
+ const all = params.getAll(k);
26446
+ return all.length > 0 ? toList(all) : void 0;
26447
+ };
26448
+ const str3 = (k) => {
26449
+ const v = params.get(k);
26450
+ return v === null || v === "" ? void 0 : v;
26451
+ };
26452
+ const out = {};
26453
+ const q = str3("q");
26454
+ if (q !== void 0) out.q = q;
26455
+ const prefix = list("excludeLabelPrefix");
26456
+ if (prefix) out.excludeLabelPrefix = prefix;
26457
+ const labels = list("excludeLabels");
26458
+ if (labels) out.excludeLabels = labels;
26459
+ const kinds = list("excludeKinds");
26460
+ if (kinds) out.excludeKinds = kinds;
26461
+ const rootOnly = flag("rootOnly");
26462
+ if (rootOnly !== void 0) out.rootOnly = rootOnly;
26463
+ const parent = str3("parentSessionId");
26464
+ if (parent !== void 0) out.parentSessionId = parent;
26465
+ const updatedSince = str3("updatedSince");
26466
+ if (updatedSince !== void 0) out.updatedSince = updatedSince;
26467
+ const startedSince = str3("startedSince");
26468
+ if (startedSince !== void 0) out.startedSince = startedSince;
26469
+ const noise = flag("excludeNoise");
26470
+ if (noise !== void 0) out.excludeNoise = noise;
26471
+ return out;
26472
+ }
26473
+ var mcpBool2 = z.preprocess((v) => v === "true" ? true : v === "false" ? false : v, z.boolean());
26474
+ var stringOrList = z.union([z.string(), z.array(z.string())]);
26475
+ var sessionListFilterShape = {
26476
+ q: z.string().optional().describe("Case-insensitive substring over id, name, label, title, cwd."),
26477
+ excludeLabelPrefix: stringOrList.optional().describe("Drop labels starting with any of these, e.g. ['review:','wf:']."),
26478
+ excludeLabels: stringOrList.optional().describe("Drop labels exactly equal to any of these (as session_follow exclude.labels)."),
26479
+ excludeKinds: stringOrList.optional().describe("Drop kinds: terminal | agent-cli | command."),
26480
+ rootOnly: mcpBool2.optional().describe("Only sessions with no parent."),
26481
+ parentSessionId: z.string().optional().describe("Only direct children of this session (id or name)."),
26482
+ updatedSince: z.string().optional().describe("Activity at/after: ISO time or age like '30m','24h','7d'."),
26483
+ startedSince: z.string().optional().describe("Started at/after: ISO time or age like '30m','24h','7d'."),
26484
+ excludeNoise: mcpBool2.optional().describe(
26485
+ 'Drop review:*/wf:* sessions and ended command/plain-terminal runs. Details: tool_help {name:"session_list", topic:"excludeNoise"}'
26486
+ )
26487
+ };
26307
26488
 
26308
26489
  // src/session-tools.ts
26309
26490
  init_transcript_writer();
@@ -26646,15 +26827,15 @@ function computeHostWarnings(input) {
26646
26827
  function createDefaultHostProbes() {
26647
26828
  const platform = process.platform;
26648
26829
  const own = /* @__PURE__ */ new Set();
26649
- const run = (cmd, args, timeoutMs) => new Promise((resolve45, reject) => {
26830
+ const run = (cmd, args, timeoutMs) => new Promise((resolve47, reject) => {
26650
26831
  const child = execFile(
26651
26832
  cmd,
26652
26833
  args,
26653
26834
  { maxBuffer: 16 * 1024 * 1024, timeout: timeoutMs, env: { ...process.env, LC_ALL: "C" } },
26654
26835
  (err, stdout) => {
26655
- if (!err) return resolve45(stdout);
26836
+ if (!err) return resolve47(stdout);
26656
26837
  if (err.killed) return reject(new Error(`${cmd} timed out after ${timeoutMs}ms`));
26657
- return stdout ? resolve45(stdout) : reject(err);
26838
+ return stdout ? resolve47(stdout) : reject(err);
26658
26839
  }
26659
26840
  );
26660
26841
  if (child.pid !== void 0) own.add(child.pid);
@@ -27419,9 +27600,9 @@ async function callJevSystemOne(state, questions, opts) {
27419
27600
  const answers = {};
27420
27601
  const answerErrors = {};
27421
27602
  for (const [key, value] of Object.entries(parsed.data.answers)) {
27422
- const answer = jevChoiceAnswerSchema.safeParse(value);
27423
- if (answer.success) answers[key] = answer.data;
27424
- else answerErrors[key] = describeZodIssues(answer.error);
27603
+ const answer2 = jevChoiceAnswerSchema.safeParse(value);
27604
+ if (answer2.success) answers[key] = answer2.data;
27605
+ else answerErrors[key] = describeZodIssues(answer2.error);
27425
27606
  }
27426
27607
  return {
27427
27608
  ok: true,
@@ -27505,24 +27686,24 @@ async function judgeSessionWithJev(input) {
27505
27686
  }
27506
27687
  );
27507
27688
  if (!outcome.ok) return fail4(outcome.error);
27508
- const answer = outcome.answers[VERDICT_QUESTION_KEY];
27509
- if (!answer) {
27689
+ const answer2 = outcome.answers[VERDICT_QUESTION_KEY];
27690
+ if (!answer2) {
27510
27691
  const why = outcome.answerErrors?.[VERDICT_QUESTION_KEY];
27511
27692
  return fail4(`response missing a valid verdict answer${why ? ` \u2014 ${why}` : ""}`);
27512
27693
  }
27513
- if (!WRAPUP_VERDICTS.includes(answer.choice)) {
27514
- return fail4(`unknown verdict choice '${answer.choice}'`);
27694
+ if (!WRAPUP_VERDICTS.includes(answer2.choice)) {
27695
+ return fail4(`unknown verdict choice '${answer2.choice}'`);
27515
27696
  }
27516
- const confidence = answer.confidence ?? answer.probabilities[answer.choice];
27697
+ const confidence = answer2.confidence ?? answer2.probabilities[answer2.choice];
27517
27698
  if (typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
27518
27699
  return fail4("no usable confidence for the chosen verdict");
27519
27700
  }
27520
27701
  return {
27521
27702
  ok: true,
27522
27703
  sessionId: input.sessionId,
27523
- verdict: answer.choice,
27704
+ verdict: answer2.choice,
27524
27705
  confidence,
27525
- probabilities: answer.probabilities,
27706
+ probabilities: answer2.probabilities,
27526
27707
  model: outcome.model ?? model
27527
27708
  };
27528
27709
  }
@@ -27723,7 +27904,6 @@ function createBackgroundJobRegistry(opts) {
27723
27904
  jobId: job.id,
27724
27905
  status: "running",
27725
27906
  startedAt: job.startedAt,
27726
- resultPath: resultPathFor(job.id),
27727
27907
  followUp: { tool: followUp.tool, args: { jobId: job.id }, pollAfterMs: JOB_POLL_AFTER_MS, hint: followUp.hint }
27728
27908
  }),
27729
27909
  progressView: (job) => job.status === "failed" ? { jobId: job.id, status: job.status, endedAt: job.endedAt, error: job.error } : {
@@ -27731,22 +27911,86 @@ function createBackgroundJobRegistry(opts) {
27731
27911
  status: job.status,
27732
27912
  startedAt: job.startedAt,
27733
27913
  elapsedMs: Date.now() - job.startedMs,
27734
- resultPath: resultPathFor(job.id),
27735
27914
  followUp: { pollAfterMs: JOB_POLL_AFTER_MS }
27736
27915
  }
27737
27916
  };
27738
27917
  }
27739
27918
  function timedOutWaiting(promise, waitMs) {
27740
- return new Promise((resolve45) => {
27741
- const handle = setTimeout(() => resolve45(true), waitMs);
27919
+ return new Promise((resolve47) => {
27920
+ const handle = setTimeout(() => resolve47(true), waitMs);
27742
27921
  const settled = () => {
27743
27922
  clearTimeout(handle);
27744
- resolve45(false);
27923
+ resolve47(false);
27745
27924
  };
27746
27925
  promise.then(settled, settled);
27747
27926
  });
27748
27927
  }
27749
27928
 
27929
+ // src/branch-gc.ts
27930
+ function summarizeBranchGcApply(result) {
27931
+ const zero = () => ({ deleted: 0, skipped: 0, failed: 0 });
27932
+ const byScope = { local: zero(), remote: zero(), orphan: zero() };
27933
+ const totals = zero();
27934
+ const byResult = {};
27935
+ let aborted = 0;
27936
+ for (const o of result.outcomes) {
27937
+ byResult[o.result] = (byResult[o.result] ?? 0) + 1;
27938
+ const bucket = o.result === "deleted" ? "deleted" : o.result === "failed" ? "failed" : "skipped";
27939
+ byScope[o.kind][bucket]++;
27940
+ totals[bucket]++;
27941
+ if (o.result.startsWith("aborted-")) aborted++;
27942
+ }
27943
+ const status = totals.deleted === 0 ? totals.failed > 0 ? "failed" : "noop" : totals.failed > 0 || aborted > 0 ? "partial" : "ok";
27944
+ return { status, totals, byScope, byResult, restoreLog: result.restoreLog ?? null };
27945
+ }
27946
+ function withBranchGcApplySummary(result) {
27947
+ if (result.mode !== "apply") return result;
27948
+ const applySummary = summarizeBranchGcApply(result);
27949
+ return { ...result, status: applySummary.status, applySummary };
27950
+ }
27951
+ var BRANCH_GC_PAGE_DEFAULT = 100;
27952
+ var BRANCH_GC_PAGE_MAX = 500;
27953
+ var encodeOffset = (n) => Buffer.from(`o:${n}`).toString("base64url");
27954
+ function decodeOffset(cursor) {
27955
+ const m = /^o:(\d+)$/.exec(Buffer.from(cursor, "base64url").toString("utf8"));
27956
+ if (!m) throw new Error("branch_gc_status: invalid `cursor`");
27957
+ return Number(m[1]);
27958
+ }
27959
+ function sliceBranchGcResult(result, opts) {
27960
+ const section = opts.section ?? (result.mode === "apply" ? "outcomes" : "entries");
27961
+ const scopeOk = (kind) => !opts.scopes?.length || opts.scopes.includes(kind);
27962
+ let rows;
27963
+ if (section === "outcomes") {
27964
+ if (result.mode !== "apply") throw new Error("branch_gc_status: `section: outcomes` is only available on an apply result");
27965
+ rows = result.outcomes.filter((o) => scopeOk(o.kind) && (!opts.results?.length || opts.results.includes(o.result)));
27966
+ } else {
27967
+ rows = result.plan.entries.filter(
27968
+ (e) => scopeOk(e.kind) && (!opts.classes?.length || opts.classes.includes(e.class))
27969
+ );
27970
+ }
27971
+ const limit = Math.min(Math.max(Math.trunc(opts.limit ?? BRANCH_GC_PAGE_DEFAULT), 1), BRANCH_GC_PAGE_MAX);
27972
+ const start = opts.cursor !== void 0 ? decodeOffset(opts.cursor) : 0;
27973
+ const pageRows = rows.slice(start, start + limit);
27974
+ const next = start + limit;
27975
+ const page = {
27976
+ section,
27977
+ total: rows.length,
27978
+ returned: pageRows.length,
27979
+ ...next < rows.length ? { nextCursor: encodeOffset(next) } : {}
27980
+ };
27981
+ const { plan, ...rest } = result;
27982
+ const { entries: _entries, ...planMeta } = plan;
27983
+ const { outcomes: _outcomes, ...restNoOutcomes } = rest;
27984
+ return {
27985
+ page,
27986
+ result: {
27987
+ ...restNoOutcomes,
27988
+ plan: section === "entries" ? { ...planMeta, entries: pageRows } : planMeta,
27989
+ ...section === "outcomes" ? { outcomes: pageRows } : {}
27990
+ }
27991
+ };
27992
+ }
27993
+
27750
27994
  // src/session-tools.ts
27751
27995
  init_command_allowlist();
27752
27996
  init_agent_tools();
@@ -28245,7 +28489,7 @@ function buildSessionTree(sessions) {
28245
28489
  });
28246
28490
  return sessions.filter((s) => !s.parentSessionId || !idSet.has(s.parentSessionId)).sort((a, b) => a.startedAt.localeCompare(b.startedAt)).map(toNode);
28247
28491
  }
28248
- var mcpBool2 = z.preprocess(
28492
+ var mcpBool3 = z.preprocess(
28249
28493
  (v) => v === "true" ? true : v === "false" ? false : v,
28250
28494
  z.boolean()
28251
28495
  );
@@ -28366,25 +28610,27 @@ var sessionWrapupApplyJobs = createBackgroundJobRegistry({
28366
28610
  var BACKGROUND_DEFAULT_WAIT_MS = 25e3;
28367
28611
  var branchGcBackgroundView = (job) => branchGcJobs.backgroundView(job, {
28368
28612
  tool: "branch_gc_status",
28369
- hint: "Running in the background; a plan on a large repo takes a few minutes. Call branch_gc_status with this jobId about every 30 s. When done it returns the summary; the full result is written to resultPath (pass full: true to get it inline)."
28613
+ hint: "Running in the background; a plan on a large repo takes a few minutes. Call branch_gc_status with this jobId about every 30 s. When done it returns the summary and, once the file exists, a resultPath holding the full result (pass full: true to get a filtered/paged slice inline)."
28370
28614
  });
28371
- var branchGcDoneView = (jobId, resultPath, result, full, endedAt) => ({
28615
+ var branchGcDoneView = (jobId, resultPath, result, slice, endedAt) => ({
28372
28616
  jobId,
28373
28617
  status: "done",
28374
28618
  ...endedAt !== void 0 ? { endedAt } : {},
28375
- resultPath,
28619
+ ...resultPath !== void 0 ? { resultPath } : {},
28376
28620
  summary: result.summary,
28377
- // Apply results carry the restore log path and a per-outcome tally —
28378
- // exactly what a caller needs to decide "safe?" without fetching the
28379
- // full ~MB result with `full: true`.
28621
+ // Apply results carry the restore log path, a per-outcome tally and the
28622
+ // per-scope deleted/skipped/failed summary — exactly what a caller needs
28623
+ // to decide "safe?" without fetching the full ~MB result with `full: true`.
28624
+ // Result files written before `applySummary` existed are summarised here.
28380
28625
  ...result.mode === "apply" ? {
28381
28626
  restoreLog: result.restoreLog ?? null,
28382
28627
  outcomeCounts: result.outcomes.reduce((acc, o) => {
28383
28628
  acc[o.result] = (acc[o.result] ?? 0) + 1;
28384
28629
  return acc;
28385
- }, {})
28630
+ }, {}),
28631
+ applySummary: result.applySummary ?? summarizeBranchGcApply(result)
28386
28632
  } : {},
28387
- ...full ? { result } : {}
28633
+ ...slice ? sliceBranchGcResult(result, slice) : {}
28388
28634
  });
28389
28635
  var worktreeGcBackgroundView = (job) => worktreeGcJobs.backgroundView(job, {
28390
28636
  tool: "worktree_gc_status",
@@ -28424,7 +28670,7 @@ var backgroundStatusResult = async (toolName, jobs, jobId) => {
28424
28670
  jobId: job.id,
28425
28671
  status: "done",
28426
28672
  endedAt: job.endedAt,
28427
- resultPath: jobs.resultPathFor(job.id),
28673
+ ...job.resultPath !== void 0 ? { resultPath: job.resultPath } : {},
28428
28674
  result: job.result
28429
28675
  })
28430
28676
  }
@@ -28471,35 +28717,29 @@ function registerSessionTools(rawServer, opts) {
28471
28717
  project: args.project,
28472
28718
  keyOf: args.keyOf,
28473
28719
  maxLimit: 200,
28474
- itemKey: args.itemKey
28720
+ itemKey: args.itemKey,
28721
+ ...args.defaultLimit !== void 0 ? { defaultLimit: args.defaultLimit } : {}
28475
28722
  })
28476
28723
  ]
28477
28724
  });
28478
28725
  };
28479
28726
  registerAgentTools(server, opts);
28480
28727
  const sessionListSchema = z.object({
28481
- kind: z.enum(["terminal", "agent-cli", "command", "all"]).optional().describe(
28482
- "Filter by session kind. `all` (default) returns every live-able kind (terminal + agent-cli) but excludes `command` unless `includeCommands` is set. Use `terminal` to list only PTY sessions, `agent-cli` for structured ACP agents, or `command` to list only raw shell-command runs."
28483
- ),
28484
- includeCommands: z.boolean().optional().describe(
28485
- "When true and `kind` is unset/`all`, also include `kind:'command'` rows in the result (they're excluded by default \u2014 see `kind`). No effect when `kind` is set explicitly. Default false."
28486
- ),
28728
+ kind: z.enum(["terminal", "agent-cli", "command", "all"]).optional().describe("Filter by kind. `all` (default) = terminal + agent-cli, without `command` unless `includeCommands`."),
28729
+ includeCommands: z.boolean().optional().describe("With kind unset/`all`, also include `kind:'command'` rows. Default false."),
28487
28730
  onlyAlive: z.boolean().optional().describe("When true, only running/starting sessions. Default false."),
28488
28731
  status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive)."),
28489
- includeArchived: z.boolean().optional().describe(
28490
- "When true, also include archived sessions (hidden from every other view by `session_archive`). Default false."
28491
- ),
28492
- withMemory: z.boolean().optional().describe(
28493
- "When true, add `rssBytes` (summed RSS in bytes of the process tree, via `ps`) to every live session that has a pid. Default false \u2014 a distinct opt-in from `full`/`compact` since it costs one `ps` spawn per call; omitted otherwise, so a plain listing never pays for it."
28494
- ),
28732
+ includeArchived: z.boolean().optional().describe("Also include archived sessions. Default false."),
28733
+ withMemory: z.boolean().optional().describe("Add `rssBytes` (process-tree RSS) to live sessions with a pid; one `ps` per call. Default false."),
28495
28734
  stats: statsParamSchema.describe(
28496
- 'Resource stats per live session - process-tree RSS, %CPU, process count and the top commands by RSS (normalized: `pnpm install`, `vitest`, `tsc`, `git`, \u2026), under each row\'s `stats`. `true` = summary; `"full"` = also every process (pid, ppid, command, RSS, CPU, elapsed). Sampled on demand and cached ~3s; rows with no live process carry no `stats`. Use `session_stats` for the host-level view (daemon / provisioning / orphan buckets, load, free memory).'
28735
+ 'Per-live-session RSS, %CPU, process count and top commands under `stats`; `true` = summary, `"full"` = every process. Cached ~3s. Host-level view: `session_stats`.'
28497
28736
  ),
28737
+ ...sessionListFilterShape,
28498
28738
  ...pageParamsShape
28499
28739
  });
28500
28740
  registerBuiltinTool(server, {
28501
28741
  id: "session_list",
28502
- description: "List sessions tracked by the daemon \u2014 agent-CLI sessions (claude-code, hermes, \u2026) and terminal/PTY sessions (claude TUI, bash, \u2026). Each entry includes `kind`, `pty` (true for real PTYs), `name` (when set at spawn), `status`, `command`, age + exit code. Use this when you need to know what's already running before spawning anything new, or to discover a session id by name. COMPACT BY DEFAULT: each entry is a slim projection (id/kind/name/label/status/command/cwd/model/busy/awaitingInput/blockedOn/lastActivityAt/depth/parentSessionId/continuedFrom/lastTurnErroredAt/interrupted); `interrupted: true` marks a session a daemon restart cut off mid-turn (see `session_continue_interrupted`); pass `full: true` (or `compact: false`) for the complete, unprojected per-session record. Raw shell-command runs (`kind:'command'`) are a log, not a resumable session, so they're excluded from the default view \u2014 pass `kind:'command'` or `includeCommands:true` to see them, or use `command_list`.",
28742
+ description: "List sessions tracked by the daemon (agent-CLI and terminal/PTY). Use it to see what's already running before spawning, or to find a session id by name. NARROW BEFORE YOU READ: hundreds of sessions are mostly noise and without `limit` all come back. Filter (`q`, `excludeNoise`, `rootOnly`, `updatedSince`, \u2026) and/or pass `limit`; rows are newest-activity first, `total` is the filtered count. E.g. `{q:'X', excludeNoise:true, limit:10}`. Rows are COMPACT by default (`fields:[\u2026]` picks keys, `full:true` returns everything). Detail: tool_help {name:\"session_list\"}; ranked text search: `session_search`.",
28503
28743
  inputSchema: sessionListSchema,
28504
28744
  handler: async (input) => {
28505
28745
  let rows = registry.list({ includeArchived: true });
@@ -28522,6 +28762,12 @@ function registerSessionTools(rawServer, opts) {
28522
28762
  (s) => s.status === "running" || s.status === "starting"
28523
28763
  );
28524
28764
  }
28765
+ const { parentSessionId, ...filterRest } = pickSessionListFilters(input);
28766
+ rows = applySessionListFilters(rows, {
28767
+ ...filterRest,
28768
+ ...parentSessionId ? { parentSessionId: registry.findByIdOrName(parentSessionId)?.id ?? parentSessionId } : {}
28769
+ });
28770
+ rows = sortNewestActivityFirst(rows);
28525
28771
  if (input.withMemory) {
28526
28772
  const live = rows.filter(
28527
28773
  (s) => typeof s.pid === "number" && (s.status === "running" || s.status === "starting")
@@ -28546,11 +28792,13 @@ function registerSessionTools(rawServer, opts) {
28546
28792
  return rows.map(publicSessionDescriptor);
28547
28793
  },
28548
28794
  transformers: [
28795
+ catchErrors(),
28549
28796
  paginated({
28550
28797
  project: compactSessionItem,
28551
28798
  keyOf: (s) => s.id,
28552
28799
  maxLimit: 200,
28553
- itemKey: "sessions"
28800
+ itemKey: "sessions",
28801
+ includeTotal: true
28554
28802
  })
28555
28803
  ]
28556
28804
  });
@@ -28620,7 +28868,7 @@ function registerSessionTools(rawServer, opts) {
28620
28868
  description: 'Resource usage per session, sampled from the OS process table: RSS bytes, %CPU, process count and top commands by RSS for every live session, sorted by RSS (each row labelled with the session\'s `label`/`name`), plus `daemon` (the daemon process + its own children), `provisioning` (worktree setup work such as `pnpm install` not yet attached to a session, with `inFlight` provisions), `orphans` (processes that look agentproto-owned but belong to no live session - reported only, never killed), `totals`, and `host` (load average, free/total memory). `detail:"full"` adds every process (pid, command, RSS, CPU, elapsed) per row. Cached ~3s; `fresh:true` forces a new sample. A subtree-scoped caller sees only its own sessions (no daemon/provisioning/orphan buckets).',
28621
28869
  inputSchema: z.object({
28622
28870
  detail: z.enum(["summary", "full"]).optional().describe('Default "summary".'),
28623
- fresh: mcpBool2.optional().describe("Bypass the ~3s cache and sample now.")
28871
+ fresh: mcpBool3.optional().describe("Bypass the ~3s cache and sample now.")
28624
28872
  }),
28625
28873
  handler: async (input) => {
28626
28874
  const all = registry.list({ includeArchived: true });
@@ -28640,7 +28888,7 @@ function registerSessionTools(rawServer, opts) {
28640
28888
  description: 'Host-level load report: load average vs core count, CPU user/sys/idle, RAM (used/wired/compressor/free) and swap, per-disk transfers/s + MB/s, the top 10 processes by CPU and by memory footprint (compressed pages included on macOS) each tagged with its owning session (`session`/`daemon`/`provisioning`/`orphan`/`system`/`other`), and `warnings` (swap > 50%, orphaned processes older than 30 min that are busy or serving, deleted-cwd processes, filesystem-wide `find`/`bfs`/`du` scans, several servers on one port, load > 4x cores). `detail:"full"` adds a per-session rollup and every process. Bounded to ~2s: a probe that is slow or unavailable is named in `partial` and the rest still ships (raise `budgetMs` to give the macOS `top` footprint probe longer on a saturated host). Cached ~2s; `fresh:true` resamples. Never needs sudo. A subtree-scoped caller sees host metrics plus only its own sessions\' processes.',
28641
28889
  inputSchema: z.object({
28642
28890
  detail: z.enum(["summary", "full"]).optional().describe('Default "summary".'),
28643
- fresh: mcpBool2.optional().describe("Bypass the ~2s cache and sample now."),
28891
+ fresh: mcpBool3.optional().describe("Bypass the ~2s cache and sample now."),
28644
28892
  budgetMs: z.coerce.number().int().min(300).max(6e4).optional().describe("Time budget for the sample in ms (default 1900).")
28645
28893
  }),
28646
28894
  handler: async (input) => {
@@ -28659,7 +28907,7 @@ function registerSessionTools(rawServer, opts) {
28659
28907
  "session_continue_interrupted",
28660
28908
  "List the sessions the LAST daemon restart cut off mid-turn (`interrupted: true`) and \u2014 with `dryRun: false` \u2014 send each a one-shot \"continue\" prompt (the interrupted prompt itself is never re-run). Goes through the normal prompt path, so a dead-but-resumable session resumes in place first. Skips sessions that aren't interrupted, were interrupted by an older restart, aren't resumable, hit the resume attempt cap, are already busy, or whose cwd no longer exists. Returns a per-session outcome (`eligible` on a dry run, else `sent`/`skipped` with a reason/`failed`).",
28661
28909
  {
28662
- dryRun: mcpBool2.optional().describe("Default true: only report what would be sent. Pass false to send."),
28910
+ dryRun: mcpBool3.optional().describe("Default true: only report what would be sent. Pass false to send."),
28663
28911
  ids: z.array(z.string().min(1)).optional().describe(
28664
28912
  "Restrict to these sessions (id or name). Omitted \u21D2 every session interrupted by the last restart."
28665
28913
  ),
@@ -29218,7 +29466,7 @@ function registerSessionTools(rawServer, opts) {
29218
29466
  });
29219
29467
  registerPaginatedListTool({
29220
29468
  id: "terminal_sessions_list",
29221
- description: "List terminal/PTY sessions tracked by the daemon. Equivalent to `session_list({kind: 'terminal'})`. Each entry includes `kind`, `pty`, `status`, age, etc. Use this when you only want the terminal subset. COMPACT BY DEFAULT: each entry is session_list's slim projection; pass `full: true` (or `compact: false`) for the complete, unprojected per-session record.",
29469
+ description: "List terminal/PTY sessions tracked by the daemon. Equivalent to `session_list({kind: 'terminal'})`. Each entry includes `kind`, `pty`, `status`, age, etc. Use this when you only want the terminal subset. COMPACT BY DEFAULT: each entry is session_list's slim projection; pass `full: true` (or `compact: false`) for the complete, unprojected per-session record. Without `limit`/`cursor` at most 50 rows come back; when there are more the reply carries `total`, `truncated: true` and a `nextCursor` \u2014 pass it as `cursor` (with a `limit`) for the rest, or filter with `onlyAlive` / `status`.",
29222
29470
  schema: terminalSessionsListSchema,
29223
29471
  body: async (input) => {
29224
29472
  let rows = registry.list({ includeArchived: true });
@@ -29242,7 +29490,10 @@ function registerSessionTools(rawServer, opts) {
29242
29490
  },
29243
29491
  project: compactSessionItemWithProvenance,
29244
29492
  keyOf: (s) => s.id,
29245
- itemKey: "sessions"
29493
+ itemKey: "sessions",
29494
+ // Dead terminals pile up: an unpaged call used to return every row
29495
+ // (65k+ chars, over the MCP output cap). Cap it and say so.
29496
+ defaultLimit: 50
29246
29497
  });
29247
29498
  const commandListSchema = z.object({
29248
29499
  kind: z.enum(["terminal", "agent-cli", "command", "all"]).optional().describe(
@@ -29974,7 +30225,7 @@ function registerSessionTools(rawServer, opts) {
29974
30225
  workspaceSlug: z.string().optional().describe(
29975
30226
  "Workspace slug from `agentproto workspace list`. Resolves the repo root via the active workspace when omitted."
29976
30227
  ),
29977
- openOnly: mcpBool2.optional().describe(
30228
+ openOnly: mcpBool3.optional().describe(
29978
30229
  "When true, only return worktrees whose `pr.state` is `open`. Default false."
29979
30230
  ),
29980
30231
  sessionId: z.string().optional().describe(
@@ -30025,19 +30276,19 @@ function registerSessionTools(rawServer, opts) {
30025
30276
  workspaceSlug: z.string().optional().describe(
30026
30277
  "Workspace slug from `agentproto workspace list`. Resolves the repo root via the active workspace when omitted."
30027
30278
  ),
30028
- apply: mcpBool2.optional().describe(
30279
+ apply: mcpBool3.optional().describe(
30029
30280
  "When true, EXECUTE the plan (reclaim/salvage). Default false \u2014 a dry run that returns the plan and mutates nothing."
30030
30281
  ),
30031
- salvageDirty: mcpBool2.optional().describe(
30282
+ salvageDirty: mcpBool3.optional().describe(
30032
30283
  "When true, archive (snapshot-then-remove) every `salvage`-class worktree during `apply`. Default false \u2014 salvage entries are left untouched. Ignored on a dry run."
30033
30284
  ),
30034
- includeDetached: mcpBool2.optional().describe(
30285
+ includeDetached: mcpBool3.optional().describe(
30035
30286
  "When true, a clean, idle detached worktree is reclaimed instead of held. Default false. This is the only flag that can promote an entry toward reclaim; no flag ever weakens a hold otherwise."
30036
30287
  ),
30037
30288
  noisePaths: z.array(z.string()).optional().describe(
30038
30289
  "Worktree-relative paths whose dirt is known noise (a worktree dirty ONLY on these counts as clean). Default `.opencode/package-lock.json`; pass [] to disable."
30039
30290
  ),
30040
- wait: mcpBool2.optional().describe(
30291
+ wait: mcpBool3.optional().describe(
30041
30292
  "false \u21D2 return a jobId immediately; poll `worktree_gc_status`. true \u21D2 block until the gc finishes (no background fallback). Default: wait up to `waitMs`, then fall back to background."
30042
30293
  ),
30043
30294
  waitMs: mcpNumber.optional().describe(
@@ -30124,19 +30375,19 @@ function registerSessionTools(rawServer, opts) {
30124
30375
  const branchGcKind = z.enum(["local", "remote", "orphan"]);
30125
30376
  server.tool(
30126
30377
  "branch_gc",
30127
- "Garbage-collect a repo's branches. DEFAULTS TO A DRY RUN: returns a plan classifying every local branch, base-remote branch and orphan tracking ref (refs/remotes/<ns>/* of a removed remote) as `reclaim` (work provably in base: the head commit of a merged PR, merged, squash-merged, patch-merged or content-merged), `review` (unmerged, old enough, not protected \u2014 carries coverage + the files not provably in base for a reviewer), or `hold` (base/protected, checked out in a worktree or its remote twin, open PR head, PR check unavailable, or younger than `minAgeDays`). `apply: true` (requires explicit `scopes`) deletes only `reclaim` entries, re-classifying each right before deleting it, and returns the path of a restore log (sha + re-create command per deleted ref). A plan can take minutes on a big repo: as an MCP caller, pass `wait: false` (or `waitMs: 40000`) and poll `branch_gc_status` with the returned jobId instead of blocking. A `wait: false` (or `waitMs` timed-out) response returns a jobId plus a `followUp` block naming the poll tool and cadence \u2014 poll `branch_gc_status` with that jobId.",
30378
+ "Garbage-collect a repo's branches. DEFAULTS TO A DRY RUN: returns a plan classifying every local branch, base-remote branch and orphan tracking ref (refs/remotes/<ns>/* of a removed remote) as `reclaim` (work provably in base: the head commit of a merged PR, merged, squash-merged, patch-merged or content-merged), `review` (unmerged, old enough, not protected \u2014 carries coverage + the files not provably in base for a reviewer), or `hold` (base/protected, checked out in a worktree or its remote twin, open PR head, PR check unavailable, or younger than `minAgeDays`). `apply: true` (requires explicit `scopes`) deletes only `reclaim` entries, re-classifying each right before deleting it, and returns the path of a restore log (sha + re-create command per deleted ref) plus a top-level `status` and an `applySummary` (deleted / skipped / failed counts per scope, and the restore log path). A plan can take minutes on a big repo: as an MCP caller, pass `wait: false` (or `waitMs: 40000`) and poll `branch_gc_status` with the returned jobId instead of blocking. A `wait: false` (or `waitMs` timed-out) response returns a jobId plus a `followUp` block naming the poll tool and cadence \u2014 poll `branch_gc_status` with that jobId.",
30128
30379
  {
30129
30380
  repoRoot: z.string().optional().describe("Absolute path to the git repo. Wins over `workspaceSlug`."),
30130
30381
  workspaceSlug: z.string().optional().describe("Workspace slug from `agentproto workspace list`. The active workspace when omitted."),
30131
30382
  base: z.string().optional().describe("Base ref the work must be in. Default `origin/main`."),
30132
30383
  scopes: z.array(branchGcKind).optional().describe("Ref kinds to consider: local, remote, orphan. Default all three for a plan; REQUIRED for apply."),
30133
30384
  minAgeDays: mcpNumber.optional().describe("Unmerged refs younger than this many days are held. Default 3."),
30134
- includeReviewed: mcpBool2.optional().describe(
30385
+ includeReviewed: mcpBool3.optional().describe(
30135
30386
  "When true, a `review` ref whose stored verdict (branch_gc_verdict) has gate.agree=true for the SAME tip sha becomes `reclaim` (`reviewed`). Default false."
30136
30387
  ),
30137
30388
  anchor: z.string().optional().describe("Explicit anchor commit for a re-rooted base. Auto-detected when omitted."),
30138
- apply: mcpBool2.optional().describe("When true, EXECUTE the plan for `scopes`. Default false \u2014 a dry run."),
30139
- wait: mcpBool2.optional().describe("Block until the gc finishes (default true \u2014 today's behaviour). false \u21D2 return a jobId immediately; poll `branch_gc_status`."),
30389
+ apply: mcpBool3.optional().describe("When true, EXECUTE the plan for `scopes`. Default false \u2014 a dry run."),
30390
+ wait: mcpBool3.optional().describe("Block until the gc finishes (default true \u2014 today's behaviour). false \u21D2 return a jobId immediately; poll `branch_gc_status`."),
30140
30391
  waitMs: mcpNumber.optional().describe('Block at most this many milliseconds, then fall back to background: returns `{ jobId, status: "running" }` to poll with `branch_gc_status`. Only meaningful with the default `wait: true`.')
30141
30392
  },
30142
30393
  async (input) => {
@@ -30174,7 +30425,7 @@ function registerSessionTools(rawServer, opts) {
30174
30425
  ...input.minAgeDays !== void 0 ? { minAgeDays: input.minAgeDays } : {},
30175
30426
  ...input.anchor ? { anchor: input.anchor } : {}
30176
30427
  };
30177
- const { job, promise } = branchGcJobs.start(() => runBranchGc(runInput));
30428
+ const { job, promise } = branchGcJobs.start(async () => withBranchGcApplySummary(await runBranchGc(runInput)));
30178
30429
  if (input.wait === false) {
30179
30430
  return { content: [{ type: "text", text: JSON.stringify(branchGcBackgroundView(job)) }] };
30180
30431
  }
@@ -30193,12 +30444,36 @@ function registerSessionTools(rawServer, opts) {
30193
30444
  );
30194
30445
  server.tool(
30195
30446
  "branch_gc_status",
30196
- "Poll a branch_gc run started with `wait: false` (or one that fell back to the background via `waitMs`). While running: status + elapsed time. When done: the plan's own summary, the path of the full result saved on disk (`resultPath`), and \u2014 with `full: true` \u2014 the full result itself. While running the view also carries `followUp.pollAfterMs` (poll every 30 s). When failed: the error.",
30447
+ 'Poll a branch_gc run started with `wait: false` (or one that fell back to the background via `waitMs`). While running: status + elapsed time. When done: the plan\'s own summary (plus, for an apply, `applySummary` \u2014 deleted/skipped/failed per scope and the restore log path) and the path of the full result saved on disk (`resultPath`). The full result is too big to return whole, so `full: true` (or any of `section`, `classes`, `scopes`, `results`, `limit`, `cursor`) returns ONE filtered page of it: `result` (summary + plan metadata + the selected list) and `page { section, total, returned, nextCursor }` \u2014 default 100 rows, pass `nextCursor` back as `cursor` for the next page. Use `classes: ["reclaim"]` to see only what an apply would delete. While running the view carries `followUp.pollAfterMs` (poll every 30 s). When failed: the error.',
30197
30448
  {
30198
30449
  jobId: z.string().describe("Job id returned by `branch_gc` (`bgc_\u2026`)."),
30199
- full: mcpBool2.optional().describe("Include the full result JSON, not just the summary. Default false.")
30450
+ full: mcpBool3.optional().describe("Include a filtered/paged slice of the full result (see description). Implied by any other slice param. Default false."),
30451
+ section: z.enum(["entries", "outcomes"]).optional().describe("Which list to return: plan `entries` or apply `outcomes`. Default `entries` for a plan, `outcomes` for an apply."),
30452
+ classes: z.array(z.enum(["reclaim", "review", "hold"])).optional().describe('Entries only: keep these classes (e.g. ["reclaim"]).'),
30453
+ scopes: z.array(branchGcKind).optional().describe("Keep these ref kinds (local, remote, orphan)."),
30454
+ results: z.array(
30455
+ z.enum(["deleted", "held", "skipped-review", "aborted-moved", "aborted-vanished", "aborted-reclassified", "failed"])
30456
+ ).optional().describe("Outcomes only: keep these outcome results."),
30457
+ limit: mcpNumber.optional().describe(`Rows per page. Default 100, max ${BRANCH_GC_PAGE_MAX}.`),
30458
+ cursor: z.string().optional().describe("`page.nextCursor` from the previous page.")
30200
30459
  },
30201
30460
  async (input) => {
30461
+ const wantsSlice = input.full === true || input.section !== void 0 || input.classes !== void 0 || input.scopes !== void 0 || input.results !== void 0 || input.limit !== void 0 || input.cursor !== void 0;
30462
+ const slice = wantsSlice ? {
30463
+ ...input.section ? { section: input.section } : {},
30464
+ ...input.classes ? { classes: input.classes } : {},
30465
+ ...input.scopes ? { scopes: input.scopes } : {},
30466
+ ...input.results ? { results: input.results } : {},
30467
+ ...input.limit !== void 0 ? { limit: input.limit } : {},
30468
+ ...input.cursor !== void 0 ? { cursor: input.cursor } : {}
30469
+ } : void 0;
30470
+ const doneView = (...args) => {
30471
+ try {
30472
+ return { content: [{ type: "text", text: JSON.stringify(branchGcDoneView(...args)) }] };
30473
+ } catch (err) {
30474
+ return { content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }], isError: true };
30475
+ }
30476
+ };
30202
30477
  const job = branchGcJobs.get(input.jobId);
30203
30478
  if (!job) {
30204
30479
  const parsed = await branchGcJobs.readResultFile(input.jobId);
@@ -30213,14 +30488,12 @@ function registerSessionTools(rawServer, opts) {
30213
30488
  isError: true
30214
30489
  };
30215
30490
  }
30216
- const view3 = branchGcDoneView(input.jobId, branchGcJobs.resultPathFor(input.jobId), parsed, input.full === true);
30217
- return { content: [{ type: "text", text: JSON.stringify(view3) }] };
30491
+ return doneView(input.jobId, branchGcJobs.resultPathFor(input.jobId), parsed, slice);
30218
30492
  }
30219
30493
  if (job.status !== "done") {
30220
30494
  return { content: [{ type: "text", text: JSON.stringify(branchGcJobs.progressView(job)) }] };
30221
30495
  }
30222
- const view2 = branchGcDoneView(job.id, branchGcJobs.resultPathFor(job.id), job.result, input.full === true, job.endedAt);
30223
- return { content: [{ type: "text", text: JSON.stringify(view2) }] };
30496
+ return doneView(job.id, job.resultPath, job.result, slice, job.endedAt);
30224
30497
  }
30225
30498
  );
30226
30499
  const branchVerdictEnum = z.enum(["obsolete", "superseded", "salvage", "in-progress", "unclear"]);
@@ -30239,7 +30512,7 @@ function registerSessionTools(rawServer, opts) {
30239
30512
  salvage: z.string().optional().describe("What is worth keeping, if anything.")
30240
30513
  }),
30241
30514
  gate: z.object({
30242
- agree: mcpBool2.describe("true only if deleting the branch loses nothing of value"),
30515
+ agree: mcpBool3.describe("true only if deleting the branch loses nothing of value"),
30243
30516
  verdict: branchVerdictEnum,
30244
30517
  reason: z.string(),
30245
30518
  evidence: z.array(z.string()).describe("Concrete shas/paths. Required non-empty when agree is true.")
@@ -30852,10 +31125,10 @@ function registerSessionTools(rawServer, opts) {
30852
31125
  "DRY RUN, mutates nothing \u2014 classify every idle agent-cli session for the session steward: `close` (safe to auto-close: idle past `idleMinutes` AND the session's worktree merged or its parent already ended, with no tool call pending, and not `keepAlive`), `stuck` (stuck in `status:'starting'` with no pid for 10+ minutes \u2014 closing it is free, it never ran), `judge` (idle-enough but ambiguous \u2014 no merge/parent-ended signal, a pending tool call, or a `keepAlive` session that would otherwise close, since `keepAlive` can only ever reach `judge`, never `close`), or `keep` (not idle long enough yet, or busy/awaitingInput/awaitingPermission/archived/pinned/has busy children/has a live parent/is the caller's own session \u2014 NEVER eligible for `session_wrapup_apply`; omitted here unless `includeKeep` is set). Also reports `rssBytes` (process-tree RSS) per entry and summed per class in `totals`. Feed `close`/`stuck` ids straight to `session_wrapup_apply`; `judge` ids need a judge's verdict first (FIX-9B). Computing worktree merge status can take minutes: this call waits up to 25 s (`waitMs`), then returns `{ jobId, status: \"running\", followUp }` \u2014 poll `session_wrapup_status` with that jobId.",
30853
31126
  {
30854
31127
  idleMinutes: z.number().int().positive().optional().describe("Idle threshold in minutes a `close` candidate must clear. Default 20."),
30855
- includeKeep: mcpBool2.optional().describe(
31128
+ includeKeep: mcpBool3.optional().describe(
30856
31129
  "Include `keep`-class entries too. Default false \u2014 `keep` rows are never acted on, so they're omitted to keep the plan focused on what the steward might actually do."
30857
31130
  ),
30858
- wait: mcpBool2.optional().describe(
31131
+ wait: mcpBool3.optional().describe(
30859
31132
  "false \u21D2 return a jobId immediately; poll `session_wrapup_status`. true \u21D2 block until the plan is computed. Default: wait up to `waitMs`, then fall back to background."
30860
31133
  ),
30861
31134
  waitMs: mcpNumber.optional().describe("Block at most this many milliseconds, then fall back to background. Default 25000.")
@@ -30921,7 +31194,7 @@ function registerSessionTools(rawServer, opts) {
30921
31194
  judgedBy: z.string().optional().describe(
30922
31195
  "A judge session id. When set, the outcome's `source` is `'judged'` and a `judge`-class session also becomes eligible (not just `close`/`stuck`). Omitted \u21D2 `source:'declared'`, `judgedBy:'steward-rules'`, and only `close`/`stuck` are eligible."
30923
31196
  ),
30924
- wait: mcpBool2.optional().describe(
31197
+ wait: mcpBool3.optional().describe(
30925
31198
  "false \u21D2 return a jobId immediately; poll `session_wrapup_status`. true \u21D2 block until done. Default: wait up to `waitMs`, then fall back to background (the apply keeps running)."
30926
31199
  ),
30927
31200
  waitMs: mcpNumber.optional().describe("Block at most this many milliseconds, then fall back to background. Default 25000.")
@@ -31372,7 +31645,7 @@ function registerSessionTools(rawServer, opts) {
31372
31645
  "Set or clear a session's idle-reaper exemption. When `keepAlive` is true, the idle-reaper (`isReapable`) never auto-retires this session no matter how long it sits idle \u2014 for a supervisor that legitimately parks waiting on a child or a scheduled wake, which otherwise looks identical to a finished session. Set false to clear the exemption. Persists across daemon restarts. Does NOT touch the running agent.",
31373
31646
  {
31374
31647
  idOrName: z.string().min(1).describe("Session id or name to update \u2014 from `session_list`."),
31375
- keepAlive: mcpBool2.describe(
31648
+ keepAlive: mcpBool3.describe(
31376
31649
  "true to exempt this session from the idle-reaper, false to clear the exemption."
31377
31650
  )
31378
31651
  },
@@ -31434,7 +31707,7 @@ function registerSessionTools(rawServer, opts) {
31434
31707
  "Set or clear a session's list-visibility pin. When `pinned` is true, the session sorts to the top of `agentproto sessions` and the VS Code sessions webview's dedicated Pinned group. Set false to clear it. Persists across daemon restarts. Purely a sort/display flag \u2014 does NOT touch the idle-reaper, keepAlive, or emit any notification, and does NOT touch the running agent.",
31435
31708
  {
31436
31709
  idOrName: z.string().min(1).describe("Session id or name to update \u2014 from `session_list`."),
31437
- pinned: mcpBool2.describe(
31710
+ pinned: mcpBool3.describe(
31438
31711
  "true to pin this session to the top of the list, false to unpin it."
31439
31712
  )
31440
31713
  },
@@ -31719,7 +31992,7 @@ function registerSessionTools(rawServer, opts) {
31719
31992
  {
31720
31993
  idOrName: z.string().min(1).describe("Session id or name \u2014 from `session_list`."),
31721
31994
  key: z.string().min(1).describe("Artifact key, from `session_artifact_list`."),
31722
- pinned: mcpBool2.describe("true to pin, false to unpin.")
31995
+ pinned: mcpBool3.describe("true to pin, false to unpin.")
31723
31996
  },
31724
31997
  async (input) => {
31725
31998
  const prev = registry.findByIdOrName(input.idOrName);
@@ -31940,7 +32213,7 @@ function registerSessionTools(rawServer, opts) {
31940
32213
  lastBytes: z.number().int().min(1).max(64 * 1024).optional().describe(
31941
32214
  "Max bytes from the tail. Default 4096; the full ~64 KiB ring remains reachable by passing `lastBytes: 65536` explicitly."
31942
32215
  ),
31943
- clean: mcpBool2.optional().describe(
32216
+ clean: mcpBool3.optional().describe(
31944
32217
  "Strip ANSI codes, returning human-readable text (as `text` instead of `b64`). Default false = raw base64."
31945
32218
  )
31946
32219
  },
@@ -32032,7 +32305,7 @@ function registerSessionTools(rawServer, opts) {
32032
32305
 
32033
32306
  // src/index.ts
32034
32307
  init_agent_tools();
32035
- var mcpBool3 = z.preprocess(
32308
+ var mcpBool4 = z.preprocess(
32036
32309
  (v) => v === "true" ? true : v === "false" ? false : v,
32037
32310
  z.boolean()
32038
32311
  );
@@ -32117,10 +32390,10 @@ function registerBrainTools(server, opts) {
32117
32390
  {
32118
32391
  workspace: z.string().optional().describe("Workspace slug. Omit to use the calling session's workspace."),
32119
32392
  sessionId: z.string().optional().describe("Ingest one specific session (sess_xxx) instead of the backlog."),
32120
- reindex: mcpBool3.optional().describe(
32393
+ reindex: mcpBool4.optional().describe(
32121
32394
  'When `sessionId` is set and it was already ingested, force a re-ingest from the current transcript. Default false. Pass `true` or the string "true" to enable.'
32122
32395
  ),
32123
- reindexStale: mcpBool3.optional().describe(
32396
+ reindexStale: mcpBool4.optional().describe(
32124
32397
  'When `sessionId` is NOT set (backlog mode), also force-reingest every already-indexed session whose pipeline version is older than the code running now. Default false. Pass `true` or the string "true" to enable.'
32125
32398
  )
32126
32399
  },
@@ -32318,7 +32591,7 @@ function createWorkspaceBrainSubscriber(opts) {
32318
32591
  }
32319
32592
  };
32320
32593
  }
32321
- function makeBrowserHandle(entry, resolve45) {
32594
+ function makeBrowserHandle(entry, resolve47) {
32322
32595
  return {
32323
32596
  slug: entry.id,
32324
32597
  name: entry.name,
@@ -32329,7 +32602,7 @@ function makeBrowserHandle(entry, resolve45) {
32329
32602
  // check() is available for on-demand health probes but is never called
32330
32603
  // by the lister (kit invariant OQ-5). Returns true when the adapter
32331
32604
  // is present in the injected resolver map, false otherwise.
32332
- check: async () => resolve45 ? resolve45(entry.id) != null : true
32605
+ check: async () => resolve47 ? resolve47(entry.id) != null : true
32333
32606
  };
32334
32607
  }
32335
32608
  var noopLedger = {
@@ -34936,7 +35209,7 @@ function startHeartbeat(opts) {
34936
35209
  let timer = null;
34937
35210
  let currentEveryMs = null;
34938
35211
  let firing = false;
34939
- async function load() {
35212
+ async function load2() {
34940
35213
  if (!existsSync(path)) return null;
34941
35214
  let source;
34942
35215
  try {
@@ -34967,7 +35240,7 @@ function startHeartbeat(opts) {
34967
35240
  firing = true;
34968
35241
  let agentId;
34969
35242
  try {
34970
- const hb = await load();
35243
+ const hb = await load2();
34971
35244
  if (!hb || !hb.enabled) return;
34972
35245
  agentId = hb.agent;
34973
35246
  const agent = await opts.buildAgent(hb.agent);
@@ -35055,7 +35328,7 @@ function startHeartbeat(opts) {
35055
35328
  return {
35056
35329
  start() {
35057
35330
  if (timer) return;
35058
- void load().then((hb) => {
35331
+ void load2().then((hb) => {
35059
35332
  const everyMs = hb?.everyMs ?? 6e4;
35060
35333
  reschedule(everyMs);
35061
35334
  });
@@ -35104,6 +35377,356 @@ function preview(text13, max = 120) {
35104
35377
  // src/http-server.ts
35105
35378
  init_process_stats();
35106
35379
  init_browser_mount();
35380
+
35381
+ // src/mcp-modern/constants.ts
35382
+ var MODERN_PROTOCOL_VERSION = "2026-07-28";
35383
+ var DEFAULT_SUPPORTED_VERSIONS = [MODERN_PROTOCOL_VERSION];
35384
+ var META_PROTOCOL_VERSION = "io.modelcontextprotocol/protocolVersion";
35385
+ var META_CLIENT_INFO = "io.modelcontextprotocol/clientInfo";
35386
+ var META_CLIENT_CAPABILITIES = "io.modelcontextprotocol/clientCapabilities";
35387
+ var META_SERVER_INFO = "io.modelcontextprotocol/serverInfo";
35388
+ var ERR_PARSE = -32700;
35389
+ var ERR_INVALID_REQUEST = -32600;
35390
+ var ERR_METHOD_NOT_FOUND = -32601;
35391
+ var ERR_INVALID_PARAMS = -32602;
35392
+ var ERR_INTERNAL = -32603;
35393
+ var ERR_HEADER_MISMATCH = -32020;
35394
+ var ERR_UNSUPPORTED_VERSION = -32022;
35395
+ var FORWARDED_METHODS = /* @__PURE__ */ new Set([
35396
+ "tools/list",
35397
+ "tools/call",
35398
+ "events/list",
35399
+ "events/subscribe",
35400
+ "events/unsubscribe",
35401
+ "resources/list",
35402
+ "resources/templates/list",
35403
+ "resources/read",
35404
+ "prompts/list",
35405
+ "prompts/get"
35406
+ ]);
35407
+ var ALLOWED_METHODS = /* @__PURE__ */ new Set([...FORWARDED_METHODS, "server/discover"]);
35408
+ var CACHE_HINT_METHODS = /* @__PURE__ */ new Set([
35409
+ "server/discover",
35410
+ "tools/list",
35411
+ "prompts/list",
35412
+ "resources/list",
35413
+ "resources/templates/list",
35414
+ "resources/read"
35415
+ ]);
35416
+ var NAME_HEADER_SOURCE = /* @__PURE__ */ new Map([
35417
+ ["tools/call", "name"],
35418
+ ["prompts/get", "name"],
35419
+ ["resources/read", "uri"]
35420
+ ]);
35421
+
35422
+ // src/mcp-modern/bridge.ts
35423
+ var HANDSHAKE_VERSION = "2025-11-25";
35424
+ var BridgeError = class extends Error {
35425
+ code;
35426
+ data;
35427
+ constructor(code, message, data) {
35428
+ super(message);
35429
+ this.code = code;
35430
+ this.data = data;
35431
+ }
35432
+ };
35433
+ function forwardParams(params) {
35434
+ const { _meta, ...rest } = params;
35435
+ if (typeof _meta !== "object" || _meta === null || Array.isArray(_meta)) return rest;
35436
+ const handshake = /* @__PURE__ */ new Set([META_PROTOCOL_VERSION, META_CLIENT_INFO, META_CLIENT_CAPABILITIES]);
35437
+ const kept = Object.fromEntries(Object.entries(_meta).filter(([key]) => !handshake.has(key)));
35438
+ return Object.keys(kept).length > 0 ? { ...rest, _meta: kept } : rest;
35439
+ }
35440
+ var isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
35441
+ async function openBridge(server) {
35442
+ const [clientSide, serverSide] = InMemoryTransport.createLinkedPair();
35443
+ const pending = /* @__PURE__ */ new Map();
35444
+ let nextId = 1;
35445
+ clientSide.onmessage = (message) => {
35446
+ if (!("id" in message) || typeof message.id !== "number") return;
35447
+ const entry = pending.get(message.id);
35448
+ if (!entry) return;
35449
+ if ("error" in message) {
35450
+ pending.delete(message.id);
35451
+ entry.reject(new BridgeError(message.error.code, message.error.message, message.error.data));
35452
+ } else if ("result" in message) {
35453
+ pending.delete(message.id);
35454
+ entry.resolve(isRecord7(message.result) ? message.result : {});
35455
+ }
35456
+ };
35457
+ const close = async () => {
35458
+ for (const entry of pending.values()) entry.reject(new Error("bridge closed"));
35459
+ pending.clear();
35460
+ await clientSide.close().catch(() => {
35461
+ });
35462
+ await server.close().catch(() => {
35463
+ });
35464
+ };
35465
+ const call = (method, params, signal) => new Promise((resolve47, reject) => {
35466
+ if (signal?.aborted) {
35467
+ reject(signal.reason ?? new Error("aborted"));
35468
+ return;
35469
+ }
35470
+ const id = nextId++;
35471
+ const onAbort = () => {
35472
+ pending.delete(id);
35473
+ void clientSide.send({ jsonrpc: "2.0", method: "notifications/cancelled", params: { requestId: id, reason: "client disconnected" } }).catch(() => {
35474
+ });
35475
+ reject(signal?.reason ?? new Error("aborted"));
35476
+ };
35477
+ signal?.addEventListener("abort", onAbort, { once: true });
35478
+ pending.set(id, {
35479
+ resolve: (value) => {
35480
+ signal?.removeEventListener("abort", onAbort);
35481
+ resolve47(value);
35482
+ },
35483
+ reject: (error) => {
35484
+ signal?.removeEventListener("abort", onAbort);
35485
+ reject(error);
35486
+ }
35487
+ });
35488
+ clientSide.send({ jsonrpc: "2.0", id, method, params }).catch((error) => {
35489
+ pending.delete(id);
35490
+ signal?.removeEventListener("abort", onAbort);
35491
+ reject(error);
35492
+ });
35493
+ });
35494
+ try {
35495
+ await server.connect(serverSide);
35496
+ await clientSide.start();
35497
+ const init = await call("initialize", {
35498
+ protocolVersion: HANDSHAKE_VERSION,
35499
+ capabilities: {},
35500
+ clientInfo: { name: "agentproto-modern-bridge", version: "0.0.1" }
35501
+ });
35502
+ await clientSide.send({ jsonrpc: "2.0", method: "notifications/initialized" });
35503
+ const info = isRecord7(init.serverInfo) ? init.serverInfo : {};
35504
+ return {
35505
+ capabilities: isRecord7(init.capabilities) ? { ...init.capabilities } : {},
35506
+ serverInfo: {
35507
+ name: typeof info.name === "string" ? info.name : "agentproto",
35508
+ version: typeof info.version === "string" ? info.version : "0"
35509
+ },
35510
+ instructions: typeof init.instructions === "string" ? init.instructions : void 0,
35511
+ request: call,
35512
+ close
35513
+ };
35514
+ } catch (error) {
35515
+ await close();
35516
+ throw error;
35517
+ }
35518
+ }
35519
+
35520
+ // src/mcp-modern/validate.ts
35521
+ var isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
35522
+ function normalizeHeaders(raw) {
35523
+ const out = {};
35524
+ for (const [name, value] of Object.entries(raw)) {
35525
+ if (value === void 0) continue;
35526
+ const joined = (Array.isArray(value) ? value.join(", ") : value).trim();
35527
+ const key = name.toLowerCase();
35528
+ const previous = out[key];
35529
+ out[key] = previous === void 0 ? joined : `${previous}, ${joined}`;
35530
+ }
35531
+ return out;
35532
+ }
35533
+ function acceptsJson(accept) {
35534
+ if (accept === void 0) return false;
35535
+ return accept.split(",").some((part) => {
35536
+ const type = (part.split(";")[0] ?? "").trim().toLowerCase();
35537
+ return type === "application/json" || type === "application/*" || type === "*/*";
35538
+ });
35539
+ }
35540
+ function isJsonContentType(contentType) {
35541
+ if (contentType === void 0) return false;
35542
+ return (contentType.split(";")[0] ?? "").trim().toLowerCase() === "application/json";
35543
+ }
35544
+ function decodeHeaderValue(raw) {
35545
+ const match = /^=\?base64\?(.*)\?=$/.exec(raw);
35546
+ if (!match) return raw;
35547
+ return Buffer.from(match[1] ?? "", "base64").toString("utf8");
35548
+ }
35549
+ function parseBody(body) {
35550
+ const fail4 = (status, code, message) => ({ ok: false, error: { status, code, message, id: null } });
35551
+ let value;
35552
+ try {
35553
+ value = JSON.parse(body);
35554
+ } catch {
35555
+ return fail4(400, ERR_PARSE, "Parse error");
35556
+ }
35557
+ if (Array.isArray(value)) return fail4(400, ERR_INVALID_REQUEST, "Batch requests are not supported");
35558
+ if (!isRecord8(value)) return fail4(400, ERR_INVALID_REQUEST, "The body must be a single JSON-RPC request or notification");
35559
+ const method = value.method;
35560
+ if (typeof method !== "string" && ("result" in value || "error" in value)) {
35561
+ return fail4(400, ERR_INVALID_REQUEST, "JSON-RPC responses must not be sent to the server");
35562
+ }
35563
+ if (value.jsonrpc !== "2.0" || typeof method !== "string") return fail4(400, ERR_INVALID_REQUEST, "Not a JSON-RPC 2.0 request");
35564
+ const hasId = "id" in value;
35565
+ const id = value.id;
35566
+ if (hasId && typeof id !== "string" && typeof id !== "number") return fail4(400, ERR_INVALID_REQUEST, "`id` must be a string or a number");
35567
+ if (value.params !== void 0 && !isRecord8(value.params)) return fail4(400, ERR_INVALID_PARAMS, "`params` must be an object");
35568
+ return {
35569
+ ok: true,
35570
+ request: {
35571
+ id: hasId ? id : void 0,
35572
+ method,
35573
+ params: isRecord8(value.params) ? value.params : {}
35574
+ }
35575
+ };
35576
+ }
35577
+ function validateRequest(request3, headers, supported, allowed) {
35578
+ const id = request3.id ?? null;
35579
+ const err = (status, code, message, data) => ({
35580
+ status,
35581
+ code,
35582
+ message,
35583
+ id,
35584
+ ...data !== void 0 ? { data } : {}
35585
+ });
35586
+ const { method, params } = request3;
35587
+ if (method === "initialize") {
35588
+ const requested = typeof params.protocolVersion === "string" ? params.protocolVersion : "unknown";
35589
+ return err(404, ERR_METHOD_NOT_FOUND, "Method not found: initialize (this endpoint is stateless, send server/discover)", {
35590
+ supported: [...supported],
35591
+ requested
35592
+ });
35593
+ }
35594
+ const meta = params._meta;
35595
+ const missing = [];
35596
+ if (!isRecord8(meta) || typeof meta[META_PROTOCOL_VERSION] !== "string") missing.push(META_PROTOCOL_VERSION);
35597
+ if (!isRecord8(meta) || !isRecord8(meta[META_CLIENT_CAPABILITIES])) missing.push(META_CLIENT_CAPABILITIES);
35598
+ if (!isRecord8(meta) || missing.length > 0) return err(400, ERR_INVALID_PARAMS, `Missing required _meta field(s): ${missing.join(", ")}`);
35599
+ const version = String(meta[META_PROTOCOL_VERSION]);
35600
+ if (!supported.includes(version)) {
35601
+ return err(400, ERR_UNSUPPORTED_VERSION, "Unsupported protocol version", { supported: [...supported], requested: version });
35602
+ }
35603
+ const headerVersion = headers["mcp-protocol-version"];
35604
+ if (headerVersion === void 0) return err(400, ERR_HEADER_MISMATCH, "Header mismatch: MCP-Protocol-Version header is missing");
35605
+ if (headerVersion !== version) {
35606
+ return err(400, ERR_HEADER_MISMATCH, `Header mismatch: MCP-Protocol-Version ${headerVersion} does not match _meta ${version}`);
35607
+ }
35608
+ const headerMethod = headers["mcp-method"];
35609
+ if (headerMethod === void 0) return err(400, ERR_HEADER_MISMATCH, "Header mismatch: Mcp-Method header is missing");
35610
+ if (headerMethod !== method) {
35611
+ return err(400, ERR_HEADER_MISMATCH, `Header mismatch: Mcp-Method ${headerMethod} does not match body ${method}`);
35612
+ }
35613
+ const source = NAME_HEADER_SOURCE.get(method);
35614
+ if (source !== void 0) {
35615
+ const expected = params[source];
35616
+ if (typeof expected !== "string") return err(400, ERR_INVALID_PARAMS, `Missing required params.${source}`);
35617
+ const rawName = headers["mcp-name"];
35618
+ if (rawName === void 0) return err(400, ERR_HEADER_MISMATCH, "Header mismatch: Mcp-Name header is missing");
35619
+ if (decodeHeaderValue(rawName) !== expected) {
35620
+ return err(400, ERR_HEADER_MISMATCH, `Header mismatch: Mcp-Name does not match params.${source}`);
35621
+ }
35622
+ }
35623
+ if (!allowed.has(method)) return err(404, ERR_METHOD_NOT_FOUND, `Method not found: ${method}`);
35624
+ return null;
35625
+ }
35626
+
35627
+ // src/mcp-modern/index.ts
35628
+ var DEFAULT_CACHE_POLICY = () => ({ ttlMs: 0, cacheScope: "private" });
35629
+ var JSON_HEADERS = { "content-type": "application/json" };
35630
+ function errorResponse(error, extraHeaders = {}) {
35631
+ return {
35632
+ status: error.status,
35633
+ headers: { ...JSON_HEADERS, ...extraHeaders },
35634
+ body: JSON.stringify({
35635
+ jsonrpc: "2.0",
35636
+ id: error.id,
35637
+ error: { code: error.code, message: error.message, ...error.data !== void 0 ? { data: error.data } : {} }
35638
+ })
35639
+ };
35640
+ }
35641
+ function okResponse(id, result) {
35642
+ return { status: 200, headers: { ...JSON_HEADERS }, body: JSON.stringify({ jsonrpc: "2.0", id, result }) };
35643
+ }
35644
+ function decorate(method, result, serverInfo, hints) {
35645
+ const meta = typeof result._meta === "object" && result._meta !== null && !Array.isArray(result._meta) ? result._meta : {};
35646
+ const resultType = typeof result.resultType === "string" ? result.resultType : "complete";
35647
+ const decorated = {
35648
+ ...result,
35649
+ resultType,
35650
+ _meta: { ...meta, [META_SERVER_INFO]: meta[META_SERVER_INFO] ?? serverInfo }
35651
+ };
35652
+ if (resultType === "complete" && CACHE_HINT_METHODS.has(method)) {
35653
+ decorated.ttlMs = typeof result.ttlMs === "number" ? result.ttlMs : hints.ttlMs;
35654
+ decorated.cacheScope = typeof result.cacheScope === "string" ? result.cacheScope : hints.cacheScope;
35655
+ }
35656
+ return decorated;
35657
+ }
35658
+ function fromThrown(error, id) {
35659
+ if (error instanceof BridgeError) {
35660
+ const message = error.message.replace(/^(?:MCP error -?\d+: )+/, "");
35661
+ const status = error.code === ERR_METHOD_NOT_FOUND ? 404 : 200;
35662
+ return errorResponse({ status, code: error.code, message, id, ...error.data !== void 0 ? { data: error.data } : {} });
35663
+ }
35664
+ return errorResponse({ status: 500, code: ERR_INTERNAL, message: "Internal error", id });
35665
+ }
35666
+ async function answer(request3, bridge, supported, hints, signal) {
35667
+ if (request3.method === "server/discover") {
35668
+ const capabilities = { ...bridge.capabilities };
35669
+ delete capabilities.logging;
35670
+ return decorate(
35671
+ "server/discover",
35672
+ {
35673
+ supportedVersions: [...supported],
35674
+ capabilities,
35675
+ ...bridge.instructions ? { instructions: bridge.instructions } : {}
35676
+ },
35677
+ bridge.serverInfo,
35678
+ hints
35679
+ );
35680
+ }
35681
+ return decorate(request3.method, await bridge.request(request3.method, forwardParams(request3.params), signal), bridge.serverInfo, hints);
35682
+ }
35683
+ async function handleModernRequest(input, deps2) {
35684
+ const supported = deps2.supportedVersions ?? DEFAULT_SUPPORTED_VERSIONS;
35685
+ const cachePolicy = deps2.cachePolicy ?? DEFAULT_CACHE_POLICY;
35686
+ const headers = normalizeHeaders(input.headers);
35687
+ if (input.method !== "POST") {
35688
+ return errorResponse({ status: 405, code: ERR_INVALID_REQUEST, message: "Only POST is supported", id: null }, { allow: "POST" });
35689
+ }
35690
+ if (!acceptsJson(headers["accept"])) {
35691
+ return errorResponse({ status: 406, code: ERR_INVALID_REQUEST, message: "Accept must allow application/json", id: null });
35692
+ }
35693
+ if (!isJsonContentType(headers["content-type"])) {
35694
+ return errorResponse({ status: 415, code: ERR_INVALID_REQUEST, message: "Content-Type must be application/json", id: null });
35695
+ }
35696
+ const parsed = parseBody(input.body);
35697
+ if (!parsed.ok) return errorResponse(parsed.error);
35698
+ const request3 = parsed.request;
35699
+ if (request3.id === void 0) return { status: 202, headers: {} };
35700
+ const invalid = validateRequest(request3, headers, supported, ALLOWED_METHODS);
35701
+ if (invalid) return errorResponse(invalid);
35702
+ const id = request3.id;
35703
+ let bridge;
35704
+ try {
35705
+ bridge = await openBridge(await deps2.createServer());
35706
+ const result = await answer({ ...request3, id }, bridge, supported, cachePolicy(request3.method), deps2.signal);
35707
+ if (deps2.signal?.aborted) return { status: 499, headers: {} };
35708
+ const response = okResponse(id, result);
35709
+ if ((request3.method === "server/discover" || request3.method === "tools/list") && deps2.onObserved) {
35710
+ try {
35711
+ deps2.onObserved(request3.method, { jsonrpc: "2.0", id, result });
35712
+ } catch {
35713
+ }
35714
+ }
35715
+ return response;
35716
+ } catch (error) {
35717
+ if (deps2.signal?.aborted) return { status: 499, headers: {} };
35718
+ const response = fromThrown(error, id);
35719
+ if ((request3.method === "server/discover" || request3.method === "tools/list") && deps2.onObserved && response.body) {
35720
+ try {
35721
+ deps2.onObserved(request3.method, JSON.parse(response.body));
35722
+ } catch {
35723
+ }
35724
+ }
35725
+ return response;
35726
+ } finally {
35727
+ await bridge?.close();
35728
+ }
35729
+ }
35107
35730
  init_sessions();
35108
35731
  init_session_retirement();
35109
35732
  init_session_message();
@@ -35795,7 +36418,7 @@ function createSentinelStore(opts) {
35795
36418
  const persist = opts?.persist ?? opts?.filePath !== void 0;
35796
36419
  let persistTimer = null;
35797
36420
  let secretsPersistTimer = null;
35798
- const load = () => {
36421
+ const load2 = () => {
35799
36422
  const out = /* @__PURE__ */ new Map();
35800
36423
  if (!persist) return out;
35801
36424
  let raw;
@@ -35819,7 +36442,7 @@ function createSentinelStore(opts) {
35819
36442
  }
35820
36443
  return out;
35821
36444
  };
35822
- const sentinels = load();
36445
+ const sentinels = load2();
35823
36446
  const loadSecrets = () => {
35824
36447
  const out = /* @__PURE__ */ new Map();
35825
36448
  if (!persist) return out;
@@ -36039,6 +36662,15 @@ function createSentinelStore(opts) {
36039
36662
  function singleMatch(subject, types) {
36040
36663
  return [{ subject, ...types ? { types } : {} }];
36041
36664
  }
36665
+ var SentinelBackingExpiredError = class extends Error {
36666
+ code = "backing_subscription_expired";
36667
+ remoteId;
36668
+ constructor(message, remoteId) {
36669
+ super(message);
36670
+ this.name = "SentinelBackingExpiredError";
36671
+ if (remoteId !== void 0) this.remoteId = remoteId;
36672
+ }
36673
+ };
36042
36674
  function deliveryPreferenceFor(provider, intervalMs) {
36043
36675
  if (provider.preferredDelivery) return provider.preferredDelivery(intervalMs);
36044
36676
  return provider.capabilities.poll ? { mode: "poll", intervalMs } : { mode: "push" };
@@ -36602,7 +37234,7 @@ function compactPrState(snapshot, extra) {
36602
37234
  const slim = snapshot.state !== "open" && extra.closedEmitted ? { fetchedAt: snapshot.fetchedAt, state: snapshot.state, reviews: [], checks: [] } : snapshot;
36603
37235
  return { snapshot: slim, ...extra };
36604
37236
  }
36605
- function decorate(events, pr) {
37237
+ function decorate2(events, pr) {
36606
37238
  const title = pr.title.length > 80 ? `${pr.title.slice(0, 77)}...` : pr.title;
36607
37239
  return events.map((e) => ({
36608
37240
  ...e,
@@ -36666,7 +37298,7 @@ function diffRepoPr(prev, pr, repo) {
36666
37298
  closedEmitted = true;
36667
37299
  }
36668
37300
  return {
36669
- events: decorate(events, pr),
37301
+ events: decorate2(events, pr),
36670
37302
  next: compactPrState(now, { draft: pr.draft, closedEmitted, ...ciReported ? { ciReported } : {} })
36671
37303
  };
36672
37304
  }
@@ -36698,7 +37330,7 @@ function diffRepo(cursor, prs, repo, fetchedAt) {
36698
37330
  })
36699
37331
  ];
36700
37332
  if (closed) fresh.push(closedEvent(pr.snapshot, { repo, number: pr.number, subject }, false));
36701
- events.push(...decorate(fresh, pr));
37333
+ events.push(...decorate2(fresh, pr));
36702
37334
  }
36703
37335
  const settled = verdict === "success" || verdict === "failure" || verdict === "neutral";
36704
37336
  next[key] = compactPrState(pr.snapshot, {
@@ -37636,11 +38268,11 @@ function verifySignatureV2(secret, headers, body, nowMs, skewMs = SIGNATURE_V2_S
37636
38268
  if (a.length !== b.length || !timingSafeEqual(a, b)) return { ok: false, reason: "signature_mismatch" };
37637
38269
  return { ok: true, timestampSec };
37638
38270
  }
37639
- var isRecord7 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
38271
+ var isRecord9 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
37640
38272
  function toSentinelEvent(raw, seq) {
37641
- if (!isRecord7(raw)) return null;
38273
+ if (!isRecord9(raw)) return null;
37642
38274
  const { id, source, type, subject, time, data, summary, subjects, terminal, consumerref } = raw;
37643
- if (raw.specversion !== "1.0" || typeof id !== "string" || id === "" || typeof source !== "string" || typeof type !== "string" || type === "" || typeof subject !== "string" || subject === "" || typeof time !== "string" || !isRecord7(data) || typeof summary !== "string" || !Array.isArray(subjects) || !subjects.every((s) => typeof s === "string") || typeof terminal !== "boolean") {
38275
+ if (raw.specversion !== "1.0" || typeof id !== "string" || id === "" || typeof source !== "string" || typeof type !== "string" || type === "" || typeof subject !== "string" || subject === "" || typeof time !== "string" || !isRecord9(data) || typeof summary !== "string" || !Array.isArray(subjects) || !subjects.every((s) => typeof s === "string") || typeof terminal !== "boolean") {
37644
38276
  return null;
37645
38277
  }
37646
38278
  const envSeq = typeof raw.seq === "number" ? raw.seq : seq;
@@ -37686,9 +38318,36 @@ function stateOf(handle) {
37686
38318
  mode: s.mode === "push" ? "push" : "poll",
37687
38319
  ...typeof s.consumerRef === "string" ? { consumerRef: s.consumerRef } : {},
37688
38320
  ...typeof s.hookKey === "string" ? { hookKey: s.hookKey } : {},
37689
- ...typeof s.callbackSecret === "string" ? { callbackSecret: s.callbackSecret } : {}
38321
+ ...typeof s.callbackSecret === "string" ? { callbackSecret: s.callbackSecret } : {},
38322
+ ...s.signature === "v2" ? { signature: "v2" } : {}
37690
38323
  };
37691
38324
  }
38325
+ function excerptOf(envelope) {
38326
+ if (!isRecord9(envelope)) return void 0;
38327
+ const cap = (v) => typeof v === "string" ? v.slice(0, 200) : void 0;
38328
+ const out = {};
38329
+ const id = cap(envelope.id);
38330
+ const type = cap(envelope.type);
38331
+ const subject = cap(envelope.subject);
38332
+ const source = cap(envelope.source);
38333
+ if (id !== void 0) out.id = id;
38334
+ if (type !== void 0) out.type = type;
38335
+ if (subject !== void 0) out.subject = subject;
38336
+ if (source !== void 0) out.source = source;
38337
+ return Object.keys(out).length > 0 ? out : void 0;
38338
+ }
38339
+ function whyNotEnvelope(envelope) {
38340
+ if (envelope === void 0 || envelope === null) return "item has no envelope";
38341
+ if (!isRecord9(envelope)) return "envelope is not a JSON object";
38342
+ const missing = [];
38343
+ if (envelope.specversion !== "1.0") missing.push("specversion");
38344
+ for (const k of ["id", "type", "subject"]) if (typeof envelope[k] !== "string" || envelope[k] === "") missing.push(k);
38345
+ for (const k of ["source", "time", "summary"]) if (typeof envelope[k] !== "string") missing.push(k);
38346
+ if (!isRecord9(envelope.data)) missing.push("data");
38347
+ if (!Array.isArray(envelope.subjects) || !envelope.subjects.every((x) => typeof x === "string")) missing.push("subjects");
38348
+ if (typeof envelope.terminal !== "boolean") missing.push("terminal");
38349
+ return `envelope failed validation (invalid or missing: ${missing.join(", ") || "unknown"})`;
38350
+ }
37692
38351
  var SUBJECT_SCHEME_RE = /^([A-Za-z][A-Za-z0-9_-]*):/;
37693
38352
  function defaultTypesFor(subject) {
37694
38353
  return subject.startsWith("github:") ? [...GITHUB_DEFAULT_PR_TYPES] : ["*"];
@@ -37746,7 +38405,7 @@ function agentpushSentinelProvider(opts = {}) {
37746
38405
  json3 = void 0;
37747
38406
  }
37748
38407
  if (!res.ok) {
37749
- const detail = isRecord7(json3) && typeof json3.error === "string" ? json3.error : text13.slice(0, 200);
38408
+ const detail = isRecord9(json3) && typeof json3.error === "string" ? json3.error : text13.slice(0, 200);
37750
38409
  throw new AgentpushHttpError(res.status, `agentpush ${method} ${path} \u2192 HTTP ${res.status}${detail ? `: ${detail}` : ""}`);
37751
38410
  }
37752
38411
  return json3;
@@ -37779,6 +38438,7 @@ function agentpushSentinelProvider(opts = {}) {
37779
38438
  // Credentials come from `setup_sentinel_provider` OR an imported alias, so
37780
38439
  // "set up" is decided by `readiness()`, not the kit's ledger/creds check.
37781
38440
  requiresSetup: false,
38441
+ exclusiveRemote: true,
37782
38442
  capabilities: {
37783
38443
  subjects: ["*"],
37784
38444
  push: true,
@@ -37832,7 +38492,7 @@ function agentpushSentinelProvider(opts = {}) {
37832
38492
  ...push ? { callback_url: callbackUrl2(delivery.callbackUrl, hookKey), callback_secret: callbackSecret } : {}
37833
38493
  }
37834
38494
  });
37835
- const id = isRecord7(json3) && isRecord7(json3.subscription) ? json3.subscription.id : void 0;
38495
+ const id = isRecord9(json3) && isRecord9(json3.subscription) ? json3.subscription.id : void 0;
37836
38496
  if (typeof id !== "string" || id === "") throw new Error("agentpush: create returned no subscription id");
37837
38497
  return {
37838
38498
  provider: AGENTPUSH_SLUG,
@@ -37841,6 +38501,7 @@ function agentpushSentinelProvider(opts = {}) {
37841
38501
  state: {
37842
38502
  mode: push ? "push" : "poll",
37843
38503
  consumerRef,
38504
+ signature: "v2",
37844
38505
  ...push ? { hookKey, callbackSecret } : {}
37845
38506
  }
37846
38507
  };
@@ -37859,7 +38520,7 @@ function agentpushSentinelProvider(opts = {}) {
37859
38520
  let sub;
37860
38521
  try {
37861
38522
  const json3 = await api("GET", subPath(handle));
37862
- sub = isRecord7(json3) && isRecord7(json3.subscription) ? json3.subscription : void 0;
38523
+ sub = isRecord9(json3) && isRecord9(json3.subscription) ? json3.subscription : void 0;
37863
38524
  } catch (err) {
37864
38525
  if (isHttp(err, 404)) {
37865
38526
  throw new Error(`agentpush subscription ${handle.remoteId} no longer exists \u2014 re-create the sentinel`);
@@ -37887,10 +38548,34 @@ function agentpushSentinelProvider(opts = {}) {
37887
38548
  if (!isHttp(err, 404)) throw err;
37888
38549
  }
37889
38550
  },
38551
+ /**
38552
+ * Extend the remote subscription's lifetime. `PATCH` accepts `until`
38553
+ * without rejecting an already-expired row, so the returned `status` is
38554
+ * checked: anything but `active` (or a 404) means the backing subscription
38555
+ * is gone and must be recreated — never silently re-provisioned here.
38556
+ */
38557
+ async renew(handle, until) {
38558
+ if (!handle.remoteId) throw new SentinelBackingExpiredError("agentpush: handle has no subscription id");
38559
+ let json3;
38560
+ try {
38561
+ json3 = await api("PATCH", subPath(handle), { body: { until } });
38562
+ } catch (err) {
38563
+ if (isHttp(err, 404)) {
38564
+ throw new SentinelBackingExpiredError(`the agentpush subscription ${handle.remoteId} was deleted`, handle.remoteId);
38565
+ }
38566
+ throw err;
38567
+ }
38568
+ const sub = isRecord9(json3) && isRecord9(json3.subscription) ? json3.subscription : void 0;
38569
+ const status = sub && typeof sub.status === "string" ? sub.status : void 0;
38570
+ if (status === "expired" || status === "deleted") {
38571
+ throw new SentinelBackingExpiredError(`the agentpush subscription ${handle.remoteId} is ${status}`, handle.remoteId);
38572
+ }
38573
+ return { ...handle };
38574
+ },
37890
38575
  async status(handle) {
37891
38576
  try {
37892
38577
  const json3 = await api("GET", subPath(handle));
37893
- const sub = isRecord7(json3) && isRecord7(json3.subscription) ? json3.subscription : void 0;
38578
+ const sub = isRecord9(json3) && isRecord9(json3.subscription) ? json3.subscription : void 0;
37894
38579
  const status = sub && typeof sub.status === "string" ? sub.status : "unknown";
37895
38580
  if (status === "active") return { ok: true };
37896
38581
  return { ok: false, detail: `the agentpush subscription is ${status}` };
@@ -37902,18 +38587,35 @@ function agentpushSentinelProvider(opts = {}) {
37902
38587
  const after = handle.cursor ?? "0";
37903
38588
  if (stateOf(handle).mode === "push") return { events: [], cursor: after };
37904
38589
  const json3 = await api("GET", `${subPath(handle)}/events`, { query: { after, limit: Math.max(1, Math.min(limit, 1e3)) } });
37905
- const items = isRecord7(json3) && Array.isArray(json3.items) ? json3.items : [];
38590
+ const items = isRecord9(json3) && Array.isArray(json3.items) ? json3.items : [];
37906
38591
  const events = [];
38592
+ const malformed = [];
37907
38593
  let cursor = Number(after);
37908
38594
  if (!Number.isFinite(cursor)) cursor = 0;
37909
38595
  for (const item of items) {
37910
- if (!isRecord7(item)) continue;
37911
- const seq = typeof item.seq === "number" ? item.seq : void 0;
38596
+ const seq = isRecord9(item) && typeof item.seq === "number" ? item.seq : void 0;
37912
38597
  if (seq !== void 0 && seq > cursor) cursor = seq;
37913
- const event = toSentinelEvent(item.envelope, seq);
37914
- if (event) events.push(event);
38598
+ const envelope = isRecord9(item) ? item.envelope : void 0;
38599
+ const event = toSentinelEvent(envelope, seq);
38600
+ if (event) {
38601
+ events.push(event);
38602
+ continue;
38603
+ }
38604
+ let serialized = "";
38605
+ try {
38606
+ serialized = JSON.stringify(item) ?? "";
38607
+ } catch {
38608
+ }
38609
+ const excerpt = excerptOf(envelope);
38610
+ malformed.push({
38611
+ ...seq !== void 0 ? { seq } : {},
38612
+ ...isRecord9(item) && typeof item.delivery_id === "string" ? { remoteDeliveryId: item.delivery_id } : {},
38613
+ error: isRecord9(item) ? whyNotEnvelope(envelope) : "poll item is not a JSON object",
38614
+ ...excerpt ? { excerpt } : {},
38615
+ ...serialized ? { digest: createHash("sha256").update(serialized).digest("hex"), bytes: Buffer.byteLength(serialized) } : {}
38616
+ });
37915
38617
  }
37916
- return { events, cursor: String(cursor) };
38618
+ return { events, cursor: String(cursor), ...malformed.length > 0 ? { malformed } : {} };
37917
38619
  },
37918
38620
  /** Called by the runtime after a polled batch is delivered. Nothing new
37919
38621
  * (cursor unchanged) is a no-op — no request on an empty tick. */
@@ -37930,6 +38632,7 @@ function agentpushSentinelProvider(opts = {}) {
37930
38632
  const signature = headerValue3(req.headers, "x-agentpush-signature");
37931
38633
  if (!signature || signature.trim() === "") return { ok: false, reason: "missing_signature" };
37932
38634
  if (signature.trim().startsWith("sha256=")) {
38635
+ if (st.signature === "v2") return { ok: false, reason: "legacy_signature_rejected" };
37933
38636
  const legacy = verifyInboundSignature("agentpush", {
37934
38637
  rawBody: req.rawBody,
37935
38638
  headers: req.headers,
@@ -38360,6 +39063,13 @@ async function createSentinelWatch(deps2, input) {
38360
39063
  async function cancelSentinelWatch(deps2, id) {
38361
39064
  const sentinel = deps2.store.get(id);
38362
39065
  if (!sentinel) return false;
39066
+ if (deps2.tombstones) {
39067
+ try {
39068
+ await deps2.tombstones.cancel(sentinel.handle, () => deps2.store.remove(id));
39069
+ return true;
39070
+ } catch {
39071
+ }
39072
+ }
38363
39073
  const provider = await deps2.resolveProvider(sentinel.provider);
38364
39074
  if (provider) {
38365
39075
  try {
@@ -38392,6 +39102,9 @@ function sentinelView(s) {
38392
39102
  ...s.lastError ? { lastError: s.lastError } : {}
38393
39103
  };
38394
39104
  }
39105
+ function sentinelViewWithDelivery(s, deliveryStatus) {
39106
+ return deliveryStatus ? { ...sentinelView(s), deliveryStatus } : sentinelView(s);
39107
+ }
38395
39108
  var ok2 = (body) => ({
38396
39109
  content: [{ type: "text", text: JSON.stringify({ ok: true, ...body }) }]
38397
39110
  });
@@ -38402,7 +39115,7 @@ var fail2 = (code, message) => ({
38402
39115
  var urgencyField2 = z.enum(MESSAGE_URGENCIES);
38403
39116
  var untilField = z.enum(["subject_terminal", "never"]);
38404
39117
  function registerSentinelTools(server, opts) {
38405
- const { store, runtime, resolveProvider, isSessionAlive, callerSessionId, activeIntervalMs } = opts;
39118
+ const { store, runtime, resolveProvider, isSessionAlive, callerSessionId, activeIntervalMs, tombstones } = opts;
38406
39119
  server.tool(
38407
39120
  "sentinel_watch",
38408
39121
  "Watch a subject \u2014 a GitHub PR/repo (default) or ANOTHER SESSION's own lifecycle \u2014 and deliver matching events as a `system`/`notice` message into a session's AIP-46 inbox. Provide either `prUrl` (sugar: `https://github.com/o/r/pull/N` -> subject `github:o/r#N`, the default PR type set, and `until: subject_terminal`) or a raw `subject`: a GitHub one (`github:o/r#N`, `github:o/r`), or `session:<id>` to watch another session \u2014 woken on its turn-end, awaiting-input, or exit, even if it never calls `message_parent`, with the watch self-expiring once it exits (`until` defaults to `subject_terminal` for this scheme too). Prefer this over `session_follow` for a single, known target that should auto-expire on exit; prefer `session_follow` for a broad selector (`all`/`cwdPrefix`) with coalesced digests across many sessions. `sessionId` (the DELIVERY target) defaults to the calling session \u2014 distinct from the `<id>` inside a `session:<id>` subject (the WATCHED session). `provider` picks the backend: `local-gh` (poll every ~15-60s over the host's authenticated `gh` CLI, zero infra), `webhook` (near-real-time push via a GitHub repo hook; needs a public daemon URL \u2014 a named tunnel or AGENTPROTO_PUBLIC_URL \u2014 and a `gh` token with admin:repo_hook), `agentpush` (hosted, durable subscription: events queue server-side across daemon downtime; needs an agentpush API key; one subject per sentinel) or `session` (in-process, zero infra, no credentials \u2014 the only backend that understands a `session:<id>` subject, and picked automatically for one); see `list_sentinel_adapters` for readiness. Omitted for a GitHub subject, it defaults to `agentpush` when set up, else `webhook` when a stable public URL exists and webhook is ready, else `local-gh`.",
@@ -38425,16 +39138,16 @@ function registerSentinelTools(server, opts) {
38425
39138
  );
38426
39139
  server.tool(
38427
39140
  "sentinel_list",
38428
- "List every sentinel this daemon is tracking, with status, match clauses, target, and event counters. Credentials are never returned.",
39141
+ "List every sentinel this daemon is tracking, with status, match clauses, target, and event counters (webhook targets also report `deliveryStatus`). Credentials are never returned.",
38429
39142
  {},
38430
- async () => ok2({ sentinels: store.list().map(sentinelView) })
39143
+ async () => ok2({ sentinels: store.list().map((s) => sentinelViewWithDelivery(s, opts.deliveryStatus?.(s.id))) })
38431
39144
  );
38432
39145
  server.tool(
38433
39146
  "sentinel_unwatch",
38434
39147
  "Stop a sentinel: best-effort cancels the provider-side watch, then removes the record. Idempotent-ish: an unknown id is reported, not thrown.",
38435
39148
  { id: z.string().describe("Sentinel id (sen_...).") },
38436
39149
  async ({ id }) => {
38437
- const removed = await cancelSentinelWatch({ store, resolveProvider }, id);
39150
+ const removed = await cancelSentinelWatch({ store, resolveProvider, ...tombstones ? { tombstones } : {} }, id);
38438
39151
  if (!removed) return fail2("not_found", `sentinel_unwatch: no sentinel "${id}"`);
38439
39152
  return ok2({ removed: true, id });
38440
39153
  }
@@ -38495,7 +39208,7 @@ function createSessionFollowStore(opts) {
38495
39208
  const debounceMs = opts?.debounceMs ?? PERSIST_DEBOUNCE_MS4;
38496
39209
  const persist = opts?.persist ?? true;
38497
39210
  let persistTimer = null;
38498
- const load = () => {
39211
+ const load2 = () => {
38499
39212
  const out = /* @__PURE__ */ new Map();
38500
39213
  if (!persist) return out;
38501
39214
  let raw;
@@ -38521,7 +39234,7 @@ function createSessionFollowStore(opts) {
38521
39234
  }
38522
39235
  return out;
38523
39236
  };
38524
- const follows = load();
39237
+ const follows = load2();
38525
39238
  const snapshot = () => {
38526
39239
  const out = {};
38527
39240
  for (const [id, follow] of follows.entries()) out[id] = follow;
@@ -38803,7 +39516,7 @@ function createHostRegistry(deps2) {
38803
39516
  let probesRunning = 0;
38804
39517
  const probeWaiters = [];
38805
39518
  async function withProbeSlot(fn) {
38806
- if (probesRunning >= probeConcurrency) await new Promise((resolve45) => probeWaiters.push(resolve45));
39519
+ if (probesRunning >= probeConcurrency) await new Promise((resolve47) => probeWaiters.push(resolve47));
38807
39520
  probesRunning++;
38808
39521
  try {
38809
39522
  return await fn();
@@ -39342,7 +40055,7 @@ function createHostRegistry(deps2) {
39342
40055
  };
39343
40056
  }
39344
40057
  async function awaitTunnelHello(client, sink, record2, closeReason) {
39345
- return new Promise((resolve45, reject) => {
40058
+ return new Promise((resolve47, reject) => {
39346
40059
  if (!sink.isOpen) {
39347
40060
  reject(
39348
40061
  new Error(
@@ -39361,7 +40074,7 @@ async function awaitTunnelHello(client, sink, record2, closeReason) {
39361
40074
  client.ready().then(
39362
40075
  () => {
39363
40076
  offClose();
39364
- resolve45();
40077
+ resolve47();
39365
40078
  },
39366
40079
  (err) => {
39367
40080
  offClose();
@@ -39406,13 +40119,13 @@ function isLegacyJoinedShape(rec) {
39406
40119
  return rec.addedVia === void 0 && rec.name === rec.fingerprint && rec.provider === void 0 && rec.sandboxId === void 0 && rec.labels === void 0 && rec.lastSeen === rec.createdAt;
39407
40120
  }
39408
40121
  function withTimeout(p, ms, what) {
39409
- return new Promise((resolve45, reject) => {
40122
+ return new Promise((resolve47, reject) => {
39410
40123
  const timer = setTimeout(() => reject(new Error(`${what} timed out after ${ms}ms`)), ms);
39411
40124
  if (typeof timer.unref === "function") timer.unref();
39412
40125
  p.then(
39413
40126
  (v) => {
39414
40127
  clearTimeout(timer);
39415
- resolve45(v);
40128
+ resolve47(v);
39416
40129
  },
39417
40130
  (e) => {
39418
40131
  clearTimeout(timer);
@@ -39641,7 +40354,7 @@ function isValidPrompt(prompt) {
39641
40354
  return prompt !== null && typeof prompt === "object";
39642
40355
  }
39643
40356
  function sleep(ms) {
39644
- return new Promise((resolve45) => setTimeout(resolve45, ms));
40357
+ return new Promise((resolve47) => setTimeout(resolve47, ms));
39645
40358
  }
39646
40359
  function httpErrorMessage(body) {
39647
40360
  const raw = Buffer.from(body).toString("utf8");
@@ -40374,10 +41087,12 @@ init_transcript_export();
40374
41087
  init_spawn_defaults();
40375
41088
  init_app_boundary();
40376
41089
  var WORKFLOW_PROMPT_SOURCE = "workflow:agent-step";
41090
+ var TRANSPORT_END_REASONS = /* @__PURE__ */ new Set([void 0, "crashed", "daemon-restart"]);
41091
+ var TRANSPORT_CLOSED_RE = /\bACP connection closed\b/i;
40377
41092
  function withReviewWorktreeZones(boundary, cwd) {
40378
41093
  const zones = reviewWorktreeReadZones(cwd);
40379
41094
  if (!zones) return boundary;
40380
- const extra = [zones.worktree, zones.gitCommonDir].filter((z61) => !boundary.readOnly.includes(z61));
41095
+ const extra = [zones.worktree, zones.gitCommonDir].filter((z62) => !boundary.readOnly.includes(z62));
40381
41096
  return extra.length === 0 ? boundary : { ...boundary, readOnly: [...boundary.readOnly, ...extra] };
40382
41097
  }
40383
41098
  function agentStepMcpServers(input) {
@@ -40681,7 +41396,35 @@ var SessionsRegistryAgentHost = class {
40681
41396
  * synchronously, before the event bus can fire the rejection asynchronously.
40682
41397
  */
40683
41398
  async sendPromptAndWait(sessionId, prompt) {
40684
- await Promise.all([this.waitTurnEnd(sessionId), this.registry.sendPrompt(sessionId, prompt, { source: WORKFLOW_PROMPT_SOURCE })]);
41399
+ await Promise.all([
41400
+ this.waitTurnEnd(sessionId),
41401
+ this.registry.sendPrompt(sessionId, prompt, { source: WORKFLOW_PROMPT_SOURCE }).catch((err) => {
41402
+ throw this.classifyPromptFailure(sessionId, err);
41403
+ })
41404
+ ]);
41405
+ }
41406
+ /** True when `sessionId` dying (with `reason`) is the session's transport
41407
+ * failing under the step — not this host's own cancel/release, and not a
41408
+ * deliberate end (see {@link TRANSPORT_END_REASONS}). */
41409
+ diedUnderStep(sessionId, reason) {
41410
+ if (this.opts?.signal?.aborted) return false;
41411
+ if (!this.unreleased.has(sessionId)) return false;
41412
+ return TRANSPORT_END_REASONS.has(reason);
41413
+ }
41414
+ /** The error a wait rejects with when the session ends mid-turn. */
41415
+ sessionEndedError(sessionId, status, reason) {
41416
+ const message = `session ${sessionId} ended with status '${status}'${reason ? ` (${reason})` : ""}`;
41417
+ return this.diedUnderStep(sessionId, reason) ? new AgentSessionLostError(sessionId, message) : new Error(message);
41418
+ }
41419
+ /** `registry.sendPrompt` rejected: a closed ACP connection or an already-
41420
+ * dead session is a transport loss ({@link AgentSessionLostError});
41421
+ * anything else propagates unchanged. */
41422
+ classifyPromptFailure(sessionId, err) {
41423
+ if (!(err instanceof Error)) return err;
41424
+ const desc = this.registry.get(sessionId);
41425
+ const dead = err instanceof SessionNotAliveError || TRANSPORT_CLOSED_RE.test(err.message) || desc?.status === "killed" || desc?.status === "error";
41426
+ if (!dead || !this.diedUnderStep(sessionId, desc?.endedReason)) return err;
41427
+ return new AgentSessionLostError(sessionId, `session ${sessionId} lost before its turn ended \u2014 ${err.message}`, err);
40685
41428
  }
40686
41429
  resolveByLabel(stepId) {
40687
41430
  return this.sessionsByLabel.get(stepId);
@@ -40769,15 +41512,15 @@ var SessionsRegistryAgentHost = class {
40769
41512
  }
40770
41513
  // ── Internal helpers ──────────────────────────────────────────────────
40771
41514
  waitTurnEnd(sessionId) {
40772
- return new Promise((resolve45, reject) => {
41515
+ return new Promise((resolve47, reject) => {
40773
41516
  const unsubs = [];
40774
41517
  const done = () => {
40775
41518
  for (const u of unsubs) u();
40776
- resolve45();
41519
+ resolve47();
40777
41520
  };
40778
41521
  const fail4 = (reason) => {
40779
41522
  for (const u of unsubs) u();
40780
- reject(new Error(reason));
41523
+ reject(typeof reason === "string" ? new Error(reason) : reason);
40781
41524
  };
40782
41525
  unsubs.push(
40783
41526
  this.sessionEvents.on("session:turn-end", (ev) => {
@@ -40804,7 +41547,7 @@ var SessionsRegistryAgentHost = class {
40804
41547
  this.sessionEvents.on("session:exited", (ev) => {
40805
41548
  if (ev.sessionId !== sessionId) return;
40806
41549
  if (ev.status === "killed" || ev.status === "error") {
40807
- fail4(`session ${sessionId} ended with status '${ev.status}'`);
41550
+ fail4(this.sessionEndedError(sessionId, ev.status, ev.reason));
40808
41551
  } else {
40809
41552
  done();
40810
41553
  }
@@ -40814,7 +41557,7 @@ var SessionsRegistryAgentHost = class {
40814
41557
  if (desc?.status === "exited" || desc?.awaitingInput === true) {
40815
41558
  done();
40816
41559
  } else if (desc?.status === "killed" || desc?.status === "error") {
40817
- fail4(`session ${sessionId} ended with status '${desc.status}'`);
41560
+ fail4(this.sessionEndedError(sessionId, desc.status, desc.endedReason));
40818
41561
  }
40819
41562
  });
40820
41563
  }
@@ -40824,11 +41567,11 @@ function sanitize(cacheKey) {
40824
41567
  const cleaned = cacheKey.replace(/[^a-zA-Z0-9._-]/g, "_");
40825
41568
  return cleaned.length > 0 ? cleaned : "default";
40826
41569
  }
40827
- function isRecord8(v) {
41570
+ function isRecord10(v) {
40828
41571
  return typeof v === "object" && v !== null && !Array.isArray(v);
40829
41572
  }
40830
41573
  function isEntry2(v) {
40831
- return isRecord8(v) && typeof v.resolvedInputHash === "string" && "output" in v;
41574
+ return isRecord10(v) && typeof v.resolvedInputHash === "string" && "output" in v;
40832
41575
  }
40833
41576
  function createFileStepCache(cacheKey, opts) {
40834
41577
  const dir = opts?.dir ?? DEFAULT_CACHE_DIR();
@@ -40841,7 +41584,7 @@ function createFileStepCache(cacheKey, opts) {
40841
41584
  } catch {
40842
41585
  return {};
40843
41586
  }
40844
- if (!isRecord8(parsed)) return {};
41587
+ if (!isRecord10(parsed)) return {};
40845
41588
  const out = {};
40846
41589
  for (const [k, v] of Object.entries(parsed)) if (isEntry2(v)) out[k] = v;
40847
41590
  return out;
@@ -41137,6 +41880,7 @@ function findStepDef(defs, stepId) {
41137
41880
  var DEFAULT_PERSIST_PATH2 = () => join(homedir(), ".agentproto", "workflow-runs.json");
41138
41881
  var DEFAULT_LEASE_TTL_MS = 6e4;
41139
41882
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 15e3;
41883
+ var DEFAULT_PERSIST_MIN_INTERVAL_MS = 2e3;
41140
41884
  function cancelledStageStatus(stage) {
41141
41885
  return stage.steps.some((s) => s.status === "failed") ? "failed" : "cancelled";
41142
41886
  }
@@ -41228,9 +41972,7 @@ function saveRuns(runs, persistPath) {
41228
41972
  try {
41229
41973
  mkdirSync(dirname(persistPath), { recursive: true });
41230
41974
  const payload = JSON.stringify(
41231
- Array.from(runs.values()).map((s) => s.run),
41232
- null,
41233
- 2
41975
+ Array.from(runs.values()).map((s) => s.run)
41234
41976
  ) + "\n";
41235
41977
  const tmp = `${persistPath}.tmp.${process.pid}`;
41236
41978
  writeFileSync(tmp, payload, "utf8");
@@ -41306,7 +42048,7 @@ function createOnEscalate(state, persist) {
41306
42048
  state.run.status = "awaiting-input";
41307
42049
  persist();
41308
42050
  try {
41309
- return await new Promise((resolve45, reject) => {
42051
+ return await new Promise((resolve47, reject) => {
41310
42052
  const timeoutMs = policy.timeoutMs ?? 3e5;
41311
42053
  const timer = setTimeout(() => {
41312
42054
  state.pendingResolve = void 0;
@@ -41318,7 +42060,7 @@ function createOnEscalate(state, persist) {
41318
42060
  resolver: (response) => {
41319
42061
  clearTimeout(timer);
41320
42062
  state.pendingResolve = void 0;
41321
- resolve45(response);
42063
+ resolve47(response);
41322
42064
  }
41323
42065
  };
41324
42066
  });
@@ -41454,7 +42196,7 @@ async function executeRunWorkflow(state, runtimeWf, agents, signal, sessionEvent
41454
42196
  // `resolveApproval` (wired to `workflow_escalation_resolve`'s approval
41455
42197
  // form); the decision lands on the app ledger (`kind: "approval"`,
41456
42198
  // `by: "human"`) per the trame rule.
41457
- approve: (req) => new Promise((resolve45) => {
42199
+ approve: (req) => new Promise((resolve47) => {
41458
42200
  const approvalId = `wfappr_${randomUUID()}`;
41459
42201
  const requestedAt = (/* @__PURE__ */ new Date()).toISOString();
41460
42202
  state.run.status = "awaiting-approval";
@@ -41511,7 +42253,7 @@ async function executeRunWorkflow(state, runtimeWf, agents, signal, sessionEvent
41511
42253
  runId: state.run.runId
41512
42254
  }
41513
42255
  });
41514
- resolve45(decision);
42256
+ resolve47(decision);
41515
42257
  };
41516
42258
  if (req.timeoutMs !== void 0) {
41517
42259
  timer = setTimeout(() => {
@@ -41528,7 +42270,7 @@ async function executeRunWorkflow(state, runtimeWf, agents, signal, sessionEvent
41528
42270
  // resumes through `resumeSuspend` (workflow_escalation_resolve's
41529
42271
  // suspend form); after a daemon restart the run stays suspended and
41530
42272
  // the pending entry is re-registered below.
41531
- resume: (req) => new Promise((resolve45) => {
42273
+ resume: (req) => new Promise((resolve47) => {
41532
42274
  const since = (/* @__PURE__ */ new Date()).toISOString();
41533
42275
  state.run.status = "awaiting-input";
41534
42276
  state.run.awaitingSuspend = {
@@ -41566,7 +42308,7 @@ async function executeRunWorkflow(state, runtimeWf, agents, signal, sessionEvent
41566
42308
  });
41567
42309
  eventLog?.append({ stepId: req.stepId, type: "step.resumed", data: {} });
41568
42310
  eventLog?.append({ type: "run.resumed", data: { stepId: req.stepId } });
41569
- resolve45(payload);
42311
+ resolve47(payload);
41570
42312
  };
41571
42313
  signal.addEventListener("abort", onAbort, { once: true });
41572
42314
  state.pendingSuspend = { stepId: req.stepId, resolve: finish };
@@ -41582,7 +42324,7 @@ async function executeRunWorkflow(state, runtimeWf, agents, signal, sessionEvent
41582
42324
  // read of the turn's text). On resume, `execAgentStep` sends the
41583
42325
  // validated payload back to the SAME session as its next prompt and
41584
42326
  // re-applies the outcome rule — this hook only parks and resolves.
41585
- onInputRequired: (req) => new Promise((resolve45) => {
42327
+ onInputRequired: (req) => new Promise((resolve47) => {
41586
42328
  const since = (/* @__PURE__ */ new Date()).toISOString();
41587
42329
  const suspend = {
41588
42330
  reason: "input-required",
@@ -41635,7 +42377,7 @@ async function executeRunWorkflow(state, runtimeWf, agents, signal, sessionEvent
41635
42377
  });
41636
42378
  eventLog?.append({ stepId: req.stepId, type: "step.resumed", data: {} });
41637
42379
  eventLog?.append({ type: "run.resumed", data: { stepId: req.stepId } });
41638
- resolve45(payload);
42380
+ resolve47(payload);
41639
42381
  };
41640
42382
  signal.addEventListener("abort", onAbort, { once: true });
41641
42383
  state.pendingSuspend = { stepId: req.stepId, resolve: finish };
@@ -41671,6 +42413,19 @@ async function executeRunWorkflow(state, runtimeWf, agents, signal, sessionEvent
41671
42413
  }
41672
42414
  }
41673
42415
  },
42416
+ onAgentRetry: (ev) => {
42417
+ eventLog?.append({
42418
+ stepId: ev.stepId,
42419
+ type: "step.retrying",
42420
+ data: {
42421
+ attempt: ev.attempt,
42422
+ maxAttempts: ev.maxAttempts,
42423
+ error: ev.error,
42424
+ delayMs: ev.delayMs,
42425
+ ...ev.sessionId !== void 0 ? { sessionId: ev.sessionId } : {}
42426
+ }
42427
+ });
42428
+ },
41674
42429
  onStepStart: (stepId, info) => {
41675
42430
  const cached = info?.cached === true;
41676
42431
  if (nonLeafStepIds.has(stepId)) return;
@@ -41974,8 +42729,44 @@ function createWorkflowRunner(opts) {
41974
42729
  });
41975
42730
  };
41976
42731
  const runs = shouldPersist ? loadRuns(persistPath, runsRoot) : /* @__PURE__ */ new Map();
42732
+ const persistMinIntervalMs = opts.persistMinIntervalMs ?? DEFAULT_PERSIST_MIN_INTERVAL_MS;
42733
+ let lastFlushAt = 0;
42734
+ let lastStatusSig = "";
42735
+ let trailingFlush;
42736
+ let dirty = false;
42737
+ const flushRuns = () => {
42738
+ if (trailingFlush) {
42739
+ clearTimeout(trailingFlush);
42740
+ trailingFlush = void 0;
42741
+ }
42742
+ dirty = false;
42743
+ lastFlushAt = Date.now();
42744
+ saveRuns(runs, persistPath);
42745
+ };
42746
+ const statusSig = () => {
42747
+ let sig = "";
42748
+ for (const st of runs.values()) sig += `${st.run.runId}:${st.run.status}|`;
42749
+ return sig;
42750
+ };
42751
+ if (shouldPersist && persistMinIntervalMs > 0) {
42752
+ process.once("exit", () => {
42753
+ if (dirty) flushRuns();
42754
+ });
42755
+ }
41977
42756
  const persist = () => {
41978
- if (shouldPersist) saveRuns(runs, persistPath);
42757
+ if (!shouldPersist) return;
42758
+ if (persistMinIntervalMs <= 0) return flushRuns();
42759
+ const sig = statusSig();
42760
+ const wait = lastFlushAt + persistMinIntervalMs - Date.now();
42761
+ if (sig !== lastStatusSig || wait <= 0) {
42762
+ lastStatusSig = sig;
42763
+ return flushRuns();
42764
+ }
42765
+ dirty = true;
42766
+ if (!trailingFlush) {
42767
+ trailingFlush = setTimeout(flushRuns, wait);
42768
+ trailingFlush.unref?.();
42769
+ }
41979
42770
  };
41980
42771
  const newEventLog = (runId) => shouldPersist ? createRunEventLog(runId, runsRoot) : void 0;
41981
42772
  const workspaceRunsRoot = () => shouldPersist ? runsRoot : void 0;
@@ -43628,14 +44419,14 @@ function createSupervisorTaskGateRunner(opts) {
43628
44419
  const { supervisor, registry, workspace } = opts;
43629
44420
  const exec = opts.runCommand ?? runCommand;
43630
44421
  const settleTimeoutMs = opts.settleTimeoutMs ?? DEFAULT_VERIFY_SETTLE_TIMEOUT_MS;
43631
- const waitForSettle = (policyId) => new Promise((resolve45) => {
44422
+ const waitForSettle = (policyId) => new Promise((resolve47) => {
43632
44423
  let settled = false;
43633
44424
  const finish = (outcome) => {
43634
44425
  if (settled) return;
43635
44426
  settled = true;
43636
44427
  clearTimeout(timer);
43637
44428
  unsub();
43638
- resolve45(outcome);
44429
+ resolve47(outcome);
43639
44430
  };
43640
44431
  const read = () => {
43641
44432
  const state = supervisor.getStatus(policyId);
@@ -44643,9 +45434,17 @@ function policyToActivities(policy) {
44643
45434
  return records;
44644
45435
  }
44645
45436
  var STALE_TURN_AFTER_MS = 10 * 6e4;
45437
+ var TURN_NAME_MAX = 60;
45438
+ function sessionDisplayName(session) {
45439
+ const name = session.title?.trim() || session.label?.trim();
45440
+ if (!name) return session.id;
45441
+ const clipped = name.length > TURN_NAME_MAX ? `${name.slice(0, TURN_NAME_MAX - 1).trimEnd()}\u2026` : name;
45442
+ return `"${clipped}"`;
45443
+ }
44646
45444
  function turnToActivities(session, opts = {}) {
44647
45445
  if (session.kind !== "agent-cli") return [];
44648
45446
  const completed = session.turnsCompleted ?? 0;
45447
+ const name = sessionDisplayName(session);
44649
45448
  const base = (turn, title) => ({
44650
45449
  id: `turn:${session.id}:${turn}`,
44651
45450
  kind: "turn",
@@ -44659,20 +45458,20 @@ function turnToActivities(session, opts = {}) {
44659
45458
  if (terminal) {
44660
45459
  if (session.killedMidTurn === true) {
44661
45460
  const rec = record(
44662
- base(completed + 1, `Turn ${completed + 1} interrupted on ${session.id}`),
45461
+ base(completed + 1, `Turn ${completed + 1} interrupted on ${name}`),
44663
45462
  session.status === "error" ? "failed" : "cancelled"
44664
45463
  );
44665
45464
  return [{ ...rec, ...session.lastActivityAt ? { endedAt: session.lastActivityAt } : {} }];
44666
45465
  }
44667
45466
  if (completed > 0) {
44668
- const rec = record(base(completed, `Turn ${completed} completed on ${session.id}`), "done");
45467
+ const rec = record(base(completed, `Turn ${completed} completed on ${name}`), "done");
44669
45468
  return [{ ...rec, ...session.lastActivityAt ? { endedAt: session.lastActivityAt } : {} }];
44670
45469
  }
44671
45470
  return [];
44672
45471
  }
44673
45472
  if (session.awaitingPermission === true) {
44674
45473
  return [
44675
- pendingRecord(base(completed + 1, `Turn ${completed + 1} on ${session.id} held on a permission`), {
45474
+ pendingRecord(base(completed + 1, `Turn ${completed + 1} on ${name} held on a permission`), {
44676
45475
  kind: "human-ack",
44677
45476
  refs: [session.id],
44678
45477
  detail: "permission request parked for permissions_respond"
@@ -44680,7 +45479,7 @@ function turnToActivities(session, opts = {}) {
44680
45479
  ];
44681
45480
  }
44682
45481
  if (session.busy === true) {
44683
- const rec = record(base(completed + 1, `Turn ${completed + 1} running on ${session.id}`), "active");
45482
+ const rec = record(base(completed + 1, `Turn ${completed + 1} running on ${name}`), "active");
44684
45483
  if (opts.now && session.lastActivityAt) {
44685
45484
  const nowMs = Date.parse(opts.now);
44686
45485
  const lastMs = Date.parse(session.lastActivityAt);
@@ -44692,7 +45491,7 @@ function turnToActivities(session, opts = {}) {
44692
45491
  }
44693
45492
  if (session.awaitingInput === true) {
44694
45493
  return [
44695
- pendingRecord(base(completed + 1, `Awaiting human input on ${session.id}`), {
45494
+ pendingRecord(base(completed + 1, `Awaiting human input on ${name}`), {
44696
45495
  kind: "human-ack",
44697
45496
  refs: [session.id],
44698
45497
  detail: "session asked a question; next turn needs a human prompt"
@@ -44700,7 +45499,7 @@ function turnToActivities(session, opts = {}) {
44700
45499
  ];
44701
45500
  }
44702
45501
  if (completed > 0) {
44703
- const rec = record(base(completed, `Turn ${completed} completed on ${session.id}`), "done");
45502
+ const rec = record(base(completed, `Turn ${completed} completed on ${name}`), "done");
44704
45503
  return [{ ...rec, ...session.lastActivityAt ? { endedAt: session.lastActivityAt } : {} }];
44705
45504
  }
44706
45505
  return [];
@@ -44982,7 +45781,7 @@ async function monitorSessionWait(opts) {
44982
45781
  };
44983
45782
  }
44984
45783
  }
44985
- return new Promise((resolve45) => {
45784
+ return new Promise((resolve47) => {
44986
45785
  const unsubs = [];
44987
45786
  let settled = false;
44988
45787
  const watcherLabel = callerScope?.ownerSessionId ? registry.get(callerScope.ownerSessionId)?.label ?? registry.get(callerScope.ownerSessionId)?.title : void 0;
@@ -45022,7 +45821,7 @@ async function monitorSessionWait(opts) {
45022
45821
  ts: (/* @__PURE__ */ new Date()).toISOString()
45023
45822
  });
45024
45823
  }
45025
- resolve45(result);
45824
+ resolve47(result);
45026
45825
  };
45027
45826
  const relevantTypes = targetEvent === "any" ? ["session:turn-end", "session:awaiting-input", "session:exited"] : targetEvent === "turn-end" ? ["session:turn-end", "session:awaiting-input"] : targetEvent === "awaiting-input" ? ["session:awaiting-input"] : ["session:exited"];
45028
45827
  const idSet = new Set(resolvedIds);
@@ -45063,13 +45862,13 @@ async function monitorPolicyWait(opts) {
45063
45862
  if (isSettledStatus(initial.status)) {
45064
45863
  return { timedOut: false, state: initial };
45065
45864
  }
45066
- return new Promise((resolve45) => {
45865
+ return new Promise((resolve47) => {
45067
45866
  let settled = false;
45068
45867
  const timer = setTimeout(() => {
45069
45868
  if (settled) return;
45070
45869
  settled = true;
45071
45870
  unsub();
45072
- resolve45({ timedOut: true });
45871
+ resolve47({ timedOut: true });
45073
45872
  }, timeoutMs);
45074
45873
  const unsub = supervisor.onSettle((id) => {
45075
45874
  if (id !== policyId) return;
@@ -45079,7 +45878,7 @@ async function monitorPolicyWait(opts) {
45079
45878
  settled = true;
45080
45879
  clearTimeout(timer);
45081
45880
  unsub();
45082
- resolve45({ timedOut: false, state });
45881
+ resolve47({ timedOut: false, state });
45083
45882
  });
45084
45883
  });
45085
45884
  }
@@ -46944,16 +47743,34 @@ var FIRST_PARTY_CATALOG_ENTRIES = [
46944
47743
  "description": "View and continue any agentproto daemon session's conversation in the Agentik chat UI (assistant-ui based). Pick a session from the live/recent list or deep-link with ?session=<id>; the page replays the session's full transcript, then follows it live over SSE, and lets you send follow-up prompts into the same session. Built on @agstudio/react-agentproto (SSE transcript projection, permission approval, thread list) and @agstudio/chat-starter's assistant-ui shell, reused almost as-is.",
46945
47744
  "source": {
46946
47745
  "kind": "agentapp",
46947
- "url": "https://github.com/agentproto/apps/releases/download/session-chat%400.1.0/session-chat-0.1.0.agentapp",
46948
- "sha256": "12216f65fa3cff9bfb9fcd49c46c9166eb68a790d278d466e07e0a95df5f4eef",
46949
- "version": "0.1.0",
46950
- "size": 518621
47746
+ "url": "https://github.com/agentproto/apps/releases/download/session-chat%400.1.1/session-chat-0.1.1.agentapp",
47747
+ "sha256": "e8084a125e335cbc408b0e8dd3371711191f6aa0019dad4907f26b19ec9f344b",
47748
+ "version": "0.1.1",
47749
+ "size": 516437
46951
47750
  },
46952
- "version": "0.1.0",
47751
+ "icon": "https://raw.githubusercontent.com/agentproto/apps/main/media/@agentik/session-chat/0.1.1/icon.svg",
47752
+ "version": "0.1.1",
46953
47753
  "tier": "bundle",
47754
+ "publisher": "Agentik",
46954
47755
  "license": {
46955
47756
  "kind": "free"
46956
- }
47757
+ },
47758
+ "tagline": "Read and continue any agentproto session as a chat thread.",
47759
+ "longDescription": "Session Chat turns any agentproto session into a chat thread you can read and continue.\n\nPick a running or recent session, or open one directly with `?session=<id>`. The page replays the full transcript, then follows the session live as the agent works, and lets you send follow-up prompts into the same session.\n\n## What you get\n\n- **Transcript replay and live follow**: the whole conversation, then new turns as they stream in.\n- **Tool activity at a glance**: file reads, commands and edits grouped under each reply, with the files an agent changed.\n- **Reply into the session**: send a follow-up, interrupt a running turn, queue prompts while the agent works.\n- **Approvals**: answer the agent's permission requests from the thread.\n- **Session list**: running and recent sessions, grouped by workspace, with search and pins.\n- **Start a session**: pick a harness and model and start a new chat from the same page.\n\n## Requirements\n\nSession Chat talks to your local agentproto daemon. Its bundled agent runs on the `mastra-agent` adapter: install it first with `agentproto install mastra-agent`.",
47760
+ "screenshots": [
47761
+ {
47762
+ "url": "https://raw.githubusercontent.com/agentproto/apps/main/media/@agentik/session-chat/0.1.1/conversation.png",
47763
+ "alt": "A finished session replayed as a chat thread, with a markdown table in the agent's reply and the composer below",
47764
+ "width": 1280,
47765
+ "height": 800
47766
+ }
47767
+ ],
47768
+ "categories": [
47769
+ "chat",
47770
+ "sessions",
47771
+ "developer-tools"
47772
+ ],
47773
+ "homepage": "https://agentproto.sh/apps/session-chat"
46957
47774
  }
46958
47775
  ];
46959
47776
 
@@ -46994,7 +47811,7 @@ async function waitForSessionTerminal(registry, sessionId) {
46994
47811
  for (let attempt = 0; attempt < MAX_SEQUENTIAL_POLLS; attempt++) {
46995
47812
  const status = registry.get(sessionId)?.status;
46996
47813
  if (isSessionTerminal(status)) return;
46997
- await new Promise((resolve45) => setTimeout(resolve45, SEQUENTIAL_POLL_INTERVAL_MS));
47814
+ await new Promise((resolve47) => setTimeout(resolve47, SEQUENTIAL_POLL_INTERVAL_MS));
46998
47815
  }
46999
47816
  }
47000
47817
  var MODEL_ROUTED_ADAPTER = "claude-code";
@@ -47160,7 +47977,7 @@ function buildAppInstallPreview(args) {
47160
47977
  runsBuildCommand: !isAgentapp && args.allowBuild === true
47161
47978
  };
47162
47979
  }
47163
- async function dispatchAllowlistedAppTool(declaredAllowlist, input, deps2) {
47980
+ async function dispatchAllowlistedAppTool(declaredAllowlist, input, deps2, appDir) {
47164
47981
  const effectiveAllowlist = [...declaredAllowlist, ...APP_UI_DISCOVERY_TOOLS];
47165
47982
  if (!effectiveAllowlist.includes(input.tool)) {
47166
47983
  return errorResult2(
@@ -47168,8 +47985,14 @@ async function dispatchAllowlistedAppTool(declaredAllowlist, input, deps2) {
47168
47985
  );
47169
47986
  }
47170
47987
  const args = input.args ?? {};
47171
- if (input.tool === "app_install" && appInstallConfirmationClass(args)?.confirmed === false) {
47172
- return textResult2(buildAppInstallPreview(args));
47988
+ let dispatchArgs = args;
47989
+ if (input.tool === "app_install") {
47990
+ const cls = appInstallConfirmationClass(args);
47991
+ if (cls?.confirmed === false) return textResult2(buildAppInstallPreview(args));
47992
+ if (cls?.confirmed === true) {
47993
+ const { confirm: _confirmNonce, ...rest } = args;
47994
+ dispatchArgs = rest;
47995
+ }
47173
47996
  }
47174
47997
  try {
47175
47998
  if (input.tool.startsWith("imported:")) {
@@ -47181,11 +48004,19 @@ async function dispatchAllowlistedAppTool(declaredAllowlist, input, deps2) {
47181
48004
  `app_tool_call: malformed imported tool id "${input.tool}" \u2014 expected "imported:<alias>/<toolName>".`
47182
48005
  );
47183
48006
  }
47184
- const result2 = await deps2.callImportedTool(rest.slice(0, slash), rest.slice(slash + 1), args);
48007
+ const result2 = await deps2.callImportedTool(rest.slice(0, slash), rest.slice(slash + 1), dispatchArgs);
47185
48008
  return textResult2(result2);
47186
48009
  }
48010
+ if (appDir !== void 0) {
48011
+ const bundled = await loadAppBundledTools(appDir);
48012
+ const bundledTool = bundled.tools.find((t) => t.id === input.tool);
48013
+ if (bundledTool) {
48014
+ const output = await runTool({ tool: bundledTool, candidates: bundled.drivers, input: args });
48015
+ return textResult2(output);
48016
+ }
48017
+ }
47187
48018
  if (!deps2.dispatchTool) return notEnabled("app_tool_call");
47188
- const result = await deps2.dispatchTool(input.tool, args);
48019
+ const result = await deps2.dispatchTool(input.tool, dispatchArgs);
47189
48020
  return textResult2(result);
47190
48021
  } catch (err) {
47191
48022
  return errorResult2(`app_tool_call: ${err instanceof Error ? err.message : String(err)}`);
@@ -47196,7 +48027,7 @@ async function performAppToolCall(appRegistry, input, deps2) {
47196
48027
  if (!installed || !installed.ui) {
47197
48028
  return errorResult2(`app_tool_call: app "${input.appId}" is not installed or has no UI.`);
47198
48029
  }
47199
- return dispatchAllowlistedAppTool(installed.ui.tools ?? [], input, deps2);
48030
+ return dispatchAllowlistedAppTool(installed.ui.tools ?? [], input, deps2, installed.dir);
47200
48031
  }
47201
48032
  async function performBuiltinPanelToolCall(tools, input, deps2) {
47202
48033
  if (tools === void 0) {
@@ -47323,7 +48154,7 @@ async function performAppCall(appRegistry, input, deps2) {
47323
48154
  error: `app_call: workflow "${input.workflow}" on "${input.appId}" did not finish within ${timeoutMs}ms (run ${run.runId} still ${status?.status ?? "unknown"}).`
47324
48155
  };
47325
48156
  }
47326
- await new Promise((resolve45) => setTimeout(resolve45, APP_CALL_POLL_INTERVAL_MS));
48157
+ await new Promise((resolve47) => setTimeout(resolve47, APP_CALL_POLL_INTERVAL_MS));
47327
48158
  }
47328
48159
  }
47329
48160
  function refIdOf(ref) {
@@ -48709,12 +49540,12 @@ async function readBody2(req) {
48709
49540
  }
48710
49541
  return { raw: Buffer.concat(chunks).toString("utf8") };
48711
49542
  }
48712
- var isRecord9 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
49543
+ var isRecord11 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
48713
49544
  function parseParts(raw) {
48714
49545
  if (!Array.isArray(raw)) throw new RpcFailure(JSON_RPC_ERROR.invalidParams, "message.parts must be an array");
48715
49546
  const parts = [];
48716
49547
  for (const p of raw) {
48717
- if (!isRecord9(p)) throw new RpcFailure(JSON_RPC_ERROR.invalidParams, "each message part must be an object");
49548
+ if (!isRecord11(p)) throw new RpcFailure(JSON_RPC_ERROR.invalidParams, "each message part must be an object");
48718
49549
  if ("text" in p) {
48719
49550
  if (typeof p.text !== "string") throw new RpcFailure(JSON_RPC_ERROR.invalidParams, "part.text must be a string");
48720
49551
  parts.push({ text: p.text });
@@ -48747,13 +49578,13 @@ async function handleA2aTaskRequest(req, res, path, deps2) {
48747
49578
  send(res, 200, { jsonrpc: "2.0", id: null, error: { code: JSON_RPC_ERROR.parse, message: "parse error: body is not valid JSON" } });
48748
49579
  return true;
48749
49580
  }
48750
- if (!isRecord9(rpc) || rpc.jsonrpc !== "2.0" || typeof rpc.method !== "string") {
49581
+ if (!isRecord11(rpc) || rpc.jsonrpc !== "2.0" || typeof rpc.method !== "string") {
48751
49582
  send(res, 200, { jsonrpc: "2.0", id: null, error: { code: JSON_RPC_ERROR.invalidRequest, message: 'invalid request: expected a JSON-RPC 2.0 object with a string "method" (batches are not supported)' } });
48752
49583
  return true;
48753
49584
  }
48754
49585
  const id = typeof rpc.id === "string" || typeof rpc.id === "number" ? rpc.id : null;
48755
49586
  const method = rpc.method;
48756
- const params = isRecord9(rpc.params) ? rpc.params : {};
49587
+ const params = isRecord11(rpc.params) ? rpc.params : {};
48757
49588
  const requested = negotiateVersion(
48758
49589
  typeof req.headers["a2a-version"] === "string" ? req.headers["a2a-version"] : void 0,
48759
49590
  new URL(req.url ?? "/", "http://localhost").searchParams.get("version")
@@ -48811,14 +49642,14 @@ async function handleA2aTaskRequest(req, res, path, deps2) {
48811
49642
  throw new RpcFailure(JSON_RPC_ERROR.unsupportedOperation, `method "SendMessage" is not allowed for app "${appId}": its manifest does not declare accepts.tasks: true`);
48812
49643
  }
48813
49644
  const rawMessage = params.message;
48814
- if (!isRecord9(rawMessage) || rawMessage.role !== "ROLE_USER") {
49645
+ if (!isRecord11(rawMessage) || rawMessage.role !== "ROLE_USER") {
48815
49646
  throw new RpcFailure(JSON_RPC_ERROR.invalidParams, 'params.message must be an object with role "ROLE_USER"');
48816
49647
  }
48817
49648
  const parts = parseParts(rawMessage.parts);
48818
49649
  const { skill, prompt } = extractSkillRequest(
48819
- { parts, ...isRecord9(rawMessage.metadata) ? { metadata: rawMessage.metadata } : {} },
49650
+ { parts, ...isRecord11(rawMessage.metadata) ? { metadata: rawMessage.metadata } : {} },
48820
49651
  appId,
48821
- isRecord9(params.metadata) ? params.metadata : void 0
49652
+ isRecord11(params.metadata) ? params.metadata : void 0
48822
49653
  );
48823
49654
  if (!skill) {
48824
49655
  throw new RpcFailure(JSON_RPC_ERROR.invalidParams, 'no skill requested: set message.metadata.skill or start the first text part with "skill:<id>"');
@@ -49828,6 +50659,7 @@ async function startHttpServer(opts) {
49828
50659
  const auth = readAuth();
49829
50660
  if (auth.mode !== "bearer") return true;
49830
50661
  if (path === "/health") return true;
50662
+ if (opts.eventsMcp && path.startsWith("/mcp/events/")) return true;
49831
50663
  if (/^\/inbound\/[^/]+$/.test(path)) return true;
49832
50664
  if (isAppUiShellRequest(method, path)) return true;
49833
50665
  const header = req.headers.authorization;
@@ -50094,6 +50926,47 @@ async function startHttpServer(opts) {
50094
50926
  const server2 = await opts.orchestratorMcpServerFactory(scope);
50095
50927
  await serveMcp(req, res, server2);
50096
50928
  }
50929
+ const EVENTS_BODY_CAP = 1024 * 1024;
50930
+ async function handleEventsMcp(req, res, presented) {
50931
+ const cfg = opts.eventsMcp;
50932
+ const send2 = (status, headers, body) => {
50933
+ res.writeHead(status, { "cache-control": "no-store", ...headers });
50934
+ res.end(body);
50935
+ };
50936
+ if (!cfg) return send2(404, { "content-type": "application/json" }, JSON.stringify({ error: "not_found" }));
50937
+ const want = Buffer.from(cfg.secret);
50938
+ const got = Buffer.from(presented);
50939
+ if (got.length !== want.length || !timingSafeEqual(got, want)) {
50940
+ return send2(404, { "content-type": "application/json" }, JSON.stringify({ error: "not_found" }));
50941
+ }
50942
+ const origin = req.headers.origin;
50943
+ if (typeof origin === "string" && !(cfg.allowedOrigins ?? []).includes(origin)) {
50944
+ return send2(403, { "content-type": "application/json" }, JSON.stringify({ error: "origin_not_allowed" }));
50945
+ }
50946
+ const chunks = [];
50947
+ let size = 0;
50948
+ for await (const chunk of req) {
50949
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
50950
+ size += buf.length;
50951
+ if (size > EVENTS_BODY_CAP) {
50952
+ return send2(413, { "content-type": "application/json" }, JSON.stringify({ error: "body_too_large" }));
50953
+ }
50954
+ chunks.push(buf);
50955
+ }
50956
+ const controller = new AbortController();
50957
+ res.on("close", () => {
50958
+ if (!res.writableEnded) controller.abort();
50959
+ });
50960
+ const out = await handleModernRequest(
50961
+ {
50962
+ method: req.method ?? "GET",
50963
+ headers: req.headers,
50964
+ body: Buffer.concat(chunks).toString("utf8")
50965
+ },
50966
+ { createServer: cfg.createServer, signal: controller.signal }
50967
+ );
50968
+ send2(out.status, out.headers, out.body);
50969
+ }
50097
50970
  async function handleImportedMcp(req, res, importId) {
50098
50971
  if (!authorizeMcp(req, res)) return;
50099
50972
  if (!opts.mcpProxy) {
@@ -50360,7 +51233,7 @@ async function startHttpServer(opts) {
50360
51233
  });
50361
51234
  }
50362
51235
  applyCors(req, res);
50363
- if (req.method === "OPTIONS") {
51236
+ if (req.method === "OPTIONS" && !(opts.eventsMcp && path.startsWith("/mcp/events/"))) {
50364
51237
  res.writeHead(204);
50365
51238
  res.end();
50366
51239
  return;
@@ -50387,6 +51260,21 @@ async function startHttpServer(opts) {
50387
51260
  handleEvents(req, res);
50388
51261
  return;
50389
51262
  }
51263
+ if (path.startsWith("/mcp/events/")) {
51264
+ let presented;
51265
+ try {
51266
+ presented = decodeURIComponent(path.slice("/mcp/events/".length));
51267
+ } catch (error) {
51268
+ if (!(error instanceof URIError)) throw error;
51269
+ }
51270
+ if (presented === void 0) {
51271
+ res.writeHead(404, { "content-type": "application/json", "cache-control": "no-store" });
51272
+ res.end(JSON.stringify({ error: "not_found" }));
51273
+ return;
51274
+ }
51275
+ await handleEventsMcp(req, res, presented);
51276
+ return;
51277
+ }
50390
51278
  if (path === "/mcp/orchestrator") {
50391
51279
  await handleOrchestratorMcp(req, res);
50392
51280
  return;
@@ -50994,15 +51882,17 @@ async function startHttpServer(opts) {
50994
51882
  try {
50995
51883
  const base = str3("base");
50996
51884
  const anchor = str3("anchor");
50997
- const result = await opts.runBranchGc({
50998
- repoRoot: resolved.repoRoot,
50999
- apply,
51000
- includeReviewed: bool3("includeReviewed"),
51001
- ...base ? { base } : {},
51002
- ...scopes?.length ? { scopes } : {},
51003
- ...minAgeDays !== void 0 && Number.isFinite(minAgeDays) ? { minAgeDays } : {},
51004
- ...anchor ? { anchor } : {}
51005
- });
51885
+ const result = withBranchGcApplySummary(
51886
+ await opts.runBranchGc({
51887
+ repoRoot: resolved.repoRoot,
51888
+ apply,
51889
+ includeReviewed: bool3("includeReviewed"),
51890
+ ...base ? { base } : {},
51891
+ ...scopes?.length ? { scopes } : {},
51892
+ ...minAgeDays !== void 0 && Number.isFinite(minAgeDays) ? { minAgeDays } : {},
51893
+ ...anchor ? { anchor } : {}
51894
+ })
51895
+ );
51006
51896
  res.writeHead(200, { "content-type": "application/json" });
51007
51897
  res.end(JSON.stringify(result));
51008
51898
  } catch (err) {
@@ -52074,16 +52964,16 @@ async function startHttpServer(opts) {
52074
52964
  });
52075
52965
  });
52076
52966
  const bind = opts.bind ?? "127.0.0.1";
52077
- await new Promise((resolve45, reject) => {
52967
+ await new Promise((resolve47, reject) => {
52078
52968
  server.once("error", reject);
52079
- server.listen(opts.port, bind, () => resolve45());
52969
+ server.listen(opts.port, bind, () => resolve47());
52080
52970
  });
52081
52971
  return {
52082
52972
  url: `http://${bind}:${opts.port}`,
52083
52973
  async stop() {
52084
52974
  wss.close();
52085
52975
  server.closeAllConnections();
52086
- await new Promise((resolve45) => server.close(() => resolve45()));
52976
+ await new Promise((resolve47) => server.close(() => resolve47()));
52087
52977
  }
52088
52978
  };
52089
52979
  }
@@ -52292,9 +53182,9 @@ async function currentTranscriptSeq(id) {
52292
53182
  let stream;
52293
53183
  try {
52294
53184
  stream = createReadStream(filePath, { encoding: "utf8" });
52295
- await new Promise((resolve45, reject) => {
53185
+ await new Promise((resolve47, reject) => {
52296
53186
  stream.once("error", reject);
52297
- stream.once("open", resolve45);
53187
+ stream.once("open", resolve47);
52298
53188
  });
52299
53189
  } catch (err) {
52300
53190
  if (err.code === "ENOENT") return 0;
@@ -52319,9 +53209,9 @@ async function* transcriptDiskRecords(id) {
52319
53209
  let stream;
52320
53210
  try {
52321
53211
  stream = createReadStream(filePath, { encoding: "utf8" });
52322
- await new Promise((resolve45, reject) => {
53212
+ await new Promise((resolve47, reject) => {
52323
53213
  stream.once("error", reject);
52324
- stream.once("open", resolve45);
53214
+ stream.once("open", resolve47);
52325
53215
  });
52326
53216
  } catch (err) {
52327
53217
  if (err.code === "ENOENT") return;
@@ -52823,8 +53713,45 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
52823
53713
  if (kindParam && kindParam !== "all") return s.kind === kindParam;
52824
53714
  return includeCommands || s.kind !== "command";
52825
53715
  };
53716
+ const listFilters = parseSessionListFilterParams(params);
53717
+ if (listFilters.parentSessionId) {
53718
+ listFilters.parentSessionId = registry.findByIdOrName(listFilters.parentSessionId)?.id ?? listFilters.parentSessionId;
53719
+ }
53720
+ const statusParam = params.get("status");
53721
+ const onlyAlive = params.get("onlyAlive") === "true";
53722
+ const limitParam = params.get("limit");
53723
+ const limit = limitParam === null ? void 0 : Number.parseInt(limitParam, 10);
53724
+ if (limit !== void 0 && (!Number.isInteger(limit) || limit < 1)) {
53725
+ json3(400, {
53726
+ error: "invalid_limit",
53727
+ message: `?limit must be a positive integer, got ${JSON.stringify(limitParam)}.`
53728
+ });
53729
+ return true;
53730
+ }
53731
+ const narrowed = hasSessionListFilters(listFilters) || statusParam !== null || onlyAlive || limit !== void 0;
53732
+ let narrow;
53733
+ try {
53734
+ const keep = compileSessionListFilters(listFilters);
53735
+ narrow = (list) => list.filter((s) => {
53736
+ if (statusParam) {
53737
+ if (s.status !== statusParam) return false;
53738
+ } else if (onlyAlive && s.status !== "running" && s.status !== "starting") {
53739
+ return false;
53740
+ }
53741
+ return keep(s);
53742
+ });
53743
+ } catch (err) {
53744
+ if (err instanceof SessionListFilterError) {
53745
+ json3(400, { error: "invalid_filter", message: err.message });
53746
+ return true;
53747
+ }
53748
+ throw err;
53749
+ }
52826
53750
  let rows = registry.list({ includeArchived });
52827
- rows = rows.filter(matchesFilter);
53751
+ rows = narrow(rows.filter(matchesFilter));
53752
+ if (narrowed) rows = sortNewestActivityFirst(rows);
53753
+ const total = rows.length;
53754
+ if (limit !== void 0 && sinceParam === null) rows = rows.slice(0, limit);
52828
53755
  let body;
52829
53756
  if (sinceParam !== null) {
52830
53757
  const sinceMs = Date.parse(sinceParam);
@@ -52840,11 +53767,14 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
52840
53767
  return Number.isNaN(ts) || ts >= sinceMs;
52841
53768
  });
52842
53769
  const removed = includeArchived ? [] : registry.list({ includeArchived: true }).filter(
52843
- (s) => s.archived && matchesFilter(s) && (s.archivedAt === void 0 || Date.parse(s.archivedAt) >= sinceMs)
53770
+ (s) => s.archived && matchesFilter(s) && narrow([s]).length > 0 && (s.archivedAt === void 0 || Date.parse(s.archivedAt) >= sinceMs)
52844
53771
  ).map((s) => s.id);
52845
53772
  body = { sessions: changed.map((s) => projectSessionForHttp(s, fields)), removed };
52846
53773
  } else {
52847
- body = { sessions: rows.map((s) => projectSessionForHttp(s, fields)) };
53774
+ body = {
53775
+ sessions: rows.map((s) => projectSessionForHttp(s, fields)),
53776
+ ...narrowed ? { total } : {}
53777
+ };
52848
53778
  }
52849
53779
  const serialized = JSON.stringify(body);
52850
53780
  const etag = strongEtag(serialized);
@@ -54077,9 +55007,9 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
54077
55007
  let fileStream;
54078
55008
  try {
54079
55009
  fileStream = createReadStream(filePath, { encoding: "utf8" });
54080
- await new Promise((resolve45, reject) => {
55010
+ await new Promise((resolve47, reject) => {
54081
55011
  fileStream.once("error", reject);
54082
- fileStream.once("open", resolve45);
55012
+ fileStream.once("open", resolve47);
54083
55013
  });
54084
55014
  } catch (err) {
54085
55015
  const code = err.code;
@@ -54135,9 +55065,9 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
54135
55065
  let fileStream;
54136
55066
  try {
54137
55067
  fileStream = createReadStream(filePath, { encoding: "utf8" });
54138
- await new Promise((resolve45, reject) => {
55068
+ await new Promise((resolve47, reject) => {
54139
55069
  fileStream.once("error", reject);
54140
- fileStream.once("open", resolve45);
55070
+ fileStream.once("open", resolve47);
54141
55071
  });
54142
55072
  } catch (err) {
54143
55073
  const code = err.code;
@@ -54527,7 +55457,7 @@ async function handleSentinels(req, res, path, deps2) {
54527
55457
  res.end(JSON.stringify(body));
54528
55458
  };
54529
55459
  if (path === "/sentinels" && req.method === "GET") {
54530
- json3(200, { sentinels: deps2.store.list().map(sentinelView) });
55460
+ json3(200, { sentinels: deps2.store.list().map((s) => sentinelViewWithDelivery(s, deps2.deliveryStatus?.(s.id))) });
54531
55461
  return true;
54532
55462
  }
54533
55463
  if (path === "/sentinels" && req.method === "POST") {
@@ -54581,7 +55511,10 @@ async function handleSentinels(req, res, path, deps2) {
54581
55511
  return true;
54582
55512
  }
54583
55513
  if (req.method === "DELETE") {
54584
- const removed = await cancelSentinelWatch({ store: deps2.store, resolveProvider: deps2.resolveProvider }, id);
55514
+ const removed = await cancelSentinelWatch(
55515
+ { store: deps2.store, resolveProvider: deps2.resolveProvider, ...deps2.tombstones ? { tombstones: deps2.tombstones } : {} },
55516
+ id
55517
+ );
54585
55518
  if (!removed) {
54586
55519
  json3(404, { error: "sentinel_not_found", id });
54587
55520
  return true;
@@ -56891,7 +57824,7 @@ function createTransmitterBindingStore(opts) {
56891
57824
  const nowMs = Date.now;
56892
57825
  const debounceMs = PERSIST_DEBOUNCE_MS6;
56893
57826
  let persistTimer = null;
56894
- const load = () => {
57827
+ const load2 = () => {
56895
57828
  const out = /* @__PURE__ */ new Map();
56896
57829
  let raw;
56897
57830
  try {
@@ -56916,7 +57849,7 @@ function createTransmitterBindingStore(opts) {
56916
57849
  }
56917
57850
  return out;
56918
57851
  };
56919
- const bindings = load();
57852
+ const bindings = load2();
56920
57853
  const snapshot = () => {
56921
57854
  const out = {};
56922
57855
  for (const [key, binding] of bindings.entries()) out[key] = binding;
@@ -56976,7 +57909,7 @@ function createInboundEndpointStore(opts) {
56976
57909
  const debounceMs = opts?.debounceMs ?? PERSIST_DEBOUNCE_MS7;
56977
57910
  const persist = opts?.persist ?? opts?.filePath !== void 0;
56978
57911
  let persistTimer = null;
56979
- const load = () => {
57912
+ const load2 = () => {
56980
57913
  const out = /* @__PURE__ */ new Map();
56981
57914
  if (!persist) return out;
56982
57915
  let raw;
@@ -57002,7 +57935,7 @@ function createInboundEndpointStore(opts) {
57002
57935
  }
57003
57936
  return out;
57004
57937
  };
57005
- const endpoints = load();
57938
+ const endpoints = load2();
57006
57939
  const seen = /* @__PURE__ */ new Map();
57007
57940
  let totalSeen = 0;
57008
57941
  const snapshot = () => {
@@ -57165,6 +58098,299 @@ init_session_message();
57165
58098
  init_sessions();
57166
58099
  init_session_end_reason();
57167
58100
  init_session_retirement();
58101
+ var tmpSeq6 = 0;
58102
+ var tmpPathFor2 = (filePath) => `${filePath}.tmp.${process.pid}.${++tmpSeq6}`;
58103
+ function isDirSyncUnsupported(err) {
58104
+ const code = err.code;
58105
+ return code === "EINVAL" || code === "EPERM" || code === "EISDIR" || code === "ENOTSUP";
58106
+ }
58107
+ async function writeFileDurable(filePath, data, opts = {}) {
58108
+ const mode = opts.mode ?? 384;
58109
+ const dir = dirname(filePath);
58110
+ await promises.mkdir(dir, { recursive: true });
58111
+ const tmp = tmpPathFor2(filePath);
58112
+ try {
58113
+ const fh = await promises.open(tmp, "w", mode);
58114
+ try {
58115
+ await fh.writeFile(data);
58116
+ await fh.chmod(mode);
58117
+ await fh.sync();
58118
+ } finally {
58119
+ await fh.close();
58120
+ }
58121
+ if (opts.commitIf && !opts.commitIf()) {
58122
+ await promises.unlink(tmp).catch(() => void 0);
58123
+ return;
58124
+ }
58125
+ await promises.rename(tmp, filePath);
58126
+ } catch (err) {
58127
+ await promises.unlink(tmp).catch(() => void 0);
58128
+ throw err;
58129
+ }
58130
+ try {
58131
+ const dh = await promises.open(dir, "r");
58132
+ try {
58133
+ await dh.sync();
58134
+ } finally {
58135
+ await dh.close();
58136
+ }
58137
+ } catch (err) {
58138
+ if (!isDirSyncUnsupported(err)) throw err;
58139
+ }
58140
+ }
58141
+ function writeFileDurableSync(filePath, data, mode = 384) {
58142
+ const dir = dirname(filePath);
58143
+ mkdirSync(dir, { recursive: true });
58144
+ const tmp = tmpPathFor2(filePath);
58145
+ try {
58146
+ const fd = openSync(tmp, "w", mode);
58147
+ try {
58148
+ writeSync(fd, data);
58149
+ fsyncSync(fd);
58150
+ } finally {
58151
+ closeSync(fd);
58152
+ }
58153
+ renameSync(tmp, filePath);
58154
+ } catch (err) {
58155
+ try {
58156
+ unlinkSync(tmp);
58157
+ } catch {
58158
+ }
58159
+ throw err;
58160
+ }
58161
+ try {
58162
+ const dfd = openSync(dir, "r");
58163
+ try {
58164
+ fsyncSync(dfd);
58165
+ } finally {
58166
+ closeSync(dfd);
58167
+ }
58168
+ } catch (err) {
58169
+ if (!isDirSyncUnsupported(err)) throw err;
58170
+ }
58171
+ }
58172
+ function appendLineDurableSync(filePath, line, mode = 384) {
58173
+ mkdirSync(dirname(filePath), { recursive: true });
58174
+ const existed = (() => {
58175
+ try {
58176
+ closeSync(openSync(filePath, "r"));
58177
+ return true;
58178
+ } catch {
58179
+ return false;
58180
+ }
58181
+ })();
58182
+ const fd = openSync(filePath, "a", mode);
58183
+ try {
58184
+ appendFileSync(fd, line.endsWith("\n") ? line : `${line}
58185
+ `);
58186
+ fsyncSync(fd);
58187
+ } finally {
58188
+ closeSync(fd);
58189
+ }
58190
+ if (!existed) {
58191
+ try {
58192
+ const dfd = openSync(dirname(filePath), "r");
58193
+ try {
58194
+ fsyncSync(dfd);
58195
+ } finally {
58196
+ closeSync(dfd);
58197
+ }
58198
+ } catch (err) {
58199
+ if (!isDirSyncUnsupported(err)) throw err;
58200
+ }
58201
+ }
58202
+ }
58203
+
58204
+ // src/sentinel-cancel-tombstones.ts
58205
+ function defaultCancelTombstonePath() {
58206
+ return resolve(process.env.AGENTPROTO_HOME ?? join(homedir(), ".agentproto"), "sentinel-cancel-tombstones.json");
58207
+ }
58208
+ var SECRETISH = /secret|token|key|password|credential|authorization/i;
58209
+ function stripSecrets(handle) {
58210
+ const state = {};
58211
+ for (const [k, v] of Object.entries(handle.state ?? {})) if (!SECRETISH.test(k)) state[k] = v;
58212
+ return {
58213
+ provider: handle.provider,
58214
+ ...handle.remoteId !== void 0 ? { remoteId: handle.remoteId } : {},
58215
+ ...handle.cursor !== void 0 ? { cursor: handle.cursor } : {},
58216
+ ...Object.keys(state).length > 0 ? { state } : {}
58217
+ };
58218
+ }
58219
+ var keyOf2 = (provider, remoteId) => `${provider}|${remoteId}`;
58220
+ function load(filePath) {
58221
+ try {
58222
+ const parsed = JSON.parse(readFileSync(filePath, "utf8"));
58223
+ if (!Array.isArray(parsed.tombstones)) return [];
58224
+ return parsed.tombstones.filter(
58225
+ (t) => typeof t === "object" && t !== null && typeof t.key === "string"
58226
+ );
58227
+ } catch {
58228
+ return [];
58229
+ }
58230
+ }
58231
+ function createCancelTombstoneStore(opts) {
58232
+ const nowMs = opts.nowMs ?? Date.now;
58233
+ const log = opts.log ?? ((line) => console.warn(line));
58234
+ const base = opts.baseBackoffMs ?? 3e4;
58235
+ const max = opts.maxBackoffMs ?? 60 * 60 * 1e3;
58236
+ const rows = /* @__PURE__ */ new Map();
58237
+ if (opts.filePath) for (const t of load(opts.filePath)) rows.set(t.key, t);
58238
+ let writeTail = Promise.resolve();
58239
+ function persist() {
58240
+ if (!opts.filePath) return Promise.resolve();
58241
+ const filePath = opts.filePath;
58242
+ const run = () => writeFileDurable(filePath, JSON.stringify({ version: 1, tombstones: [...rows.values()] }, null, 2));
58243
+ const next = writeTail.then(run, run);
58244
+ writeTail = next.catch(() => void 0);
58245
+ return next;
58246
+ }
58247
+ const inFlight2 = /* @__PURE__ */ new Map();
58248
+ async function attempt(t, explicit = false) {
58249
+ const running = inFlight2.get(t.key);
58250
+ if (running) return running;
58251
+ const p = (async () => {
58252
+ if (!explicit && opts.isRemoteInUse?.(t.provider, t.remoteId)) {
58253
+ rows.delete(t.key);
58254
+ await persist().catch((err) => log(`[sentinel-tombstones] persist failed: ${String(err)}`));
58255
+ return true;
58256
+ }
58257
+ const provider = await opts.resolveProvider(t.provider);
58258
+ let error;
58259
+ if (!provider) error = `unknown sentinel provider "${t.provider}"`;
58260
+ else {
58261
+ try {
58262
+ await provider.cancel(t.handle);
58263
+ } catch (err) {
58264
+ error = err instanceof Error ? err.message : String(err);
58265
+ }
58266
+ }
58267
+ const live = rows.get(t.key);
58268
+ if (error === void 0) {
58269
+ if (live) rows.delete(t.key);
58270
+ await persist().catch((err) => log(`[sentinel-tombstones] persist failed: ${String(err)}`));
58271
+ return true;
58272
+ }
58273
+ if (live) {
58274
+ live.attempts += 1;
58275
+ live.lastAttemptAt = nowMs();
58276
+ live.lastError = error.slice(0, 300);
58277
+ live.nextAttemptAt = nowMs() + Math.min(max, base * 2 ** Math.min(live.attempts, 20));
58278
+ await persist().catch((err) => log(`[sentinel-tombstones] persist failed: ${String(err)}`));
58279
+ }
58280
+ return false;
58281
+ })().finally(() => inFlight2.delete(t.key));
58282
+ inFlight2.set(t.key, p);
58283
+ return p;
58284
+ }
58285
+ return {
58286
+ async cancel(handle, afterRecorded) {
58287
+ const provider = await opts.resolveProvider(handle.provider);
58288
+ if (!handle.remoteId || !provider?.exclusiveRemote) {
58289
+ afterRecorded?.();
58290
+ let converged2 = true;
58291
+ try {
58292
+ await provider?.cancel(handle);
58293
+ } catch {
58294
+ converged2 = false;
58295
+ }
58296
+ return { converged: converged2 };
58297
+ }
58298
+ const key = keyOf2(handle.provider, handle.remoteId);
58299
+ let t = rows.get(key);
58300
+ if (!t) {
58301
+ const now = nowMs();
58302
+ t = {
58303
+ key,
58304
+ provider: handle.provider,
58305
+ remoteId: handle.remoteId,
58306
+ handle: stripSecrets(handle),
58307
+ createdAt: now,
58308
+ attempts: 0,
58309
+ nextAttemptAt: now
58310
+ };
58311
+ rows.set(key, t);
58312
+ try {
58313
+ await persist();
58314
+ } catch (err) {
58315
+ rows.delete(key);
58316
+ throw err;
58317
+ }
58318
+ }
58319
+ afterRecorded?.();
58320
+ const converged = await attempt(t, true);
58321
+ const remaining = rows.get(key);
58322
+ return { converged, ...remaining ? { tombstone: remaining } : {} };
58323
+ },
58324
+ async sweep() {
58325
+ const now = nowMs();
58326
+ const due = [...rows.values()].filter((t) => t.nextAttemptAt <= now);
58327
+ let converged = 0;
58328
+ for (const t of due) if (await attempt(t)) converged++;
58329
+ return { attempted: due.length, converged, remaining: rows.size };
58330
+ },
58331
+ list: () => [...rows.values()],
58332
+ has: (provider, remoteId) => rows.has(keyOf2(provider, remoteId))
58333
+ };
58334
+ }
58335
+ function defaultSentinelQuarantinePath() {
58336
+ return resolve(process.env.AGENTPROTO_HOME ?? join(homedir(), ".agentproto"), "sentinel-quarantine.jsonl");
58337
+ }
58338
+ function keyFor(sentinelId, provider, item) {
58339
+ const identity = item.remoteDeliveryId ?? (item.seq !== void 0 ? `seq:${item.seq}` : "");
58340
+ return [sentinelId, provider, identity, item.digest ?? ""].join("|");
58341
+ }
58342
+ function loadFile(filePath) {
58343
+ if (!existsSync(filePath)) return [];
58344
+ const out = [];
58345
+ for (const line of readFileSync(filePath, "utf8").split("\n")) {
58346
+ if (line.trim() === "") continue;
58347
+ try {
58348
+ const rec = JSON.parse(line);
58349
+ if (rec && typeof rec.key === "string") out.push(rec);
58350
+ } catch {
58351
+ }
58352
+ }
58353
+ return out;
58354
+ }
58355
+ function createSentinelQuarantine(opts = {}) {
58356
+ const nowMs = opts.nowMs ?? Date.now;
58357
+ const records = /* @__PURE__ */ new Map();
58358
+ let loaded = false;
58359
+ function ensureLoaded() {
58360
+ if (loaded) return;
58361
+ if (opts.filePath) for (const rec of loadFile(opts.filePath)) records.set(rec.key, rec);
58362
+ loaded = true;
58363
+ }
58364
+ return {
58365
+ record({ sentinelId, provider, remoteId, item }) {
58366
+ ensureLoaded();
58367
+ const key = keyFor(sentinelId, provider, item);
58368
+ const existing = records.get(key);
58369
+ if (existing) return existing;
58370
+ const rec = {
58371
+ key,
58372
+ quarantinedAt: new Date(nowMs()).toISOString(),
58373
+ sentinelId,
58374
+ provider,
58375
+ ...remoteId !== void 0 ? { remoteId } : {},
58376
+ ...item.seq !== void 0 ? { seq: item.seq } : {},
58377
+ ...item.remoteDeliveryId !== void 0 ? { remoteDeliveryId: item.remoteDeliveryId } : {},
58378
+ error: item.error,
58379
+ ...item.excerpt ? { excerpt: item.excerpt } : {},
58380
+ ...item.digest !== void 0 ? { digest: item.digest } : {},
58381
+ ...item.bytes !== void 0 ? { bytes: item.bytes } : {}
58382
+ };
58383
+ if (opts.filePath) appendLineDurableSync(opts.filePath, JSON.stringify(rec));
58384
+ records.set(key, rec);
58385
+ return rec;
58386
+ },
58387
+ list(filter) {
58388
+ ensureLoaded();
58389
+ const all = [...records.values()];
58390
+ return filter?.sentinelId ? all.filter((r) => r.sentinelId === filter.sentinelId) : all;
58391
+ }
58392
+ };
58393
+ }
57168
58394
 
57169
58395
  // src/webhook-egress/delivery.ts
57170
58396
  init_signing();
@@ -57352,7 +58578,7 @@ function finalHeaders(headers, hostname6, port) {
57352
58578
  return { ...headers, Host: port === 443 ? hostname6 : `${hostname6}:${port}` };
57353
58579
  }
57354
58580
  function rawPost(args) {
57355
- return new Promise((resolve45, reject) => {
58581
+ return new Promise((resolve47, reject) => {
57356
58582
  const req = request({
57357
58583
  // Connect to the PRE-VALIDATED address; keep the original hostname in
57358
58584
  // `servername` so SNI and the certificate check never loosen.
@@ -57376,7 +58602,7 @@ function rawPost(args) {
57376
58602
  const ok4 = (status, body) => {
57377
58603
  if (settled) return;
57378
58604
  settled = true;
57379
- resolve45({ status, body });
58605
+ resolve47({ status, body });
57380
58606
  };
57381
58607
  req.setTimeout(args.timeoutMs, () => fail4("timeout", `no response within ${args.timeoutMs}ms`));
57382
58608
  req.on("error", (err) => {
@@ -57411,6 +58637,23 @@ function rawPost(args) {
57411
58637
  }
57412
58638
 
57413
58639
  // src/webhook-egress/delivery.ts
58640
+ function callbackHost(url) {
58641
+ try {
58642
+ return new URL(url).host;
58643
+ } catch {
58644
+ return "invalid-url";
58645
+ }
58646
+ }
58647
+ function redactUrls(text13) {
58648
+ return text13.replace(/https?:\/\/[^\s'"<>)]+/gi, (m) => {
58649
+ try {
58650
+ const u = new URL(m);
58651
+ return `${u.protocol}//${u.host}`;
58652
+ } catch {
58653
+ return "<url>";
58654
+ }
58655
+ });
58656
+ }
57414
58657
  var CLAMP_LIMIT_BYTES = 262144;
57415
58658
  var MAX_ATTEMPTS = 5;
57416
58659
  var BACKOFF_BASE_MS = 250;
@@ -57442,6 +58685,12 @@ async function deliverEventEnvelope(replay, event, deps2 = {}) {
57442
58685
  const sleep3 = deps2.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
57443
58686
  const fetcher = deps2.fetch ?? ((url, init) => ssrfFetch(url, init));
57444
58687
  const { bytes: payloadBytes } = envelopeClampedBytes(event);
58688
+ const note = (info) => {
58689
+ try {
58690
+ deps2.onAttempt?.(info);
58691
+ } catch {
58692
+ }
58693
+ };
57445
58694
  const delivery = { attempts: 0 };
57446
58695
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
57447
58696
  if (attempt > 0) await sleep3(deliveryBackoffDelay(attempt));
@@ -57453,30 +58702,40 @@ async function deliverEventEnvelope(replay, event, deps2 = {}) {
57453
58702
  signed = signWebhook({ msgId: event.eventId, timestamp, payload: payloadBytes, secrets: replay.secrets });
57454
58703
  } catch (err) {
57455
58704
  delivery.lastError = `signing failed: ${err.message}`;
58705
+ note({ attempt: delivery.attempts, error: "invalid_secret", outcome: "dead" });
57456
58706
  return { ok: false, reason: "invalid_secret", delivery };
57457
58707
  }
57458
58708
  let response;
57459
58709
  try {
57460
58710
  response = await fetcher(replay.callbackUrl, {
57461
58711
  method: "POST",
57462
- headers: { ...signed, "content-type": "application/json" },
58712
+ headers: { ...signed, "content-type": "application/json", "X-MCP-Subscription-Id": replay.subId },
57463
58713
  body: payloadBytes,
57464
58714
  timeoutMs: 15e3
57465
58715
  // delivery default (challenge uses 10_000)
57466
58716
  });
57467
58717
  } catch (err) {
57468
- delivery.lastError = err instanceof Error ? err.message : String(err);
57469
- if (attempt < MAX_ATTEMPTS - 1) continue;
58718
+ delivery.lastError = redactUrls(err instanceof Error ? err.message : String(err));
58719
+ const last = attempt >= MAX_ATTEMPTS - 1;
58720
+ note({ attempt: delivery.attempts, error: delivery.lastError, outcome: last ? "dead" : "retry" });
58721
+ if (!last) continue;
57470
58722
  return { ok: false, reason: "network", delivery };
57471
58723
  }
57472
58724
  if (response.status >= 200 && response.status <= 299) {
58725
+ note({ attempt: delivery.attempts, status: response.status, outcome: "delivered" });
57473
58726
  return { ok: true, delivery };
57474
58727
  }
57475
58728
  if (TERMINAL_NO_RETRY_STATUSES.has(response.status)) {
57476
58729
  delivery.lastError = `terminal status ${response.status}`;
58730
+ note({ attempt: delivery.attempts, status: response.status, outcome: "dead" });
57477
58731
  return { ok: false, reason: `http_${response.status}`, delivery };
57478
58732
  }
57479
58733
  delivery.lastError = `retryable status ${response.status}`;
58734
+ note({
58735
+ attempt: delivery.attempts,
58736
+ status: response.status,
58737
+ outcome: attempt >= MAX_ATTEMPTS - 1 ? "dead" : "retry"
58738
+ });
57480
58739
  }
57481
58740
  return { ok: false, reason: "retries_exhausted", delivery };
57482
58741
  }
@@ -57891,13 +59150,16 @@ function invalidParams(reason, detail) {
57891
59150
  function callbackEndpointError(reason, detail) {
57892
59151
  return new McpEventsError(-32015, `CallbackEndpointError: ${detail}`, { reason });
57893
59152
  }
59153
+ function backingSubscriptionError(reason, detail, subscriptionId2) {
59154
+ return new McpEventsError(-32016, `BackingSubscriptionError: ${detail}`, { reason, subscriptionId: subscriptionId2 });
59155
+ }
57894
59156
  var mutationChain = Promise.resolve();
57895
59157
  function withSubscribeMutex(fn) {
57896
59158
  const run = mutationChain.then(fn, fn);
57897
59159
  mutationChain = run.catch(() => void 0);
57898
59160
  return run;
57899
59161
  }
57900
- function isRecord10(value) {
59162
+ function isRecord12(value) {
57901
59163
  return typeof value === "object" && value !== null && !Array.isArray(value);
57902
59164
  }
57903
59165
  var LIST_PAGE_DEFAULT = 50;
@@ -57918,7 +59180,7 @@ function decodeListCursor(cursor) {
57918
59180
  }
57919
59181
  }
57920
59182
  function requireWebhookDelivery(delivery) {
57921
- if (!isRecord10(delivery) || delivery.mode !== "webhook") {
59183
+ if (!isRecord12(delivery) || delivery.mode !== "webhook") {
57922
59184
  throw invalidParams("unsupported_delivery_mode", 'delivery.mode must be "webhook"');
57923
59185
  }
57924
59186
  if (typeof delivery.url !== "string" || delivery.url.length === 0) {
@@ -57927,7 +59189,7 @@ function requireWebhookDelivery(delivery) {
57927
59189
  return { mode: "webhook", url: delivery.url, ...typeof delivery.secret === "string" ? { secret: delivery.secret } : {} };
57928
59190
  }
57929
59191
  function validateArgsOrThrow(name, registration, args) {
57930
- if (!isRecord10(args)) throw invalidParams("invalid_arguments", "`arguments` must be an object");
59192
+ if (!isRecord12(args)) throw invalidParams("invalid_arguments", "`arguments` must be an object");
57931
59193
  const schemaCheck = validateAgainstInputSchema(registration.definition.inputSchema, args);
57932
59194
  if (!schemaCheck.ok) {
57933
59195
  throw invalidParams("invalid_arguments", `arguments for "${name}": ${schemaCheck.reason}`);
@@ -57983,10 +59245,30 @@ async function eventsSubscribe(input, ctx) {
57983
59245
  if (target.kind !== "webhook") {
57984
59246
  throw invalidParams("identity_conflict", `subscription ${id} exists with a non-webhook target`);
57985
59247
  }
59248
+ let handle2 = existing.handle;
59249
+ const provider2 = await ctx.resolveProvider(existing.provider);
59250
+ if (provider2?.renew) {
59251
+ try {
59252
+ handle2 = await provider2.renew(existing.handle, until);
59253
+ } catch (err) {
59254
+ if (err instanceof SentinelBackingExpiredError) {
59255
+ throw backingSubscriptionError(
59256
+ "backing_subscription_expired",
59257
+ `${err.message}; unsubscribe and subscribe again`,
59258
+ id
59259
+ );
59260
+ }
59261
+ throw backingSubscriptionError(
59262
+ "backing_renew_failed",
59263
+ err instanceof Error ? err.message : String(err),
59264
+ id
59265
+ );
59266
+ }
59267
+ }
57986
59268
  const ref = ctx.store.materializeWebhookTargetRef(target);
57987
59269
  if (ref !== void 0) ctx.store.putSentinelSecret(ref, { secret });
57988
- ctx.store.update(id, { spec: { ...existing.spec, until }, status: "active" });
57989
- return { id, refreshBefore: computeRefreshBefore(until), cursor: null };
59270
+ ctx.store.update(id, { spec: { ...existing.spec, until }, handle: handle2, status: "active" });
59271
+ return { id, refreshBefore: computeRefreshBefore(until), cursor: null, truncated: false };
57990
59272
  }
57991
59273
  const provider = await ctx.resolveProvider(mapped.providerSlug);
57992
59274
  if (!provider) {
@@ -58002,7 +59284,7 @@ async function eventsSubscribe(input, ctx) {
58002
59284
  sentinelId: id
58003
59285
  });
58004
59286
  ctx.store.create({ id, spec, provider: mapped.providerSlug, handle });
58005
- return { id, refreshBefore: computeRefreshBefore(until), cursor: null };
59287
+ return { id, refreshBefore: computeRefreshBefore(until), cursor: null, truncated: false };
58006
59288
  });
58007
59289
  }
58008
59290
  async function eventsUnsubscribe(input, ctx) {
@@ -58022,6 +59304,13 @@ async function eventsUnsubscribe(input, ctx) {
58022
59304
  await withSubscribeMutex(async () => {
58023
59305
  const existing = ctx.store.get(id);
58024
59306
  if (!existing) return;
59307
+ if (ctx.tombstones) {
59308
+ try {
59309
+ await ctx.tombstones.cancel(existing.handle, () => ctx.store.remove(id));
59310
+ return;
59311
+ } catch {
59312
+ }
59313
+ }
58025
59314
  const provider = await ctx.resolveProvider(existing.provider);
58026
59315
  if (provider) {
58027
59316
  try {
@@ -58051,7 +59340,6 @@ var PERSIST_DEBOUNCE_MS8 = 1500;
58051
59340
  var DELIVERED_REAP_MS = 24 * 60 * 60 * 1e3;
58052
59341
  var PENDING_AGE_OUT_MS = 7 * 24 * 60 * 60 * 1e3;
58053
59342
  var ROW_CAP = 2e3;
58054
- var tmpSeq6 = 0;
58055
59343
  function webhookRequestId(sentinelId, eventId) {
58056
59344
  const digest = createHash("sha256").update(`${sentinelId}:${eventId}`).digest("hex");
58057
59345
  return `sub_${digest.slice(0, 32)}`;
@@ -58065,9 +59353,22 @@ function createSentinelWebhookOutbox(opts) {
58065
59353
  const debounceMs = opts.debounceMs ?? PERSIST_DEBOUNCE_MS8;
58066
59354
  const log = opts.log ?? ((line) => console.warn(line));
58067
59355
  const persist = opts.persist ?? opts.filePath !== void 0;
58068
- const deliverEvent = opts.deliverEvent ?? (async (input) => deliverEventEnvelope(input.replay, input.row.envelope));
59356
+ const deliveryLog = (row, host, detail) => {
59357
+ log(`[sentinel-webhook-outbox] sentinel=${row.sentinelId} event=${row.eventId} host=${host} ${detail}`);
59358
+ };
59359
+ const deliverEvent = opts.deliverEvent ?? (async (input) => {
59360
+ const host = callbackHost(input.replay.callbackUrl);
59361
+ return deliverEventEnvelope(input.replay, input.row.envelope, {
59362
+ ...opts.deliverDeps,
59363
+ onAttempt: (a) => deliveryLog(
59364
+ input.row,
59365
+ host,
59366
+ `attempt=${a.attempt} ${a.status !== void 0 ? `http=${a.status}` : `error="${a.error ?? "unknown"}"`} ${a.outcome}`
59367
+ )
59368
+ });
59369
+ });
58069
59370
  const rowKey = (sentinelId, eventId) => `${sentinelId}::${eventId}`;
58070
- const load = () => {
59371
+ const load2 = () => {
58071
59372
  const out = /* @__PURE__ */ new Map();
58072
59373
  if (!persist) return out;
58073
59374
  let raw;
@@ -58091,28 +59392,34 @@ function createSentinelWebhookOutbox(opts) {
58091
59392
  }
58092
59393
  return out;
58093
59394
  };
58094
- const rows = load();
59395
+ const rows = load2();
58095
59396
  const snapshot = () => {
58096
59397
  const out = {};
58097
59398
  for (const [key, row] of rows.entries()) out[key] = row;
58098
59399
  return out;
58099
59400
  };
58100
59401
  let persistTimer = null;
59402
+ let writeTail = Promise.resolve();
59403
+ let syncGeneration = 0;
59404
+ const persistNow = () => {
59405
+ if (!persist) return Promise.resolve();
59406
+ const run = async () => {
59407
+ const generation = syncGeneration;
59408
+ const body = JSON.stringify(snapshot(), null, 2) + "\n";
59409
+ await writeFileDurable(filePath, body, { commitIf: () => generation === syncGeneration });
59410
+ };
59411
+ const next = writeTail.then(run, run);
59412
+ writeTail = next.catch(() => void 0);
59413
+ return next;
59414
+ };
58101
59415
  const schedulePersist = () => {
58102
59416
  if (!persist) return;
58103
59417
  if (persistTimer) clearTimeout(persistTimer);
58104
59418
  persistTimer = setTimeout(() => {
58105
- void (async () => {
58106
- try {
58107
- await promises.mkdir(dirname(filePath), { recursive: true });
58108
- const tmp = `${filePath}.tmp.${process.pid}.${++tmpSeq6}`;
58109
- await promises.writeFile(tmp, JSON.stringify(snapshot(), null, 2) + "\n", { mode: 384 });
58110
- await promises.chmod(tmp, 384);
58111
- await promises.rename(tmp, filePath);
58112
- } catch (err) {
58113
- log(`[sentinel-webhook-outbox] persist failed: ${err instanceof Error ? err.message : String(err)}`);
58114
- }
58115
- })();
59419
+ persistTimer = null;
59420
+ persistNow().catch((err) => {
59421
+ log(`[sentinel-webhook-outbox] persist failed: ${err instanceof Error ? err.message : String(err)}`);
59422
+ });
58116
59423
  }, debounceMs);
58117
59424
  };
58118
59425
  const flushSync = () => {
@@ -58121,12 +59428,9 @@ function createSentinelWebhookOutbox(opts) {
58121
59428
  clearTimeout(persistTimer);
58122
59429
  persistTimer = null;
58123
59430
  }
59431
+ syncGeneration++;
58124
59432
  try {
58125
- mkdirSync(dirname(filePath), { recursive: true });
58126
- const tmp = `${filePath}.tmp.${process.pid}.${++tmpSeq6}`;
58127
- writeFileSync(tmp, JSON.stringify(snapshot(), null, 2) + "\n", { mode: 384 });
58128
- chmodSync(tmp, 384);
58129
- renameSync(tmp, filePath);
59433
+ writeFileDurableSync(filePath, JSON.stringify(snapshot(), null, 2) + "\n");
58130
59434
  } catch {
58131
59435
  }
58132
59436
  };
@@ -58173,14 +59477,19 @@ function createSentinelWebhookOutbox(opts) {
58173
59477
  row.failReason = outcome.reason;
58174
59478
  }
58175
59479
  row.terminalAt = nowMs();
59480
+ deliveryLog(
59481
+ row,
59482
+ callbackHost(replay.callbackUrl),
59483
+ `final=${row.status} attempts=${outcome.delivery.attempts}` + (outcome.ok ? "" : ` reason=${outcome.reason}`)
59484
+ );
58176
59485
  } catch (err) {
58177
59486
  row.deliveryState = {
58178
59487
  ...row.deliveryState ?? { attempts: 0 },
58179
- lastError: err instanceof Error ? err.message : String(err),
59488
+ lastError: redactUrls(err instanceof Error ? err.message : String(err)),
58180
59489
  lastAt: new Date(nowMs()).toISOString()
58181
59490
  };
58182
59491
  row.status = "pending";
58183
- log(`[sentinel-webhook-outbox] dispatch failed, row left pending: ${row.sentinelId} ${row.eventId}`);
59492
+ deliveryLog(row, callbackHost(replay.callbackUrl), "dispatch failed, row left pending");
58184
59493
  }
58185
59494
  schedulePersist();
58186
59495
  if (row.status === "delivered" || row.status === "dead") {
@@ -58203,7 +59512,14 @@ function createSentinelWebhookOutbox(opts) {
58203
59512
  const key = rowKey(sentinelId, event.id);
58204
59513
  const existing = rows.get(key);
58205
59514
  if (existing) {
58206
- if (existing.status !== "pending") opts.onTerminal(sentinelId, existing);
59515
+ if (existing.status !== "pending") {
59516
+ opts.onTerminal(sentinelId, existing);
59517
+ return void 0;
59518
+ }
59519
+ await writeTail;
59520
+ if (rows.get(key) !== existing) {
59521
+ throw new Error(`webhook outbox row ${key} was not persisted`);
59522
+ }
58207
59523
  return void 0;
58208
59524
  }
58209
59525
  const envelope = toWebhookEnvelope(event);
@@ -58219,12 +59535,23 @@ function createSentinelWebhookOutbox(opts) {
58219
59535
  envelope,
58220
59536
  event
58221
59537
  };
59538
+ let evicted;
58222
59539
  if (rows.size >= ROW_CAP) {
58223
59540
  const oldest = oldestTerminalRowKey();
58224
- if (oldest) rows.delete(oldest);
59541
+ const evictedRow = oldest ? rows.get(oldest) : void 0;
59542
+ if (oldest && evictedRow) {
59543
+ evicted = [oldest, evictedRow];
59544
+ rows.delete(oldest);
59545
+ }
58225
59546
  }
58226
59547
  rows.set(key, row);
58227
- schedulePersist();
59548
+ try {
59549
+ await persistNow();
59550
+ } catch (err) {
59551
+ if (rows.get(key) === row) rows.delete(key);
59552
+ if (evicted && !rows.has(evicted[0])) rows.set(evicted[0], evicted[1]);
59553
+ throw err;
59554
+ }
58228
59555
  enqueueDispatch();
58229
59556
  return row;
58230
59557
  },
@@ -58257,6 +59584,27 @@ function createSentinelWebhookOutbox(opts) {
58257
59584
  rows() {
58258
59585
  return Array.from(rows.values());
58259
59586
  },
59587
+ deliveryStatus(sentinelId) {
59588
+ let latest;
59589
+ let active = false;
59590
+ let dead = 0;
59591
+ for (const row of rows.values()) {
59592
+ if (row.sentinelId !== sentinelId) continue;
59593
+ if (row.status === "pending") active = true;
59594
+ if (row.status === "dead") dead++;
59595
+ if (!latest || (row.terminalAt ?? row.createdAt) >= (latest.terminalAt ?? latest.createdAt)) latest = row;
59596
+ }
59597
+ if (!latest) return void 0;
59598
+ const lastError = latest.deliveryState.lastError ?? latest.failReason ?? latest.deadReason;
59599
+ return {
59600
+ active,
59601
+ ...latest.deliveryState.lastAt ? { lastDeliveryAt: latest.deliveryState.lastAt } : {},
59602
+ lastStatus: latest.status,
59603
+ ...lastError && latest.status !== "delivered" ? { lastError: redactUrls(lastError) } : {},
59604
+ attempts: latest.deliveryState.attempts,
59605
+ dead
59606
+ };
59607
+ },
58260
59608
  flushSync
58261
59609
  };
58262
59610
  }
@@ -58354,14 +59702,27 @@ function createSentinelRuntime(opts) {
58354
59702
  if (store.get(sentinel.id)?.status !== "expired") {
58355
59703
  store.update(sentinel.id, { status: "expired" });
58356
59704
  }
58357
- const provider = await opts.resolveProvider(sentinel.provider);
58358
- if (!provider) return;
58359
59705
  try {
58360
- await provider.cancel(sentinel.handle);
59706
+ await cancelTombstones.cancel(sentinel.handle);
58361
59707
  } catch (err) {
58362
59708
  log(`[sentinel-runtime] expiry cancel failed for ${sentinel.id}: ${describeError(err)}`);
58363
59709
  }
58364
59710
  }
59711
+ const outboxPersistent = opts.outboxPersist ?? (opts.outboxPath !== void 0 || opts.deliverEvent === void 0);
59712
+ const quarantinePath = opts.quarantinePath ?? (outboxPersistent ? defaultSentinelQuarantinePath() : void 0);
59713
+ const quarantine = createSentinelQuarantine({
59714
+ ...quarantinePath !== void 0 ? { filePath: quarantinePath } : {},
59715
+ nowMs
59716
+ });
59717
+ const cancelTombstonePath = opts.cancelTombstonePath ?? (outboxPersistent ? defaultCancelTombstonePath() : void 0);
59718
+ const cancelTombstones = createCancelTombstoneStore({
59719
+ ...cancelTombstonePath !== void 0 ? { filePath: cancelTombstonePath } : {},
59720
+ resolveProvider: opts.resolveProvider,
59721
+ // A live local watch that owns the remote makes the tombstone stale.
59722
+ isRemoteInUse: (provider, remoteId) => store.list().some((s) => s.provider === provider && s.handle.remoteId === remoteId && s.status !== "expired"),
59723
+ nowMs,
59724
+ log
59725
+ });
58365
59726
  const outbox = createSentinelWebhookOutbox({
58366
59727
  ...opts.outboxPath !== void 0 ? { filePath: opts.outboxPath } : {},
58367
59728
  nowMs,
@@ -58383,7 +59744,7 @@ function createSentinelRuntime(opts) {
58383
59744
  if (stored.prevSecret && stored.rotatedAt !== void 0 && nowMs() - stored.rotatedAt < windowMs) {
58384
59745
  secretsOut.push(stored.prevSecret);
58385
59746
  }
58386
- const subId = `sub_${createHash("sha256").update(`${sentinelId}:${target.url}`).digest("hex").slice(0, 32)}`;
59747
+ const subId = sentinelId;
58387
59748
  return { subId, callbackUrl: target.url, secrets: secretsOut };
58388
59749
  },
58389
59750
  /** Expiry gate consulted BEFORE every dispatch (plan §4 W-B task 2b). */
@@ -58578,13 +59939,21 @@ function createSentinelRuntime(opts) {
58578
59939
  }
58579
59940
  async function processEvents(sentinelId, provider, events) {
58580
59941
  let haltedOnError = false;
59942
+ let stoppedEarly = false;
58581
59943
  let delivered = 0;
58582
59944
  for (const event of events) {
58583
59945
  const current = store.get(sentinelId);
58584
- if (!current) break;
58585
- if (!POLLABLE_STATUSES.has(current.status)) break;
59946
+ if (!current) {
59947
+ stoppedEarly = true;
59948
+ break;
59949
+ }
59950
+ if (!POLLABLE_STATUSES.has(current.status)) {
59951
+ stoppedEarly = true;
59952
+ break;
59953
+ }
58586
59954
  if (isExpiredSpec(current)) {
58587
59955
  await expireSentinel(current);
59956
+ stoppedEarly = true;
58588
59957
  break;
58589
59958
  }
58590
59959
  if (store.isSeen(current.id, event.id)) continue;
@@ -58623,7 +59992,7 @@ function createSentinelRuntime(opts) {
58623
59992
  });
58624
59993
  if (updated) await applyLifetime(updated, event, provider);
58625
59994
  }
58626
- return { haltedOnError, delivered };
59995
+ return { haltedOnError, delivered, stoppedEarly };
58627
59996
  }
58628
59997
  async function pollSentinel(sentinel) {
58629
59998
  const provider = await opts.resolveProvider(sentinel.provider);
@@ -58635,19 +60004,35 @@ function createSentinelRuntime(opts) {
58635
60004
  store.update(sentinel.id, { lastError: describeError(err) });
58636
60005
  return;
58637
60006
  }
58638
- const { haltedOnError } = await processEvents(sentinel.id, provider, result.events);
58639
- if (!haltedOnError) {
58640
- const latest = store.get(sentinel.id);
58641
- if (latest) {
58642
- if (provider.ack) {
58643
- try {
58644
- await provider.ack(latest.handle, result.cursor);
58645
- } catch (err) {
58646
- log(`[sentinel-runtime] ack failed for ${sentinel.id}: ${describeError(err)}`);
58647
- }
60007
+ let quarantineFailed = false;
60008
+ for (const item of result.malformed ?? []) {
60009
+ try {
60010
+ quarantine.record({
60011
+ sentinelId: sentinel.id,
60012
+ provider: sentinel.provider,
60013
+ ...sentinel.handle.remoteId !== void 0 ? { remoteId: sentinel.handle.remoteId } : {},
60014
+ item
60015
+ });
60016
+ } catch (err) {
60017
+ quarantineFailed = true;
60018
+ log(`[sentinel-runtime] quarantine write failed for ${sentinel.id}: ${describeError(err)}`);
60019
+ }
60020
+ }
60021
+ const { haltedOnError, stoppedEarly } = await processEvents(sentinel.id, provider, result.events);
60022
+ if (haltedOnError || stoppedEarly || quarantineFailed) {
60023
+ if (quarantineFailed) store.update(sentinel.id, { lastError: "quarantine write failed; batch not acknowledged" });
60024
+ return;
60025
+ }
60026
+ const latest = store.get(sentinel.id);
60027
+ if (latest) {
60028
+ if (provider.ack) {
60029
+ try {
60030
+ await provider.ack(latest.handle, result.cursor);
60031
+ } catch (err) {
60032
+ log(`[sentinel-runtime] ack failed for ${sentinel.id}: ${describeError(err)}`);
58648
60033
  }
58649
- store.update(sentinel.id, { handle: { ...latest.handle, cursor: result.cursor } });
58650
60034
  }
60035
+ store.update(sentinel.id, { handle: { ...latest.handle, cursor: result.cursor } });
58651
60036
  }
58652
60037
  }
58653
60038
  async function pollOnce() {
@@ -58655,6 +60040,7 @@ function createSentinelRuntime(opts) {
58655
60040
  inFlight2 = true;
58656
60041
  try {
58657
60042
  await sweepExpiredSentinels();
60043
+ await cancelTombstones.sweep().catch((err) => log(`[sentinel-runtime] tombstone sweep failed: ${describeError(err)}`));
58658
60044
  outbox.sweep();
58659
60045
  const pollable = store.list().filter((s) => POLLABLE_STATUSES.has(s.status));
58660
60046
  for (const sentinel of pollable) {
@@ -58724,6 +60110,7 @@ function createSentinelRuntime(opts) {
58724
60110
  async start() {
58725
60111
  await reattachAll();
58726
60112
  await sweepExpiredSentinels();
60113
+ await cancelTombstones.sweep().catch((err) => log(`[sentinel-runtime] tombstone sweep failed: ${describeError(err)}`));
58727
60114
  await outbox.dispatch();
58728
60115
  if (!timer) scheduleNext();
58729
60116
  },
@@ -58735,7 +60122,9 @@ function createSentinelRuntime(opts) {
58735
60122
  },
58736
60123
  pollOnce,
58737
60124
  deliverPushed,
58738
- webhookOutbox: outbox
60125
+ webhookOutbox: outbox,
60126
+ quarantine,
60127
+ cancelTombstones
58739
60128
  };
58740
60129
  }
58741
60130
  var BUILTIN_SENTINEL_PROVIDERS = {
@@ -58812,7 +60201,7 @@ async function spawnCloudflaredUntil(argv, opts) {
58812
60201
  };
58813
60202
  let settled = false;
58814
60203
  let forwardedLen = 0;
58815
- return await new Promise((resolve45, reject) => {
60204
+ return await new Promise((resolve47, reject) => {
58816
60205
  const poll = setInterval(() => {
58817
60206
  const text13 = readAll();
58818
60207
  if (opts.onLog) {
@@ -58830,7 +60219,7 @@ async function spawnCloudflaredUntil(argv, opts) {
58830
60219
  if (m) {
58831
60220
  settled = true;
58832
60221
  clearTimeout(timer);
58833
- resolve45({ proc, match: m[0], stopTail: () => clearInterval(poll) });
60222
+ resolve47({ proc, match: m[0], stopTail: () => clearInterval(poll) });
58834
60223
  }
58835
60224
  }, POLL_INTERVAL_MS2);
58836
60225
  if (poll.unref) poll.unref();
@@ -58967,15 +60356,15 @@ function quickTunnelProvider() {
58967
60356
  }
58968
60357
  }, 3e3);
58969
60358
  timer.unref();
58970
- await new Promise((resolve45) => {
60359
+ await new Promise((resolve47) => {
58971
60360
  if (proc.exitCode !== null) {
58972
60361
  clearTimeout(timer);
58973
- resolve45();
60362
+ resolve47();
58974
60363
  return;
58975
60364
  }
58976
60365
  proc.once("exit", () => {
58977
60366
  clearTimeout(timer);
58978
- resolve45();
60367
+ resolve47();
58979
60368
  });
58980
60369
  });
58981
60370
  }
@@ -59128,15 +60517,15 @@ function namedTunnelProvider(cfg) {
59128
60517
  }
59129
60518
  }, 3e3);
59130
60519
  timer.unref();
59131
- await new Promise((resolve45) => {
60520
+ await new Promise((resolve47) => {
59132
60521
  if (proc.exitCode !== null) {
59133
60522
  clearTimeout(timer);
59134
- resolve45();
60523
+ resolve47();
59135
60524
  return;
59136
60525
  }
59137
60526
  proc.once("exit", () => {
59138
60527
  clearTimeout(timer);
59139
- resolve45();
60528
+ resolve47();
59140
60529
  });
59141
60530
  });
59142
60531
  }
@@ -59260,7 +60649,7 @@ function ngrokTunnelProvider(opts) {
59260
60649
  };
59261
60650
  proc.stderr?.on("data", forwardLogs);
59262
60651
  proc.stdout?.on("data", forwardLogs);
59263
- const url = await new Promise((resolve45, reject) => {
60652
+ const url = await new Promise((resolve47, reject) => {
59264
60653
  let settled = false;
59265
60654
  const timer = setTimeout(() => {
59266
60655
  if (settled) return;
@@ -59288,7 +60677,7 @@ function ngrokTunnelProvider(opts) {
59288
60677
  settled = true;
59289
60678
  clearTimeout(timer);
59290
60679
  if (apiPollHandle) clearInterval(apiPollHandle);
59291
- resolve45(pubUrl);
60680
+ resolve47(pubUrl);
59292
60681
  }
59293
60682
  } catch {
59294
60683
  }
@@ -59303,7 +60692,7 @@ function ngrokTunnelProvider(opts) {
59303
60692
  settled = true;
59304
60693
  clearTimeout(timer);
59305
60694
  if (apiPollHandle) clearInterval(apiPollHandle);
59306
- resolve45(reMatch[1]);
60695
+ resolve47(reMatch[1]);
59307
60696
  return;
59308
60697
  }
59309
60698
  for (const line of text13.split(/\r?\n/)) {
@@ -59314,7 +60703,7 @@ function ngrokTunnelProvider(opts) {
59314
60703
  settled = true;
59315
60704
  clearTimeout(timer);
59316
60705
  if (apiPollHandle) clearInterval(apiPollHandle);
59317
- resolve45(parsed.url);
60706
+ resolve47(parsed.url);
59318
60707
  return;
59319
60708
  }
59320
60709
  } catch {
@@ -59361,15 +60750,15 @@ function ngrokTunnelProvider(opts) {
59361
60750
  }
59362
60751
  }, 3e3);
59363
60752
  timer.unref();
59364
- await new Promise((resolve45) => {
60753
+ await new Promise((resolve47) => {
59365
60754
  if (proc.exitCode !== null) {
59366
60755
  clearTimeout(timer);
59367
- resolve45();
60756
+ resolve47();
59368
60757
  return;
59369
60758
  }
59370
60759
  proc.once("exit", () => {
59371
60760
  clearTimeout(timer);
59372
- resolve45();
60761
+ resolve47();
59373
60762
  });
59374
60763
  });
59375
60764
  }
@@ -59581,7 +60970,7 @@ function sameCollapseGroup(a, b) {
59581
60970
  if ((a.key ?? "") !== (b.key ?? "")) return false;
59582
60971
  return a.event === b.event || TURN_FAMILY.has(a.event) && TURN_FAMILY.has(b.event);
59583
60972
  }
59584
- function excerptOf(text13) {
60973
+ function excerptOf2(text13) {
59585
60974
  if (!text13) return void 0;
59586
60975
  const flat = text13.replace(/\s+/g, " ").trim();
59587
60976
  if (!flat) return void 0;
@@ -59596,7 +60985,7 @@ function formatFollowDigest(entries, resolveText, desc) {
59596
60985
  const cwd = d?.cwd ?? entry.cwd;
59597
60986
  const parts = [`[session-follow] ${who} ${entry.event}`];
59598
60987
  if (cwd) parts.push(basename(cwd) || cwd);
59599
- const text13 = excerptOf(resolveText(entry));
60988
+ const text13 = excerptOf2(resolveText(entry));
59600
60989
  if (text13) parts.push(text13);
59601
60990
  return parts.join(" \u2014 ");
59602
60991
  });
@@ -59872,6 +61261,31 @@ function createSentinelAutoLinker(opts) {
59872
61261
  linkNow: link
59873
61262
  };
59874
61263
  }
61264
+ function isSubscriptionPermitted(params, repoAllowlist) {
61265
+ const args = params.arguments;
61266
+ if (typeof args !== "object" || args === null || Array.isArray(args)) return false;
61267
+ const repo = args.repo;
61268
+ return typeof repo === "string" && repoAllowlist.includes(repo);
61269
+ }
61270
+ function createEventsSurfaceServer(opts) {
61271
+ const server = new McpServer({ name: "agentproto-events", version: opts.version }, { capabilities: { tools: {} } });
61272
+ server.registerTool(
61273
+ "events_ping",
61274
+ { description: "Liveness probe for the events surface. Returns ok.", inputSchema: {} },
61275
+ async () => ({ content: [{ type: "text", text: "ok" }] })
61276
+ );
61277
+ registerEventsMethods(server, {
61278
+ list: (params) => opts.handlers.list(params),
61279
+ subscribe: async (params) => {
61280
+ if (!isSubscriptionPermitted(params, opts.repoAllowlist)) {
61281
+ throw new McpError(ErrorCode.InvalidParams, "subscription not permitted");
61282
+ }
61283
+ return opts.handlers.subscribe(params);
61284
+ },
61285
+ unsubscribe: (params) => opts.handlers.unsubscribe(params)
61286
+ });
61287
+ return server;
61288
+ }
59875
61289
  var TELEGRAM_BOT_CREDS_FAMILY = "telegram-bot";
59876
61290
  function makeTelegramBotCredsStore(home) {
59877
61291
  const store = makeCredsStore({
@@ -60350,12 +61764,12 @@ function canonicalJson(value) {
60350
61764
  }
60351
61765
  function isToolAppVisible(tool) {
60352
61766
  const meta = tool._meta;
60353
- const ui = isRecord11(meta?.ui) ? meta.ui : void 0;
61767
+ const ui = isRecord13(meta?.ui) ? meta.ui : void 0;
60354
61768
  const visibility = ui?.visibility;
60355
61769
  if (!Array.isArray(visibility)) return true;
60356
61770
  return visibility.includes("app");
60357
61771
  }
60358
- function isRecord11(v) {
61772
+ function isRecord13(v) {
60359
61773
  return typeof v === "object" && v !== null && !Array.isArray(v);
60360
61774
  }
60361
61775
  function buildUiToolIndex(tools) {
@@ -60880,19 +62294,19 @@ function classifyMcpError(err) {
60880
62294
  return { status: "unreachable", error };
60881
62295
  }
60882
62296
  function uiMetaOf(meta) {
60883
- if (!isRecord12(meta) || !isRecord12(meta.ui)) return {};
62297
+ if (!isRecord14(meta) || !isRecord14(meta.ui)) return {};
60884
62298
  const ui = meta.ui;
60885
62299
  const csp = cspOf(ui.csp);
60886
62300
  return {
60887
62301
  ...csp ? { csp } : {},
60888
- ...isRecord12(ui.permissions) ? { permissions: ui.permissions } : {},
62302
+ ...isRecord14(ui.permissions) ? { permissions: ui.permissions } : {},
60889
62303
  ...typeof ui.prefersBorder === "boolean" ? { prefersBorder: ui.prefersBorder } : {},
60890
62304
  ...typeof ui.domain === "string" ? { domain: ui.domain } : {}
60891
62305
  };
60892
62306
  }
60893
62307
  var CSP_DOMAIN_KEYS = ["connectDomains", "resourceDomains", "frameDomains", "baseUriDomains"];
60894
62308
  function cspOf(raw) {
60895
- if (!isRecord12(raw)) return void 0;
62309
+ if (!isRecord14(raw)) return void 0;
60896
62310
  const csp = {};
60897
62311
  for (const key of CSP_DOMAIN_KEYS) {
60898
62312
  const value = raw[key];
@@ -60902,7 +62316,7 @@ function cspOf(raw) {
60902
62316
  }
60903
62317
  return Object.keys(csp).length > 0 ? csp : void 0;
60904
62318
  }
60905
- function isRecord12(v) {
62319
+ function isRecord14(v) {
60906
62320
  return typeof v === "object" && v !== null && !Array.isArray(v);
60907
62321
  }
60908
62322
  function errorMessage(err) {
@@ -63139,14 +64553,14 @@ function createActivityProjector(opts) {
63139
64553
  if (current && isTerminalActivityState(current.state)) {
63140
64554
  return Promise.resolve(current);
63141
64555
  }
63142
- return new Promise((resolve45) => {
64556
+ return new Promise((resolve47) => {
63143
64557
  let settled = false;
63144
64558
  const finish = (value) => {
63145
64559
  if (settled) return;
63146
64560
  settled = true;
63147
64561
  clearTimeout(timer);
63148
64562
  unsubscribeWait();
63149
- resolve45(value);
64563
+ resolve47(value);
63150
64564
  };
63151
64565
  const timer = setTimeout(() => finish(null), timeoutMs);
63152
64566
  const unsubscribeWait = opts.sessionEvents.on("activity:changed", (ev) => {
@@ -63308,7 +64722,7 @@ function createApprovalsEngine(opts) {
63308
64722
  const list2 = waiters.get(record2.id);
63309
64723
  if (!list2) return;
63310
64724
  waiters.delete(record2.id);
63311
- for (const resolve45 of list2) resolve45(record2);
64725
+ for (const resolve47 of list2) resolve47(record2);
63312
64726
  };
63313
64727
  const emit = (type, record2) => {
63314
64728
  const ts = (/* @__PURE__ */ new Date()).toISOString();
@@ -63378,17 +64792,17 @@ function createApprovalsEngine(opts) {
63378
64792
  const current = liveGet(id);
63379
64793
  if (!current) return Promise.resolve(void 0);
63380
64794
  if (current.status !== "pending") return Promise.resolve(current);
63381
- return new Promise((resolve45) => {
64795
+ return new Promise((resolve47) => {
63382
64796
  const list2 = waiters.get(id) ?? [];
63383
- list2.push(resolve45);
64797
+ list2.push(resolve47);
63384
64798
  waiters.set(id, list2);
63385
64799
  const timer = setTimeout(() => {
63386
64800
  const list22 = waiters.get(id);
63387
64801
  if (!list22) return;
63388
- const idx = list22.indexOf(resolve45);
64802
+ const idx = list22.indexOf(resolve47);
63389
64803
  if (idx >= 0) list22.splice(idx, 1);
63390
64804
  if (list22.length === 0) waiters.delete(id);
63391
- resolve45(liveGet(id));
64805
+ resolve47(liveGet(id));
63392
64806
  }, timeoutMs);
63393
64807
  timer.unref?.();
63394
64808
  });
@@ -63551,7 +64965,7 @@ function humanizeKey(key) {
63551
64965
  const words = key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_\-.]+/g, " ").replace(/\s+/g, " ").trim().toLowerCase();
63552
64966
  return words.length === 0 ? "Value" : words.charAt(0).toUpperCase() + words.slice(1);
63553
64967
  }
63554
- function isRecord13(value) {
64968
+ function isRecord15(value) {
63555
64969
  return value !== null && typeof value === "object" && !Array.isArray(value);
63556
64970
  }
63557
64971
  function scalarText(value) {
@@ -63581,7 +64995,7 @@ function flattenRows(value, label, depth, out) {
63581
64995
  });
63582
64996
  return;
63583
64997
  }
63584
- if (isRecord13(value)) {
64998
+ if (isRecord15(value)) {
63585
64999
  if (depth >= MAX_DEPTH) return;
63586
65000
  for (const [key, child] of Object.entries(value)) {
63587
65001
  flattenRows(child, label ? `${label} / ${humanizeKey(key)}` : humanizeKey(key), depth + 1, out);
@@ -63603,7 +65017,7 @@ function renderMail(preview2) {
63603
65017
  const cc = addressList(preview2.cc);
63604
65018
  const subject = scalarText(preview2.subject) ?? "";
63605
65019
  const body = scalarText(preview2.body) ?? "";
63606
- const attachments = Array.isArray(preview2.attachments) ? preview2.attachments.filter(isRecord13).map((a) => {
65020
+ const attachments = Array.isArray(preview2.attachments) ? preview2.attachments.filter(isRecord15).map((a) => {
63607
65021
  const name = scalarText(a.fileName) ?? "";
63608
65022
  const size = typeof a.sizeBytes === "number" ? humanSize(a.sizeBytes) : "";
63609
65023
  return size ? `${name} (${size})` : name;
@@ -63618,9 +65032,9 @@ function renderMail(preview2) {
63618
65032
  }
63619
65033
  function renderPreview(record2) {
63620
65034
  const preview2 = record2.preview;
63621
- if (isRecord13(preview2) && preview2.kind === "mail") return renderMail(preview2);
65035
+ if (isRecord15(preview2) && preview2.kind === "mail") return renderMail(preview2);
63622
65036
  const rows = [];
63623
- if (isRecord13(preview2)) {
65037
+ if (isRecord15(preview2)) {
63624
65038
  for (const [key, value] of Object.entries(preview2)) flattenRows(value, humanizeKey(key), 1, rows);
63625
65039
  } else {
63626
65040
  flattenRows(preview2, "Details", 1, rows);
@@ -65717,9 +67131,9 @@ async function* readEventRecords(path) {
65717
67131
  let stream;
65718
67132
  try {
65719
67133
  stream = createReadStream(path, { encoding: "utf8" });
65720
- await new Promise((resolve45, reject) => {
67134
+ await new Promise((resolve47, reject) => {
65721
67135
  stream.once("error", reject);
65722
- stream.once("open", resolve45);
67136
+ stream.once("open", resolve47);
65723
67137
  });
65724
67138
  } catch (err) {
65725
67139
  if (err.code === "ENOENT") return;
@@ -66177,13 +67591,13 @@ function createLinkGuard(opts) {
66177
67591
  return {
66178
67592
  async start() {
66179
67593
  server = createServer(onRequest);
66180
- return new Promise((resolve45, reject) => {
67594
+ return new Promise((resolve47, reject) => {
66181
67595
  server.once("error", reject);
66182
67596
  server.listen(0, "127.0.0.1", () => {
66183
67597
  log(
66184
67598
  `[link-guard] listening on 127.0.0.1:${currentPort()} -> ${opts.target.host}:${opts.target.port}`
66185
67599
  );
66186
- resolve45(handle());
67600
+ resolve47(handle());
66187
67601
  });
66188
67602
  });
66189
67603
  },
@@ -66191,8 +67605,8 @@ function createLinkGuard(opts) {
66191
67605
  if (!server) return;
66192
67606
  const toClose = server;
66193
67607
  server = null;
66194
- await new Promise((resolve45, reject) => {
66195
- toClose.close((err) => err ? reject(err) : resolve45());
67608
+ await new Promise((resolve47, reject) => {
67609
+ toClose.close((err) => err ? reject(err) : resolve47());
66196
67610
  });
66197
67611
  },
66198
67612
  revoke() {
@@ -68203,13 +69617,13 @@ function createJoinTokenRegistry(deps2) {
68203
69617
  return { create, list, revoke, startAutoconnect, shutdown };
68204
69618
  }
68205
69619
  function sleep2(ms, signal) {
68206
- return new Promise((resolve45) => {
68207
- if (signal.aborted) return resolve45();
68208
- const timer = setTimeout(resolve45, ms);
69620
+ return new Promise((resolve47) => {
69621
+ if (signal.aborted) return resolve47();
69622
+ const timer = setTimeout(resolve47, ms);
68209
69623
  if (typeof timer.unref === "function") timer.unref();
68210
69624
  signal.addEventListener("abort", () => {
68211
69625
  clearTimeout(timer);
68212
- resolve45();
69626
+ resolve47();
68213
69627
  });
68214
69628
  });
68215
69629
  }
@@ -68899,7 +70313,14 @@ async function createGateway(opts) {
68899
70313
  registry: sessions,
68900
70314
  followStore: sessionFollowStore,
68901
70315
  sentinelStore,
68902
- cancelSentinel: (id) => cancelSentinelWatch({ store: sentinelStore, resolveProvider: resolveSentinelProviderResolved }, id)
70316
+ cancelSentinel: (id) => cancelSentinelWatch(
70317
+ {
70318
+ store: sentinelStore,
70319
+ resolveProvider: resolveSentinelProviderResolved,
70320
+ tombstones: sentinelRuntime.cancelTombstones
70321
+ },
70322
+ id
70323
+ )
68903
70324
  });
68904
70325
  const inboundWatcher = opts.resolveAgentAdapter ? createInboundWatcher({
68905
70326
  mcpProxy,
@@ -69281,6 +70702,8 @@ async function createGateway(opts) {
69281
70702
  registerSentinelTools(server, {
69282
70703
  store: sentinelStore,
69283
70704
  runtime: sentinelRuntime,
70705
+ tombstones: sentinelRuntime.cancelTombstones,
70706
+ deliveryStatus: (id) => sentinelRuntime.webhookOutbox.deliveryStatus(id),
69284
70707
  resolveProvider: resolveSentinelProviderResolved,
69285
70708
  isSessionAlive,
69286
70709
  ...callerSessionId ? { callerSessionId } : {}
@@ -69310,11 +70733,39 @@ async function createGateway(opts) {
69310
70733
  unsubscribe: (params) => eventsUnsubscribe(params, {
69311
70734
  principal: eventsPrincipal,
69312
70735
  store: sentinelStore,
69313
- resolveProvider: resolveSentinelProviderResolved
70736
+ resolveProvider: resolveSentinelProviderResolved,
70737
+ tombstones: sentinelRuntime.cancelTombstones
69314
70738
  })
69315
70739
  });
69316
70740
  return server;
69317
70741
  };
70742
+ const eventsSecret = process.env.AGENTPROTO_MCP_EVENTS_SECRET ?? "";
70743
+ const eventsRepos = (process.env.AGENTPROTO_MCP_EVENTS_REPOS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
70744
+ const eventsMcp = eventsSecret.length >= 32 ? {
70745
+ secret: eventsSecret,
70746
+ allowedOrigins: (process.env.AGENTPROTO_MCP_EVENTS_ORIGINS ?? "").split(",").map((s) => s.trim()).filter(Boolean),
70747
+ createServer: async () => {
70748
+ const principal = sessionPrincipal("mcp-events-origin");
70749
+ return createEventsSurfaceServer({
70750
+ version: opts.version ?? "0.1.0-alpha",
70751
+ repoAllowlist: eventsRepos,
70752
+ handlers: {
70753
+ list: (params) => eventsList(params, { principal }),
70754
+ subscribe: (params) => eventsSubscribe(params, {
70755
+ principal,
70756
+ store: sentinelStore,
70757
+ resolveProvider: resolveSentinelProviderResolved
70758
+ }),
70759
+ unsubscribe: (params) => eventsUnsubscribe(params, {
70760
+ principal,
70761
+ store: sentinelStore,
70762
+ resolveProvider: resolveSentinelProviderResolved,
70763
+ tombstones: sentinelRuntime.cancelTombstones
70764
+ })
70765
+ }
70766
+ });
70767
+ }
70768
+ } : void 0;
69318
70769
  let internalToolServerPromise;
69319
70770
  dispatchToolBox.fn = async (name, inputs) => {
69320
70771
  if (!internalToolServerPromise) internalToolServerPromise = mcpServerFactory();
@@ -69385,6 +70836,7 @@ async function createGateway(opts) {
69385
70836
  },
69386
70837
  mcpServerFactory,
69387
70838
  orchestratorMcpServerFactory,
70839
+ ...eventsMcp ? { eventsMcp } : {},
69388
70840
  verifyOrchestratorScope: scopeTokens.verify,
69389
70841
  conversations,
69390
70842
  events,
@@ -69402,7 +70854,9 @@ async function createGateway(opts) {
69402
70854
  store: sentinelStore,
69403
70855
  resolveProvider: resolveSentinelProviderResolved,
69404
70856
  isSessionAlive,
69405
- runtime: sentinelRuntime
70857
+ runtime: sentinelRuntime,
70858
+ tombstones: sentinelRuntime.cancelTombstones,
70859
+ deliveryStatus: (id) => sentinelRuntime.webhookOutbox.deliveryStatus(id)
69406
70860
  },
69407
70861
  follows: {
69408
70862
  store: sessionFollowStore,
@@ -69595,7 +71049,12 @@ async function createGateway(opts) {
69595
71049
  }, pendingPromptSweepMs);
69596
71050
  pendingPromptTimer.unref?.();
69597
71051
  const livenessSweepIntervalMs = 3e4;
71052
+ const livenessTickGuard = createLivenessTickGuard(livenessSweepIntervalMs, 5e3);
69598
71053
  const livenessSweepTimer = setInterval(() => {
71054
+ if (livenessTickGuard.shouldSkip()) {
71055
+ console.warn("[liveness-sweep] tick fired late (event loop starved) \u2014 skipping this sweep");
71056
+ return;
71057
+ }
69599
71058
  try {
69600
71059
  const orphanedRuns = workflowRunner?.sweep().orphaned ?? [];
69601
71060
  if (orphanedRuns.length > 0) {
@@ -69746,6 +71205,6 @@ var export_readPairingsSnapshot = pairing_registry_exports.readPairingsSnapshot;
69746
71205
  var export_removeProviderKey = providers_store_exports.removeProviderKey;
69747
71206
  var export_setProviderKey = providers_store_exports.setProviderKey;
69748
71207
 
69749
- export { AnthropicRemainingQuotaReader, AuthResolutionError, BUCKETS_ROOT, CLAMP_LIMIT_BYTES, CLAUDE_CODE_OAUTH_SOURCE, CONTINUE_PROMPT_SOURCE, DEAD_SESSION_STATUSES, DEFAULT_ALWAYS_ON_TOOLS, DEFAULT_BUCKET, DEFAULT_CONTINUE_PROMPT, DEFAULT_HOST_LOAD_BUDGET_MS, DEFAULT_HOST_LOAD_THRESHOLDS, DEFAULT_MODEL_ROLES, DEFAULT_ORCHESTRATOR_TOOLS, DEFAULT_TTL_MS, DEFAULT_WORKTREE_ISOLATION, DEFAULT_WORKTREE_PROVISION_CONCURRENCY, DEVICE_PROMPT_PATH_PREFIX, EVENT_REGISTRY, FOLLOW_EVENTS, GC_REAPABLE_STATES, GITHUB_SCHEME, HOSTS_VERSION, HOST_HANDSHAKE_REMEDIATION_HINT, HOST_SCOPE_HEADER, HarnessPresetValidationError, INBOUND_PROVIDERS, INDEX_DEFAULT_LIMIT, INDEX_MAX_OUTPUT, INDEX_MAX_PROMPT, INDEX_TAIL_BYTES, JOIN_TOKENS_VERSION, LEGACY_SESSIONS_FILE, MAX_ATTEMPTS, MAX_AUTO_CONTINUE_ATTEMPTS, MAX_MESSAGE_DATA_BYTES, MAX_TTL_MS, MCP_APP_BRIDGE_SCRIPT, MESSAGE_KINDS, MESSAGE_PREAMBLE, MESSAGE_TAG, MESSAGE_URGENCIES, MIN_TTL_MS, MODEL_ROLE_REF_PREFIX, McpEventsError, NO_OUTPUT_STALL_TURN_ERROR, export_PAIRINGS_VERSION as PAIRINGS_VERSION, POSTURE_NATIVE_ALIASES, POSTURE_PREAMBLES, export_PROVIDER_ENV_VARS as PROVIDER_ENV_VARS, PrLookupError, ResumeDisabledError, SESSION_END_REASONS, SESSION_ID_ENV, SIGN_NAMESPACE, SessionRetiredError, SsrfFetchError, SubscriptionSourceError, TASK_STATUSES, WORKSPACE_SLUG_ENV, WORKTREE_ISOLATION_ENV, WORKTREE_PROVISION_CONCURRENCY_ENV, activityCounts, addHarnessPreset, allowedSignersLine, appendConversationRecord, attachSandbox, backfillSessionIndexes, bucketDir, bucketSessionsFile, bucketTranscriptDir, buildCatalogModels, buildCatalogProviderModels, buildHostLoadReport, buildMcpConfigSnippet, buildRouteAwareLaunchConfig, buildSessionRecap, canonicalForModeId, canonicalizeJcs, challengeCacheSize, challengeReasonForSsrf, claudeProjectSlug, collectGcCandidates, collectHostSample, compactOutcome, composeMode, composeSessionObservers, composedFromRangeSha, computeHostWarnings, computeRefreshBefore, continueInterruptedSessions, continueSkipReason, conversationIndexPath, createActivityProjector, createDaemonReviewerHost, createDefaultHostProbes, createDeviceRegistry, createFileStepCache, createGateway, createHostLoadService, createHostRegistry, createInboundEndpointStore, createJoinTokenRegistry, createOrchestratorInjector, createOrchestratorMcpServerFactory, export_createPairingRegistry as createPairingRegistry, createPrProvenanceReconciler, createProcessStatsService, createReviewLaneExecutor, createReviewLedger, createReviewPackLoader, createReviewRunner, createScopeTokenRegistry, createSentinelWebhookOutbox, createSessionFollowStore, createSessionMessage, createSupervisorTaskGateRunner, createTaskLedger, createWorkspaceBrainSubscriber, createWorkspaceBrains, createWorkspaceFs, credentialFingerprint, daemonBearerPrincipal, daemonRegistryDir, decideWorktreeIsolation, declaredPresetToProviderPreset, decodeWhsecSecret, decomposeMode, defaultBrowserAdapterIds, defaultBrowserAdapterResolution, defaultReviewKeysDir, defaultReviewLedgerRoot, defaultReviewPackCacheDir, defaultTranscriptBaseDir, defineBraveSearchHttpDriver, defineSerperHttpDriver, deleteUserPreset, deliverEventEnvelope, deliveryBackoffDelay, deriveIndexFromTranscript, deriveSessionOutcome, deriveSessionUsage, describeOwner, encodeWhsecSecret, enrichRollupWithProviderQuota, enrichWithRemainingQuota, ensureReviewSigningKey, envelopeClampedBytes, escapeHumanPrompt, escapeMessageBody, evaluateCostBudget, eventsList, eventsSubscribe, eventsUnsubscribe, execGh, extractHelpSection, fetchHostTurns, fetchPrStatus, fileConversationStore, fileExists3 as fileExists, filterActivities, findComposeCandidate, findConversationRecord, findEventDefinition, findNativeMode, findPrForCommit, formatToolCall, formatToolResult, getDefaultHarnessPreset, getHarnessPreset, getHostLoadService, getMcpCredentialDeps, getProcessStatsService, export_getProviderKey as getProviderKey, getToolHelp, getUserPreset, githubRepoOf, harnessPresetsPath, hostPlaceholders, hostRecordToMirrorRecord, indexEntryFromDescriptor, injectMcpAppBridge, export_injectProviderKeysIntoEnv as injectProviderKeysIntoEnv, isAgentCliAuthConfigured, isAutomatedPromptSource, isClosedTaskStatus, isKnownSessionEndReason, isMessageAllowed, isNoSessionPromptFailure, isProviderLimitError, isPubliclyRoutable, isRetired, isSafeBucketSlug, isTerminalActivityState, listBuckets, listHarnessPresets, listModelRoles, listPresets, listToolHelpTopics, listUserPresets, loadHarnessPresets, export_loadProviders as loadProviders, loadQuotaStore, loadUserPresets, loadWorkspaceModelRoles, loadWorktreeIsolation, locateConversationByNativePath, locateConversationBySessionId, makeBrowserAdapterLister, makeRestartForRouting, makeSandboxCredsStore, makeSandboxResolver, matchesMessageFilter, matchesSessionQuery, measureTool, measureToolList, messageFrom, migrateLegacySessionsFile, migrationMarkerPath, mintMessageId, mirrorCursorForSession, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, normalizeCallbackUrl, normalizeCommand, normalizeInbound, normalizeModeId, normalizeRemote, normalizeSkillsOption, normalizeWorktreeField, parseAnthropicRateLimitHeaders, parseDuration, parseModelRoleRef, parsePrUrl, parseTaskStatus, parseWindow, parseWorktreeIsolationMode, pickUsageDetail, policyToActivities, policyWatchesSession, prHeadSha, prToActivities, projectBrowserTools, projectSessionUsage, promptDeviceSession, promptHostSession, export_providerEnvVar as providerEnvVar, export_providersPath as providersPath, readAllSessionIndexes, readConversationIndex, readDaemonRegistry, readHostsSnapshot, export_readPairingsSnapshot as readPairingsSnapshot, readProfileQuota, readRegisteredSlugs, readRuntimeMeta, readSandboxLedger, readSessionForBrain, readSessionIndex, readTranscriptTail, readUsageSnapshots, reapGcEntry, reapOrphanedDescendants, reconcileSandboxLedger, recordProfileQuota, recordSandboxBoot, recordSandboxLiveness, recordSandboxOrigin, recordSandboxState, registerBrainTools, registerBuiltinRoutes, registerDeviceTools, registerPairingTools, registerReviewTools, registerSandboxAttachTool, registerWebSearchTools, removeHarnessPreset, export_removeProviderKey as removeProviderKey, removeSandboxLedgerEntry, renderInboxDigest, renderSessionMessage, renderSessionMessages, repoSlug, resetChallengeCacheForTests, resolveAuthSpec, resolveBucketSlug, resolveDeferredToolsGatewayOption, resolveDevicePromptMapping, resolveEffectiveLlmEndpointFlag, resolveModelRole, resolveNativeLink, resolvePosture, resolvePrincipal, resolveRelation, resolveReuseFromLedger, resolveReviewerPreset, resolveSpawnDefaults, resolveSubscriptionCredential, resolveSuccessor, rollupSessionSubtree, rollupUsage, runContinueOnBootPass, runCrashDetectPass, runEagerResumePass, runIdleReapPass, runShellLane, runStallWatchdogPass, sandboxLedgerPath, sanitizeLabel, saveUserPreset, searchSessionIndexes, seedWalkForTranscript, serializeEnvelope, sessionEventsPath, sessionIndexPath, sessionPrincipal, sessionTranscriptDir, setDefaultPreset, setDefaultSessionsBaseDir, setEgressDispatcherForTests, setMcpCredentialDeps, export_setProviderKey as setProviderKey, signAttestation, signWebhook, ssrfFetch, subscriptionId, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, syncDeviceMirror, tenantScope, toMcpEvent, toWebhookEnvelope, toWorktreeStatusView, trackWorktreeProvision, turnToActivities, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, untilForTtl, updateHarnessPreset, upsertSandboxLedger, userPresetsPath, validateAgainstInputSchema, verifyCallback, verifyInboundSignature, verifySignedAttestation, webSearchTool, webhookRequestId, wireSessionFollow, withPr, withPrStatus, workflowToActivities, writeDaemonRegistryEntry, writeSessionIndex };
71208
+ export { AnthropicRemainingQuotaReader, AuthResolutionError, BUCKETS_ROOT, CLAMP_LIMIT_BYTES, CLAUDE_CODE_OAUTH_SOURCE, CONTINUE_PROMPT_SOURCE, DEAD_SESSION_STATUSES, DEFAULT_ALWAYS_ON_TOOLS, DEFAULT_BUCKET, DEFAULT_CONTINUE_PROMPT, DEFAULT_HOST_LOAD_BUDGET_MS, DEFAULT_HOST_LOAD_THRESHOLDS, DEFAULT_MODEL_ROLES, DEFAULT_ORCHESTRATOR_TOOLS, DEFAULT_TTL_MS, DEFAULT_WORKTREE_ISOLATION, DEFAULT_WORKTREE_PROVISION_CONCURRENCY, DEVICE_PROMPT_PATH_PREFIX, EVENT_REGISTRY, FOLLOW_EVENTS, GC_REAPABLE_STATES, GITHUB_SCHEME, HOSTS_VERSION, HOST_HANDSHAKE_REMEDIATION_HINT, HOST_SCOPE_HEADER, HarnessPresetValidationError, INBOUND_PROVIDERS, INDEX_DEFAULT_LIMIT, INDEX_MAX_OUTPUT, INDEX_MAX_PROMPT, INDEX_TAIL_BYTES, JOIN_TOKENS_VERSION, LEGACY_SESSIONS_FILE, MAX_ATTEMPTS, MAX_AUTO_CONTINUE_ATTEMPTS, MAX_MESSAGE_DATA_BYTES, MAX_TTL_MS, MCP_APP_BRIDGE_SCRIPT, MESSAGE_KINDS, MESSAGE_PREAMBLE, MESSAGE_TAG, MESSAGE_URGENCIES, MIN_TTL_MS, MODEL_ROLE_REF_PREFIX, McpEventsError, NO_OUTPUT_STALL_TURN_ERROR, export_PAIRINGS_VERSION as PAIRINGS_VERSION, POSTURE_NATIVE_ALIASES, POSTURE_PREAMBLES, export_PROVIDER_ENV_VARS as PROVIDER_ENV_VARS, PrLookupError, ResumeDisabledError, SESSION_END_REASONS, SESSION_ID_ENV, SESSION_LIST_FILTER_KEYS, SIGN_NAMESPACE, SentinelBackingExpiredError, SessionListFilterError, SessionRetiredError, SsrfFetchError, SubscriptionSourceError, TASK_STATUSES, WORKSPACE_SLUG_ENV, WORKTREE_ISOLATION_ENV, WORKTREE_PROVISION_CONCURRENCY_ENV, activityCounts, addHarnessPreset, allowedSignersLine, appendConversationRecord, applySessionListFilters, attachSandbox, backfillSessionIndexes, bucketDir, bucketSessionsFile, bucketTranscriptDir, buildCatalogModels, buildCatalogProviderModels, buildHostLoadReport, buildMcpConfigSnippet, buildRouteAwareLaunchConfig, buildSessionRecap, canonicalForModeId, canonicalizeJcs, challengeCacheSize, challengeReasonForSsrf, claudeProjectSlug, collectGcCandidates, collectHostSample, compactOutcome, compileSessionListFilters, composeMode, composeSessionObservers, composedFromRangeSha, computeHostWarnings, computeRefreshBefore, continueInterruptedSessions, continueSkipReason, conversationIndexPath, createActivityProjector, createDaemonReviewerHost, createDefaultHostProbes, createDeviceRegistry, createFileStepCache, createGateway, createHostLoadService, createHostRegistry, createInboundEndpointStore, createJoinTokenRegistry, createOrchestratorInjector, createOrchestratorMcpServerFactory, export_createPairingRegistry as createPairingRegistry, createPrProvenanceReconciler, createProcessStatsService, createReviewLaneExecutor, createReviewLedger, createReviewPackLoader, createReviewRunner, createScopeTokenRegistry, createSentinelWebhookOutbox, createSessionFollowStore, createSessionMessage, createSupervisorTaskGateRunner, createTaskLedger, createWorkspaceBrainSubscriber, createWorkspaceBrains, createWorkspaceFs, credentialFingerprint, daemonBearerPrincipal, daemonRegistryDir, decideWorktreeIsolation, declaredPresetToProviderPreset, decodeWhsecSecret, decomposeMode, defaultBrowserAdapterIds, defaultBrowserAdapterResolution, defaultReviewKeysDir, defaultReviewLedgerRoot, defaultReviewPackCacheDir, defaultTranscriptBaseDir, defineBraveSearchHttpDriver, defineSerperHttpDriver, deleteUserPreset, deliverEventEnvelope, deliveryBackoffDelay, deriveIndexFromTranscript, deriveSessionOutcome, deriveSessionUsage, describeOwner, encodeWhsecSecret, enrichRollupWithProviderQuota, enrichWithRemainingQuota, ensureReviewSigningKey, envelopeClampedBytes, escapeHumanPrompt, escapeMessageBody, evaluateCostBudget, eventsList, eventsSubscribe, eventsUnsubscribe, execGh, extractHelpSection, fetchHostTurns, fetchPrStatus, fileConversationStore, fileExists3 as fileExists, filterActivities, findComposeCandidate, findConversationRecord, findEventDefinition, findNativeMode, findPrForCommit, formatToolCall, formatToolResult, getDefaultHarnessPreset, getHarnessPreset, getHostLoadService, getMcpCredentialDeps, getProcessStatsService, export_getProviderKey as getProviderKey, getToolHelp, getUserPreset, githubRepoOf, harnessPresetsPath, hasSessionListFilters, hostPlaceholders, hostRecordToMirrorRecord, indexEntryFromDescriptor, injectMcpAppBridge, export_injectProviderKeysIntoEnv as injectProviderKeysIntoEnv, isAgentCliAuthConfigured, isAutomatedPromptSource, isClosedTaskStatus, isKnownSessionEndReason, isMessageAllowed, isNoSessionPromptFailure, isNoiseSession, isProviderLimitError, isPubliclyRoutable, isRetired, isSafeBucketSlug, isTerminalActivityState, listBuckets, listHarnessPresets, listModelRoles, listPresets, listToolHelpTopics, listUserPresets, loadHarnessPresets, export_loadProviders as loadProviders, loadQuotaStore, loadUserPresets, loadWorkspaceModelRoles, loadWorktreeIsolation, locateConversationByNativePath, locateConversationBySessionId, makeBrowserAdapterLister, makeRestartForRouting, makeSandboxCredsStore, makeSandboxResolver, matchesMessageFilter, matchesSessionQuery, measureTool, measureToolList, messageFrom, migrateLegacySessionsFile, migrationMarkerPath, mintMessageId, mirrorCursorForSession, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, normalizeCallbackUrl, normalizeCommand, normalizeInbound, normalizeModeId, normalizeRemote, normalizeSkillsOption, normalizeWorktreeField, parseAnthropicRateLimitHeaders, parseDuration, parseModelRoleRef, parsePrUrl, parseSessionListFilterParams, parseTaskStatus, parseTimeBound, parseWindow, parseWorktreeIsolationMode, pickSessionListFilters, pickUsageDetail, policyToActivities, policyWatchesSession, prHeadSha, prToActivities, projectBrowserTools, projectSessionUsage, promptDeviceSession, promptHostSession, export_providerEnvVar as providerEnvVar, export_providersPath as providersPath, readAllSessionIndexes, readConversationIndex, readDaemonRegistry, readHostsSnapshot, export_readPairingsSnapshot as readPairingsSnapshot, readProfileQuota, readRegisteredSlugs, readRuntimeMeta, readSandboxLedger, readSessionForBrain, readSessionIndex, readTranscriptTail, readUsageSnapshots, reapGcEntry, reapOrphanedDescendants, reconcileSandboxLedger, recordProfileQuota, recordSandboxBoot, recordSandboxLiveness, recordSandboxOrigin, recordSandboxState, registerBrainTools, registerBuiltinRoutes, registerDeviceTools, registerPairingTools, registerReviewTools, registerSandboxAttachTool, registerWebSearchTools, removeHarnessPreset, export_removeProviderKey as removeProviderKey, removeSandboxLedgerEntry, renderInboxDigest, renderSessionMessage, renderSessionMessages, repoSlug, resetChallengeCacheForTests, resolveAuthSpec, resolveBucketSlug, resolveDeferredToolsGatewayOption, resolveDevicePromptMapping, resolveEffectiveLlmEndpointFlag, resolveModelRole, resolveNativeLink, resolvePosture, resolvePrincipal, resolveRelation, resolveReuseFromLedger, resolveReviewerPreset, resolveSpawnDefaults, resolveSubscriptionCredential, resolveSuccessor, rollupSessionSubtree, rollupUsage, runContinueOnBootPass, runCrashDetectPass, runEagerResumePass, runIdleReapPass, runShellLane, runStallWatchdogPass, sandboxLedgerPath, sanitizeLabel, saveUserPreset, searchSessionIndexes, seedWalkForTranscript, serializeEnvelope, sessionActivityMs, sessionEventsPath, sessionIndexPath, sessionListFilterShape, sessionPrincipal, sessionTranscriptDir, setDefaultPreset, setDefaultSessionsBaseDir, setEgressDispatcherForTests, setMcpCredentialDeps, export_setProviderKey as setProviderKey, signAttestation, signWebhook, sortNewestActivityFirst, ssrfFetch, subscriptionId, summarizeBranchGcApply, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, syncDeviceMirror, tenantScope, toMcpEvent, toWebhookEnvelope, toWorktreeStatusView, trackWorktreeProvision, turnToActivities, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, untilForTtl, updateHarnessPreset, upsertSandboxLedger, userPresetsPath, validateAgainstInputSchema, verifyCallback, verifyInboundSignature, verifySignedAttestation, webSearchTool, webhookRequestId, wireSessionFollow, withBranchGcApplySummary, withPr, withPrStatus, workflowToActivities, writeDaemonRegistryEntry, writeSessionIndex };
69750
71209
  //# sourceMappingURL=index.mjs.map
69751
71210
  //# sourceMappingURL=index.mjs.map