@agentproto/runtime 0.8.0 → 1.0.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
@@ -12,10 +12,12 @@ import { getModelProvider, resolvePricing } from '@agentproto/model-catalog/llm'
12
12
  import * as providers_store_star from '@agentproto/providers-store';
13
13
  import { makeAdapterLister, makeAdapterResolver, makeCredsStore, discoverAdapterPackages, makeSetupLedger, makeListTool, makeSetupTool } from '@agentproto/provider-kit';
14
14
  import { SandboxSpecSchema, resolveLifecyclePolicy, createSandboxAgentSessionHost } from '@agentproto/sandbox';
15
+ import { eligibleProfiles, getAuthProfile, KeychainStore } from '@agentproto/auth';
15
16
  import matter from 'gray-matter';
16
17
  import { createServer } from 'http';
17
18
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
18
19
  import { WebSocketServer } from 'ws';
20
+ import { resolveLlmModelRoute, formatModelRef, tryParseModelRef } from '@agentproto/model-catalog/route-identity';
19
21
  import { anthropicGatewayPresetList } from '@agentproto/provider-presets';
20
22
  import { createIngestionClient } from '@agentproto/telemetry-langfuse';
21
23
  import { resolveRedactor } from '@agentproto/redaction';
@@ -33,6 +35,7 @@ import { createServer as createServer$1 } from 'net';
33
35
  import { daemonHandshakeOverSink } from '@agentproto/acp/tunnel';
34
36
  import { PairingError, encodeOfferUrl, HOSTED_RENDEZVOUS_URL, decodePairingHello, respondToHandshake, encodePairingMessage, derivePairRoot, deriveEpochRoutingToken, currentEpoch } from '@agentproto/secrets/pairing';
35
37
  import { identityFingerprint } from '@agentproto/secrets/identity';
38
+ import { inferLegacyModeKind } from '@agentproto/driver-agent-cli';
36
39
 
37
40
  /**
38
41
  * @agentproto/runtime v0.1.0-alpha
@@ -445,6 +448,156 @@ var init_transcript_writer = __esm({
445
448
  DEBOUNCE_MS = 250;
446
449
  }
447
450
  });
451
+ function claudeProjectSlug(cwd) {
452
+ return cwd.replace(/[^a-zA-Z0-9]/g, "-");
453
+ }
454
+ function claudeCodeProjectDir(cwd) {
455
+ return resolve(homedir(), ".claude", "projects", claudeProjectSlug(cwd));
456
+ }
457
+ function extractFirstText(content) {
458
+ if (typeof content === "string") {
459
+ const t = content.trim();
460
+ return t || void 0;
461
+ }
462
+ if (Array.isArray(content)) {
463
+ for (const block of content) {
464
+ if (block && typeof block === "object" && block.type === "text" && typeof block.text === "string") {
465
+ const t = block.text.trim();
466
+ if (t) return t;
467
+ }
468
+ }
469
+ }
470
+ return void 0;
471
+ }
472
+ async function scanClaudeJsonl(filePath) {
473
+ const stream = createReadStream(filePath, { encoding: "utf8" });
474
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
475
+ let startedAt;
476
+ let lastActivityAt;
477
+ let messageCount = 0;
478
+ let preview2;
479
+ let lastWriter;
480
+ for await (const line of rl) {
481
+ const trimmed = line.trim();
482
+ if (!trimmed) continue;
483
+ let entry;
484
+ try {
485
+ entry = JSON.parse(trimmed);
486
+ } catch {
487
+ continue;
488
+ }
489
+ if (typeof entry.timestamp === "string") {
490
+ if (!startedAt) startedAt = entry.timestamp;
491
+ lastActivityAt = entry.timestamp;
492
+ }
493
+ if (typeof entry.entrypoint === "string") {
494
+ lastWriter = entry.entrypoint;
495
+ }
496
+ if (entry.type === "user" || entry.type === "assistant") {
497
+ messageCount += 1;
498
+ if (preview2 === void 0 && entry.type === "user") {
499
+ const text6 = extractFirstText(entry.message?.content);
500
+ if (text6 !== void 0) {
501
+ preview2 = text6.length > 120 ? text6.slice(0, 120) : text6;
502
+ }
503
+ }
504
+ }
505
+ }
506
+ return { startedAt, lastActivityAt, messageCount, preview: preview2, lastWriter };
507
+ }
508
+ async function buildClaudeCandidate(filePath, conversationId) {
509
+ const scanned = await scanClaudeJsonl(filePath);
510
+ return { conversationId, ...scanned };
511
+ }
512
+ function claudeEntrypointFor(mode) {
513
+ return mode === "native" ? "cli" : "sdk-ts";
514
+ }
515
+ async function discoverClaudeCode(input) {
516
+ const { cwd, since, until, attachmentMode, expectedId } = input;
517
+ const dir = claudeCodeProjectDir(cwd);
518
+ if (expectedId) {
519
+ const filePath = join(dir, `${expectedId}.jsonl`);
520
+ try {
521
+ await promises.stat(filePath);
522
+ } catch {
523
+ return [];
524
+ }
525
+ return [await buildClaudeCandidate(filePath, expectedId)];
526
+ }
527
+ let entries;
528
+ try {
529
+ entries = await promises.readdir(dir);
530
+ } catch {
531
+ return [];
532
+ }
533
+ const jsonlFiles = entries.filter((e) => e.endsWith(".jsonl"));
534
+ if (jsonlFiles.length === 0) return [];
535
+ const sinceMs = since ? Date.parse(since) : NaN;
536
+ const untilMs = until ? Date.parse(until) : NaN;
537
+ const wantEntrypoint = attachmentMode ? claudeEntrypointFor(attachmentMode) : void 0;
538
+ const scored = [];
539
+ for (const f of jsonlFiles) {
540
+ const filePath = join(dir, f);
541
+ let mtimeMs;
542
+ try {
543
+ mtimeMs = (await promises.stat(filePath)).mtimeMs;
544
+ } catch {
545
+ continue;
546
+ }
547
+ if (Number.isFinite(sinceMs) && mtimeMs < sinceMs - 1e3) continue;
548
+ const conversationId = f.replace(/\.jsonl$/, "");
549
+ const candidate = await buildClaudeCandidate(filePath, conversationId);
550
+ if (Number.isFinite(untilMs) && candidate.startedAt !== void 0) {
551
+ const startedMs = Date.parse(candidate.startedAt);
552
+ if (Number.isFinite(startedMs) && startedMs > untilMs) continue;
553
+ }
554
+ if (wantEntrypoint !== void 0 && candidate.lastWriter !== void 0 && candidate.lastWriter !== wantEntrypoint) {
555
+ continue;
556
+ }
557
+ scored.push({ candidate, mtimeMs });
558
+ }
559
+ scored.sort((a, b) => b.mtimeMs - a.mtimeMs);
560
+ return scored.map((s) => s.candidate);
561
+ }
562
+ async function readClaudeCode(conversationId, cwd) {
563
+ const { exportClaudeCodeSession: exportClaudeCodeSession2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
564
+ return exportClaudeCodeSession2(conversationId, cwd);
565
+ }
566
+ async function discoverHermes(input) {
567
+ const { discoverHermesSessions: discoverHermesSessions2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
568
+ return discoverHermesSessions2(input.cwd, input.since, input.expectedId);
569
+ }
570
+ async function readHermes(conversationId) {
571
+ const { exportHermesSession: exportHermesSession2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
572
+ return exportHermesSession2(conversationId);
573
+ }
574
+ var CONVERSATION_STORES;
575
+ var init_conversation_store = __esm({
576
+ "src/conversation-store.ts"() {
577
+ CONVERSATION_STORES = {
578
+ "claude-code": {
579
+ storeAs: "claudeResumeId",
580
+ // Printed by claude on graceful exit when session persistence is on
581
+ // (default). Example: `claude --resume 0e483f81-1a44-4bec-9667-b37158450296`
582
+ outputHint: /claude\s+--resume\s+([0-9a-f-]{8,})/i,
583
+ attachArgv: (conversationId) => ["claude", "--resume", conversationId],
584
+ discover: discoverClaudeCode,
585
+ read: readClaudeCode
586
+ },
587
+ hermes: {
588
+ storeAs: "hermesResumeId",
589
+ // `hermes acp` is the ACP arm (bin_args in adapters/hermes/src/index.ts);
590
+ // `--resume SESSION --tui` is the native TUI resume path — same binary,
591
+ // different flags, unlike claude-code where the two arms are different
592
+ // binaries. Verified via `hermes --help`: `--resume SESSION, -r` =
593
+ // "Resume a previous session by ID or title", `--tui` = the real TUI.
594
+ attachArgv: (conversationId) => ["hermes", "--resume", conversationId, "--tui"],
595
+ discover: discoverHermes,
596
+ read: readHermes
597
+ }
598
+ };
599
+ }
600
+ });
448
601
 
449
602
  // src/transcript-export.ts
450
603
  var transcript_export_exports = {};
@@ -549,7 +702,7 @@ async function exportClaudeCodeSession(adapterSessionId, cwd) {
549
702
  "claude-code exporter: cwd is required to locate the JSONL file.\nPass cwd explicitly or use a session id that is in the registry."
550
703
  );
551
704
  }
552
- const encoded = cwd.replace(/\//g, "-");
705
+ const encoded = claudeProjectSlug(cwd);
553
706
  const filePath = join(homedir(), ".claude", "projects", encoded, `${adapterSessionId}.jsonl`);
554
707
  let stream;
555
708
  try {
@@ -1057,6 +1210,7 @@ var init_transcript_export = __esm({
1057
1210
  "src/transcript-export.ts"() {
1058
1211
  init_tool_presenter();
1059
1212
  init_transcript_writer();
1213
+ init_conversation_store();
1060
1214
  ROLE_ICON = {
1061
1215
  user: "\u{1F9D1} User",
1062
1216
  assistant: "\u{1F916} Assistant",
@@ -1743,6 +1897,61 @@ function loadWorkspacesConfigSync(path = DEFAULT_CONFIG_PATH()) {
1743
1897
  }
1744
1898
  return normalizeConfig(parsed);
1745
1899
  }
1900
+ async function saveWorkspacesConfig(config, path = DEFAULT_CONFIG_PATH()) {
1901
+ const normalized = normalizeConfig(config);
1902
+ await promises.mkdir(dirname(path), { recursive: true });
1903
+ const tmp = `${path}.tmp.${process.pid}`;
1904
+ await promises.writeFile(tmp, JSON.stringify(normalized, null, 2) + "\n", "utf8");
1905
+ await promises.rename(tmp, path);
1906
+ }
1907
+ function addWorkspace(config, input) {
1908
+ if (!isAbsolute(input.path)) {
1909
+ throw new Error(
1910
+ `addWorkspace: path must be absolute, got "${input.path}".`
1911
+ );
1912
+ }
1913
+ const slug = sanitizeSlug(input.slug);
1914
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1915
+ const existingIdx = config.workspaces.findIndex((w) => w.slug === slug);
1916
+ const next = existingIdx >= 0 ? {
1917
+ ...config.workspaces[existingIdx],
1918
+ path: input.path,
1919
+ ...input.label !== void 0 ? { label: input.label } : {},
1920
+ updatedAt: now
1921
+ } : {
1922
+ slug,
1923
+ path: input.path,
1924
+ ...input.label ? { label: input.label } : {},
1925
+ addedAt: now,
1926
+ updatedAt: now
1927
+ };
1928
+ const workspaces = [...config.workspaces];
1929
+ if (existingIdx >= 0) workspaces[existingIdx] = next;
1930
+ else workspaces.push(next);
1931
+ const active = config.active ?? slug;
1932
+ return { ...config, workspaces, active };
1933
+ }
1934
+ function removeWorkspace(config, slug) {
1935
+ const sanitised = sanitizeSlug(slug);
1936
+ const workspaces = config.workspaces.filter((w) => w.slug !== sanitised);
1937
+ let active = config.active;
1938
+ if (active === sanitised) {
1939
+ active = workspaces[0]?.slug;
1940
+ }
1941
+ const next = { ...config, workspaces };
1942
+ if (active !== void 0) next.active = active;
1943
+ else delete next.active;
1944
+ return next;
1945
+ }
1946
+ function setActiveWorkspace(config, slug) {
1947
+ const sanitised = sanitizeSlug(slug);
1948
+ if (!config.workspaces.some((w) => w.slug === sanitised)) {
1949
+ throw new Error(
1950
+ `setActiveWorkspace: no workspace registered with slug "${sanitised}". Run \`agentproto workspace add <path> --slug ${sanitised}\` first.`
1951
+ );
1952
+ }
1953
+ return { ...config, active: sanitised };
1954
+ }
1746
1955
  function findWorkspace(config, slug) {
1747
1956
  return config.workspaces.find((w) => w.slug === sanitizeSlug(slug));
1748
1957
  }
@@ -2215,6 +2424,8 @@ function getMcpCredentialDeps() {
2215
2424
  // src/sandbox-agent-session-proxy.ts
2216
2425
  var MAX_POLL_MS = 49e3;
2217
2426
  var MAX_OUTPUT_LINES = 500;
2427
+ var MAX_CONSECUTIVE_POLL_FAILURES = 6;
2428
+ var POLL_RETRY_DELAY_MS = 5e3;
2218
2429
  function extractPromptText(message) {
2219
2430
  if (typeof message === "string") return message;
2220
2431
  if (message && typeof message === "object" && "text" in message) {
@@ -2232,29 +2443,63 @@ function createSandboxAgentSessionProxy(opts) {
2232
2443
  async *send(message) {
2233
2444
  const prompt = extractPromptText(message);
2234
2445
  lastPrompt = prompt;
2235
- await host.prompt(remoteSessionId, prompt);
2236
2446
  let seenLength = 0;
2237
- for (; ; ) {
2238
- const result = await host.waitForAny([remoteSessionId], {
2239
- event: "any",
2240
- timeoutMs: MAX_POLL_MS
2241
- });
2242
- if (result.timedOut) continue;
2243
- const tail = await host.output(remoteSessionId, MAX_OUTPUT_LINES);
2244
- if (tail.length > seenLength) {
2245
- yield { kind: "text-delta", text: tail.slice(seenLength) };
2447
+ try {
2448
+ await host.prompt(remoteSessionId, prompt);
2449
+ let consecutivePollFailures = 0;
2450
+ for (; ; ) {
2451
+ let result;
2452
+ try {
2453
+ result = await host.waitForAny([remoteSessionId], {
2454
+ event: "any",
2455
+ timeoutMs: MAX_POLL_MS
2456
+ });
2457
+ consecutivePollFailures = 0;
2458
+ } catch (pollErr) {
2459
+ consecutivePollFailures++;
2460
+ if (consecutivePollFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
2461
+ throw new Error(
2462
+ `sandbox proxy: ${consecutivePollFailures} consecutive poll failures against the box daemon (session "${remoteSessionId}") \u2014 giving up. Last error: ${pollErr instanceof Error ? pollErr.message : String(pollErr)}`
2463
+ );
2464
+ }
2465
+ await new Promise((resolve14) => setTimeout(resolve14, POLL_RETRY_DELAY_MS));
2466
+ continue;
2467
+ }
2468
+ if (result.timedOut) continue;
2469
+ let tail;
2470
+ try {
2471
+ tail = await host.output(remoteSessionId, MAX_OUTPUT_LINES);
2472
+ } catch {
2473
+ try {
2474
+ tail = await host.output(remoteSessionId, MAX_OUTPUT_LINES);
2475
+ } catch {
2476
+ tail = void 0;
2477
+ }
2478
+ }
2479
+ if (tail !== void 0 && tail.length > seenLength) {
2480
+ yield { kind: "text-delta", text: tail.slice(seenLength) };
2481
+ }
2482
+ if (tail !== void 0) seenLength = tail.length;
2483
+ if (result.event === "exited") {
2484
+ throw new Error(
2485
+ `sandbox proxy: remote session "${remoteSessionId}" exited \u2014 the box's own agentproto daemon ended this session (crash, OOM, or an out-of-band kill).`
2486
+ );
2487
+ }
2488
+ yield {
2489
+ kind: "turn-end",
2490
+ reason: result.event === "awaiting-input" ? "awaiting-input" : "completed"
2491
+ };
2492
+ return;
2246
2493
  }
2247
- seenLength = tail.length;
2248
- if (result.event === "exited") {
2249
- throw new Error(
2250
- `sandbox proxy: remote session "${remoteSessionId}" exited \u2014 the box's own agentproto daemon ended this session (crash, OOM, or an out-of-band kill).`
2251
- );
2494
+ } catch (err) {
2495
+ try {
2496
+ const tail = await host.output(remoteSessionId, MAX_OUTPUT_LINES);
2497
+ if (tail.length > seenLength) {
2498
+ yield { kind: "text-delta", text: tail.slice(seenLength) };
2499
+ }
2500
+ } catch {
2252
2501
  }
2253
- yield {
2254
- kind: "turn-end",
2255
- reason: result.event === "awaiting-input" ? "awaiting-input" : "completed"
2256
- };
2257
- return;
2502
+ throw err;
2258
2503
  }
2259
2504
  },
2260
2505
  async cancel() {
@@ -2352,6 +2597,16 @@ async function spawnAgentSession(deps2, input) {
2352
2597
  provisionWorktree,
2353
2598
  resolveWorktreeIsolation
2354
2599
  } = deps2;
2600
+ const explicitCwd = input.cwd !== void 0;
2601
+ const explicitWorkspaceSlug = input.workspaceSlug !== void 0;
2602
+ const childDepth = callerScope ? callerScope.depth + 1 : 0;
2603
+ if (childDepth === 0 && !explicitCwd && !explicitWorkspaceSlug && normalizeWorktreeField(input.worktree) !== void 0) {
2604
+ return {
2605
+ ok: false,
2606
+ code: "worktree_requires_explicit_repo",
2607
+ message: "agent_start: `worktree` isolation was requested but neither `cwd` nor `workspaceSlug` was passed \u2014 refusing to guess the base repo from the daemon's active workspace. Pass `cwd` (an explicit path inside the repo to worktree from) or `workspaceSlug` (a slug from `agentproto workspace list`) to `agent_start`."
2608
+ };
2609
+ }
2355
2610
  let cwd = input.cwd;
2356
2611
  let resolvedSlug = input.workspaceSlug;
2357
2612
  if (!cwd || !resolvedSlug) {
@@ -2388,7 +2643,6 @@ async function spawnAgentSession(deps2, input) {
2388
2643
  message: `agent_start: adapter "${input.adapter}" could not be resolved. If it was working a moment ago, something may be mid-rebuild \u2014 wait and retry. If it has never been installed, run \`agentproto install ${input.adapter}\` first.`
2389
2644
  };
2390
2645
  }
2391
- const childDepth = callerScope ? callerScope.depth + 1 : 0;
2392
2646
  const parentSessionId = callerScope?.ownerSessionId;
2393
2647
  if (callerScope) {
2394
2648
  if (childDepth > callerScope.maxDepth) {
@@ -2430,6 +2684,13 @@ async function spawnAgentSession(deps2, input) {
2430
2684
  return { ok: false, code: "worktree_disabled", message: decision.message };
2431
2685
  }
2432
2686
  if (decision.action === "provision") {
2687
+ if (!explicitCwd && !explicitWorkspaceSlug) {
2688
+ return {
2689
+ ok: false,
2690
+ code: "worktree_requires_explicit_repo",
2691
+ message: "agent_start: `worktree` isolation was requested but neither `cwd` nor `workspaceSlug` was passed \u2014 refusing to guess the base repo from the daemon's active workspace. Pass `cwd` (an explicit path inside the repo to worktree from) or `workspaceSlug` (a slug from `agentproto workspace list`) to `agent_start`."
2692
+ };
2693
+ }
2433
2694
  if (!provisionWorktree) {
2434
2695
  return {
2435
2696
  ok: false,
@@ -2517,9 +2778,10 @@ async function spawnAgentSession(deps2, input) {
2517
2778
  options: input.options,
2518
2779
  auth: input.auth
2519
2780
  });
2781
+ const hasGatewayBaseUrlOption = typeof spawnDefaults.options?.base_url === "string" && spawnDefaults.options.base_url.length > 0;
2520
2782
  let authSpec;
2521
2783
  let authEcho;
2522
- if (resolved && input.sandbox === void 0 && resolved.authDescriptor) {
2784
+ if (resolved && input.sandbox === void 0 && resolved.authDescriptor && !hasGatewayBaseUrlOption) {
2523
2785
  const authModel = input.model ?? resolved.defaultModel;
2524
2786
  const pinnedProvider = spawnDefaults.auth.provider;
2525
2787
  const resolvedProvider = pinnedProvider ?? resolved.authDescriptor.provider ?? (authModel ? getModelProvider(authModel) : void 0);
@@ -2642,7 +2904,14 @@ ${input.prompt}` : input.prompt;
2642
2904
  ...resolvedMcpServers ? { mcpServers: resolvedMcpServers } : {},
2643
2905
  ...input.model ? { model: input.model } : {},
2644
2906
  ...input.effort ? { effort: input.effort } : {},
2645
- ...input.label ? { label: input.label } : {}
2907
+ ...input.label ? { label: input.label } : {},
2908
+ // Explicit billing-auth for the box's OWN agent_start. A fresh box
2909
+ // has no ~/.agentproto/config.json and claude-code never inherits
2910
+ // subscription auth from the shell env — the credential must ride the
2911
+ // spawn call itself. Only the caller's EXPLICIT `auth` is forwarded
2912
+ // (host config defaults stay host-scoped; the box resolves its own
2913
+ // defaults otherwise).
2914
+ ...input.auth ? { auth: input.auth } : {}
2646
2915
  });
2647
2916
  if (!booted.ok) return booted;
2648
2917
  agentSession = booted.agentSession;
@@ -2676,6 +2945,7 @@ ${input.prompt}` : input.prompt;
2676
2945
  agentSession,
2677
2946
  adapterSlug: input.adapter,
2678
2947
  ...input.model ? { model: input.model } : {},
2948
+ ...input.mode ? { mode: input.mode } : {},
2679
2949
  ...input.wait && effectivePrompt ? {} : effectivePrompt ? { initialPrompt: effectivePrompt } : {},
2680
2950
  ...input.label ? { label: input.label } : {},
2681
2951
  ...resolvedMcpServers ? { mcpServers: resolvedMcpServers } : {},
@@ -2826,7 +3096,8 @@ async function bootSandboxAgentSession(opts) {
2826
3096
  ...opts.mcpServers ? { mcpServers: toMcpServerMounts(opts.mcpServers) } : {},
2827
3097
  ...opts.model ? { model: opts.model } : {},
2828
3098
  ...opts.effort ? { effort: opts.effort } : {},
2829
- ...opts.label ? { label: opts.label } : {}
3099
+ ...opts.label ? { label: opts.label } : {},
3100
+ ...opts.auth ? { auth: opts.auth } : {}
2830
3101
  });
2831
3102
  remoteSessionId = remoteDesc.id;
2832
3103
  } catch (err) {
@@ -2857,6 +3128,60 @@ async function resolveSandboxSecret(slug) {
2857
3128
  return null;
2858
3129
  }
2859
3130
  }
3131
+
3132
+ // src/canonical-posture.ts
3133
+ var POSTURE_PREAMBLES = {
3134
+ plan: "You are in PLAN mode. Investigate and propose a concrete plan for the user to approve; do NOT edit files, run commands, or make any other changes until the plan is explicitly approved. Reading and searching are fine.",
3135
+ "accept-edits": "You are in ACCEPT-EDITS mode. File edits are auto-approved, so apply them directly without pausing for confirmation on each one; commands and other actions still warrant the usual care.",
3136
+ bypass: "You are in BYPASS-PERMISSIONS mode. No approval prompts will interrupt you \u2014 every file edit and command runs without confirmation. Be deliberate and careful: there is no safety prompt between you and a destructive action.",
3137
+ "read-only": "You are in READ-ONLY mode. You may read, search, and analyze, but you must NOT edit files, run commands that mutate state, or make any other changes. Answer and advise only."
3138
+ };
3139
+ var POSTURE_NATIVE_ALIASES = {
3140
+ default: ["default", "build", "normal", "standard"],
3141
+ plan: ["plan", "planning", "plan-mode"],
3142
+ "accept-edits": ["accept-edits", "auto-accept", "auto-edit"],
3143
+ bypass: ["bypass", "bypass-permissions", "full-access", "yolo", "dangerously-skip-permissions"],
3144
+ "read-only": ["read-only", "chat", "ask"]
3145
+ };
3146
+ var CANONICAL_POSTURES = Object.keys(
3147
+ POSTURE_NATIVE_ALIASES
3148
+ );
3149
+ var CANONICAL_POSTURE_SET = new Set(CANONICAL_POSTURES);
3150
+ function isCanonicalPosture(value) {
3151
+ return CANONICAL_POSTURE_SET.has(value);
3152
+ }
3153
+ function parsePostureInput(raw) {
3154
+ return isCanonicalPosture(raw) ? raw : { harnessModeId: raw };
3155
+ }
3156
+ function normalizeModeId(id) {
3157
+ return id.toLowerCase().replace(/[^a-z0-9]/g, "");
3158
+ }
3159
+ var CANONICAL_BY_NORMALIZED_ALIAS = (() => {
3160
+ const index = /* @__PURE__ */ new Map();
3161
+ for (const [posture, aliases] of Object.entries(POSTURE_NATIVE_ALIASES)) {
3162
+ for (const alias of aliases) index.set(normalizeModeId(alias), posture);
3163
+ }
3164
+ return index;
3165
+ })();
3166
+ function canonicalForModeId(modeId) {
3167
+ return CANONICAL_BY_NORMALIZED_ALIAS.get(normalizeModeId(modeId));
3168
+ }
3169
+ function findNativeMode(posture, availableModes) {
3170
+ if (typeof posture === "object") {
3171
+ const target = normalizeModeId(posture.harnessModeId);
3172
+ return availableModes.find((mode) => normalizeModeId(mode.id) === target);
3173
+ }
3174
+ return availableModes.find((mode) => canonicalForModeId(mode.id) === posture);
3175
+ }
3176
+ function resolvePosture(posture, availableModes) {
3177
+ const native = findNativeMode(posture, availableModes);
3178
+ if (native) return { kind: "native", mode: native };
3179
+ if (typeof posture === "object") {
3180
+ return { kind: "unavailable", requestedModeId: posture.harnessModeId };
3181
+ }
3182
+ if (posture === "default") return { kind: "noop", posture: "default" };
3183
+ return { kind: "prompt", posture, preamble: POSTURE_PREAMBLES[posture] };
3184
+ }
2860
3185
  var sandboxSpecWithReuseSchema = z.object({
2861
3186
  ...SandboxSpecSchema.shape,
2862
3187
  reuse: z.string().min(1).optional().describe(
@@ -2897,6 +3222,7 @@ function registerAgentTools(server, opts) {
2897
3222
  registry,
2898
3223
  resolveAgentAdapter,
2899
3224
  listAgentAdapters,
3225
+ listCatalogModels,
2900
3226
  buildOrchestratorMcp,
2901
3227
  callerScope,
2902
3228
  webhookNotifier,
@@ -3209,7 +3535,7 @@ function registerAgentTools(server, opts) {
3209
3535
  if (callerScope) {
3210
3536
  const subtree = collectSubtree(
3211
3537
  callerScope.ownerSessionId,
3212
- registry.list()
3538
+ registry.list({ includeArchived: true })
3213
3539
  );
3214
3540
  if (!subtree.has(sessionId)) {
3215
3541
  return {
@@ -3277,66 +3603,32 @@ function registerAgentTools(server, opts) {
3277
3603
  }
3278
3604
  );
3279
3605
  server.tool(
3280
- "agent_sessions_list",
3281
- "List agent-CLI sessions tracked by the daemon. Equivalent to `session_list({kind: 'agent-cli'})`. Each entry includes `kind`, `status`, age, etc. Use this when you only want the agent-CLI subset.",
3606
+ "agent_set_model",
3607
+ "Switch the model on a LIVE agent-cli session without restarting it \u2014 the mid-session counterpart to picking a model at `agent_start` time. Dispatches on the adapter's own apply strategy: a session whose adapter selects models via ACP session config or a `/model` control turn switches live; one that takes its model as a spawn-time CLI argument (e.g. codex) can't, and reports `{applied:false, reason:\"requires-restart\"}` instead of failing. Never throws on a rejected switch \u2014 check `applied` in the result.",
3282
3608
  {
3283
- kind: z.enum(["terminal", "agent-cli", "command", "all"]).optional().describe(
3284
- "Optional override of the default `agent-cli` filter. `all` returns every kind."
3285
- ),
3286
- onlyAlive: z.boolean().optional().describe("When true, only running/starting sessions. Default false."),
3287
- status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive).")
3609
+ sessionId: sessionIdField,
3610
+ id: sessionIdAliasField,
3611
+ model: z.string().describe("Model id to switch to.")
3288
3612
  },
3289
3613
  async (input) => {
3290
- let rows = registry.list();
3291
- if (callerScope) {
3292
- const subtree = collectSubtree(callerScope.ownerSessionId, rows);
3293
- rows = rows.filter((s) => subtree.has(s.id));
3294
- }
3295
- const kind = input.kind ?? "agent-cli";
3296
- if (kind !== "all") {
3297
- rows = rows.filter((s) => s.kind === kind);
3298
- }
3299
- if (input.status) {
3300
- rows = rows.filter((s) => s.status === input.status);
3301
- } else if (input.onlyAlive) {
3302
- rows = rows.filter(
3303
- (s) => s.status === "running" || s.status === "starting"
3304
- );
3305
- }
3306
- return {
3307
- content: [
3308
- { type: "text", text: JSON.stringify({ sessions: rows }, null, 2) }
3309
- ]
3310
- };
3311
- }
3312
- );
3313
- server.tool(
3314
- "adapter_list",
3315
- "Enumerate every agent CLI adapter installed on the host (claude-code, hermes, aider, \u2026). Returns slug + display name + version + protocol so callers can let users pick from the installed set instead of guessing. Use before `agent_start` when the model doesn't already know what's available.",
3316
- {},
3317
- async () => {
3318
- if (!listAgentAdapters) {
3614
+ const sessionId = resolveSessionIdArg(input);
3615
+ if (!sessionId) return missingSessionIdError("agent_set_model");
3616
+ try {
3617
+ const result = await registry.setModel(sessionId, input.model);
3319
3618
  return {
3320
3619
  content: [
3321
3620
  {
3322
3621
  type: "text",
3323
- text: "adapter_list is not enabled \u2014 the daemon was started without an adapter lister. Wire `@agentproto/cli`'s `listInstalledAdapters` via `createGateway({ listAgentAdapters })`."
3622
+ text: JSON.stringify({ ok: true, sessionId, ...result }, null, 2)
3324
3623
  }
3325
- ],
3326
- isError: true
3327
- };
3328
- }
3329
- try {
3330
- const adapters = await listAgentAdapters();
3331
- return {
3332
- content: [{ type: "text", text: JSON.stringify({ adapters }, null, 2) }]
3624
+ ]
3333
3625
  };
3334
3626
  } catch (err) {
3335
3627
  return {
3336
3628
  content: [
3337
3629
  {
3338
3630
  type: "text",
3339
- text: `adapter_list failed: ${err instanceof Error ? err.message : String(err)}`
3631
+ text: `agent_set_model: ${err instanceof Error ? err.message : String(err)}`
3340
3632
  }
3341
3633
  ],
3342
3634
  isError: true
@@ -3345,13 +3637,198 @@ function registerAgentTools(server, opts) {
3345
3637
  }
3346
3638
  );
3347
3639
  server.tool(
3348
- "role_list",
3349
- "Enumerate every spawn-time role known to the daemon \u2014 the two built-ins (executor, supervisor) plus any custom role installed as a role pack. Read-only: pure visibility into the same registry `agent_start`'s `role` field and privilege-lattice spawn gate use \u2014 this tool never itself grants or denies a spawn. Use before `agent_start` with `orchestrator` to discover which roles this session may in turn spawn.",
3350
- {},
3351
- async () => {
3640
+ "agent_set_effort",
3641
+ "Switch the reasoning/compute budget (effort) on a LIVE agent-cli session without restarting it \u2014 the effort-axis counterpart to `agent_set_model`. Applied via the adapter's ACP session config. Effort is model-dependent: the same label means a different budget across models and some labels are model-gated (opus offers `ultracode`, haiku doesn't), so a label the current model rejects reports `{applied:false, reason}` instead of failing. Never throws on a rejected switch \u2014 check `applied` in the result.",
3642
+ {
3643
+ sessionId: sessionIdField,
3644
+ id: sessionIdAliasField,
3645
+ effort: z.string().describe(
3646
+ "Effort label to switch to (e.g. low/medium/high/xhigh/max/ultracode; the accepted set is model-dependent)."
3647
+ )
3648
+ },
3649
+ async (input) => {
3650
+ const sessionId = resolveSessionIdArg(input);
3651
+ if (!sessionId) return missingSessionIdError("agent_set_effort");
3352
3652
  try {
3353
- const registry2 = loadRoleRegistry2 ? await loadRoleRegistry2() : await loadDefaultRoleRegistry();
3354
- const roles = listRoles(registry2).map((role) => ({
3653
+ const result = await registry.setEffort(sessionId, input.effort);
3654
+ return {
3655
+ content: [
3656
+ {
3657
+ type: "text",
3658
+ text: JSON.stringify({ ok: true, sessionId, ...result }, null, 2)
3659
+ }
3660
+ ]
3661
+ };
3662
+ } catch (err) {
3663
+ return {
3664
+ content: [
3665
+ {
3666
+ type: "text",
3667
+ text: `agent_set_effort: ${err instanceof Error ? err.message : String(err)}`
3668
+ }
3669
+ ],
3670
+ isError: true
3671
+ };
3672
+ }
3673
+ }
3674
+ );
3675
+ server.tool(
3676
+ "agent_set_posture",
3677
+ 'Switch the posture (what the agent may DO \u2014 plan / accept-edits / bypass / read-only, or a raw harness mode id) on a LIVE agent-cli session. When the posture maps to a NATIVE mode the harness advertises, it switches live (`applied:true`). When there is no native mode (the posture would have to be prompt-injected or applied at spawn), it is NOT forced live \u2014 the result is `{applied:false, reason:"requires-restart"}` so the caller can re-apply it through a session restart instead. Never throws on a rejected switch \u2014 check `applied`.',
3678
+ {
3679
+ sessionId: sessionIdField,
3680
+ id: sessionIdAliasField,
3681
+ posture: z.string().describe(
3682
+ "Posture to switch to: a canonical value (default/plan/accept-edits/bypass/read-only) or a raw harness mode id."
3683
+ )
3684
+ },
3685
+ async (input) => {
3686
+ const sessionId = resolveSessionIdArg(input);
3687
+ if (!sessionId) return missingSessionIdError("agent_set_posture");
3688
+ try {
3689
+ const result = await registry.setPosture(sessionId, parsePostureInput(input.posture));
3690
+ return {
3691
+ content: [
3692
+ {
3693
+ type: "text",
3694
+ text: JSON.stringify({ ok: true, sessionId, ...result }, null, 2)
3695
+ }
3696
+ ]
3697
+ };
3698
+ } catch (err) {
3699
+ return {
3700
+ content: [
3701
+ {
3702
+ type: "text",
3703
+ text: `agent_set_posture: ${err instanceof Error ? err.message : String(err)}`
3704
+ }
3705
+ ],
3706
+ isError: true
3707
+ };
3708
+ }
3709
+ }
3710
+ );
3711
+ server.tool(
3712
+ "agent_sessions_list",
3713
+ "List agent-CLI sessions tracked by the daemon. Equivalent to `session_list({kind: 'agent-cli'})`. Each entry includes `kind`, `status`, age, etc. Use this when you only want the agent-CLI subset.",
3714
+ {
3715
+ kind: z.enum(["terminal", "agent-cli", "command", "all"]).optional().describe(
3716
+ "Optional override of the default `agent-cli` filter. `all` returns every kind."
3717
+ ),
3718
+ onlyAlive: z.boolean().optional().describe("When true, only running/starting sessions. Default false."),
3719
+ status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive).")
3720
+ },
3721
+ async (input) => {
3722
+ let rows = registry.list({ includeArchived: true });
3723
+ if (callerScope) {
3724
+ const subtree = collectSubtree(callerScope.ownerSessionId, rows);
3725
+ rows = rows.filter((s) => subtree.has(s.id));
3726
+ }
3727
+ rows = rows.filter((s) => !s.archived);
3728
+ const kind = input.kind ?? "agent-cli";
3729
+ if (kind !== "all") {
3730
+ rows = rows.filter((s) => s.kind === kind);
3731
+ }
3732
+ if (input.status) {
3733
+ rows = rows.filter((s) => s.status === input.status);
3734
+ } else if (input.onlyAlive) {
3735
+ rows = rows.filter(
3736
+ (s) => s.status === "running" || s.status === "starting"
3737
+ );
3738
+ }
3739
+ return {
3740
+ content: [
3741
+ { type: "text", text: JSON.stringify({ sessions: rows }, null, 2) }
3742
+ ]
3743
+ };
3744
+ }
3745
+ );
3746
+ server.tool(
3747
+ "adapter_list",
3748
+ "Enumerate every agent CLI adapter installed on the host (claude-code, hermes, aider, \u2026). Returns slug + display name + version + protocol so callers can let users pick from the installed set instead of guessing. Use before `agent_start` when the model doesn't already know what's available.",
3749
+ {},
3750
+ async () => {
3751
+ if (!listAgentAdapters) {
3752
+ return {
3753
+ content: [
3754
+ {
3755
+ type: "text",
3756
+ text: "adapter_list is not enabled \u2014 the daemon was started without an adapter lister. Wire `@agentproto/cli`'s `listInstalledAdapters` via `createGateway({ listAgentAdapters })`."
3757
+ }
3758
+ ],
3759
+ isError: true
3760
+ };
3761
+ }
3762
+ try {
3763
+ const adapters = await listAgentAdapters();
3764
+ return {
3765
+ content: [{ type: "text", text: JSON.stringify({ adapters }, null, 2) }]
3766
+ };
3767
+ } catch (err) {
3768
+ return {
3769
+ content: [
3770
+ {
3771
+ type: "text",
3772
+ text: `adapter_list failed: ${err instanceof Error ? err.message : String(err)}`
3773
+ }
3774
+ ],
3775
+ isError: true
3776
+ };
3777
+ }
3778
+ }
3779
+ );
3780
+ server.tool(
3781
+ "catalog_models",
3782
+ "Read-only vendor/product/route catalog (SPEC \xA75) \u2014 every model this host can reach, widened beyond any one adapter's model list via OpenRouter/Requesty/HuggingFace routing, with a profile-aware `runnable` flag per route. Use before `agent_start` to see what's actually spawnable given the auth profiles configured on this host.",
3783
+ {
3784
+ adapter: z.string().optional().describe("Keep only routes reachable via this adapter slug."),
3785
+ vendor: z.string().optional().describe("Keep only this vendor's entry."),
3786
+ route: z.string().optional().describe("Keep only routes with this route id."),
3787
+ runnableOnly: mcpBool.optional().describe("Drop every route with runnable:false.")
3788
+ },
3789
+ async ({ adapter, vendor, route, runnableOnly }) => {
3790
+ if (!listCatalogModels) {
3791
+ return {
3792
+ content: [
3793
+ {
3794
+ type: "text",
3795
+ text: "catalog_models is not enabled \u2014 the daemon was started without a catalog lister. Wire `buildCatalogModels` via `createGateway({ listCatalogModels })`."
3796
+ }
3797
+ ],
3798
+ isError: true
3799
+ };
3800
+ }
3801
+ try {
3802
+ const catalog = await listCatalogModels({
3803
+ ...adapter ? { adapter } : {},
3804
+ ...vendor ? { vendor } : {},
3805
+ ...route ? { route } : {},
3806
+ ...runnableOnly ? { runnableOnly: true } : {}
3807
+ });
3808
+ return {
3809
+ content: [{ type: "text", text: JSON.stringify(catalog, null, 2) }]
3810
+ };
3811
+ } catch (err) {
3812
+ return {
3813
+ content: [
3814
+ {
3815
+ type: "text",
3816
+ text: `catalog_models failed: ${err instanceof Error ? err.message : String(err)}`
3817
+ }
3818
+ ],
3819
+ isError: true
3820
+ };
3821
+ }
3822
+ }
3823
+ );
3824
+ server.tool(
3825
+ "role_list",
3826
+ "Enumerate every spawn-time role known to the daemon \u2014 the two built-ins (executor, supervisor) plus any custom role installed as a role pack. Read-only: pure visibility into the same registry `agent_start`'s `role` field and privilege-lattice spawn gate use \u2014 this tool never itself grants or denies a spawn. Use before `agent_start` with `orchestrator` to discover which roles this session may in turn spawn.",
3827
+ {},
3828
+ async () => {
3829
+ try {
3830
+ const registry2 = loadRoleRegistry2 ? await loadRoleRegistry2() : await loadDefaultRoleRegistry();
3831
+ const roles = listRoles(registry2).map((role) => ({
3355
3832
  name: role.name,
3356
3833
  level: role.level,
3357
3834
  delegation: role.toolPolicy.delegation,
@@ -3655,151 +4132,9 @@ function stringifyValues(raw) {
3655
4132
  }
3656
4133
  return out;
3657
4134
  }
3658
- function claudeCodeProjectDir(cwd) {
3659
- const encoded = cwd.replace(/\//g, "-");
3660
- return resolve(homedir(), ".claude", "projects", encoded);
3661
- }
3662
- function extractFirstText(content) {
3663
- if (typeof content === "string") {
3664
- const t = content.trim();
3665
- return t || void 0;
3666
- }
3667
- if (Array.isArray(content)) {
3668
- for (const block of content) {
3669
- if (block && typeof block === "object" && block.type === "text" && typeof block.text === "string") {
3670
- const t = block.text.trim();
3671
- if (t) return t;
3672
- }
3673
- }
3674
- }
3675
- return void 0;
3676
- }
3677
- async function scanClaudeJsonl(filePath) {
3678
- const stream = createReadStream(filePath, { encoding: "utf8" });
3679
- const rl = createInterface({ input: stream, crlfDelay: Infinity });
3680
- let startedAt;
3681
- let lastActivityAt;
3682
- let messageCount = 0;
3683
- let preview2;
3684
- let lastWriter;
3685
- for await (const line of rl) {
3686
- const trimmed = line.trim();
3687
- if (!trimmed) continue;
3688
- let entry;
3689
- try {
3690
- entry = JSON.parse(trimmed);
3691
- } catch {
3692
- continue;
3693
- }
3694
- if (typeof entry.timestamp === "string") {
3695
- if (!startedAt) startedAt = entry.timestamp;
3696
- lastActivityAt = entry.timestamp;
3697
- }
3698
- if (typeof entry.entrypoint === "string") {
3699
- lastWriter = entry.entrypoint;
3700
- }
3701
- if (entry.type === "user" || entry.type === "assistant") {
3702
- messageCount += 1;
3703
- if (preview2 === void 0 && entry.type === "user") {
3704
- const text6 = extractFirstText(entry.message?.content);
3705
- if (text6 !== void 0) {
3706
- preview2 = text6.length > 120 ? text6.slice(0, 120) : text6;
3707
- }
3708
- }
3709
- }
3710
- }
3711
- return { startedAt, lastActivityAt, messageCount, preview: preview2, lastWriter };
3712
- }
3713
- async function buildClaudeCandidate(filePath, conversationId) {
3714
- const scanned = await scanClaudeJsonl(filePath);
3715
- return { conversationId, ...scanned };
3716
- }
3717
- function claudeEntrypointFor(mode) {
3718
- return mode === "native" ? "cli" : "sdk-ts";
3719
- }
3720
- async function discoverClaudeCode(input) {
3721
- const { cwd, since, until, attachmentMode, expectedId } = input;
3722
- const dir = claudeCodeProjectDir(cwd);
3723
- if (expectedId) {
3724
- const filePath = join(dir, `${expectedId}.jsonl`);
3725
- try {
3726
- await promises.stat(filePath);
3727
- } catch {
3728
- return [];
3729
- }
3730
- return [await buildClaudeCandidate(filePath, expectedId)];
3731
- }
3732
- let entries;
3733
- try {
3734
- entries = await promises.readdir(dir);
3735
- } catch {
3736
- return [];
3737
- }
3738
- const jsonlFiles = entries.filter((e) => e.endsWith(".jsonl"));
3739
- if (jsonlFiles.length === 0) return [];
3740
- const sinceMs = since ? Date.parse(since) : NaN;
3741
- const untilMs = until ? Date.parse(until) : NaN;
3742
- const wantEntrypoint = attachmentMode ? claudeEntrypointFor(attachmentMode) : void 0;
3743
- const scored = [];
3744
- for (const f of jsonlFiles) {
3745
- const filePath = join(dir, f);
3746
- let mtimeMs;
3747
- try {
3748
- mtimeMs = (await promises.stat(filePath)).mtimeMs;
3749
- } catch {
3750
- continue;
3751
- }
3752
- if (Number.isFinite(sinceMs) && mtimeMs < sinceMs - 1e3) continue;
3753
- const conversationId = f.replace(/\.jsonl$/, "");
3754
- const candidate = await buildClaudeCandidate(filePath, conversationId);
3755
- if (Number.isFinite(untilMs) && candidate.startedAt !== void 0) {
3756
- const startedMs = Date.parse(candidate.startedAt);
3757
- if (Number.isFinite(startedMs) && startedMs > untilMs) continue;
3758
- }
3759
- if (wantEntrypoint !== void 0 && candidate.lastWriter !== void 0 && candidate.lastWriter !== wantEntrypoint) {
3760
- continue;
3761
- }
3762
- scored.push({ candidate, mtimeMs });
3763
- }
3764
- scored.sort((a, b) => b.mtimeMs - a.mtimeMs);
3765
- return scored.map((s) => s.candidate);
3766
- }
3767
- async function readClaudeCode(conversationId, cwd) {
3768
- const { exportClaudeCodeSession: exportClaudeCodeSession2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
3769
- return exportClaudeCodeSession2(conversationId, cwd);
3770
- }
3771
- async function discoverHermes(input) {
3772
- const { discoverHermesSessions: discoverHermesSessions2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
3773
- return discoverHermesSessions2(input.cwd, input.since, input.expectedId);
3774
- }
3775
- async function readHermes(conversationId) {
3776
- const { exportHermesSession: exportHermesSession2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
3777
- return exportHermesSession2(conversationId);
3778
- }
3779
- var CONVERSATION_STORES = {
3780
- "claude-code": {
3781
- storeAs: "claudeResumeId",
3782
- // Printed by claude on graceful exit when session persistence is on
3783
- // (default). Example: `claude --resume 0e483f81-1a44-4bec-9667-b37158450296`
3784
- outputHint: /claude\s+--resume\s+([0-9a-f-]{8,})/i,
3785
- attachArgv: (conversationId) => ["claude", "--resume", conversationId],
3786
- discover: discoverClaudeCode,
3787
- read: readClaudeCode
3788
- },
3789
- hermes: {
3790
- storeAs: "hermesResumeId",
3791
- // `hermes acp` is the ACP arm (bin_args in adapters/hermes/src/index.ts);
3792
- // `--resume SESSION --tui` is the native TUI resume path — same binary,
3793
- // different flags, unlike claude-code where the two arms are different
3794
- // binaries. Verified via `hermes --help`: `--resume SESSION, -r` =
3795
- // "Resume a previous session by ID or title", `--tui` = the real TUI.
3796
- attachArgv: (conversationId) => ["hermes", "--resume", conversationId, "--tui"],
3797
- discover: discoverHermes,
3798
- read: readHermes
3799
- }
3800
- };
3801
4135
 
3802
4136
  // src/resume-strategies.ts
4137
+ init_conversation_store();
3803
4138
  var claudeCodeStore = CONVERSATION_STORES["claude-code"];
3804
4139
  var RESUME_STRATEGIES = {
3805
4140
  "claude-code": {
@@ -3907,6 +4242,46 @@ function tokenizeCommand(s) {
3907
4242
  if (buf) out.push(buf);
3908
4243
  return out;
3909
4244
  }
4245
+ var RestartOverrideError = class extends Error {
4246
+ code = "restart_override_invalid";
4247
+ status = 400;
4248
+ constructor(message) {
4249
+ super(message);
4250
+ this.name = "RestartOverrideError";
4251
+ }
4252
+ };
4253
+ async function resolveAccessProfileFromStore(profileRef) {
4254
+ const profile = await getAuthProfile(profileRef);
4255
+ if (!profile) return void 0;
4256
+ const stored = await new KeychainStore().read({ path: profile.credentialRef });
4257
+ return { profile, ...stored?.value !== void 0 ? { credential: stored.value } : {} };
4258
+ }
4259
+ function methodToMode(method) {
4260
+ return method === "oauth-bearer" ? "subscription" : "api-key";
4261
+ }
4262
+ function directMethods(descriptor) {
4263
+ const methods = [];
4264
+ if (descriptor?.authSubscription) methods.push("oauth-bearer");
4265
+ if (descriptor?.provider) methods.push("api-key");
4266
+ return methods;
4267
+ }
4268
+ function eligibilityManifest(adapterSlug, descriptor, route, model) {
4269
+ const baseVendor = descriptor?.provider ?? (model ? getModelProvider(model) : void 0);
4270
+ const gateway = route?.gateway;
4271
+ const routeId = gateway ?? baseVendor;
4272
+ if (routeId === void 0) return void 0;
4273
+ const isDirect = baseVendor !== void 0 && routeId === baseVendor;
4274
+ const billedVendor2 = isDirect ? baseVendor : routeId;
4275
+ const methods = isDirect ? directMethods(descriptor) : ["api-key"];
4276
+ return {
4277
+ manifest: {
4278
+ id: adapterSlug,
4279
+ vendorByRoute: { [routeId]: billedVendor2 },
4280
+ methodsByRoute: { [routeId]: methods }
4281
+ },
4282
+ routeId
4283
+ };
4284
+ }
3910
4285
  async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {}) {
3911
4286
  const augmented = opts.forceAgentResume ? prev : await augmentWithFsResume(prev);
3912
4287
  const strategy = opts.forceAgentResume ? { kind: "agent", resumeSessionId: prev.adapterSessionId } : decideRestartStrategy(augmented);
@@ -3928,15 +4303,80 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
3928
4303
  let cwd = prev.cwd;
3929
4304
  if (!cwd) console.warn(`[restartAgentSession] no cwd on prior descriptor ${prev.id} \u2014 falling back to daemon's cwd ${process.cwd()}`);
3930
4305
  cwd ??= process.cwd();
4306
+ const overrides = opts.overrides ?? {};
4307
+ const effModel = overrides.model ?? prev.model;
4308
+ const effEffort = overrides.effort ?? prev.effort;
4309
+ const effRoute = overrides.route ?? prev.route;
4310
+ const effPosture = overrides.posture ?? prev.posture;
4311
+ const effContextProfile = overrides.contextProfile ?? prev.contextProfile;
4312
+ const effMode = overrides.mode ?? prev.mode;
4313
+ const accessOverrideRef = overrides.access?.profileRef;
3931
4314
  let authSpec;
3932
4315
  let authEcho;
3933
- if (resolved.authDescriptor) {
4316
+ let accessProfileEcho = prev.accessProfile;
4317
+ if (accessOverrideRef !== void 0) {
4318
+ const resolveProfile = opts.resolveAccessProfile ?? resolveAccessProfileFromStore;
4319
+ const found = await resolveProfile(accessOverrideRef);
4320
+ if (!found) {
4321
+ throw new RestartOverrideError(
4322
+ `restart access override: no auth profile "${accessOverrideRef}" found.`
4323
+ );
4324
+ }
4325
+ const { profile, credential } = found;
4326
+ if (!resolved.authDescriptor) {
4327
+ throw new RestartOverrideError(
4328
+ `restart access override: adapter "${adapterSlug}" presents no billing-auth, so profile "${profile.id}" cannot be attached.`
4329
+ );
4330
+ }
4331
+ const projected = eligibilityManifest(
4332
+ adapterSlug,
4333
+ resolved.authDescriptor,
4334
+ effRoute,
4335
+ effModel ?? resolved.defaultModel
4336
+ );
4337
+ if (!projected) {
4338
+ throw new RestartOverrideError(
4339
+ `restart access override: cannot resolve a billing vendor for adapter "${adapterSlug}" (no fixed provider, no model) \u2014 profile "${profile.id}" eligibility is unverifiable, refusing to spawn.`
4340
+ );
4341
+ }
4342
+ const { manifest, routeId } = projected;
4343
+ if (eligibleProfiles([profile], manifest, routeId).length === 0) {
4344
+ const billed = manifest.vendorByRoute[routeId];
4345
+ const methods = manifest.methodsByRoute[routeId] ?? [];
4346
+ throw new RestartOverrideError(
4347
+ `restart access override: profile "${profile.id}" (${profile.vendor}/${profile.method}) is not eligible for adapter "${adapterSlug}" on route "${routeId}" \u2014 that endpoint bills "${billed}" via [${methods.join(", ") || "no presentable methods"}]. Attach a profile whose vendor + method match the route.`
4348
+ );
4349
+ }
4350
+ const mode = methodToMode(profile.method);
4351
+ const result = resolveAuthSpec({
4352
+ descriptor: resolved.authDescriptor,
4353
+ ...effModel ? { model: effModel } : {},
4354
+ requestedProvider: profile.vendor,
4355
+ requestedMode: mode,
4356
+ // Attaching a named profile is always an EXPLICIT billing choice — so a
4357
+ // missing credential fails loud (driver `missing_auth_credential`) rather
4358
+ // than falling back to ambient env or the prior credential.
4359
+ explicit: true,
4360
+ ...mode === "subscription" && credential !== void 0 ? { subscriptionCredential: credential } : {},
4361
+ ...mode === "api-key" && credential !== void 0 ? { apiKeyConfigCredential: credential } : {}
4362
+ });
4363
+ if (result) {
4364
+ authSpec = result.spec;
4365
+ authEcho = result.echo;
4366
+ }
4367
+ accessProfileEcho = {
4368
+ profileRef: profile.id,
4369
+ ...profile.label !== void 0 ? { label: profile.label } : {},
4370
+ vendor: profile.vendor,
4371
+ method: profile.method
4372
+ };
4373
+ } else if (resolved.authDescriptor) {
3934
4374
  const configDefaults = opts.loadDefaultsConfig ? await opts.loadDefaultsConfig() : (await loadConfig()).defaults;
3935
4375
  const explicitAuthInput = prev.auth ? { mode: prev.auth.mode } : void 0;
3936
4376
  const spawnDefaults = resolveSpawnDefaults(configDefaults, adapterSlug, {
3937
4377
  auth: explicitAuthInput
3938
4378
  });
3939
- const authModel = prev.model ?? resolved.defaultModel;
4379
+ const authModel = effModel ?? resolved.defaultModel;
3940
4380
  const pinnedProvider = spawnDefaults.auth.provider;
3941
4381
  const resolvedProvider = pinnedProvider ?? resolved.authDescriptor.provider ?? (authModel ? getModelProvider(authModel) : void 0);
3942
4382
  const apiKeyStoreCredential = resolvedProvider && spawnDefaults.auth.explicit && spawnDefaults.auth.apiKeyCredential === void 0 ? await (0, providers_store_exports.getProviderKey)(resolvedProvider) : void 0;
@@ -3966,13 +4406,20 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
3966
4406
  const agentSession = await resolved.startSession({
3967
4407
  cwd,
3968
4408
  ...resumeSessionId ? { resumeSessionId } : {},
3969
- ...prev.model ? { model: prev.model } : {},
4409
+ ...effModel ? { model: effModel } : {},
4410
+ ...effEffort ? { effort: effEffort } : {},
4411
+ // Legacy AIP-45 mode override only — the decomposed route/posture/
4412
+ // contextProfile spawn-env apply-path rides the driver's mode
4413
+ // decomposition (build step 2), out of scope here; they still round-trip
4414
+ // on the descriptor below.
4415
+ ...overrides.mode ? { mode: overrides.mode } : {},
3970
4416
  ...prev.mcpServers ? { mcpServers: prev.mcpServers } : {},
3971
4417
  ...authSpec ? { auth: authSpec } : {},
3972
4418
  onActivity: () => {
3973
4419
  if (liveSessionId) registry.pulseActivity(liveSessionId);
3974
4420
  }
3975
4421
  });
4422
+ const resumeVia = !resumeSessionId ? "" : opts.forceAgentResume ? "resumed via ACP" : describeResumePath(augmented);
3976
4423
  const desc2 = registry.spawnAgent({
3977
4424
  workspaceSlug: prev.workspaceSlug,
3978
4425
  cwd,
@@ -3980,7 +4427,17 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
3980
4427
  adapterSlug,
3981
4428
  ...prev.label ? { label: prev.label } : {},
3982
4429
  ...prev.mcpServers ? { mcpServers: prev.mcpServers } : {},
3983
- ...prev.model ? { model: prev.model } : {},
4430
+ ...effModel ? { model: effModel } : {},
4431
+ // Decomposed config-axis echoes (SPEC §3.7) — carried forward from `prev`
4432
+ // and overlaid with any override so every axis round-trips onto the fresh
4433
+ // descriptor and the picker re-opens on it (SPEC §3.8), even the axes
4434
+ // whose spawn-env apply-path isn't wired here yet (route/posture/context).
4435
+ ...effEffort ? { effort: effEffort } : {},
4436
+ ...effPosture !== void 0 ? { posture: effPosture } : {},
4437
+ ...effRoute ? { route: effRoute } : {},
4438
+ ...effContextProfile ? { contextProfile: effContextProfile } : {},
4439
+ ...accessProfileEcho ? { accessProfile: accessProfileEcho } : {},
4440
+ ...effMode ? { mode: effMode } : {},
3984
4441
  ...resolved.commandPreview ? { commandPreview: resolved.commandPreview } : {},
3985
4442
  // Verifiability echo (never the credential) — see the auth
3986
4443
  // resolution block above. Absent when no credential resolved,
@@ -3993,31 +4450,53 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
3993
4450
  credentialSource: authEcho.credentialSource,
3994
4451
  setEnv: authEcho.setEnv
3995
4452
  }
3996
- } : {}
4453
+ } : {},
4454
+ resumedFrom: prev.id,
4455
+ resumeVia
3997
4456
  });
3998
4457
  liveSessionId = desc2.id;
3999
4458
  return desc2;
4000
4459
  };
4001
4460
  let desc;
4002
4461
  let resumeFallback = false;
4003
- let usedResumeSessionId = strategy.resumeSessionId;
4004
4462
  try {
4005
4463
  desc = await spawnWithResume(strategy.resumeSessionId);
4006
4464
  } catch (err) {
4007
4465
  const msg = err instanceof Error ? err.message : String(err);
4008
4466
  if (strategy.resumeSessionId && /not found|Resource not found/i.test(msg)) {
4009
4467
  desc = await spawnWithResume(void 0);
4010
- usedResumeSessionId = void 0;
4011
4468
  resumeFallback = true;
4012
4469
  } else {
4013
4470
  throw err;
4014
4471
  }
4015
4472
  }
4016
- const resumeVia = !usedResumeSessionId ? "" : opts.forceAgentResume ? "resumed via ACP" : describeResumePath(augmented);
4473
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
4474
+ const emit = (axis, value) => {
4475
+ registry.emitConfigChanged({
4476
+ type: "session:config-changed",
4477
+ sessionId: desc.id,
4478
+ axis,
4479
+ value,
4480
+ ...desc.label ? { label: desc.label } : {},
4481
+ ts
4482
+ });
4483
+ };
4484
+ if (overrides.model !== void 0) emit("model", overrides.model);
4485
+ if (overrides.effort !== void 0) emit("effort", overrides.effort);
4486
+ if (accessOverrideRef !== void 0) emit("access", { profileRef: accessOverrideRef });
4487
+ if (overrides.route !== void 0) emit("route", overrides.route);
4488
+ if (overrides.posture !== void 0) emit("posture", overrides.posture);
4489
+ if (overrides.contextProfile !== void 0) emit("contextProfile", overrides.contextProfile);
4017
4490
  return {
4018
4491
  desc,
4019
4492
  resumedFrom: prev.id,
4020
- resumeVia,
4493
+ // `spawnWithResume` already computed + persisted this onto `desc` (the
4494
+ // fix this module carries — see `SessionDescriptor.resumedFrom`'s doc);
4495
+ // reading it back here rather than recomputing keeps the RESULT and the
4496
+ // STORED descriptor from ever being able to diverge. Never actually
4497
+ // undefined — every `spawnWithResume` call sets it, `?? ""` is just
4498
+ // satisfying the optional field's type.
4499
+ resumeVia: desc.resumeVia ?? "",
4021
4500
  ...resumeFallback ? { resumeFallback: true } : {}
4022
4501
  };
4023
4502
  }
@@ -4161,6 +4640,9 @@ function withToolExclusion(server, excluded) {
4161
4640
  }
4162
4641
  });
4163
4642
  }
4643
+
4644
+ // src/conversation-read.ts
4645
+ init_conversation_store();
4164
4646
  init_transcript_export();
4165
4647
  var BINARY_TO_STORE_KEY = {
4166
4648
  claude: "claude-code",
@@ -4340,7 +4822,7 @@ function buildSessionTree(sessions) {
4340
4822
  });
4341
4823
  return sessions.filter((s) => !s.parentSessionId || !idSet.has(s.parentSessionId)).sort((a, b) => a.startedAt.localeCompare(b.startedAt)).map(toNode);
4342
4824
  }
4343
- z.preprocess(
4825
+ var mcpBool2 = z.preprocess(
4344
4826
  (v) => v === "true" ? true : v === "false" ? false : v,
4345
4827
  z.boolean()
4346
4828
  );
@@ -4362,14 +4844,20 @@ function registerSessionTools(rawServer, opts) {
4362
4844
  "Filter by session kind. `all` (default) returns every kind. Use `terminal` to list only PTY sessions, `agent-cli` for structured ACP agents."
4363
4845
  ),
4364
4846
  onlyAlive: z.boolean().optional().describe("When true, only running/starting sessions. Default false."),
4365
- status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive).")
4847
+ status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive)."),
4848
+ includeArchived: z.boolean().optional().describe(
4849
+ "When true, also include archived sessions (hidden from every other view by `session_archive`). Default false."
4850
+ )
4366
4851
  },
4367
4852
  async (input) => {
4368
- let rows = registry.list();
4853
+ let rows = registry.list({ includeArchived: true });
4369
4854
  if (callerScope) {
4370
4855
  const subtree = collectSubtree(callerScope.ownerSessionId, rows);
4371
4856
  rows = rows.filter((s) => subtree.has(s.id));
4372
4857
  }
4858
+ if (!input.includeArchived) {
4859
+ rows = rows.filter((s) => !s.archived);
4860
+ }
4373
4861
  if (input.kind && input.kind !== "all") {
4374
4862
  rows = rows.filter((s) => s.kind === input.kind);
4375
4863
  }
@@ -4407,7 +4895,10 @@ function registerSessionTools(rawServer, opts) {
4407
4895
  };
4408
4896
  }
4409
4897
  if (callerScope) {
4410
- const subtree = collectSubtree(callerScope.ownerSessionId, registry.list());
4898
+ const subtree = collectSubtree(
4899
+ callerScope.ownerSessionId,
4900
+ registry.list({ includeArchived: true })
4901
+ );
4411
4902
  if (!subtree.has(desc.id)) {
4412
4903
  return {
4413
4904
  content: [
@@ -4443,11 +4934,12 @@ function registerSessionTools(rawServer, opts) {
4443
4934
  status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive).")
4444
4935
  },
4445
4936
  async (input) => {
4446
- let rows = registry.list();
4937
+ let rows = registry.list({ includeArchived: true });
4447
4938
  if (callerScope) {
4448
4939
  const subtree = collectSubtree(callerScope.ownerSessionId, rows);
4449
4940
  rows = rows.filter((s) => subtree.has(s.id));
4450
4941
  }
4942
+ rows = rows.filter((s) => !s.archived);
4451
4943
  const kind = input.kind ?? "terminal";
4452
4944
  if (kind !== "all") {
4453
4945
  rows = rows.filter((s) => s.kind === kind);
@@ -4477,11 +4969,12 @@ function registerSessionTools(rawServer, opts) {
4477
4969
  status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive).")
4478
4970
  },
4479
4971
  async (input) => {
4480
- let rows = registry.list();
4972
+ let rows = registry.list({ includeArchived: true });
4481
4973
  if (callerScope) {
4482
4974
  const subtree = collectSubtree(callerScope.ownerSessionId, rows);
4483
4975
  rows = rows.filter((s) => subtree.has(s.id));
4484
4976
  }
4977
+ rows = rows.filter((s) => !s.archived);
4485
4978
  const kind = input.kind ?? "command";
4486
4979
  if (kind !== "all") {
4487
4980
  rows = rows.filter((s) => s.kind === kind);
@@ -4765,11 +5258,12 @@ function registerSessionTools(rawServer, opts) {
4765
5258
  )
4766
5259
  },
4767
5260
  async (input) => {
4768
- let rows = registry.list();
5261
+ let rows = registry.list({ includeArchived: true });
4769
5262
  if (callerScope) {
4770
5263
  const subtree = collectSubtree(callerScope.ownerSessionId, rows);
4771
5264
  rows = rows.filter((s) => subtree.has(s.id));
4772
5265
  }
5266
+ rows = rows.filter((s) => !s.archived);
4773
5267
  if (input.onlyAlive) {
4774
5268
  rows = rows.filter(
4775
5269
  (s) => s.status === "running" || s.status === "starting"
@@ -4802,7 +5296,25 @@ function registerSessionTools(rawServer, opts) {
4802
5296
  cols: z.number().int().min(1).max(500).optional().describe(
4803
5297
  "PTY cols \u2014 only used when the restart resolves to a provider-native or plain PTY resume. Default 80."
4804
5298
  ),
4805
- rows: z.number().int().min(1).max(200).optional().describe("PTY rows \u2014 same case as `cols`. Default 24.")
5299
+ rows: z.number().int().min(1).max(200).optional().describe("PTY rows \u2014 same case as `cols`. Default 24."),
5300
+ // ── Restart-with-override axes (SPEC §4.3, step 6) — the single path
5301
+ // for all four restart-only axes. Each optional; an omitted axis is
5302
+ // carried forward from the prior session, an axis set here wins.
5303
+ model: z.string().min(1).optional().describe("Override the model on restart (route-identity ref)."),
5304
+ effort: z.enum(["low", "medium", "high", "xhigh", "max", "ultracode"]).optional().describe("Override the reasoning-effort level on restart."),
5305
+ access: z.object({
5306
+ profileRef: z.string().min(1).describe("Attach this NAMED auth profile (SPEC \xA71c). Rejected 400 if it's not eligible for the resolved (adapter \xD7 route).")
5307
+ }).optional().describe("Switch the session's billing wallet to a named auth profile."),
5308
+ route: z.object({
5309
+ gateway: z.string().min(1).describe("Endpoint/gateway id (anthropic|moonshot|\u2026)."),
5310
+ baseUrl: z.string().url().optional().describe("Explicit base URL for a custom gateway.")
5311
+ }).optional().describe("Override the endpoint/gateway rail on restart (access is downstream)."),
5312
+ posture: z.union([
5313
+ z.enum(["default", "plan", "accept-edits", "bypass", "read-only"]),
5314
+ z.object({ harnessModeId: z.string().min(1) })
5315
+ ]).optional().describe("Override the posture (what the agent may DO) on restart."),
5316
+ contextProfile: z.string().min(1).optional().describe("Override what enters context (full|lean|\u2026) on restart."),
5317
+ mode: z.string().min(1).optional().describe("Legacy AIP-45 mode id override, forwarded verbatim to the driver at spawn.")
4806
5318
  },
4807
5319
  async (input) => {
4808
5320
  const prev = registry.findByIdOrName(input.idOrName);
@@ -4818,7 +5330,10 @@ function registerSessionTools(rawServer, opts) {
4818
5330
  };
4819
5331
  }
4820
5332
  if (callerScope) {
4821
- const subtree = collectSubtree(callerScope.ownerSessionId, registry.list());
5333
+ const subtree = collectSubtree(
5334
+ callerScope.ownerSessionId,
5335
+ registry.list({ includeArchived: true })
5336
+ );
4822
5337
  if (!subtree.has(prev.id)) {
4823
5338
  return {
4824
5339
  content: [
@@ -4836,6 +5351,84 @@ function registerSessionTools(rawServer, opts) {
4836
5351
  };
4837
5352
  }
4838
5353
  }
5354
+ const overrides = {
5355
+ ...input.model !== void 0 ? { model: input.model } : {},
5356
+ ...input.effort !== void 0 ? { effort: input.effort } : {},
5357
+ ...input.access !== void 0 ? { access: input.access } : {},
5358
+ ...input.route !== void 0 ? { route: input.route } : {},
5359
+ ...input.posture !== void 0 ? { posture: input.posture } : {},
5360
+ ...input.contextProfile !== void 0 ? { contextProfile: input.contextProfile } : {},
5361
+ ...input.mode !== void 0 ? { mode: input.mode } : {}
5362
+ };
5363
+ if (Object.keys(overrides).length > 0) {
5364
+ if (!prev.adapterSlug || !resolveAgentAdapter) {
5365
+ return {
5366
+ content: [
5367
+ {
5368
+ type: "text",
5369
+ text: JSON.stringify({
5370
+ error: "restart_override_invalid",
5371
+ status: 400,
5372
+ message: "session_restart: restart-with-override only applies to agent-cli sessions (a PTY/command session has no config axes to override).",
5373
+ ok: false,
5374
+ sessionId: prev.id
5375
+ })
5376
+ }
5377
+ ],
5378
+ isError: true
5379
+ };
5380
+ }
5381
+ try {
5382
+ const restarted = await restartAgentSession(registry, resolveAgentAdapter, prev, {
5383
+ forceAgentResume: true,
5384
+ overrides
5385
+ });
5386
+ return {
5387
+ content: [
5388
+ {
5389
+ type: "text",
5390
+ text: JSON.stringify(
5391
+ {
5392
+ ...restarted.desc,
5393
+ resumedFrom: restarted.resumedFrom,
5394
+ resumeVia: restarted.resumeVia,
5395
+ ...restarted.resumeFallback ? { resumeFallback: true } : {}
5396
+ },
5397
+ null,
5398
+ 2
5399
+ )
5400
+ }
5401
+ ]
5402
+ };
5403
+ } catch (err) {
5404
+ if (err instanceof RestartOverrideError) {
5405
+ return {
5406
+ content: [
5407
+ {
5408
+ type: "text",
5409
+ text: JSON.stringify({
5410
+ error: err.code,
5411
+ status: err.status,
5412
+ message: err.message,
5413
+ ok: false,
5414
+ sessionId: prev.id
5415
+ })
5416
+ }
5417
+ ],
5418
+ isError: true
5419
+ };
5420
+ }
5421
+ return {
5422
+ content: [
5423
+ {
5424
+ type: "text",
5425
+ text: `session_restart: ${err instanceof Error ? err.message : String(err)}`
5426
+ }
5427
+ ],
5428
+ isError: true
5429
+ };
5430
+ }
5431
+ }
4839
5432
  const augmented = await augmentWithFsResume(prev);
4840
5433
  const strategy = decideRestartStrategy(augmented);
4841
5434
  if (strategy.kind === "unsupported") {
@@ -4867,17 +5460,15 @@ function registerSessionTools(rawServer, opts) {
4867
5460
  cols: input.cols ?? 80,
4868
5461
  rows: input.rows ?? 24,
4869
5462
  ...prev.name ? { name: prev.name } : {},
4870
- ...prev.label ? { label: prev.label } : {}
5463
+ ...prev.label ? { label: prev.label } : {},
5464
+ resumedFrom: prev.id,
5465
+ resumeVia: describeResumePath(augmented)
4871
5466
  });
4872
5467
  return {
4873
5468
  content: [
4874
5469
  {
4875
5470
  type: "text",
4876
- text: JSON.stringify(
4877
- { ...desc, resumedFrom: prev.id, resumeVia: describeResumePath(augmented) },
4878
- null,
4879
- 2
4880
- )
5471
+ text: JSON.stringify(desc, null, 2)
4881
5472
  }
4882
5473
  ]
4883
5474
  };
@@ -4898,36 +5489,158 @@ function registerSessionTools(rawServer, opts) {
4898
5489
  content: [
4899
5490
  {
4900
5491
  type: "text",
4901
- text: "session_restart: agent_start is not enabled \u2014 the daemon was started without an adapter resolver."
5492
+ text: "session_restart: agent_start is not enabled \u2014 the daemon was started without an adapter resolver."
5493
+ }
5494
+ ],
5495
+ isError: true
5496
+ };
5497
+ }
5498
+ const restarted = await restartAgentSession(registry, resolveAgentAdapter, prev);
5499
+ return {
5500
+ content: [
5501
+ {
5502
+ type: "text",
5503
+ text: JSON.stringify(
5504
+ {
5505
+ ...restarted.desc,
5506
+ resumedFrom: restarted.resumedFrom,
5507
+ resumeVia: restarted.resumeVia,
5508
+ ...restarted.resumeFallback ? { resumeFallback: true } : {}
5509
+ },
5510
+ null,
5511
+ 2
5512
+ )
5513
+ }
5514
+ ]
5515
+ };
5516
+ } catch (err) {
5517
+ return {
5518
+ content: [
5519
+ {
5520
+ type: "text",
5521
+ text: `session_restart: ${err instanceof Error ? err.message : String(err)}`
5522
+ }
5523
+ ],
5524
+ isError: true
5525
+ };
5526
+ }
5527
+ }
5528
+ );
5529
+ server.tool(
5530
+ "session_archive",
5531
+ "Archive a terminal-status session (exited/killed/error) so it drops out of `session_list`'s / `GET /sessions`'s default view \u2014 a housekeeping flag, not a daemon action: the session's history and transcript are untouched and stay fully readable (`session_usage`, `agent_export`, or `session_list({ includeArchived: true })`). Refuses a still-alive session (running/starting) \u2014 archiving one would hide it from view while it keeps working unattended. Use `session_unarchive` to restore visibility.",
5532
+ {
5533
+ idOrName: z.string().min(1).describe(
5534
+ "Session id or name to archive \u2014 from `session_list`, must be terminal-status."
5535
+ )
5536
+ },
5537
+ async (input) => {
5538
+ const prev = registry.findByIdOrName(input.idOrName);
5539
+ if (!prev) {
5540
+ return {
5541
+ content: [
5542
+ {
5543
+ type: "text",
5544
+ text: JSON.stringify({ error: `no session "${input.idOrName}" found` })
5545
+ }
5546
+ ],
5547
+ isError: true
5548
+ };
5549
+ }
5550
+ if (callerScope) {
5551
+ const subtree = collectSubtree(
5552
+ callerScope.ownerSessionId,
5553
+ registry.list({ includeArchived: true })
5554
+ );
5555
+ if (!subtree.has(prev.id)) {
5556
+ return {
5557
+ content: [
5558
+ {
5559
+ type: "text",
5560
+ text: JSON.stringify({
5561
+ error: "orchestrator_session_out_of_scope",
5562
+ message: `session_archive: session "${prev.id}" is not in your subtree \u2014 a scoped orchestrator can only archive sessions it (transitively) spawned.`,
5563
+ ok: false,
5564
+ sessionId: prev.id
5565
+ })
5566
+ }
5567
+ ],
5568
+ isError: true
5569
+ };
5570
+ }
5571
+ }
5572
+ try {
5573
+ const desc = registry.archiveSession(prev.id);
5574
+ return {
5575
+ content: [{ type: "text", text: JSON.stringify(desc, null, 2) }]
5576
+ };
5577
+ } catch (err) {
5578
+ return {
5579
+ content: [
5580
+ {
5581
+ type: "text",
5582
+ text: `session_archive: ${err instanceof Error ? err.message : String(err)}`
5583
+ }
5584
+ ],
5585
+ isError: true
5586
+ };
5587
+ }
5588
+ }
5589
+ );
5590
+ server.tool(
5591
+ "session_unarchive",
5592
+ "Restore an archived session to `session_list`'s default view \u2014 the inverse of `session_archive`. No status guard: archiving never touches daemon state, so there is nothing to re-validate \u2014 any archived session can be unarchived at any time.",
5593
+ {
5594
+ idOrName: z.string().min(1).describe(
5595
+ "Session id or name to unarchive \u2014 find it via `session_list({ includeArchived: true })`."
5596
+ )
5597
+ },
5598
+ async (input) => {
5599
+ const prev = registry.findByIdOrName(input.idOrName);
5600
+ if (!prev) {
5601
+ return {
5602
+ content: [
5603
+ {
5604
+ type: "text",
5605
+ text: JSON.stringify({ error: `no session "${input.idOrName}" found` })
5606
+ }
5607
+ ],
5608
+ isError: true
5609
+ };
5610
+ }
5611
+ if (callerScope) {
5612
+ const subtree = collectSubtree(
5613
+ callerScope.ownerSessionId,
5614
+ registry.list({ includeArchived: true })
5615
+ );
5616
+ if (!subtree.has(prev.id)) {
5617
+ return {
5618
+ content: [
5619
+ {
5620
+ type: "text",
5621
+ text: JSON.stringify({
5622
+ error: "orchestrator_session_out_of_scope",
5623
+ message: `session_unarchive: session "${prev.id}" is not in your subtree \u2014 a scoped orchestrator can only unarchive sessions it (transitively) spawned.`,
5624
+ ok: false,
5625
+ sessionId: prev.id
5626
+ })
4902
5627
  }
4903
5628
  ],
4904
5629
  isError: true
4905
5630
  };
4906
5631
  }
4907
- const restarted = await restartAgentSession(registry, resolveAgentAdapter, prev);
5632
+ }
5633
+ try {
5634
+ const desc = registry.unarchiveSession(prev.id);
4908
5635
  return {
4909
- content: [
4910
- {
4911
- type: "text",
4912
- text: JSON.stringify(
4913
- {
4914
- ...restarted.desc,
4915
- resumedFrom: restarted.resumedFrom,
4916
- resumeVia: restarted.resumeVia,
4917
- ...restarted.resumeFallback ? { resumeFallback: true } : {}
4918
- },
4919
- null,
4920
- 2
4921
- )
4922
- }
4923
- ]
5636
+ content: [{ type: "text", text: JSON.stringify(desc, null, 2) }]
4924
5637
  };
4925
5638
  } catch (err) {
4926
5639
  return {
4927
5640
  content: [
4928
5641
  {
4929
5642
  type: "text",
4930
- text: `session_restart: ${err instanceof Error ? err.message : String(err)}`
5643
+ text: `session_unarchive: ${err instanceof Error ? err.message : String(err)}`
4931
5644
  }
4932
5645
  ],
4933
5646
  isError: true
@@ -4996,7 +5709,16 @@ function registerSessionTools(rawServer, opts) {
4996
5709
  cols: input.cols ?? 80,
4997
5710
  rows: input.rows ?? 24,
4998
5711
  ...input.name ? { name: input.name } : {},
4999
- ...input.label ? { label: input.label } : {}
5712
+ ...input.label ? { label: input.label } : {},
5713
+ // Parent attribution + depth (orchestrator WP4) — same rule as
5714
+ // `agent_start` (session-spawn.ts): a spawn through a scoped
5715
+ // sub-gateway is attributed to the owning orchestrator so
5716
+ // `session_tree` shows the PTY as its child. Depth caps and
5717
+ // child quotas stay agent_start-only for now.
5718
+ ...callerScope?.ownerSessionId ? {
5719
+ parentSessionId: callerScope.ownerSessionId,
5720
+ depth: callerScope.depth + 1
5721
+ } : {}
5000
5722
  });
5001
5723
  return {
5002
5724
  content: [{ type: "text", text: JSON.stringify(desc, null, 2) }]
@@ -5075,10 +5797,13 @@ function registerSessionTools(rawServer, opts) {
5075
5797
  );
5076
5798
  server.tool(
5077
5799
  "terminal_output",
5078
- "Snapshot the recent byte buffer of a PTY session. Returns base64-encoded bytes (the buffer is RAW including ANSI escapes \u2014 strip with a regex if you want plain text). `lastBytes` caps the read from the tail.",
5800
+ "Snapshot the recent byte buffer of a PTY session. Returns base64-encoded bytes (the buffer is RAW including ANSI escapes) by default; pass `clean: true` for ANSI-stripped plain text instead. `lastBytes` caps the read from the tail.",
5079
5801
  {
5080
5802
  sessionId: z.string().describe("Session id OR name from terminal_start."),
5081
- lastBytes: z.number().int().min(1).max(64 * 1024).optional().describe("Max bytes from the tail. Default: full ring buffer (~64 KiB).")
5803
+ lastBytes: z.number().int().min(1).max(64 * 1024).optional().describe("Max bytes from the tail. Default: full ring buffer (~64 KiB)."),
5804
+ clean: mcpBool2.optional().describe(
5805
+ "Strip ANSI codes, returning human-readable text (as `text` instead of `b64`). Default false = raw base64."
5806
+ )
5082
5807
  },
5083
5808
  async (input) => {
5084
5809
  if (!ptyEnabled) return ptyNotConfigured("terminal_output");
@@ -5118,7 +5843,7 @@ function registerSessionTools(rawServer, opts) {
5118
5843
  sessionId: desc.id,
5119
5844
  status: desc.status,
5120
5845
  bytes: buf.byteLength,
5121
- b64: buf.toString("base64")
5846
+ ...input.clean ? { text: stripAnsi(buf.toString("utf8")) } : { b64: buf.toString("base64") }
5122
5847
  },
5123
5848
  null,
5124
5849
  2
@@ -7935,6 +8660,9 @@ function filterSessionObserver(inner, shouldObserve) {
7935
8660
  // src/sessions.ts
7936
8661
  init_tool_presenter();
7937
8662
  init_transcript_writer();
8663
+
8664
+ // src/conversation-index.ts
8665
+ init_conversation_store();
7938
8666
  var DEFAULT_BUCKET = "default";
7939
8667
  var BUCKETS_ROOT = () => resolve(homedir(), ".agentproto", "workspaces");
7940
8668
  var LEGACY_SESSIONS_FILE = () => resolve(homedir(), ".agentproto", "sessions.json");
@@ -8058,6 +8786,110 @@ function readBucketRows(root, slug) {
8058
8786
  return [];
8059
8787
  }
8060
8788
  }
8789
+ var rowId = (row) => row && typeof row === "object" && "id" in row && typeof row.id === "string" ? row.id : void 0;
8790
+ function mergeBucketRows(onDisk, rows, everHeldIds) {
8791
+ const incomingIds = new Set(rows.map(rowId).filter((id) => id !== void 0));
8792
+ const preserved = onDisk.filter((row) => {
8793
+ const id = rowId(row);
8794
+ return id !== void 0 && !incomingIds.has(id) && !everHeldIds.has(id);
8795
+ });
8796
+ return [...rows, ...preserved];
8797
+ }
8798
+
8799
+ // src/conversation-index.ts
8800
+ function conversationIndexPath(bucketsRoot, slug) {
8801
+ return join(bucketDir(bucketsRoot, slug), "conversations.jsonl");
8802
+ }
8803
+ async function listClaudeSubagents(projectDir, adapterSessionId) {
8804
+ const dir = join(projectDir, adapterSessionId, "subagents");
8805
+ let entries;
8806
+ try {
8807
+ entries = await promises.readdir(dir);
8808
+ } catch {
8809
+ return [];
8810
+ }
8811
+ return entries.filter((e) => e.startsWith("agent-") && e.endsWith(".jsonl")).map((e) => join(dir, e)).sort();
8812
+ }
8813
+ async function resolveNativeLink(input) {
8814
+ const { cwd, adapterSlug, adapterSessionId } = input;
8815
+ if (adapterSlug === "claude-code") {
8816
+ const dir = claudeCodeProjectDir(cwd);
8817
+ const path = join(dir, `${adapterSessionId}.jsonl`);
8818
+ const subagents = await listClaudeSubagents(dir, adapterSessionId);
8819
+ return { kind: "claude-jsonl", path, subagents };
8820
+ }
8821
+ if (adapterSlug === "hermes") {
8822
+ return {
8823
+ kind: "hermes-sqlite",
8824
+ dbPath: join(homedir(), ".hermes", "state.db"),
8825
+ rowId: adapterSessionId
8826
+ };
8827
+ }
8828
+ return void 0;
8829
+ }
8830
+ async function appendConversationRecord(bucketsRoot, slug, record) {
8831
+ const path = conversationIndexPath(bucketsRoot, slug);
8832
+ await promises.mkdir(dirname(path), { recursive: true });
8833
+ await promises.appendFile(path, JSON.stringify(record) + "\n", "utf8");
8834
+ }
8835
+ function isConversationIndexRecord(value) {
8836
+ if (!value || typeof value !== "object") return false;
8837
+ const r = value;
8838
+ return typeof r.sessionId === "string" && typeof r.workspace === "string" && typeof r.cwd === "string" && typeof r.adapterSlug === "string" && typeof r.adapterSessionId === "string" && typeof r.agentprotoTranscript === "string" && typeof r.startedAt === "string";
8839
+ }
8840
+ async function readConversationIndex(bucketsRoot, slug) {
8841
+ const path = conversationIndexPath(bucketsRoot, slug);
8842
+ let raw;
8843
+ try {
8844
+ raw = await promises.readFile(path, "utf8");
8845
+ } catch {
8846
+ return [];
8847
+ }
8848
+ const bySession = /* @__PURE__ */ new Map();
8849
+ for (const line of raw.split("\n")) {
8850
+ const trimmed = line.trim();
8851
+ if (!trimmed) continue;
8852
+ let parsed;
8853
+ try {
8854
+ parsed = JSON.parse(trimmed);
8855
+ } catch {
8856
+ continue;
8857
+ }
8858
+ if (!isConversationIndexRecord(parsed)) continue;
8859
+ bySession.set(parsed.sessionId, parsed);
8860
+ }
8861
+ return Array.from(bySession.values());
8862
+ }
8863
+ async function findConversationRecord(bucketsRoot, slug, sessionId) {
8864
+ const rows = await readConversationIndex(bucketsRoot, slug);
8865
+ return rows.find((r) => r.sessionId === sessionId);
8866
+ }
8867
+ async function locateConversationBySessionId(bucketsRoot, listBuckets2, sessionId) {
8868
+ for (const slug of listBuckets2()) {
8869
+ const record = await findConversationRecord(bucketsRoot, slug, sessionId);
8870
+ if (record) return { workspace: slug, record };
8871
+ }
8872
+ return void 0;
8873
+ }
8874
+ async function locateConversationByNativePath(bucketsRoot, listBuckets2, nativePath) {
8875
+ const target = resolve(nativePath);
8876
+ for (const slug of listBuckets2()) {
8877
+ const rows = await readConversationIndex(bucketsRoot, slug);
8878
+ for (const record of rows) {
8879
+ if (!record.native || record.native.kind !== "claude-jsonl") continue;
8880
+ if (resolve(record.native.path) === target) {
8881
+ return { workspace: slug, record };
8882
+ }
8883
+ const matchedSubagentPath = record.native.subagents.find(
8884
+ (p) => resolve(p) === target
8885
+ );
8886
+ if (matchedSubagentPath) {
8887
+ return { workspace: slug, record, matchedSubagentPath };
8888
+ }
8889
+ }
8890
+ }
8891
+ return void 0;
8892
+ }
8061
8893
 
8062
8894
  // src/terminal-transcript-writer.ts
8063
8895
  init_transcript_writer();
@@ -8235,6 +9067,15 @@ var SessionNotAliveError = class extends Error {
8235
9067
  var RECENT_LINES_CAP = 500;
8236
9068
  var RECENT_BYTES_CAP = 64 * 1024;
8237
9069
  var PERSIST_DEBOUNCE_MS = 1500;
9070
+ var EMPTY_ID_SET = /* @__PURE__ */ new Set();
9071
+ function markHeldId(map, slug, id) {
9072
+ let set = map.get(slug);
9073
+ if (!set) {
9074
+ set = /* @__PURE__ */ new Set();
9075
+ map.set(slug, set);
9076
+ }
9077
+ set.add(id);
9078
+ }
8238
9079
  var HISTORY_CAP = 200;
8239
9080
  var INTERRUPT_SETTLE_TIMEOUT_MS = 6e4;
8240
9081
  function stampProcessAlive(desc) {
@@ -8259,6 +9100,11 @@ function findPriorCommandSessionId(liveSessions, cwd) {
8259
9100
  }
8260
9101
  return best?.desc.id;
8261
9102
  }
9103
+ function currentRouteOf(desc) {
9104
+ if (desc.route?.gateway) return desc.route.gateway;
9105
+ if (desc.model) return tryParseModelRef(desc.model)?.route;
9106
+ return void 0;
9107
+ }
8262
9108
  function worktreeFields(cwd) {
8263
9109
  const identity = resolveWorktreeIdentity(cwd);
8264
9110
  if (!identity) return {};
@@ -8309,6 +9155,8 @@ function createSessionsRegistry(opts) {
8309
9155
  let nextSubId = 1;
8310
9156
  let shutdownDone = false;
8311
9157
  const knownBuckets = /* @__PURE__ */ new Set();
9158
+ const sourceBucketOf = /* @__PURE__ */ new Map();
9159
+ const heldIdsByBucket = /* @__PURE__ */ new Map();
8312
9160
  if (persist) {
8313
9161
  if (partitioned) {
8314
9162
  migrateLegacySessionsFile({
@@ -8321,13 +9169,17 @@ function createSessionsRegistry(opts) {
8321
9169
  loadHistorySnapshot(
8322
9170
  bucketSessionsFile(bucketsRoot, slug),
8323
9171
  sessions,
8324
- sessionEvents
9172
+ sessionEvents,
9173
+ slug,
9174
+ sourceBucketOf,
9175
+ heldIdsByBucket
8325
9176
  );
8326
9177
  }
8327
9178
  } else {
8328
9179
  loadHistorySnapshot(legacyPath, sessions, sessionEvents);
8329
9180
  }
8330
9181
  }
9182
+ const bootLoadedBuckets = new Set(knownBuckets);
8331
9183
  const onProcessExit = () => {
8332
9184
  shutdownImpl();
8333
9185
  };
@@ -8411,6 +9263,39 @@ function createSessionsRegistry(opts) {
8411
9263
  }
8412
9264
  delete rt.desc.awaitingPermission;
8413
9265
  };
9266
+ const recordConversationLink = (rt, adapterSessionIdOverride) => {
9267
+ if (!persist || !partitioned) return;
9268
+ const desc = rt.desc;
9269
+ if (desc.kind !== "agent-cli") return;
9270
+ const adapterSlug = desc.adapterSlug;
9271
+ const adapterSessionId = adapterSessionIdOverride ?? desc.adapterSessionId;
9272
+ const cwd = desc.cwd;
9273
+ if (!adapterSlug || !adapterSessionId || !cwd) return;
9274
+ void (async () => {
9275
+ try {
9276
+ const native = await resolveNativeLink({ cwd, adapterSlug, adapterSessionId });
9277
+ const registered = readRegisteredSlugs(workspacesConfigPath);
9278
+ const slug = resolveBucketSlug(desc.workspaceSlug, registered);
9279
+ const record = {
9280
+ sessionId: desc.id,
9281
+ workspace: slug,
9282
+ cwd,
9283
+ adapterSlug,
9284
+ adapterSessionId,
9285
+ ...native ? { native } : {},
9286
+ agentprotoTranscript: sessionEventsPath(desc.id, transcriptBaseDir),
9287
+ ...desc.title ? { title: desc.title } : {},
9288
+ startedAt: desc.startedAt,
9289
+ ...desc.endedAt ? { endedAt: desc.endedAt } : {}
9290
+ };
9291
+ await appendConversationRecord(bucketsRoot, slug, record);
9292
+ } catch (err) {
9293
+ console.warn(
9294
+ `[sessions] conversation-index write failed for ${desc.id}: ${err instanceof Error ? err.message : String(err)}`
9295
+ );
9296
+ }
9297
+ })();
9298
+ };
8414
9299
  const schedulePersist = () => {
8415
9300
  if (!persist) return;
8416
9301
  if (persistTimer) clearTimeout(persistTimer);
@@ -8427,7 +9312,8 @@ function createSessionsRegistry(opts) {
8427
9312
  const groups = /* @__PURE__ */ new Map();
8428
9313
  for (const slug of knownBuckets) groups.set(slug, []);
8429
9314
  for (const desc of snapshotRows()) {
8430
- const slug = resolveBucketSlug(desc.workspaceSlug, registered);
9315
+ const slug = sourceBucketOf.get(desc.id) ?? resolveBucketSlug(desc.workspaceSlug, registered);
9316
+ markHeldId(heldIdsByBucket, slug, desc.id);
8431
9317
  const list = groups.get(slug);
8432
9318
  if (list) list.push(desc);
8433
9319
  else groups.set(slug, [desc]);
@@ -8435,6 +9321,11 @@ function createSessionsRegistry(opts) {
8435
9321
  for (const slug of groups.keys()) knownBuckets.add(slug);
8436
9322
  return groups;
8437
9323
  };
9324
+ const rowsToWrite = (slug, rows) => bootLoadedBuckets.has(slug) ? rows : mergeBucketRows(
9325
+ readBucketRows(bucketsRoot, slug),
9326
+ rows,
9327
+ heldIdsByBucket.get(slug) ?? EMPTY_ID_SET
9328
+ );
8438
9329
  const persistSnapshot = async () => {
8439
9330
  try {
8440
9331
  const savedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -8442,7 +9333,7 @@ function createSessionsRegistry(opts) {
8442
9333
  for (const [slug, rows] of groupRowsByBucket()) {
8443
9334
  await writeBucketSnapshot(bucketsRoot, slug, {
8444
9335
  savedAt,
8445
- sessions: rows
9336
+ sessions: rowsToWrite(slug, rows)
8446
9337
  });
8447
9338
  }
8448
9339
  return;
@@ -8477,6 +9368,7 @@ function createSessionsRegistry(opts) {
8477
9368
  [strategy.storeAs]: m[1]
8478
9369
  };
8479
9370
  schedulePersist();
9371
+ recordConversationLink(rt, m[1]);
8480
9372
  }
8481
9373
  };
8482
9374
  const appendBytes = (rt, chunk) => {
@@ -8754,6 +9646,7 @@ function createSessionsRegistry(opts) {
8754
9646
  rt.emitter.emit("status", rt.desc.status);
8755
9647
  }
8756
9648
  schedulePersist();
9649
+ recordConversationLink(rt);
8757
9650
  } catch (err) {
8758
9651
  const msg = err instanceof Error ? err.message : String(err);
8759
9652
  appendLine(rt, `[error] resume failed: ${msg}`, "stderr");
@@ -9098,11 +9991,25 @@ function createSessionsRegistry(opts) {
9098
9991
  ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
9099
9992
  depth: input.depth ?? 0,
9100
9993
  ...input.model ? { model: input.model } : {},
9994
+ ...input.mode ? { mode: input.mode } : {},
9101
9995
  ...input.auth ? { auth: input.auth } : {},
9996
+ // Decomposed config-axis echoes (SPEC §3.7), same optional-spread
9997
+ // shape as `model`/`mode`/`auth` above.
9998
+ ...input.effort ? { effort: input.effort } : {},
9999
+ ...input.posture !== void 0 ? { posture: input.posture } : {},
10000
+ ...input.route ? { route: input.route } : {},
10001
+ ...input.contextProfile ? { contextProfile: input.contextProfile } : {},
10002
+ ...input.accessProfile ? { accessProfile: input.accessProfile } : {},
9102
10003
  ...priorCommandSessionId ? { priorCommandSessionId } : {},
9103
10004
  ...input.remote ? { remote: true } : {},
9104
10005
  ...input.sandboxId ? { sandboxId: input.sandboxId } : {},
9105
- ...input.sandboxTeardown ? { sandboxTeardown: input.sandboxTeardown } : {}
10006
+ ...input.sandboxTeardown ? { sandboxTeardown: input.sandboxTeardown } : {},
10007
+ // Restart lineage (see SessionDescriptor.resumedFrom's doc). `resumeVia`
10008
+ // can legitimately be "" (a fresh fallback spawn with no continuity),
10009
+ // so it's gated on `!== undefined` rather than truthiness — a truthy
10010
+ // gate would silently drop the empty-string case.
10011
+ ...input.resumedFrom ? { resumedFrom: input.resumedFrom } : {},
10012
+ ...input.resumeVia !== void 0 ? { resumeVia: input.resumeVia } : {}
9106
10013
  };
9107
10014
  if (input.trace ?? opts?.langfuseTracingDefault ?? false) {
9108
10015
  tracedSessions.add(id);
@@ -9130,6 +10037,7 @@ function createSessionsRegistry(opts) {
9130
10037
  "stdout"
9131
10038
  );
9132
10039
  schedulePersist();
10040
+ recordConversationLink(rt);
9133
10041
  if (input.initialPrompt) {
9134
10042
  void runAgentTurn(rt, input.initialPrompt).catch((err) => {
9135
10043
  appendLine(
@@ -9189,7 +10097,16 @@ function createSessionsRegistry(opts) {
9189
10097
  ...worktreeFields(input.cwd),
9190
10098
  ...input.name ? { name: input.name } : {},
9191
10099
  ...input.label ? { label: input.label } : {},
9192
- ...priorCommandSessionId ? { priorCommandSessionId } : {}
10100
+ ...priorCommandSessionId ? { priorCommandSessionId } : {},
10101
+ // Parent attribution + depth (orchestrator WP4) — same recording
10102
+ // rule as spawnAgent above: depth always set so subtree/depth
10103
+ // logic never distinguishes "absent" from "root".
10104
+ ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
10105
+ depth: input.depth ?? 0,
10106
+ // Restart lineage — same gating rule as spawnAgent above (`resumeVia`
10107
+ // can legitimately be "").
10108
+ ...input.resumedFrom ? { resumedFrom: input.resumedFrom } : {},
10109
+ ...input.resumeVia !== void 0 ? { resumeVia: input.resumeVia } : {}
9193
10110
  };
9194
10111
  const rt = {
9195
10112
  desc,
@@ -9355,14 +10272,126 @@ function createSessionsRegistry(opts) {
9355
10272
  await interruptInFlightTurn(rt, id, "interruptSession");
9356
10273
  return { wasBusy: true };
9357
10274
  },
10275
+ async setModel(id, modelId) {
10276
+ const rt = sessions.get(id);
10277
+ if (!rt) throw new Error(`setModel: no session "${id}"`);
10278
+ if (rt.desc.kind !== "agent-cli" || !rt.agentSession) {
10279
+ throw new Error(`setModel: session "${id}" is not an agent-cli session`);
10280
+ }
10281
+ const targetRoute = tryParseModelRef(modelId)?.route;
10282
+ const currentRoute = currentRouteOf(rt.desc);
10283
+ if (targetRoute && currentRoute && targetRoute !== currentRoute) {
10284
+ return {
10285
+ applied: false,
10286
+ reason: "requires-restart",
10287
+ suggestedOverride: { route: { gateway: targetRoute }, model: modelId }
10288
+ };
10289
+ }
10290
+ if (!rt.agentSession.setModel) {
10291
+ return { applied: false, reason: "not-supported" };
10292
+ }
10293
+ const result = await rt.agentSession.setModel(modelId);
10294
+ if (result.applied) {
10295
+ rt.desc.model = result.model ?? modelId;
10296
+ schedulePersist();
10297
+ if (sessionEvents) {
10298
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
10299
+ sessionEvents.emit({
10300
+ type: "session:config-changed",
10301
+ sessionId: id,
10302
+ axis: "model",
10303
+ value: rt.desc.model,
10304
+ label: rt.desc.label,
10305
+ ts
10306
+ });
10307
+ sessionEvents.emit({
10308
+ type: "session:model-changed",
10309
+ sessionId: id,
10310
+ model: rt.desc.model,
10311
+ label: rt.desc.label,
10312
+ ts
10313
+ });
10314
+ }
10315
+ }
10316
+ return result;
10317
+ },
10318
+ emitConfigChanged(ev) {
10319
+ sessionEvents?.emit(ev);
10320
+ },
10321
+ async setEffort(id, effort) {
10322
+ const rt = sessions.get(id);
10323
+ if (!rt) throw new Error(`setEffort: no session "${id}"`);
10324
+ if (rt.desc.kind !== "agent-cli" || !rt.agentSession) {
10325
+ throw new Error(`setEffort: session "${id}" is not an agent-cli session`);
10326
+ }
10327
+ if (!rt.agentSession.setEffort) {
10328
+ return { applied: false, reason: "not-supported" };
10329
+ }
10330
+ const result = await rt.agentSession.setEffort(effort);
10331
+ if (result.applied) {
10332
+ rt.desc.effort = result.effort ?? effort;
10333
+ schedulePersist();
10334
+ if (sessionEvents) {
10335
+ sessionEvents.emit({
10336
+ type: "session:config-changed",
10337
+ sessionId: id,
10338
+ axis: "effort",
10339
+ value: rt.desc.effort,
10340
+ label: rt.desc.label,
10341
+ ts: (/* @__PURE__ */ new Date()).toISOString()
10342
+ });
10343
+ }
10344
+ }
10345
+ return result;
10346
+ },
10347
+ async setPosture(id, posture) {
10348
+ const rt = sessions.get(id);
10349
+ if (!rt) throw new Error(`setPosture: no session "${id}"`);
10350
+ if (rt.desc.kind !== "agent-cli" || !rt.agentSession) {
10351
+ throw new Error(`setPosture: session "${id}" is not an agent-cli session`);
10352
+ }
10353
+ const resolution = resolvePosture(posture, rt.agentSession.availableModes ?? []);
10354
+ if (resolution.kind !== "native") {
10355
+ return {
10356
+ applied: false,
10357
+ reason: "requires-restart",
10358
+ resolution: resolution.kind
10359
+ };
10360
+ }
10361
+ if (!rt.agentSession.setSessionMode) {
10362
+ return { applied: false, reason: "not-supported", resolution: "native" };
10363
+ }
10364
+ const result = await rt.agentSession.setSessionMode(resolution.mode.id);
10365
+ if (!result.applied) {
10366
+ return {
10367
+ applied: false,
10368
+ resolution: "native",
10369
+ ...result.reason ? { reason: result.reason } : {}
10370
+ };
10371
+ }
10372
+ rt.desc.posture = posture;
10373
+ schedulePersist();
10374
+ if (sessionEvents) {
10375
+ sessionEvents.emit({
10376
+ type: "session:config-changed",
10377
+ sessionId: id,
10378
+ axis: "posture",
10379
+ value: posture,
10380
+ label: rt.desc.label,
10381
+ ts: (/* @__PURE__ */ new Date()).toISOString()
10382
+ });
10383
+ }
10384
+ return { applied: true, posture, modeId: resolution.mode.id, resolution: "native" };
10385
+ },
9358
10386
  pulseActivity(id) {
9359
10387
  const rt = sessions.get(id);
9360
10388
  if (!rt) return;
9361
10389
  rt.desc.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
9362
10390
  schedulePersist();
9363
10391
  },
9364
- list() {
9365
- return Array.from(sessions.values()).map((s) => s.desc).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).map((desc) => {
10392
+ list(opts2) {
10393
+ const includeArchived = opts2?.includeArchived ?? false;
10394
+ return Array.from(sessions.values()).map((s) => s.desc).filter((desc) => includeArchived || !desc.archived).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).map((desc) => {
9366
10395
  stampProcessAlive(desc);
9367
10396
  return desc;
9368
10397
  });
@@ -9478,6 +10507,28 @@ function createSessionsRegistry(opts) {
9478
10507
  emitExited(rt);
9479
10508
  return true;
9480
10509
  },
10510
+ archiveSession(id) {
10511
+ const rt = sessions.get(id);
10512
+ if (!rt) throw new Error(`archiveSession: no session "${id}"`);
10513
+ const isAlive = rt.desc.status === "running" || rt.desc.status === "starting";
10514
+ if (isAlive) {
10515
+ throw new Error(
10516
+ `archiveSession: session "${id}" is still ${rt.desc.status} \u2014 only a terminal-status session (exited/killed/error) can be archived.`
10517
+ );
10518
+ }
10519
+ rt.desc.archived = true;
10520
+ schedulePersist();
10521
+ stampProcessAlive(rt.desc);
10522
+ return rt.desc;
10523
+ },
10524
+ unarchiveSession(id) {
10525
+ const rt = sessions.get(id);
10526
+ if (!rt) throw new Error(`unarchiveSession: no session "${id}"`);
10527
+ rt.desc.archived = false;
10528
+ schedulePersist();
10529
+ stampProcessAlive(rt.desc);
10530
+ return rt.desc;
10531
+ },
9481
10532
  listPendingPermissions(filter) {
9482
10533
  const all = Array.from(pendingPermissions.values());
9483
10534
  const scoped = filter?.sessionId ? all.filter((p) => p.sessionId === filter.sessionId) : all;
@@ -9602,7 +10653,7 @@ function createSessionsRegistry(opts) {
9602
10653
  for (const [slug, rows] of groupRowsByBucket()) {
9603
10654
  writeBucketSnapshotSync(bucketsRoot, slug, {
9604
10655
  savedAt: nowIso,
9605
- sessions: rows
10656
+ sessions: rowsToWrite(slug, rows)
9606
10657
  });
9607
10658
  }
9608
10659
  } else {
@@ -9624,7 +10675,7 @@ function clearInFlightFlags(desc) {
9624
10675
  desc.blockedOn = void 0;
9625
10676
  desc.pendingToolCallId = void 0;
9626
10677
  }
9627
- function loadHistorySnapshot(persistPath, sessions, sessionEvents) {
10678
+ function loadHistorySnapshot(persistPath, sessions, sessionEvents, bucketSlug, sourceBucketOf, heldIdsByBucket) {
9628
10679
  let raw;
9629
10680
  try {
9630
10681
  raw = readFileSync(persistPath, "utf8");
@@ -9676,6 +10727,10 @@ function loadHistorySnapshot(persistPath, sessions, sessionEvents) {
9676
10727
  };
9677
10728
  rt.emitter.setMaxListeners(50);
9678
10729
  sessions.set(desc.id, rt);
10730
+ if (bucketSlug !== void 0) {
10731
+ sourceBucketOf?.set(desc.id, bucketSlug);
10732
+ if (heldIdsByBucket) markHeldId(heldIdsByBucket, bucketSlug, desc.id);
10733
+ }
9679
10734
  if (wasAlive) {
9680
10735
  sessionEvents?.emit({
9681
10736
  type: "session:exited",
@@ -10551,7 +11606,7 @@ function registerOrchestrationTools(rawServer, opts) {
10551
11606
  const server = opts.toolSubset ? withToolSubset(rawServer, opts.toolSubset) : rawServer;
10552
11607
  const { registry, sessionEvents, eventRing, callerScope, inboundWatcher } = opts;
10553
11608
  const isPolicyInSubtree = (policy, ownerId) => {
10554
- const subtree = collectSubtree(ownerId, registry.list());
11609
+ const subtree = collectSubtree(ownerId, registry.list({ includeArchived: true }));
10555
11610
  const ids = policy.sessionIds.length > 0 ? policy.sessionIds : [policy.sessionId];
10556
11611
  return ids.every((id) => subtree.has(id));
10557
11612
  };
@@ -10583,7 +11638,10 @@ function registerOrchestrationTools(rawServer, opts) {
10583
11638
  const isSessionInScope = (sessionId) => {
10584
11639
  if (!callerScope) return true;
10585
11640
  if (!callerScope.ownerSessionId) return false;
10586
- return collectSubtree(callerScope.ownerSessionId, registry.list()).has(sessionId);
11641
+ return collectSubtree(
11642
+ callerScope.ownerSessionId,
11643
+ registry.list({ includeArchived: true })
11644
+ ).has(sessionId);
10587
11645
  };
10588
11646
  const enrichPermission2 = (p) => {
10589
11647
  const desc = registry.get(p.sessionId);
@@ -10761,6 +11819,12 @@ function registerOrchestrationTools(rawServer, opts) {
10761
11819
  "Reuse the session spawned by an earlier step (any prior stage), by that step's `label`. Ignored if `adapter` is set. Lets a later-stage step act on an earlier stage's output (e.g. a 'verify' step reusing a 'produce' step's session)."
10762
11820
  ),
10763
11821
  cacheable: z.boolean().optional().describe("Cache this step's output under the run's cacheKey (opt-in; for idempotent/pure steps only)."),
11822
+ sandbox: z.union([
11823
+ z.string().min(1).describe("Sandbox provider slug from `list_sandbox_providers` (e.g. 'local', 'e2b')."),
11824
+ z.object({ provider: z.string().min(1) }).passthrough().describe("Inline AIP-36 SandboxSpec ({ provider, config?, env?, \u2026 }).")
11825
+ ]).optional().describe(
11826
+ "Run this step's session inside a sandbox instead of on the host \u2014 same semantics as `agent_start.sandbox`. Only meaningful with `adapter`."
11827
+ ),
10764
11828
  policy: z.discriminatedUnion("awaiting", [
10765
11829
  z.object({ awaiting: z.literal("auto-allow"), prompt: z.string() }),
10766
11830
  z.object({
@@ -10982,7 +12046,10 @@ function registerOrchestrationTools(rawServer, opts) {
10982
12046
  };
10983
12047
  }
10984
12048
  const targetIds = input.sessionIds && input.sessionIds.length > 0 ? input.sessionIds : input.sessionId ? [input.sessionId] : [];
10985
- const subtree = collectSubtree(callerScope.ownerSessionId, registry.list());
12049
+ const subtree = collectSubtree(
12050
+ callerScope.ownerSessionId,
12051
+ registry.list({ includeArchived: true })
12052
+ );
10986
12053
  const outside = targetIds.filter((id) => !subtree.has(id));
10987
12054
  if (outside.length > 0) {
10988
12055
  return {
@@ -11840,56 +12907,170 @@ async function startHttpServer(opts) {
11840
12907
  await handleHeartbeatTick(req, res);
11841
12908
  return;
11842
12909
  }
11843
- if (path === "/files/upload" && req.method === "POST") {
12910
+ if (path === "/files/upload" && req.method === "POST") {
12911
+ const gate = checkSessionsToken(req);
12912
+ if (gate !== "ok") {
12913
+ rejectUnauthorizedSession(req, res, gate);
12914
+ return;
12915
+ }
12916
+ await handleFileUpload(req, res, url);
12917
+ return;
12918
+ }
12919
+ if (opts.sessions && path.startsWith("/sessions")) {
12920
+ if (isMutatingSessionsRoute(req.method ?? "GET", path)) {
12921
+ const gate = checkSessionsToken(req);
12922
+ if (gate !== "ok") {
12923
+ rejectUnauthorizedSession(req, res, gate);
12924
+ return;
12925
+ }
12926
+ }
12927
+ const handled = await handleSessions(
12928
+ req,
12929
+ res,
12930
+ path,
12931
+ opts.sessions,
12932
+ opts.resolveAgentAdapter,
12933
+ opts.ptyEnabled === true,
12934
+ opts.resolveBrowserAdapter,
12935
+ opts.listBrowserAdapters,
12936
+ opts.sessionEvents,
12937
+ opts.eventRing,
12938
+ opts.buildOrchestratorMcp,
12939
+ opts.daemonMcpUrl,
12940
+ opts.provisionWorktree
12941
+ );
12942
+ if (handled) return;
12943
+ }
12944
+ if (path === "/workspaces" && req.method === "GET") {
12945
+ try {
12946
+ const config = await loadWorkspacesConfig();
12947
+ res.writeHead(200, { "content-type": "application/json" });
12948
+ res.end(JSON.stringify(config));
12949
+ } catch (err) {
12950
+ res.writeHead(500, { "content-type": "application/json" });
12951
+ res.end(
12952
+ JSON.stringify({
12953
+ error: "workspaces_load_failed",
12954
+ message: err instanceof Error ? err.message : String(err)
12955
+ })
12956
+ );
12957
+ }
12958
+ return;
12959
+ }
12960
+ if (path === "/workspaces" && req.method === "POST") {
12961
+ const gate = checkSessionsToken(req);
12962
+ if (gate !== "ok") {
12963
+ rejectUnauthorizedSession(req, res, gate);
12964
+ return;
12965
+ }
12966
+ const body = await readJsonBody(req);
12967
+ if (!body || typeof body.path !== "string" || !body.path) {
12968
+ res.writeHead(400, { "content-type": "application/json" });
12969
+ res.end(JSON.stringify({ error: "missing_path" }));
12970
+ return;
12971
+ }
12972
+ if (!isAbsolute(body.path)) {
12973
+ res.writeHead(400, { "content-type": "application/json" });
12974
+ res.end(
12975
+ JSON.stringify({
12976
+ error: "workspace_path_not_absolute",
12977
+ message: `path must be absolute, got "${body.path}".`
12978
+ })
12979
+ );
12980
+ return;
12981
+ }
12982
+ try {
12983
+ await stat(body.path);
12984
+ } catch {
12985
+ res.writeHead(400, { "content-type": "application/json" });
12986
+ res.end(
12987
+ JSON.stringify({
12988
+ error: "workspace_path_not_found",
12989
+ message: `"${body.path}" doesn't exist.`
12990
+ })
12991
+ );
12992
+ return;
12993
+ }
12994
+ if (body.slug !== void 0 && typeof body.slug !== "string") {
12995
+ res.writeHead(400, { "content-type": "application/json" });
12996
+ res.end(JSON.stringify({ error: "invalid_slug" }));
12997
+ return;
12998
+ }
12999
+ if (body.label !== void 0 && typeof body.label !== "string") {
13000
+ res.writeHead(400, { "content-type": "application/json" });
13001
+ res.end(JSON.stringify({ error: "invalid_label" }));
13002
+ return;
13003
+ }
13004
+ try {
13005
+ const config = await loadWorkspacesConfig();
13006
+ const next = addWorkspace(config, {
13007
+ slug: body.slug || basename(body.path),
13008
+ path: body.path,
13009
+ ...body.label ? { label: body.label } : {}
13010
+ });
13011
+ await saveWorkspacesConfig(next);
13012
+ res.writeHead(201, { "content-type": "application/json" });
13013
+ res.end(JSON.stringify(next));
13014
+ } catch (err) {
13015
+ res.writeHead(400, { "content-type": "application/json" });
13016
+ res.end(
13017
+ JSON.stringify({
13018
+ error: "workspace_add_failed",
13019
+ message: err instanceof Error ? err.message : String(err)
13020
+ })
13021
+ );
13022
+ }
13023
+ return;
13024
+ }
13025
+ if (path === "/workspaces/active" && req.method === "PUT") {
11844
13026
  const gate = checkSessionsToken(req);
11845
13027
  if (gate !== "ok") {
11846
13028
  rejectUnauthorizedSession(req, res, gate);
11847
13029
  return;
11848
13030
  }
11849
- await handleFileUpload(req, res, url);
11850
- return;
11851
- }
11852
- if (opts.sessions && path.startsWith("/sessions")) {
11853
- if (isMutatingSessionsRoute(req.method ?? "GET", path)) {
11854
- const gate = checkSessionsToken(req);
11855
- if (gate !== "ok") {
11856
- rejectUnauthorizedSession(req, res, gate);
11857
- return;
11858
- }
13031
+ const body = await readJsonBody(req);
13032
+ if (!body || typeof body.slug !== "string" || !body.slug) {
13033
+ res.writeHead(400, { "content-type": "application/json" });
13034
+ res.end(JSON.stringify({ error: "missing_slug" }));
13035
+ return;
11859
13036
  }
11860
- const handled = await handleSessions(
11861
- req,
11862
- res,
11863
- path,
11864
- opts.sessions,
11865
- opts.resolveAgentAdapter,
11866
- opts.ptyEnabled === true,
11867
- opts.resolveBrowserAdapter,
11868
- opts.listBrowserAdapters,
11869
- opts.sessionEvents,
11870
- opts.eventRing,
11871
- opts.buildOrchestratorMcp,
11872
- opts.daemonMcpUrl,
11873
- opts.provisionWorktree
11874
- );
11875
- if (handled) return;
11876
- }
11877
- if (path === "/workspaces" && req.method === "GET") {
11878
13037
  try {
11879
13038
  const config = await loadWorkspacesConfig();
13039
+ const next = setActiveWorkspace(config, body.slug);
13040
+ await saveWorkspacesConfig(next);
11880
13041
  res.writeHead(200, { "content-type": "application/json" });
11881
- res.end(JSON.stringify(config));
13042
+ res.end(JSON.stringify(next));
11882
13043
  } catch (err) {
11883
- res.writeHead(500, { "content-type": "application/json" });
13044
+ res.writeHead(404, { "content-type": "application/json" });
11884
13045
  res.end(
11885
13046
  JSON.stringify({
11886
- error: "workspaces_load_failed",
13047
+ error: "workspace_not_found",
11887
13048
  message: err instanceof Error ? err.message : String(err)
11888
13049
  })
11889
13050
  );
11890
13051
  }
11891
13052
  return;
11892
13053
  }
13054
+ const workspaceSlugMatch = path.match(/^\/workspaces\/([^/]+)$/);
13055
+ if (workspaceSlugMatch && req.method === "DELETE") {
13056
+ const gate = checkSessionsToken(req);
13057
+ if (gate !== "ok") {
13058
+ rejectUnauthorizedSession(req, res, gate);
13059
+ return;
13060
+ }
13061
+ const slug = decodeURIComponent(workspaceSlugMatch[1] ?? "");
13062
+ const config = await loadWorkspacesConfig();
13063
+ if (!findWorkspace(config, slug)) {
13064
+ res.writeHead(404, { "content-type": "application/json" });
13065
+ res.end(JSON.stringify({ error: "workspace_not_found", slug }));
13066
+ return;
13067
+ }
13068
+ const next = removeWorkspace(config, slug);
13069
+ await saveWorkspacesConfig(next);
13070
+ res.writeHead(200, { "content-type": "application/json" });
13071
+ res.end(JSON.stringify(next));
13072
+ return;
13073
+ }
11893
13074
  if (path === "/mcps/imports" && req.method === "GET") {
11894
13075
  const config = await loadImportedMcps();
11895
13076
  res.writeHead(200, { "content-type": "application/json" });
@@ -12045,6 +13226,41 @@ async function startHttpServer(opts) {
12045
13226
  }
12046
13227
  return;
12047
13228
  }
13229
+ if (path === "/catalog/models" && req.method === "GET") {
13230
+ if (!opts.listCatalogModels) {
13231
+ res.writeHead(501, { "content-type": "application/json" });
13232
+ res.end(
13233
+ JSON.stringify({
13234
+ error: "lister_not_configured",
13235
+ message: "Daemon was started without `listCatalogModels` \u2014 see `buildCatalogModels` in `catalog-models.ts`."
13236
+ })
13237
+ );
13238
+ return;
13239
+ }
13240
+ try {
13241
+ const qs = new URLSearchParams(
13242
+ url.includes("?") ? url.slice(url.indexOf("?") + 1) : ""
13243
+ );
13244
+ const runnableOnlyParam = qs.get("runnableOnly");
13245
+ const catalog = await opts.listCatalogModels({
13246
+ ...qs.get("adapter") ? { adapter: qs.get("adapter") } : {},
13247
+ ...qs.get("vendor") ? { vendor: qs.get("vendor") } : {},
13248
+ ...qs.get("route") ? { route: qs.get("route") } : {},
13249
+ ...runnableOnlyParam === "true" ? { runnableOnly: true } : {}
13250
+ });
13251
+ res.writeHead(200, { "content-type": "application/json" });
13252
+ res.end(JSON.stringify(catalog));
13253
+ } catch (err) {
13254
+ res.writeHead(500, { "content-type": "application/json" });
13255
+ res.end(
13256
+ JSON.stringify({
13257
+ error: "list_failed",
13258
+ message: err instanceof Error ? err.message : String(err)
13259
+ })
13260
+ );
13261
+ }
13262
+ return;
13263
+ }
12048
13264
  if (path === "/presets" && req.method === "GET") {
12049
13265
  const handled = await handlePresets(req, res, path);
12050
13266
  if (handled) return;
@@ -12400,7 +13616,10 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
12400
13616
  res.end(JSON.stringify(body));
12401
13617
  };
12402
13618
  if (path === "/sessions" && req.method === "GET") {
12403
- json(200, { sessions: registry.list() });
13619
+ const reqUrl = req.url ?? "";
13620
+ const queryString = reqUrl.includes("?") ? reqUrl.slice(reqUrl.indexOf("?") + 1) : "";
13621
+ const includeArchived = new URLSearchParams(queryString).get("includeArchived") === "true";
13622
+ json(200, { sessions: registry.list({ includeArchived }) });
12404
13623
  return true;
12405
13624
  }
12406
13625
  if (path === "/sessions/agent" && req.method === "POST") {
@@ -12485,7 +13704,7 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
12485
13704
  }
12486
13705
  );
12487
13706
  if (!result.ok) {
12488
- const status = result.code === "adapter_not_found" || result.code === "no_cwd" ? 404 : result.code === "orchestrator_not_enabled" ? 501 : result.code === "orchestrator_max_depth_exceeded" || result.code === "orchestrator_child_quota_exceeded" || result.code === "role_spawn_denied" ? 409 : result.code === "invalid_role" ? 400 : 500;
13707
+ const status = result.code === "adapter_not_found" || result.code === "no_cwd" ? 404 : result.code === "orchestrator_not_enabled" ? 501 : result.code === "orchestrator_max_depth_exceeded" || result.code === "orchestrator_child_quota_exceeded" || result.code === "role_spawn_denied" ? 409 : result.code === "invalid_role" || result.code === "worktree_requires_explicit_repo" ? 400 : 500;
12489
13708
  json(status, {
12490
13709
  error: result.code,
12491
13710
  message: result.message,
@@ -12704,6 +13923,129 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
12704
13923
  }
12705
13924
  return true;
12706
13925
  }
13926
+ const modelMatch = path.match(/^\/sessions\/([^/]+)\/model$/);
13927
+ if (modelMatch && req.method === "POST") {
13928
+ const id2 = modelMatch[1];
13929
+ if (!id2) return false;
13930
+ const body = await readJsonBody(req);
13931
+ const model = body && typeof body === "object" && typeof body.model === "string" ? body.model : void 0;
13932
+ if (!model) {
13933
+ json(400, { error: "missing_model" });
13934
+ return true;
13935
+ }
13936
+ try {
13937
+ const result = await registry.setModel(id2, model);
13938
+ json(200, { ok: true, id: id2, ...result });
13939
+ } catch (err) {
13940
+ const msg = err instanceof Error ? err.message : String(err);
13941
+ const status = msg.includes("no session") ? 404 : msg.includes("not an agent-cli session") ? 400 : 500;
13942
+ json(status, { error: "set_model_failed", message: msg });
13943
+ }
13944
+ return true;
13945
+ }
13946
+ const restartMatch = path.match(/^\/sessions\/([^/]+)\/restart$/);
13947
+ if (restartMatch && req.method === "POST") {
13948
+ const id2 = restartMatch[1];
13949
+ if (!id2) return false;
13950
+ if (!resolveAgentAdapter) {
13951
+ json(501, {
13952
+ error: "restart_not_enabled",
13953
+ message: "POST /sessions/:id/restart needs the host to inject `resolveAgentAdapter`."
13954
+ });
13955
+ return true;
13956
+ }
13957
+ const prev = registry.findByIdOrName(id2);
13958
+ if (!prev) {
13959
+ json(404, { error: "no_session", message: `no session "${id2}" found` });
13960
+ return true;
13961
+ }
13962
+ if (!prev.adapterSlug) {
13963
+ json(400, {
13964
+ error: "restart_override_invalid",
13965
+ message: "restart-with-override only applies to agent-cli sessions (a PTY/command session has no config axes to override)."
13966
+ });
13967
+ return true;
13968
+ }
13969
+ const body = await readJsonBody(req);
13970
+ const b = body && typeof body === "object" ? body : {};
13971
+ const str = (v) => typeof v === "string" ? v : void 0;
13972
+ const overrides = {
13973
+ ...str(b.model) !== void 0 ? { model: str(b.model) } : {},
13974
+ ...str(b.effort) !== void 0 ? { effort: str(b.effort) } : {},
13975
+ ...b.access && typeof b.access === "object" && str(b.access.profileRef) !== void 0 ? { access: { profileRef: str(b.access.profileRef) } } : {},
13976
+ ...b.route && typeof b.route === "object" && str(b.route.gateway) !== void 0 ? {
13977
+ route: {
13978
+ gateway: str(b.route.gateway),
13979
+ ...str(b.route.baseUrl) !== void 0 ? { baseUrl: str(b.route.baseUrl) } : {}
13980
+ }
13981
+ } : {},
13982
+ ...b.posture !== void 0 ? { posture: b.posture } : {},
13983
+ ...str(b.contextProfile) !== void 0 ? { contextProfile: str(b.contextProfile) } : {},
13984
+ ...str(b.mode) !== void 0 ? { mode: str(b.mode) } : {}
13985
+ };
13986
+ try {
13987
+ const restarted = await restartAgentSession(registry, resolveAgentAdapter, prev, {
13988
+ forceAgentResume: true,
13989
+ overrides
13990
+ });
13991
+ json(200, {
13992
+ ...restarted.desc,
13993
+ resumedFrom: restarted.resumedFrom,
13994
+ resumeVia: restarted.resumeVia,
13995
+ ...restarted.resumeFallback ? { resumeFallback: true } : {}
13996
+ });
13997
+ } catch (err) {
13998
+ if (err instanceof RestartOverrideError) {
13999
+ json(err.status, { error: err.code, message: err.message, sessionId: prev.id });
14000
+ return true;
14001
+ }
14002
+ json(500, {
14003
+ error: "restart_failed",
14004
+ message: err instanceof Error ? err.message : String(err)
14005
+ });
14006
+ }
14007
+ return true;
14008
+ }
14009
+ const effortMatch = path.match(/^\/sessions\/([^/]+)\/effort$/);
14010
+ if (effortMatch && req.method === "POST") {
14011
+ const id2 = effortMatch[1];
14012
+ if (!id2) return false;
14013
+ const body = await readJsonBody(req);
14014
+ const effort = body && typeof body === "object" && typeof body.effort === "string" ? body.effort : void 0;
14015
+ if (!effort) {
14016
+ json(400, { error: "missing_effort" });
14017
+ return true;
14018
+ }
14019
+ try {
14020
+ const result = await registry.setEffort(id2, effort);
14021
+ json(200, { ok: true, id: id2, ...result });
14022
+ } catch (err) {
14023
+ const msg = err instanceof Error ? err.message : String(err);
14024
+ const status = msg.includes("no session") ? 404 : msg.includes("not an agent-cli session") ? 400 : 500;
14025
+ json(status, { error: "set_effort_failed", message: msg });
14026
+ }
14027
+ return true;
14028
+ }
14029
+ const postureMatch = path.match(/^\/sessions\/([^/]+)\/posture$/);
14030
+ if (postureMatch && req.method === "POST") {
14031
+ const id2 = postureMatch[1];
14032
+ if (!id2) return false;
14033
+ const body = await readJsonBody(req);
14034
+ const postureRaw = body && typeof body === "object" && typeof body.posture === "string" ? body.posture : void 0;
14035
+ if (!postureRaw) {
14036
+ json(400, { error: "missing_posture" });
14037
+ return true;
14038
+ }
14039
+ try {
14040
+ const result = await registry.setPosture(id2, parsePostureInput(postureRaw));
14041
+ json(200, { ok: true, id: id2, ...result });
14042
+ } catch (err) {
14043
+ const msg = err instanceof Error ? err.message : String(err);
14044
+ const status = msg.includes("no session") ? 404 : msg.includes("not an agent-cli session") ? 400 : 500;
14045
+ json(status, { error: "set_posture_failed", message: msg });
14046
+ }
14047
+ return true;
14048
+ }
12707
14049
  if (path === "/sessions" && req.method === "POST") {
12708
14050
  const body = await readJsonBody(req);
12709
14051
  if (!body || typeof body !== "object") {
@@ -14544,8 +15886,6 @@ function createRoutineRunner(opts) {
14544
15886
  }
14545
15887
  };
14546
15888
  }
14547
-
14548
- // src/sessions-registry-agent-host.ts
14549
15889
  init_transcript_export();
14550
15890
  var SessionsRegistryAgentHost = class {
14551
15891
  constructor(registry, sessionEvents, resolveAgentAdapter, opts) {
@@ -14560,10 +15900,45 @@ var SessionsRegistryAgentHost = class {
14560
15900
  opts;
14561
15901
  sessionsByLabel = /* @__PURE__ */ new Map();
14562
15902
  async spawn(adapter, opts) {
14563
- const resolved = await this.resolveAgentAdapter(adapter);
14564
- if (!resolved) throw new Error(`adapter '${adapter}' not found`);
14565
15903
  const workspaceSlug = opts.workspaceSlug ?? this.opts?.workspaceSlug ?? "default";
14566
15904
  const cwd = opts.cwd ?? this.opts?.cwd ?? process.cwd();
15905
+ if (opts.sandbox !== void 0) {
15906
+ let sandbox;
15907
+ if (typeof opts.sandbox === "string") {
15908
+ sandbox = opts.sandbox;
15909
+ } else {
15910
+ const parsed = SandboxSpecSchema.safeParse({ config: {}, ...opts.sandbox });
15911
+ if (!parsed.success) {
15912
+ throw new Error(
15913
+ `agent step sandbox spec invalid (provider "${opts.sandbox.provider}"): ${parsed.error.message}`
15914
+ );
15915
+ }
15916
+ sandbox = parsed.data;
15917
+ }
15918
+ const result = await spawnAgentSession(
15919
+ {
15920
+ registry: this.registry,
15921
+ resolveAgentAdapter: this.resolveAgentAdapter,
15922
+ ...this.opts?.resolveSandboxProvider ? { resolveSandboxProvider: this.opts.resolveSandboxProvider } : {}
15923
+ },
15924
+ {
15925
+ adapter,
15926
+ cwd,
15927
+ workspaceSlug,
15928
+ sandbox,
15929
+ label: `agent-step:${adapter}`
15930
+ }
15931
+ );
15932
+ if (!result.ok) {
15933
+ throw new Error(`agent step sandbox spawn failed (${result.code}): ${result.message}`);
15934
+ }
15935
+ if (opts.stepId) {
15936
+ this.sessionsByLabel.set(opts.stepId, result.descriptor.id);
15937
+ }
15938
+ return result.descriptor.id;
15939
+ }
15940
+ const resolved = await this.resolveAgentAdapter(adapter);
15941
+ if (!resolved) throw new Error(`adapter '${adapter}' not found`);
14567
15942
  const agentSession = await resolved.startSession({ cwd });
14568
15943
  const desc = this.registry.spawnAgent({
14569
15944
  workspaceSlug,
@@ -14658,7 +16033,14 @@ var SessionsRegistryAgentHost = class {
14658
16033
  };
14659
16034
  unsubs.push(
14660
16035
  this.sessionEvents.on("session:turn-end", (ev) => {
14661
- if (ev.sessionId === sessionId) done();
16036
+ if (ev.sessionId !== sessionId) return;
16037
+ if (ev.empty === true) {
16038
+ fail(
16039
+ `session ${sessionId} produced an empty turn \u2014 no assistant output or tool call (commonly an auth failure or an invalid model id)`
16040
+ );
16041
+ } else {
16042
+ done();
16043
+ }
14662
16044
  })
14663
16045
  );
14664
16046
  unsubs.push(
@@ -14739,6 +16121,7 @@ function translateStages(stages, workflowId) {
14739
16121
  id: step.label,
14740
16122
  ...step.adapter !== void 0 ? { adapter: step.adapter } : {},
14741
16123
  ...step.sessionRef !== void 0 ? { sessionRef: step.sessionRef } : {},
16124
+ ...step.sandbox !== void 0 ? { sandbox: step.sandbox } : {},
14742
16125
  ...step.cacheable ? { cacheable: true } : {},
14743
16126
  prompt: () => step.prompt ?? "",
14744
16127
  policy: step.policy ?? { awaiting: "fail" }
@@ -14756,6 +16139,45 @@ function translateStages(stages, workflowId) {
14756
16139
  steps
14757
16140
  };
14758
16141
  }
16142
+ function collectAgentSteps(steps) {
16143
+ const collected = [];
16144
+ for (const step of steps) {
16145
+ if (step.kind === "agent") {
16146
+ const adapter = typeof step.adapter === "string" ? step.adapter : void 0;
16147
+ collected.push({ id: step.id, adapter, sessionRef: step.sessionRef });
16148
+ } else if (step.kind === "parallel") {
16149
+ for (const branch of step.branches) collected.push(...collectAgentSteps(branch.steps));
16150
+ } else if (step.kind === "group") {
16151
+ collected.push(...collectAgentSteps(step.steps));
16152
+ } else if (step.kind === "map") ; else if (step.kind === "pipeline") {
16153
+ for (const stage of step.stages) {
16154
+ }
16155
+ } else if (step.kind === "branch") {
16156
+ collected.push(...collectAgentSteps(step.then));
16157
+ if (step.otherwise) collected.push(...collectAgentSteps(step.otherwise));
16158
+ } else if (step.kind === "loop") {
16159
+ collected.push(...collectAgentSteps(step.body));
16160
+ } else if (step.kind === "subworkflow") {
16161
+ collected.push(...collectAgentSteps(step.workflow.steps));
16162
+ }
16163
+ }
16164
+ return collected;
16165
+ }
16166
+ function runtimeWorkflowToStages(workflow) {
16167
+ const agents = collectAgentSteps(workflow.steps);
16168
+ if (agents.length === 0) {
16169
+ return [{ steps: [{ label: "workflow" }] }];
16170
+ }
16171
+ return [
16172
+ {
16173
+ steps: agents.map((a) => ({
16174
+ label: a.id,
16175
+ ...a.adapter !== void 0 ? { adapter: a.adapter } : {},
16176
+ ...a.sessionRef !== void 0 ? { sessionRef: a.sessionRef } : {}
16177
+ }))
16178
+ }
16179
+ ];
16180
+ }
14759
16181
  var DEFAULT_PERSIST_PATH2 = () => join(homedir(), ".agentproto", "workflow-runs.json");
14760
16182
  function loadRuns2(persistPath) {
14761
16183
  const result = /* @__PURE__ */ new Map();
@@ -14813,13 +16235,10 @@ function fireNotifyUrl(run) {
14813
16235
  }).catch(() => void 0);
14814
16236
  }
14815
16237
  function resolveStepSessionId(step, agents) {
14816
- if (step.adapter) {
14817
- return agents.resolveByLabel(step.label);
14818
- }
14819
16238
  if (step.sessionRef) {
14820
16239
  return agents.resolveByLabel(step.sessionRef);
14821
16240
  }
14822
- return void 0;
16241
+ return agents.resolveByLabel(step.label);
14823
16242
  }
14824
16243
  function fillStepStates(stages, defs, agents) {
14825
16244
  const sessionIds = [];
@@ -14879,6 +16298,8 @@ async function executeRunWorkflow(state, runtimeWf, agents, signal, cache, cache
14879
16298
  }
14880
16299
  }
14881
16300
  }
16301
+ const sessionIds = fillStepStates(state.run.stages, state.stages, agents);
16302
+ if (sessionIds.length > 0) state.run.result = { sessionIds };
14882
16303
  }
14883
16304
  }
14884
16305
  fireNotifyUrl(state.run);
@@ -14930,7 +16351,8 @@ function createWorkflowRunner(opts) {
14930
16351
  {
14931
16352
  workspaceSlug: input.workspaceSlug,
14932
16353
  cwd: input.cwd,
14933
- notifyUrl: input.notifyUrl
16354
+ notifyUrl: input.notifyUrl,
16355
+ ...opts.resolveSandboxProvider ? { resolveSandboxProvider: opts.resolveSandboxProvider } : {}
14934
16356
  }
14935
16357
  );
14936
16358
  const cache = input.cacheKey ? createFileStepCache(input.cacheKey) : void 0;
@@ -14947,26 +16369,30 @@ function createWorkflowRunner(opts) {
14947
16369
  }
14948
16370
  const handle = await loadWorkflowHandle(args.path);
14949
16371
  const workflow = await compileWorkflow2(handle);
16372
+ const fileStages = runtimeWorkflowToStages(workflow);
14950
16373
  const runId = `wfrun_${randomUUID()}`;
14951
16374
  const run = {
14952
16375
  runId,
14953
16376
  workflowId: handle.id,
14954
16377
  status: "running",
14955
16378
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
14956
- stages: [
14957
- {
14958
- index: 0,
14959
- status: "pending",
14960
- steps: [{ index: 0, label: "workflow", status: "pending" }]
14961
- }
14962
- ]
16379
+ stages: fileStages.map((stage, si) => ({
16380
+ index: si,
16381
+ ...stage.label !== void 0 ? { label: stage.label } : {},
16382
+ status: "pending",
16383
+ steps: stage.steps.map((s, i) => ({
16384
+ index: i,
16385
+ label: s.label,
16386
+ status: "pending"
16387
+ }))
16388
+ }))
14963
16389
  };
14964
16390
  const abort = new AbortController();
14965
16391
  const state = {
14966
16392
  run,
14967
16393
  cancelled: false,
14968
16394
  abort,
14969
- stages: [],
16395
+ stages: fileStages,
14970
16396
  ...args.cwd !== void 0 ? { cwd: args.cwd } : {},
14971
16397
  ...args.workspaceSlug !== void 0 ? { workspaceSlug: args.workspaceSlug } : {}
14972
16398
  };
@@ -14978,7 +16404,8 @@ function createWorkflowRunner(opts) {
14978
16404
  resolveAgentAdapter,
14979
16405
  {
14980
16406
  workspaceSlug: args.workspaceSlug,
14981
- cwd: args.cwd
16407
+ cwd: args.cwd,
16408
+ ...opts.resolveSandboxProvider ? { resolveSandboxProvider: opts.resolveSandboxProvider } : {}
14982
16409
  }
14983
16410
  );
14984
16411
  const cache = args.cacheKey ? createFileStepCache(args.cacheKey) : void 0;
@@ -15743,6 +17170,216 @@ function createOrchestratorInjector(deps2) {
15743
17170
  return { entry, scope, bindLifecycle };
15744
17171
  };
15745
17172
  }
17173
+ var WIDENING_ROUTES = ["openrouter", "requesty", "huggingface"];
17174
+ function normalizeRouterPrefixedId(id) {
17175
+ const firstSlash = id.indexOf("/");
17176
+ if (firstSlash === -1) return id;
17177
+ const head = id.slice(0, firstSlash);
17178
+ if (!WIDENING_ROUTES.includes(head)) return id;
17179
+ const remainder = id.slice(firstSlash + 1);
17180
+ if (!remainder.includes("/") || remainder.includes("@")) return id;
17181
+ return `${remainder}@${head}`;
17182
+ }
17183
+ function tryResolveLlmModelRoute(id) {
17184
+ try {
17185
+ return resolveLlmModelRoute(id);
17186
+ } catch {
17187
+ return void 0;
17188
+ }
17189
+ }
17190
+ function vendorFromIdPrefix(bareId) {
17191
+ if (/^claude[-/]/.test(bareId)) return "anthropic";
17192
+ if (/^(gpt[-/]|o[1-9](-|$)|chatgpt)/.test(bareId)) return "openai";
17193
+ if (/^gemini[-/]/.test(bareId)) return "google";
17194
+ if (/^grok[-/]/.test(bareId)) return "x-ai";
17195
+ if (/^deepseek[-/]/.test(bareId)) return "deepseek";
17196
+ return void 0;
17197
+ }
17198
+ function resolveModelId(id) {
17199
+ const normalized = normalizeRouterPrefixedId(id);
17200
+ const resolved = tryResolveLlmModelRoute(normalized);
17201
+ if (resolved) {
17202
+ return {
17203
+ vendor: resolved.vendor,
17204
+ product: resolved.product,
17205
+ directRoute: resolved.route,
17206
+ ref: formatModelRef(resolved.ref),
17207
+ baseUrl: resolved.transport.baseUrl ?? null,
17208
+ pricing: {
17209
+ inPer1M: resolved.pricing.inputPer1M,
17210
+ outPer1M: resolved.pricing.outputPer1M
17211
+ }
17212
+ };
17213
+ }
17214
+ const parsed = tryParseModelRef(normalized);
17215
+ if (parsed) {
17216
+ return {
17217
+ vendor: parsed.vendor,
17218
+ product: parsed.product,
17219
+ directRoute: parsed.route,
17220
+ ref: formatModelRef(parsed),
17221
+ baseUrl: null,
17222
+ pricing: null
17223
+ };
17224
+ }
17225
+ const vendor = vendorFromIdPrefix(id) ?? "unknown";
17226
+ return {
17227
+ vendor,
17228
+ product: id,
17229
+ directRoute: vendor,
17230
+ ref: `${vendor}/${id}`,
17231
+ baseUrl: null,
17232
+ pricing: null
17233
+ };
17234
+ }
17235
+ function methodsForDirect(descriptor) {
17236
+ const methods = [];
17237
+ if (descriptor?.authSubscription) methods.push("oauth-bearer");
17238
+ if (descriptor?.provider) methods.push("api-key");
17239
+ return methods;
17240
+ }
17241
+ function isDirectRoute(mode, resolved) {
17242
+ return mode === void 0 && resolved.directRoute === resolved.vendor;
17243
+ }
17244
+ function curatedContributions(adapters) {
17245
+ const out = [];
17246
+ for (const adapter of adapters) {
17247
+ for (const model of adapter.models) {
17248
+ const resolved = resolveModelId(model.id);
17249
+ const route = model.mode ?? resolved.directRoute;
17250
+ const methods = isDirectRoute(model.mode, resolved) ? methodsForDirect(adapter.authDescriptor) : ["api-key"];
17251
+ out.push({
17252
+ vendor: resolved.vendor,
17253
+ product: resolved.product,
17254
+ route,
17255
+ ref: resolved.ref,
17256
+ baseUrl: resolved.baseUrl,
17257
+ pricing: resolved.pricing,
17258
+ curated: true,
17259
+ adapterSlug: adapter.slug,
17260
+ ...model.mode ? { adapterMode: model.mode } : {},
17261
+ methods
17262
+ });
17263
+ }
17264
+ }
17265
+ return out;
17266
+ }
17267
+ function widenedContributions(curated) {
17268
+ const seenProducts = /* @__PURE__ */ new Map();
17269
+ for (const c of curated) {
17270
+ const key = `${c.vendor}/${c.product}`;
17271
+ const routes = seenProducts.get(key) ?? /* @__PURE__ */ new Set();
17272
+ routes.add(c.route);
17273
+ seenProducts.set(key, routes);
17274
+ }
17275
+ const out = [];
17276
+ for (const [key, existingRoutes] of seenProducts) {
17277
+ const [vendor, product] = key.split("/", 2);
17278
+ for (const router of WIDENING_ROUTES) {
17279
+ if (existingRoutes.has(router)) continue;
17280
+ const resolved = resolveLlmModelRoute(`${vendor}/${product}@${router}`);
17281
+ if (!resolved) continue;
17282
+ out.push({
17283
+ vendor,
17284
+ product,
17285
+ route: router,
17286
+ ref: formatModelRef(resolved.ref),
17287
+ baseUrl: resolved.transport.baseUrl ?? null,
17288
+ pricing: {
17289
+ inPer1M: resolved.pricing.inputPer1M,
17290
+ outPer1M: resolved.pricing.outputPer1M
17291
+ },
17292
+ curated: false,
17293
+ methods: ["api-key"]
17294
+ });
17295
+ }
17296
+ }
17297
+ return out;
17298
+ }
17299
+ function mergeContributions(contributions) {
17300
+ const rows = /* @__PURE__ */ new Map();
17301
+ for (const c of contributions) {
17302
+ const key = `${c.vendor}\0${c.product}\0${c.route}`;
17303
+ const existing = rows.get(key);
17304
+ if (!existing) {
17305
+ rows.set(key, {
17306
+ vendor: c.vendor,
17307
+ product: c.product,
17308
+ route: c.route,
17309
+ ref: c.ref,
17310
+ baseUrl: c.baseUrl,
17311
+ pricing: c.pricing,
17312
+ curated: c.curated,
17313
+ adapters: c.adapterSlug ? [c.adapterSlug] : [],
17314
+ adapterModes: c.adapterMode ? [c.adapterMode] : [],
17315
+ methods: [...c.methods]
17316
+ });
17317
+ continue;
17318
+ }
17319
+ existing.curated = existing.curated || c.curated;
17320
+ existing.baseUrl = existing.baseUrl ?? c.baseUrl;
17321
+ existing.pricing = existing.pricing ?? c.pricing;
17322
+ if (c.adapterSlug && !existing.adapters.includes(c.adapterSlug)) {
17323
+ existing.adapters.push(c.adapterSlug);
17324
+ }
17325
+ if (c.adapterMode && !existing.adapterModes.includes(c.adapterMode)) {
17326
+ existing.adapterModes.push(c.adapterMode);
17327
+ }
17328
+ for (const m of c.methods) {
17329
+ if (!existing.methods.includes(m)) existing.methods.push(m);
17330
+ }
17331
+ }
17332
+ return [...rows.values()];
17333
+ }
17334
+ function billedVendor(vendor, route) {
17335
+ return route === vendor ? vendor : route;
17336
+ }
17337
+ function buildCatalogModels(input) {
17338
+ const contributions = [
17339
+ ...curatedContributions(input.adapters),
17340
+ ...widenedContributions(curatedContributions(input.adapters))
17341
+ ];
17342
+ const merged = mergeContributions(contributions);
17343
+ const query = input.query ?? {};
17344
+ const vendors = /* @__PURE__ */ new Map();
17345
+ for (const row of merged) {
17346
+ if (query.vendor && row.vendor !== query.vendor) continue;
17347
+ if (query.route && row.route !== query.route) continue;
17348
+ if (query.adapter && !row.adapters.includes(query.adapter)) continue;
17349
+ const manifest = {
17350
+ id: `${row.vendor}/${row.product}@${row.route}`,
17351
+ vendorByRoute: { [row.route]: billedVendor(row.vendor, row.route) },
17352
+ methodsByRoute: { [row.route]: row.methods }
17353
+ };
17354
+ const eligible = eligibleProfiles(input.profiles, manifest, row.route);
17355
+ const runnable = eligible.length > 0;
17356
+ if (query.runnableOnly && !runnable) continue;
17357
+ const route = {
17358
+ route: row.route,
17359
+ ref: row.ref,
17360
+ baseUrl: row.baseUrl,
17361
+ pricing: row.pricing,
17362
+ runnable,
17363
+ eligibleProfiles: eligible.map((p) => p.id),
17364
+ adapterModes: row.adapterModes,
17365
+ adapters: row.adapters,
17366
+ curated: row.curated
17367
+ };
17368
+ const products = vendors.get(row.vendor) ?? /* @__PURE__ */ new Map();
17369
+ vendors.set(row.vendor, products);
17370
+ const routes = products.get(row.product) ?? [];
17371
+ products.set(row.product, routes);
17372
+ routes.push(route);
17373
+ }
17374
+ const result = [...vendors.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([vendor, products]) => ({
17375
+ vendor,
17376
+ products: [...products.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([product, routes]) => ({
17377
+ product,
17378
+ routes: [...routes].sort((a, b) => a.route.localeCompare(b.route))
17379
+ }))
17380
+ }));
17381
+ return { vendors: result };
17382
+ }
15746
17383
  function makeBrowserHandle(entry, resolve14) {
15747
17384
  return {
15748
17385
  slug: entry.id,
@@ -17642,7 +19279,7 @@ var WorkspacePathError = class extends Error {
17642
19279
  };
17643
19280
  function createWorkspaceFs(opts) {
17644
19281
  const root = resolve(opts.workspace);
17645
- function resolvePath4(path) {
19282
+ function resolvePath5(path) {
17646
19283
  if (typeof path !== "string" || path.length === 0) {
17647
19284
  throw new WorkspacePathError("path must be a non-empty string");
17648
19285
  }
@@ -17662,18 +19299,18 @@ function createWorkspaceFs(opts) {
17662
19299
  }
17663
19300
  return {
17664
19301
  async readFile(path) {
17665
- const abs = resolvePath4(path);
19302
+ const abs = resolvePath5(path);
17666
19303
  const buf = await readFile(abs);
17667
19304
  return buf.toString("utf8");
17668
19305
  },
17669
19306
  async writeFile(path, content) {
17670
- const abs = resolvePath4(path);
19307
+ const abs = resolvePath5(path);
17671
19308
  await mkdir(dirname(abs), { recursive: true });
17672
19309
  await writeFile(abs, content);
17673
19310
  },
17674
19311
  async exists(path) {
17675
19312
  try {
17676
- const abs = resolvePath4(path);
19313
+ const abs = resolvePath5(path);
17677
19314
  return existsSync(abs);
17678
19315
  } catch {
17679
19316
  return false;
@@ -18031,6 +19668,39 @@ function isEnoent(err) {
18031
19668
  function errMsg(err) {
18032
19669
  return err instanceof Error ? err.message : String(err);
18033
19670
  }
19671
+ var POSTURE_MODE_VALUES = {
19672
+ default: "default",
19673
+ plan: "plan",
19674
+ "accept-edits": "accept-edits",
19675
+ "bypass-permissions": "bypass",
19676
+ "read-only": "read-only",
19677
+ "full-access": "bypass",
19678
+ build: "default"
19679
+ };
19680
+ function decomposeMode(modes, modeId) {
19681
+ const declared = modes.find((mode) => mode.id === modeId);
19682
+ const kind = declared?.kind ?? inferLegacyModeKind(modeId);
19683
+ if (kind === "route") return { route: { gateway: modeId } };
19684
+ if (kind === "posture") return { posture: POSTURE_MODE_VALUES[modeId] ?? "default" };
19685
+ return { contextProfile: modeId };
19686
+ }
19687
+ function decomposedAxisMatches(cfg, decomposed) {
19688
+ if (decomposed.route) return cfg.route?.gateway === decomposed.route.gateway;
19689
+ if (decomposed.posture !== void 0) return cfg.posture === decomposed.posture;
19690
+ if (decomposed.contextProfile !== void 0) {
19691
+ return cfg.contextProfile === decomposed.contextProfile;
19692
+ }
19693
+ return false;
19694
+ }
19695
+ function composeMode(cfg, modes) {
19696
+ for (const mode of modes) {
19697
+ if (decomposedAxisMatches(cfg, decomposeMode(modes, mode.id))) return mode.id;
19698
+ }
19699
+ return void 0;
19700
+ }
19701
+
19702
+ // src/index.ts
19703
+ init_conversation_store();
18034
19704
  async function isAgentCliAuthConfigured(slug, descriptor, model) {
18035
19705
  const config = await loadConfig();
18036
19706
  const spawnDefaults = resolveSpawnDefaults(config.defaults, slug, {});
@@ -18203,6 +19873,10 @@ async function createGateway(opts) {
18203
19873
  sessionEvents,
18204
19874
  resolveAgentAdapter: opts.resolveAgentAdapter,
18205
19875
  persist,
19876
+ // Sandbox-capable agent steps (`AgentStep.sandbox` / workflow_start's
19877
+ // step `sandbox`) resolve providers through the same resolver
19878
+ // `agent_start.sandbox` uses.
19879
+ resolveSandboxProvider: resolveSandboxProviderResolved,
18206
19880
  // Compile a loaded WORKFLOW.md handle into a runnable RuntimeWorkflow
18207
19881
  // for `workflow_run_file` / `startFromFile`. The daemon's workflow
18208
19882
  // surface is agent-step based (like the stage primitive), so no tool/
@@ -18266,7 +19940,8 @@ async function createGateway(opts) {
18266
19940
  resolveSandboxProvider: resolveSandboxProviderResolved,
18267
19941
  ...opts.provisionWorktree ? { provisionWorktree: opts.provisionWorktree } : {},
18268
19942
  ...opts.resolveAgentAdapter ? { resolveAgentAdapter: opts.resolveAgentAdapter } : {},
18269
- ...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {}
19943
+ ...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {},
19944
+ ...opts.listCatalogModels ? { listCatalogModels: opts.listCatalogModels } : {}
18270
19945
  });
18271
19946
  registerBrowserTools(server, {
18272
19947
  registry: sessions,
@@ -18422,6 +20097,7 @@ async function createGateway(opts) {
18422
20097
  daemonMcpUrl,
18423
20098
  ...opts.provisionWorktree ? { provisionWorktree: opts.provisionWorktree } : {},
18424
20099
  ...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {},
20100
+ ...opts.listCatalogModels ? { listCatalogModels: opts.listCatalogModels } : {},
18425
20101
  ...opts.resolveBrowserAdapter ? { resolveBrowserAdapter: opts.resolveBrowserAdapter } : {},
18426
20102
  ...opts.listBrowserAdapters ? { listBrowserAdapters: opts.listBrowserAdapters } : {},
18427
20103
  meta: { workspace, registered, startedAt },
@@ -18508,6 +20184,6 @@ var export_providersPath = providers_store_exports.providersPath;
18508
20184
  var export_removeProviderKey = providers_store_exports.removeProviderKey;
18509
20185
  var export_setProviderKey = providers_store_exports.setProviderKey;
18510
20186
 
18511
- export { AuthResolutionError, BUCKETS_ROOT, DEFAULT_BUCKET, DEFAULT_ORCHESTRATOR_TOOLS, DEFAULT_WORKTREE_ISOLATION, LEGACY_SESSIONS_FILE, PAIRINGS_VERSION, export_PROVIDER_ENV_VARS as PROVIDER_ENV_VARS, WORKTREE_ISOLATION_ENV, bucketDir, bucketSessionsFile, bucketTranscriptDir, composeSessionObservers, createFileStepCache, createGateway, createOrchestratorInjector, createOrchestratorMcpServerFactory, createPairingRegistry, createScopeTokenRegistry, createWorkspaceFs, credentialFingerprint, daemonRegistryDir, decideWorktreeIsolation, declaredPresetToProviderPreset, deriveSessionUsage, fileConversationStore, formatToolCall, formatToolResult, getMcpCredentialDeps, export_getProviderKey as getProviderKey, export_injectProviderKeysIntoEnv as injectProviderKeysIntoEnv, isAgentCliAuthConfigured, isSafeBucketSlug, listBuckets, listPresets, export_loadProviders as loadProviders, loadWorktreeIsolation, makeBrowserAdapterLister, migrateLegacySessionsFile, migrationMarkerPath, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, normalizeSkillsOption, normalizeWorktreeField, parseDuration, parseWorktreeIsolationMode, policyWatchesSession, projectSessionUsage, export_providerEnvVar as providerEnvVar, export_providersPath as providersPath, readDaemonRegistry, readRegisteredSlugs, readRuntimeMeta, registerPairingTools, export_removeProviderKey as removeProviderKey, resolveAuthSpec, resolveBucketSlug, resolveSpawnDefaults, setMcpCredentialDeps, export_setProviderKey as setProviderKey, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, writeDaemonRegistryEntry };
20187
+ export { AuthResolutionError, BUCKETS_ROOT, DEFAULT_BUCKET, DEFAULT_ORCHESTRATOR_TOOLS, DEFAULT_WORKTREE_ISOLATION, LEGACY_SESSIONS_FILE, PAIRINGS_VERSION, POSTURE_NATIVE_ALIASES, POSTURE_PREAMBLES, export_PROVIDER_ENV_VARS as PROVIDER_ENV_VARS, WORKTREE_ISOLATION_ENV, appendConversationRecord, bucketDir, bucketSessionsFile, bucketTranscriptDir, buildCatalogModels, canonicalForModeId, claudeProjectSlug, composeMode, composeSessionObservers, conversationIndexPath, createFileStepCache, createGateway, createOrchestratorInjector, createOrchestratorMcpServerFactory, createPairingRegistry, createScopeTokenRegistry, createWorkspaceFs, credentialFingerprint, daemonRegistryDir, decideWorktreeIsolation, declaredPresetToProviderPreset, decomposeMode, deriveSessionUsage, fileConversationStore, findConversationRecord, findNativeMode, formatToolCall, formatToolResult, getMcpCredentialDeps, export_getProviderKey as getProviderKey, export_injectProviderKeysIntoEnv as injectProviderKeysIntoEnv, isAgentCliAuthConfigured, isSafeBucketSlug, listBuckets, listPresets, export_loadProviders as loadProviders, loadWorktreeIsolation, locateConversationByNativePath, locateConversationBySessionId, makeBrowserAdapterLister, migrateLegacySessionsFile, migrationMarkerPath, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, normalizeModeId, normalizeSkillsOption, normalizeWorktreeField, parseDuration, parseWorktreeIsolationMode, policyWatchesSession, projectSessionUsage, export_providerEnvVar as providerEnvVar, export_providersPath as providersPath, readConversationIndex, readDaemonRegistry, readRegisteredSlugs, readRuntimeMeta, registerPairingTools, export_removeProviderKey as removeProviderKey, resolveAuthSpec, resolveBucketSlug, resolveNativeLink, resolvePosture, resolveSpawnDefaults, setMcpCredentialDeps, export_setProviderKey as setProviderKey, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, writeDaemonRegistryEntry };
18512
20188
  //# sourceMappingURL=index.mjs.map
18513
20189
  //# sourceMappingURL=index.mjs.map