@agentproto/runtime 0.8.0 → 1.1.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
@@ -1,4 +1,4 @@
1
- import { createReadStream, existsSync, promises, readdirSync, readFileSync, mkdirSync, writeFileSync, renameSync, createWriteStream, statSync, openSync, closeSync } from 'fs';
1
+ import { createReadStream, existsSync, promises, readdirSync, readFileSync, mkdirSync, writeFileSync, renameSync, realpathSync, createWriteStream, statSync, openSync, closeSync } from 'fs';
2
2
  import { homedir, tmpdir } from 'os';
3
3
  import { join, resolve, dirname, isAbsolute, normalize, relative, basename } from 'path';
4
4
  import { createInterface } from 'readline';
@@ -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,12 +1897,77 @@ 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
  }
1958
+ function canonical(p) {
1959
+ try {
1960
+ return realpathSync(resolve(p));
1961
+ } catch {
1962
+ return resolve(p);
1963
+ }
1964
+ }
1749
1965
  function findWorkspaceByPath(config, dir) {
1750
- const resolved = resolve(dir);
1751
- const candidates = config.workspaces.filter((w) => resolved.startsWith(resolve(w.path) + "/") || resolved === resolve(w.path)).sort((a, b) => b.path.length - a.path.length);
1966
+ const resolved = canonical(dir);
1967
+ const candidates = config.workspaces.filter((w) => {
1968
+ const wPath = canonical(w.path);
1969
+ return resolved.startsWith(wPath + "/") || resolved === wPath;
1970
+ }).sort((a, b) => b.path.length - a.path.length);
1752
1971
  return candidates[0];
1753
1972
  }
1754
1973
  function getActiveWorkspace(config) {
@@ -1834,6 +2053,76 @@ async function loadConfig(path) {
1834
2053
  return {};
1835
2054
  }
1836
2055
  }
2056
+ var markerSchema = z.object({ worktreeId: z.string() });
2057
+ function statOrUndefined(path) {
2058
+ try {
2059
+ return statSync(path);
2060
+ } catch {
2061
+ return void 0;
2062
+ }
2063
+ }
2064
+ function readWorktreeGitDir(dir) {
2065
+ const link = (() => {
2066
+ try {
2067
+ return readFileSync(join(dir, ".git"), "utf8");
2068
+ } catch {
2069
+ return void 0;
2070
+ }
2071
+ })();
2072
+ if (link === void 0) return void 0;
2073
+ const target = /^gitdir:\s*(.+)$/m.exec(link)?.[1]?.trim();
2074
+ if (!target) return void 0;
2075
+ const gitDir = isAbsolute(target) ? target : resolve(dir, target);
2076
+ return statOrUndefined(join(gitDir, "gitdir"))?.isFile() === true ? gitDir : void 0;
2077
+ }
2078
+ function readMainRepoPath(gitDir) {
2079
+ let raw;
2080
+ try {
2081
+ raw = readFileSync(join(gitDir, "commondir"), "utf8").trim();
2082
+ } catch {
2083
+ return void 0;
2084
+ }
2085
+ if (!raw) return void 0;
2086
+ const commonGitDir = isAbsolute(raw) ? raw : resolve(gitDir, raw);
2087
+ return dirname(commonGitDir);
2088
+ }
2089
+ function readWorktreeId(gitDir) {
2090
+ let raw;
2091
+ try {
2092
+ raw = readFileSync(join(gitDir, "agentproto-worktree.json"), "utf8");
2093
+ } catch {
2094
+ return void 0;
2095
+ }
2096
+ let parsed;
2097
+ try {
2098
+ parsed = JSON.parse(raw);
2099
+ } catch {
2100
+ return void 0;
2101
+ }
2102
+ const result = markerSchema.safeParse(parsed);
2103
+ return result.success ? result.data.worktreeId : void 0;
2104
+ }
2105
+ function resolveWorktreeIdentity(cwd) {
2106
+ let dir = resolve(cwd);
2107
+ for (; ; ) {
2108
+ const dotGit = statOrUndefined(join(dir, ".git"));
2109
+ if (dotGit) {
2110
+ if (!dotGit.isFile()) return void 0;
2111
+ const gitDir = readWorktreeGitDir(dir);
2112
+ if (gitDir === void 0) return void 0;
2113
+ const worktreeId = readWorktreeId(gitDir);
2114
+ const mainRepoPath = readMainRepoPath(gitDir);
2115
+ return {
2116
+ worktreePath: dir,
2117
+ ...worktreeId === void 0 ? {} : { worktreeId },
2118
+ ...mainRepoPath === void 0 ? {} : { mainRepoPath }
2119
+ };
2120
+ }
2121
+ const parent = dirname(dir);
2122
+ if (parent === dir) return void 0;
2123
+ dir = parent;
2124
+ }
2125
+ }
1837
2126
 
1838
2127
  // src/providers-store.ts
1839
2128
  var providers_store_exports = {};
@@ -2215,6 +2504,8 @@ function getMcpCredentialDeps() {
2215
2504
  // src/sandbox-agent-session-proxy.ts
2216
2505
  var MAX_POLL_MS = 49e3;
2217
2506
  var MAX_OUTPUT_LINES = 500;
2507
+ var MAX_CONSECUTIVE_POLL_FAILURES = 6;
2508
+ var POLL_RETRY_DELAY_MS = 5e3;
2218
2509
  function extractPromptText(message) {
2219
2510
  if (typeof message === "string") return message;
2220
2511
  if (message && typeof message === "object" && "text" in message) {
@@ -2232,29 +2523,63 @@ function createSandboxAgentSessionProxy(opts) {
2232
2523
  async *send(message) {
2233
2524
  const prompt = extractPromptText(message);
2234
2525
  lastPrompt = prompt;
2235
- await host.prompt(remoteSessionId, prompt);
2236
2526
  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) };
2527
+ try {
2528
+ await host.prompt(remoteSessionId, prompt);
2529
+ let consecutivePollFailures = 0;
2530
+ for (; ; ) {
2531
+ let result;
2532
+ try {
2533
+ result = await host.waitForAny([remoteSessionId], {
2534
+ event: "any",
2535
+ timeoutMs: MAX_POLL_MS
2536
+ });
2537
+ consecutivePollFailures = 0;
2538
+ } catch (pollErr) {
2539
+ consecutivePollFailures++;
2540
+ if (consecutivePollFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
2541
+ throw new Error(
2542
+ `sandbox proxy: ${consecutivePollFailures} consecutive poll failures against the box daemon (session "${remoteSessionId}") \u2014 giving up. Last error: ${pollErr instanceof Error ? pollErr.message : String(pollErr)}`
2543
+ );
2544
+ }
2545
+ await new Promise((resolve14) => setTimeout(resolve14, POLL_RETRY_DELAY_MS));
2546
+ continue;
2547
+ }
2548
+ if (result.timedOut) continue;
2549
+ let tail;
2550
+ try {
2551
+ tail = await host.output(remoteSessionId, MAX_OUTPUT_LINES);
2552
+ } catch {
2553
+ try {
2554
+ tail = await host.output(remoteSessionId, MAX_OUTPUT_LINES);
2555
+ } catch {
2556
+ tail = void 0;
2557
+ }
2558
+ }
2559
+ if (tail !== void 0 && tail.length > seenLength) {
2560
+ yield { kind: "text-delta", text: tail.slice(seenLength) };
2561
+ }
2562
+ if (tail !== void 0) seenLength = tail.length;
2563
+ if (result.event === "exited") {
2564
+ throw new Error(
2565
+ `sandbox proxy: remote session "${remoteSessionId}" exited \u2014 the box's own agentproto daemon ended this session (crash, OOM, or an out-of-band kill).`
2566
+ );
2567
+ }
2568
+ yield {
2569
+ kind: "turn-end",
2570
+ reason: result.event === "awaiting-input" ? "awaiting-input" : "completed"
2571
+ };
2572
+ return;
2246
2573
  }
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
- );
2574
+ } catch (err) {
2575
+ try {
2576
+ const tail = await host.output(remoteSessionId, MAX_OUTPUT_LINES);
2577
+ if (tail.length > seenLength) {
2578
+ yield { kind: "text-delta", text: tail.slice(seenLength) };
2579
+ }
2580
+ } catch {
2252
2581
  }
2253
- yield {
2254
- kind: "turn-end",
2255
- reason: result.event === "awaiting-input" ? "awaiting-input" : "completed"
2256
- };
2257
- return;
2582
+ throw err;
2258
2583
  }
2259
2584
  },
2260
2585
  async cancel() {
@@ -2352,21 +2677,54 @@ async function spawnAgentSession(deps2, input) {
2352
2677
  provisionWorktree,
2353
2678
  resolveWorktreeIsolation
2354
2679
  } = deps2;
2680
+ const explicitCwd = input.cwd !== void 0;
2681
+ const explicitWorkspaceSlug = input.workspaceSlug !== void 0;
2682
+ const childDepth = callerScope ? callerScope.depth + 1 : 0;
2683
+ if (childDepth === 0 && !explicitCwd && !explicitWorkspaceSlug && normalizeWorktreeField(input.worktree) !== void 0) {
2684
+ return {
2685
+ ok: false,
2686
+ code: "worktree_requires_explicit_repo",
2687
+ 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`."
2688
+ };
2689
+ }
2355
2690
  let cwd = input.cwd;
2356
2691
  let resolvedSlug = input.workspaceSlug;
2357
2692
  if (!cwd || !resolvedSlug) {
2358
2693
  try {
2359
2694
  const config = await loadWorkspacesConfig();
2360
2695
  if (!cwd) {
2361
- const ws = input.workspaceSlug ? findWorkspace(config, input.workspaceSlug) : getActiveWorkspace(config);
2362
- if (ws) {
2363
- cwd = ws.path;
2364
- resolvedSlug = ws.slug;
2696
+ if (input.workspaceSlug) {
2697
+ const ws = findWorkspace(config, input.workspaceSlug);
2698
+ if (ws) {
2699
+ cwd = ws.path;
2700
+ resolvedSlug = ws.slug;
2701
+ }
2702
+ } else if (callerScope) {
2703
+ const parentCwd = callerScope.ownerSessionId ? registry.get(callerScope.ownerSessionId)?.cwd : void 0;
2704
+ if (parentCwd) {
2705
+ cwd = parentCwd;
2706
+ const ws = findWorkspaceByPath(config, parentCwd);
2707
+ if (ws) {
2708
+ resolvedSlug = ws.slug;
2709
+ }
2710
+ }
2711
+ } else {
2712
+ const ws = getActiveWorkspace(config);
2713
+ if (ws) {
2714
+ cwd = ws.path;
2715
+ resolvedSlug = ws.slug;
2716
+ }
2365
2717
  }
2366
2718
  } else if (!resolvedSlug) {
2367
2719
  const ws = findWorkspaceByPath(config, cwd);
2368
2720
  if (ws) {
2369
2721
  resolvedSlug = ws.slug;
2722
+ } else {
2723
+ const identity = resolveWorktreeIdentity(cwd);
2724
+ if (identity?.mainRepoPath) {
2725
+ const baseWs = findWorkspaceByPath(config, identity.mainRepoPath);
2726
+ if (baseWs) resolvedSlug = baseWs.slug;
2727
+ }
2370
2728
  }
2371
2729
  }
2372
2730
  } catch {
@@ -2388,7 +2746,6 @@ async function spawnAgentSession(deps2, input) {
2388
2746
  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
2747
  };
2390
2748
  }
2391
- const childDepth = callerScope ? callerScope.depth + 1 : 0;
2392
2749
  const parentSessionId = callerScope?.ownerSessionId;
2393
2750
  if (callerScope) {
2394
2751
  if (childDepth > callerScope.maxDepth) {
@@ -2430,7 +2787,14 @@ async function spawnAgentSession(deps2, input) {
2430
2787
  return { ok: false, code: "worktree_disabled", message: decision.message };
2431
2788
  }
2432
2789
  if (decision.action === "provision") {
2433
- if (!provisionWorktree) {
2790
+ if (!explicitCwd && !explicitWorkspaceSlug) {
2791
+ return {
2792
+ ok: false,
2793
+ code: "worktree_requires_explicit_repo",
2794
+ 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`."
2795
+ };
2796
+ }
2797
+ if (!provisionWorktree) {
2434
2798
  return {
2435
2799
  ok: false,
2436
2800
  code: "worktree_provisioner_not_enabled",
@@ -2517,9 +2881,10 @@ async function spawnAgentSession(deps2, input) {
2517
2881
  options: input.options,
2518
2882
  auth: input.auth
2519
2883
  });
2884
+ const hasGatewayBaseUrlOption = typeof spawnDefaults.options?.base_url === "string" && spawnDefaults.options.base_url.length > 0;
2520
2885
  let authSpec;
2521
2886
  let authEcho;
2522
- if (resolved && input.sandbox === void 0 && resolved.authDescriptor) {
2887
+ if (resolved && input.sandbox === void 0 && resolved.authDescriptor && !hasGatewayBaseUrlOption) {
2523
2888
  const authModel = input.model ?? resolved.defaultModel;
2524
2889
  const pinnedProvider = spawnDefaults.auth.provider;
2525
2890
  const resolvedProvider = pinnedProvider ?? resolved.authDescriptor.provider ?? (authModel ? getModelProvider(authModel) : void 0);
@@ -2565,7 +2930,8 @@ async function spawnAgentSession(deps2, input) {
2565
2930
  const effectivePrompt = input.prompt ? `${composeRoleContext(role, input.promptAppend, roleRegistry)}
2566
2931
 
2567
2932
  ${input.prompt}` : input.prompt;
2568
- const initialTitle = input.prompt ? deriveSessionTitle(input.prompt) : void 0;
2933
+ const explicitTitle = input.title?.trim() ? input.title.trim() : void 0;
2934
+ const initialTitle = explicitTitle ?? (input.prompt ? deriveSessionTitle(input.prompt) : void 0);
2569
2935
  let settleClaim;
2570
2936
  if (input.idempotencyKey) {
2571
2937
  const claims = claimsFor(registry);
@@ -2642,7 +3008,14 @@ ${input.prompt}` : input.prompt;
2642
3008
  ...resolvedMcpServers ? { mcpServers: resolvedMcpServers } : {},
2643
3009
  ...input.model ? { model: input.model } : {},
2644
3010
  ...input.effort ? { effort: input.effort } : {},
2645
- ...input.label ? { label: input.label } : {}
3011
+ ...input.label ? { label: input.label } : {},
3012
+ // Explicit billing-auth for the box's OWN agent_start. A fresh box
3013
+ // has no ~/.agentproto/config.json and claude-code never inherits
3014
+ // subscription auth from the shell env — the credential must ride the
3015
+ // spawn call itself. Only the caller's EXPLICIT `auth` is forwarded
3016
+ // (host config defaults stay host-scoped; the box resolves its own
3017
+ // defaults otherwise).
3018
+ ...input.auth ? { auth: input.auth } : {}
2646
3019
  });
2647
3020
  if (!booted.ok) return booted;
2648
3021
  agentSession = booted.agentSession;
@@ -2676,6 +3049,7 @@ ${input.prompt}` : input.prompt;
2676
3049
  agentSession,
2677
3050
  adapterSlug: input.adapter,
2678
3051
  ...input.model ? { model: input.model } : {},
3052
+ ...input.mode ? { mode: input.mode } : {},
2679
3053
  ...input.wait && effectivePrompt ? {} : effectivePrompt ? { initialPrompt: effectivePrompt } : {},
2680
3054
  ...input.label ? { label: input.label } : {},
2681
3055
  ...resolvedMcpServers ? { mcpServers: resolvedMcpServers } : {},
@@ -2826,7 +3200,8 @@ async function bootSandboxAgentSession(opts) {
2826
3200
  ...opts.mcpServers ? { mcpServers: toMcpServerMounts(opts.mcpServers) } : {},
2827
3201
  ...opts.model ? { model: opts.model } : {},
2828
3202
  ...opts.effort ? { effort: opts.effort } : {},
2829
- ...opts.label ? { label: opts.label } : {}
3203
+ ...opts.label ? { label: opts.label } : {},
3204
+ ...opts.auth ? { auth: opts.auth } : {}
2830
3205
  });
2831
3206
  remoteSessionId = remoteDesc.id;
2832
3207
  } catch (err) {
@@ -2857,6 +3232,60 @@ async function resolveSandboxSecret(slug) {
2857
3232
  return null;
2858
3233
  }
2859
3234
  }
3235
+
3236
+ // src/canonical-posture.ts
3237
+ var POSTURE_PREAMBLES = {
3238
+ 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.",
3239
+ "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.",
3240
+ 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.",
3241
+ "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."
3242
+ };
3243
+ var POSTURE_NATIVE_ALIASES = {
3244
+ default: ["default", "build", "normal", "standard"],
3245
+ plan: ["plan", "planning", "plan-mode"],
3246
+ "accept-edits": ["accept-edits", "auto-accept", "auto-edit"],
3247
+ bypass: ["bypass", "bypass-permissions", "full-access", "yolo", "dangerously-skip-permissions"],
3248
+ "read-only": ["read-only", "chat", "ask"]
3249
+ };
3250
+ var CANONICAL_POSTURES = Object.keys(
3251
+ POSTURE_NATIVE_ALIASES
3252
+ );
3253
+ var CANONICAL_POSTURE_SET = new Set(CANONICAL_POSTURES);
3254
+ function isCanonicalPosture(value) {
3255
+ return CANONICAL_POSTURE_SET.has(value);
3256
+ }
3257
+ function parsePostureInput(raw) {
3258
+ return isCanonicalPosture(raw) ? raw : { harnessModeId: raw };
3259
+ }
3260
+ function normalizeModeId(id) {
3261
+ return id.toLowerCase().replace(/[^a-z0-9]/g, "");
3262
+ }
3263
+ var CANONICAL_BY_NORMALIZED_ALIAS = (() => {
3264
+ const index = /* @__PURE__ */ new Map();
3265
+ for (const [posture, aliases] of Object.entries(POSTURE_NATIVE_ALIASES)) {
3266
+ for (const alias of aliases) index.set(normalizeModeId(alias), posture);
3267
+ }
3268
+ return index;
3269
+ })();
3270
+ function canonicalForModeId(modeId) {
3271
+ return CANONICAL_BY_NORMALIZED_ALIAS.get(normalizeModeId(modeId));
3272
+ }
3273
+ function findNativeMode(posture, availableModes) {
3274
+ if (typeof posture === "object") {
3275
+ const target = normalizeModeId(posture.harnessModeId);
3276
+ return availableModes.find((mode) => normalizeModeId(mode.id) === target);
3277
+ }
3278
+ return availableModes.find((mode) => canonicalForModeId(mode.id) === posture);
3279
+ }
3280
+ function resolvePosture(posture, availableModes) {
3281
+ const native = findNativeMode(posture, availableModes);
3282
+ if (native) return { kind: "native", mode: native };
3283
+ if (typeof posture === "object") {
3284
+ return { kind: "unavailable", requestedModeId: posture.harnessModeId };
3285
+ }
3286
+ if (posture === "default") return { kind: "noop", posture: "default" };
3287
+ return { kind: "prompt", posture, preamble: POSTURE_PREAMBLES[posture] };
3288
+ }
2860
3289
  var sandboxSpecWithReuseSchema = z.object({
2861
3290
  ...SandboxSpecSchema.shape,
2862
3291
  reuse: z.string().min(1).optional().describe(
@@ -2897,6 +3326,7 @@ function registerAgentTools(server, opts) {
2897
3326
  registry,
2898
3327
  resolveAgentAdapter,
2899
3328
  listAgentAdapters,
3329
+ listCatalogModels,
2900
3330
  buildOrchestratorMcp,
2901
3331
  callerScope,
2902
3332
  webhookNotifier,
@@ -3209,7 +3639,7 @@ function registerAgentTools(server, opts) {
3209
3639
  if (callerScope) {
3210
3640
  const subtree = collectSubtree(
3211
3641
  callerScope.ownerSessionId,
3212
- registry.list()
3642
+ registry.list({ includeArchived: true })
3213
3643
  );
3214
3644
  if (!subtree.has(sessionId)) {
3215
3645
  return {
@@ -3276,6 +3706,112 @@ function registerAgentTools(server, opts) {
3276
3706
  }
3277
3707
  }
3278
3708
  );
3709
+ server.tool(
3710
+ "agent_set_model",
3711
+ "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.",
3712
+ {
3713
+ sessionId: sessionIdField,
3714
+ id: sessionIdAliasField,
3715
+ model: z.string().describe("Model id to switch to.")
3716
+ },
3717
+ async (input) => {
3718
+ const sessionId = resolveSessionIdArg(input);
3719
+ if (!sessionId) return missingSessionIdError("agent_set_model");
3720
+ try {
3721
+ const result = await registry.setModel(sessionId, input.model);
3722
+ return {
3723
+ content: [
3724
+ {
3725
+ type: "text",
3726
+ text: JSON.stringify({ ok: true, sessionId, ...result }, null, 2)
3727
+ }
3728
+ ]
3729
+ };
3730
+ } catch (err) {
3731
+ return {
3732
+ content: [
3733
+ {
3734
+ type: "text",
3735
+ text: `agent_set_model: ${err instanceof Error ? err.message : String(err)}`
3736
+ }
3737
+ ],
3738
+ isError: true
3739
+ };
3740
+ }
3741
+ }
3742
+ );
3743
+ server.tool(
3744
+ "agent_set_effort",
3745
+ "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.",
3746
+ {
3747
+ sessionId: sessionIdField,
3748
+ id: sessionIdAliasField,
3749
+ effort: z.string().describe(
3750
+ "Effort label to switch to (e.g. low/medium/high/xhigh/max/ultracode; the accepted set is model-dependent)."
3751
+ )
3752
+ },
3753
+ async (input) => {
3754
+ const sessionId = resolveSessionIdArg(input);
3755
+ if (!sessionId) return missingSessionIdError("agent_set_effort");
3756
+ try {
3757
+ const result = await registry.setEffort(sessionId, input.effort);
3758
+ return {
3759
+ content: [
3760
+ {
3761
+ type: "text",
3762
+ text: JSON.stringify({ ok: true, sessionId, ...result }, null, 2)
3763
+ }
3764
+ ]
3765
+ };
3766
+ } catch (err) {
3767
+ return {
3768
+ content: [
3769
+ {
3770
+ type: "text",
3771
+ text: `agent_set_effort: ${err instanceof Error ? err.message : String(err)}`
3772
+ }
3773
+ ],
3774
+ isError: true
3775
+ };
3776
+ }
3777
+ }
3778
+ );
3779
+ server.tool(
3780
+ "agent_set_posture",
3781
+ '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`.',
3782
+ {
3783
+ sessionId: sessionIdField,
3784
+ id: sessionIdAliasField,
3785
+ posture: z.string().describe(
3786
+ "Posture to switch to: a canonical value (default/plan/accept-edits/bypass/read-only) or a raw harness mode id."
3787
+ )
3788
+ },
3789
+ async (input) => {
3790
+ const sessionId = resolveSessionIdArg(input);
3791
+ if (!sessionId) return missingSessionIdError("agent_set_posture");
3792
+ try {
3793
+ const result = await registry.setPosture(sessionId, parsePostureInput(input.posture));
3794
+ return {
3795
+ content: [
3796
+ {
3797
+ type: "text",
3798
+ text: JSON.stringify({ ok: true, sessionId, ...result }, null, 2)
3799
+ }
3800
+ ]
3801
+ };
3802
+ } catch (err) {
3803
+ return {
3804
+ content: [
3805
+ {
3806
+ type: "text",
3807
+ text: `agent_set_posture: ${err instanceof Error ? err.message : String(err)}`
3808
+ }
3809
+ ],
3810
+ isError: true
3811
+ };
3812
+ }
3813
+ }
3814
+ );
3279
3815
  server.tool(
3280
3816
  "agent_sessions_list",
3281
3817
  "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.",
@@ -3287,11 +3823,12 @@ function registerAgentTools(server, opts) {
3287
3823
  status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive).")
3288
3824
  },
3289
3825
  async (input) => {
3290
- let rows = registry.list();
3826
+ let rows = registry.list({ includeArchived: true });
3291
3827
  if (callerScope) {
3292
3828
  const subtree = collectSubtree(callerScope.ownerSessionId, rows);
3293
3829
  rows = rows.filter((s) => subtree.has(s.id));
3294
3830
  }
3831
+ rows = rows.filter((s) => !s.archived);
3295
3832
  const kind = input.kind ?? "agent-cli";
3296
3833
  if (kind !== "all") {
3297
3834
  rows = rows.filter((s) => s.kind === kind);
@@ -3344,6 +3881,50 @@ function registerAgentTools(server, opts) {
3344
3881
  }
3345
3882
  }
3346
3883
  );
3884
+ server.tool(
3885
+ "catalog_models",
3886
+ "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.",
3887
+ {
3888
+ adapter: z.string().optional().describe("Keep only routes reachable via this adapter slug."),
3889
+ vendor: z.string().optional().describe("Keep only this vendor's entry."),
3890
+ route: z.string().optional().describe("Keep only routes with this route id."),
3891
+ runnableOnly: mcpBool.optional().describe("Drop every route with runnable:false.")
3892
+ },
3893
+ async ({ adapter, vendor, route, runnableOnly }) => {
3894
+ if (!listCatalogModels) {
3895
+ return {
3896
+ content: [
3897
+ {
3898
+ type: "text",
3899
+ text: "catalog_models is not enabled \u2014 the daemon was started without a catalog lister. Wire `buildCatalogModels` via `createGateway({ listCatalogModels })`."
3900
+ }
3901
+ ],
3902
+ isError: true
3903
+ };
3904
+ }
3905
+ try {
3906
+ const catalog = await listCatalogModels({
3907
+ ...adapter ? { adapter } : {},
3908
+ ...vendor ? { vendor } : {},
3909
+ ...route ? { route } : {},
3910
+ ...runnableOnly ? { runnableOnly: true } : {}
3911
+ });
3912
+ return {
3913
+ content: [{ type: "text", text: JSON.stringify(catalog, null, 2) }]
3914
+ };
3915
+ } catch (err) {
3916
+ return {
3917
+ content: [
3918
+ {
3919
+ type: "text",
3920
+ text: `catalog_models failed: ${err instanceof Error ? err.message : String(err)}`
3921
+ }
3922
+ ],
3923
+ isError: true
3924
+ };
3925
+ }
3926
+ }
3927
+ );
3347
3928
  server.tool(
3348
3929
  "role_list",
3349
3930
  "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.",
@@ -3655,151 +4236,9 @@ function stringifyValues(raw) {
3655
4236
  }
3656
4237
  return out;
3657
4238
  }
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
4239
 
3802
4240
  // src/resume-strategies.ts
4241
+ init_conversation_store();
3803
4242
  var claudeCodeStore = CONVERSATION_STORES["claude-code"];
3804
4243
  var RESUME_STRATEGIES = {
3805
4244
  "claude-code": {
@@ -3907,6 +4346,46 @@ function tokenizeCommand(s) {
3907
4346
  if (buf) out.push(buf);
3908
4347
  return out;
3909
4348
  }
4349
+ var RestartOverrideError = class extends Error {
4350
+ code = "restart_override_invalid";
4351
+ status = 400;
4352
+ constructor(message) {
4353
+ super(message);
4354
+ this.name = "RestartOverrideError";
4355
+ }
4356
+ };
4357
+ async function resolveAccessProfileFromStore(profileRef) {
4358
+ const profile = await getAuthProfile(profileRef);
4359
+ if (!profile) return void 0;
4360
+ const stored = await new KeychainStore().read({ path: profile.credentialRef });
4361
+ return { profile, ...stored?.value !== void 0 ? { credential: stored.value } : {} };
4362
+ }
4363
+ function methodToMode(method) {
4364
+ return method === "oauth-bearer" ? "subscription" : "api-key";
4365
+ }
4366
+ function directMethods(descriptor) {
4367
+ const methods = [];
4368
+ if (descriptor?.authSubscription) methods.push("oauth-bearer");
4369
+ if (descriptor?.provider) methods.push("api-key");
4370
+ return methods;
4371
+ }
4372
+ function eligibilityManifest(adapterSlug, descriptor, route, model) {
4373
+ const baseVendor = descriptor?.provider ?? (model ? getModelProvider(model) : void 0);
4374
+ const gateway = route?.gateway;
4375
+ const routeId = gateway ?? baseVendor;
4376
+ if (routeId === void 0) return void 0;
4377
+ const isDirect = baseVendor !== void 0 && routeId === baseVendor;
4378
+ const billedVendor2 = isDirect ? baseVendor : routeId;
4379
+ const methods = isDirect ? directMethods(descriptor) : ["api-key"];
4380
+ return {
4381
+ manifest: {
4382
+ id: adapterSlug,
4383
+ vendorByRoute: { [routeId]: billedVendor2 },
4384
+ methodsByRoute: { [routeId]: methods }
4385
+ },
4386
+ routeId
4387
+ };
4388
+ }
3910
4389
  async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {}) {
3911
4390
  const augmented = opts.forceAgentResume ? prev : await augmentWithFsResume(prev);
3912
4391
  const strategy = opts.forceAgentResume ? { kind: "agent", resumeSessionId: prev.adapterSessionId } : decideRestartStrategy(augmented);
@@ -3928,15 +4407,80 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
3928
4407
  let cwd = prev.cwd;
3929
4408
  if (!cwd) console.warn(`[restartAgentSession] no cwd on prior descriptor ${prev.id} \u2014 falling back to daemon's cwd ${process.cwd()}`);
3930
4409
  cwd ??= process.cwd();
4410
+ const overrides = opts.overrides ?? {};
4411
+ const effModel = overrides.model ?? prev.model;
4412
+ const effEffort = overrides.effort ?? prev.effort;
4413
+ const effRoute = overrides.route ?? prev.route;
4414
+ const effPosture = overrides.posture ?? prev.posture;
4415
+ const effContextProfile = overrides.contextProfile ?? prev.contextProfile;
4416
+ const effMode = overrides.mode ?? prev.mode;
4417
+ const accessOverrideRef = overrides.access?.profileRef;
3931
4418
  let authSpec;
3932
4419
  let authEcho;
3933
- if (resolved.authDescriptor) {
4420
+ let accessProfileEcho = prev.accessProfile;
4421
+ if (accessOverrideRef !== void 0) {
4422
+ const resolveProfile = opts.resolveAccessProfile ?? resolveAccessProfileFromStore;
4423
+ const found = await resolveProfile(accessOverrideRef);
4424
+ if (!found) {
4425
+ throw new RestartOverrideError(
4426
+ `restart access override: no auth profile "${accessOverrideRef}" found.`
4427
+ );
4428
+ }
4429
+ const { profile, credential } = found;
4430
+ if (!resolved.authDescriptor) {
4431
+ throw new RestartOverrideError(
4432
+ `restart access override: adapter "${adapterSlug}" presents no billing-auth, so profile "${profile.id}" cannot be attached.`
4433
+ );
4434
+ }
4435
+ const projected = eligibilityManifest(
4436
+ adapterSlug,
4437
+ resolved.authDescriptor,
4438
+ effRoute,
4439
+ effModel ?? resolved.defaultModel
4440
+ );
4441
+ if (!projected) {
4442
+ throw new RestartOverrideError(
4443
+ `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.`
4444
+ );
4445
+ }
4446
+ const { manifest, routeId } = projected;
4447
+ if (eligibleProfiles([profile], manifest, routeId).length === 0) {
4448
+ const billed = manifest.vendorByRoute[routeId];
4449
+ const methods = manifest.methodsByRoute[routeId] ?? [];
4450
+ throw new RestartOverrideError(
4451
+ `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.`
4452
+ );
4453
+ }
4454
+ const mode = methodToMode(profile.method);
4455
+ const result = resolveAuthSpec({
4456
+ descriptor: resolved.authDescriptor,
4457
+ ...effModel ? { model: effModel } : {},
4458
+ requestedProvider: profile.vendor,
4459
+ requestedMode: mode,
4460
+ // Attaching a named profile is always an EXPLICIT billing choice — so a
4461
+ // missing credential fails loud (driver `missing_auth_credential`) rather
4462
+ // than falling back to ambient env or the prior credential.
4463
+ explicit: true,
4464
+ ...mode === "subscription" && credential !== void 0 ? { subscriptionCredential: credential } : {},
4465
+ ...mode === "api-key" && credential !== void 0 ? { apiKeyConfigCredential: credential } : {}
4466
+ });
4467
+ if (result) {
4468
+ authSpec = result.spec;
4469
+ authEcho = result.echo;
4470
+ }
4471
+ accessProfileEcho = {
4472
+ profileRef: profile.id,
4473
+ ...profile.label !== void 0 ? { label: profile.label } : {},
4474
+ vendor: profile.vendor,
4475
+ method: profile.method
4476
+ };
4477
+ } else if (resolved.authDescriptor) {
3934
4478
  const configDefaults = opts.loadDefaultsConfig ? await opts.loadDefaultsConfig() : (await loadConfig()).defaults;
3935
4479
  const explicitAuthInput = prev.auth ? { mode: prev.auth.mode } : void 0;
3936
4480
  const spawnDefaults = resolveSpawnDefaults(configDefaults, adapterSlug, {
3937
4481
  auth: explicitAuthInput
3938
4482
  });
3939
- const authModel = prev.model ?? resolved.defaultModel;
4483
+ const authModel = effModel ?? resolved.defaultModel;
3940
4484
  const pinnedProvider = spawnDefaults.auth.provider;
3941
4485
  const resolvedProvider = pinnedProvider ?? resolved.authDescriptor.provider ?? (authModel ? getModelProvider(authModel) : void 0);
3942
4486
  const apiKeyStoreCredential = resolvedProvider && spawnDefaults.auth.explicit && spawnDefaults.auth.apiKeyCredential === void 0 ? await (0, providers_store_exports.getProviderKey)(resolvedProvider) : void 0;
@@ -3966,13 +4510,20 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
3966
4510
  const agentSession = await resolved.startSession({
3967
4511
  cwd,
3968
4512
  ...resumeSessionId ? { resumeSessionId } : {},
3969
- ...prev.model ? { model: prev.model } : {},
4513
+ ...effModel ? { model: effModel } : {},
4514
+ ...effEffort ? { effort: effEffort } : {},
4515
+ // Legacy AIP-45 mode override only — the decomposed route/posture/
4516
+ // contextProfile spawn-env apply-path rides the driver's mode
4517
+ // decomposition (build step 2), out of scope here; they still round-trip
4518
+ // on the descriptor below.
4519
+ ...overrides.mode ? { mode: overrides.mode } : {},
3970
4520
  ...prev.mcpServers ? { mcpServers: prev.mcpServers } : {},
3971
4521
  ...authSpec ? { auth: authSpec } : {},
3972
4522
  onActivity: () => {
3973
4523
  if (liveSessionId) registry.pulseActivity(liveSessionId);
3974
4524
  }
3975
4525
  });
4526
+ const resumeVia = !resumeSessionId ? "" : opts.forceAgentResume ? "resumed via ACP" : describeResumePath(augmented);
3976
4527
  const desc2 = registry.spawnAgent({
3977
4528
  workspaceSlug: prev.workspaceSlug,
3978
4529
  cwd,
@@ -3980,7 +4531,17 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
3980
4531
  adapterSlug,
3981
4532
  ...prev.label ? { label: prev.label } : {},
3982
4533
  ...prev.mcpServers ? { mcpServers: prev.mcpServers } : {},
3983
- ...prev.model ? { model: prev.model } : {},
4534
+ ...effModel ? { model: effModel } : {},
4535
+ // Decomposed config-axis echoes (SPEC §3.7) — carried forward from `prev`
4536
+ // and overlaid with any override so every axis round-trips onto the fresh
4537
+ // descriptor and the picker re-opens on it (SPEC §3.8), even the axes
4538
+ // whose spawn-env apply-path isn't wired here yet (route/posture/context).
4539
+ ...effEffort ? { effort: effEffort } : {},
4540
+ ...effPosture !== void 0 ? { posture: effPosture } : {},
4541
+ ...effRoute ? { route: effRoute } : {},
4542
+ ...effContextProfile ? { contextProfile: effContextProfile } : {},
4543
+ ...accessProfileEcho ? { accessProfile: accessProfileEcho } : {},
4544
+ ...effMode ? { mode: effMode } : {},
3984
4545
  ...resolved.commandPreview ? { commandPreview: resolved.commandPreview } : {},
3985
4546
  // Verifiability echo (never the credential) — see the auth
3986
4547
  // resolution block above. Absent when no credential resolved,
@@ -3993,31 +4554,53 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
3993
4554
  credentialSource: authEcho.credentialSource,
3994
4555
  setEnv: authEcho.setEnv
3995
4556
  }
3996
- } : {}
4557
+ } : {},
4558
+ resumedFrom: prev.id,
4559
+ resumeVia
3997
4560
  });
3998
4561
  liveSessionId = desc2.id;
3999
4562
  return desc2;
4000
4563
  };
4001
4564
  let desc;
4002
4565
  let resumeFallback = false;
4003
- let usedResumeSessionId = strategy.resumeSessionId;
4004
4566
  try {
4005
4567
  desc = await spawnWithResume(strategy.resumeSessionId);
4006
4568
  } catch (err) {
4007
4569
  const msg = err instanceof Error ? err.message : String(err);
4008
4570
  if (strategy.resumeSessionId && /not found|Resource not found/i.test(msg)) {
4009
4571
  desc = await spawnWithResume(void 0);
4010
- usedResumeSessionId = void 0;
4011
4572
  resumeFallback = true;
4012
4573
  } else {
4013
4574
  throw err;
4014
4575
  }
4015
4576
  }
4016
- const resumeVia = !usedResumeSessionId ? "" : opts.forceAgentResume ? "resumed via ACP" : describeResumePath(augmented);
4577
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
4578
+ const emit = (axis, value) => {
4579
+ registry.emitConfigChanged({
4580
+ type: "session:config-changed",
4581
+ sessionId: desc.id,
4582
+ axis,
4583
+ value,
4584
+ ...desc.label ? { label: desc.label } : {},
4585
+ ts
4586
+ });
4587
+ };
4588
+ if (overrides.model !== void 0) emit("model", overrides.model);
4589
+ if (overrides.effort !== void 0) emit("effort", overrides.effort);
4590
+ if (accessOverrideRef !== void 0) emit("access", { profileRef: accessOverrideRef });
4591
+ if (overrides.route !== void 0) emit("route", overrides.route);
4592
+ if (overrides.posture !== void 0) emit("posture", overrides.posture);
4593
+ if (overrides.contextProfile !== void 0) emit("contextProfile", overrides.contextProfile);
4017
4594
  return {
4018
4595
  desc,
4019
4596
  resumedFrom: prev.id,
4020
- resumeVia,
4597
+ // `spawnWithResume` already computed + persisted this onto `desc` (the
4598
+ // fix this module carries — see `SessionDescriptor.resumedFrom`'s doc);
4599
+ // reading it back here rather than recomputing keeps the RESULT and the
4600
+ // STORED descriptor from ever being able to diverge. Never actually
4601
+ // undefined — every `spawnWithResume` call sets it, `?? ""` is just
4602
+ // satisfying the optional field's type.
4603
+ resumeVia: desc.resumeVia ?? "",
4021
4604
  ...resumeFallback ? { resumeFallback: true } : {}
4022
4605
  };
4023
4606
  }
@@ -4161,6 +4744,9 @@ function withToolExclusion(server, excluded) {
4161
4744
  }
4162
4745
  });
4163
4746
  }
4747
+
4748
+ // src/conversation-read.ts
4749
+ init_conversation_store();
4164
4750
  init_transcript_export();
4165
4751
  var BINARY_TO_STORE_KEY = {
4166
4752
  claude: "claude-code",
@@ -4340,7 +4926,7 @@ function buildSessionTree(sessions) {
4340
4926
  });
4341
4927
  return sessions.filter((s) => !s.parentSessionId || !idSet.has(s.parentSessionId)).sort((a, b) => a.startedAt.localeCompare(b.startedAt)).map(toNode);
4342
4928
  }
4343
- z.preprocess(
4929
+ var mcpBool2 = z.preprocess(
4344
4930
  (v) => v === "true" ? true : v === "false" ? false : v,
4345
4931
  z.boolean()
4346
4932
  );
@@ -4362,14 +4948,20 @@ function registerSessionTools(rawServer, opts) {
4362
4948
  "Filter by session kind. `all` (default) returns every kind. Use `terminal` to list only PTY sessions, `agent-cli` for structured ACP agents."
4363
4949
  ),
4364
4950
  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).")
4951
+ status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive)."),
4952
+ includeArchived: z.boolean().optional().describe(
4953
+ "When true, also include archived sessions (hidden from every other view by `session_archive`). Default false."
4954
+ )
4366
4955
  },
4367
4956
  async (input) => {
4368
- let rows = registry.list();
4957
+ let rows = registry.list({ includeArchived: true });
4369
4958
  if (callerScope) {
4370
4959
  const subtree = collectSubtree(callerScope.ownerSessionId, rows);
4371
4960
  rows = rows.filter((s) => subtree.has(s.id));
4372
4961
  }
4962
+ if (!input.includeArchived) {
4963
+ rows = rows.filter((s) => !s.archived);
4964
+ }
4373
4965
  if (input.kind && input.kind !== "all") {
4374
4966
  rows = rows.filter((s) => s.kind === input.kind);
4375
4967
  }
@@ -4407,7 +4999,10 @@ function registerSessionTools(rawServer, opts) {
4407
4999
  };
4408
5000
  }
4409
5001
  if (callerScope) {
4410
- const subtree = collectSubtree(callerScope.ownerSessionId, registry.list());
5002
+ const subtree = collectSubtree(
5003
+ callerScope.ownerSessionId,
5004
+ registry.list({ includeArchived: true })
5005
+ );
4411
5006
  if (!subtree.has(desc.id)) {
4412
5007
  return {
4413
5008
  content: [
@@ -4443,11 +5038,12 @@ function registerSessionTools(rawServer, opts) {
4443
5038
  status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive).")
4444
5039
  },
4445
5040
  async (input) => {
4446
- let rows = registry.list();
5041
+ let rows = registry.list({ includeArchived: true });
4447
5042
  if (callerScope) {
4448
5043
  const subtree = collectSubtree(callerScope.ownerSessionId, rows);
4449
5044
  rows = rows.filter((s) => subtree.has(s.id));
4450
5045
  }
5046
+ rows = rows.filter((s) => !s.archived);
4451
5047
  const kind = input.kind ?? "terminal";
4452
5048
  if (kind !== "all") {
4453
5049
  rows = rows.filter((s) => s.kind === kind);
@@ -4477,11 +5073,12 @@ function registerSessionTools(rawServer, opts) {
4477
5073
  status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive).")
4478
5074
  },
4479
5075
  async (input) => {
4480
- let rows = registry.list();
5076
+ let rows = registry.list({ includeArchived: true });
4481
5077
  if (callerScope) {
4482
5078
  const subtree = collectSubtree(callerScope.ownerSessionId, rows);
4483
5079
  rows = rows.filter((s) => subtree.has(s.id));
4484
5080
  }
5081
+ rows = rows.filter((s) => !s.archived);
4485
5082
  const kind = input.kind ?? "command";
4486
5083
  if (kind !== "all") {
4487
5084
  rows = rows.filter((s) => s.kind === kind);
@@ -4765,11 +5362,12 @@ function registerSessionTools(rawServer, opts) {
4765
5362
  )
4766
5363
  },
4767
5364
  async (input) => {
4768
- let rows = registry.list();
5365
+ let rows = registry.list({ includeArchived: true });
4769
5366
  if (callerScope) {
4770
5367
  const subtree = collectSubtree(callerScope.ownerSessionId, rows);
4771
5368
  rows = rows.filter((s) => subtree.has(s.id));
4772
5369
  }
5370
+ rows = rows.filter((s) => !s.archived);
4773
5371
  if (input.onlyAlive) {
4774
5372
  rows = rows.filter(
4775
5373
  (s) => s.status === "running" || s.status === "starting"
@@ -4802,7 +5400,25 @@ function registerSessionTools(rawServer, opts) {
4802
5400
  cols: z.number().int().min(1).max(500).optional().describe(
4803
5401
  "PTY cols \u2014 only used when the restart resolves to a provider-native or plain PTY resume. Default 80."
4804
5402
  ),
4805
- rows: z.number().int().min(1).max(200).optional().describe("PTY rows \u2014 same case as `cols`. Default 24.")
5403
+ rows: z.number().int().min(1).max(200).optional().describe("PTY rows \u2014 same case as `cols`. Default 24."),
5404
+ // ── Restart-with-override axes (SPEC §4.3, step 6) — the single path
5405
+ // for all four restart-only axes. Each optional; an omitted axis is
5406
+ // carried forward from the prior session, an axis set here wins.
5407
+ model: z.string().min(1).optional().describe("Override the model on restart (route-identity ref)."),
5408
+ effort: z.enum(["low", "medium", "high", "xhigh", "max", "ultracode"]).optional().describe("Override the reasoning-effort level on restart."),
5409
+ access: z.object({
5410
+ 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).")
5411
+ }).optional().describe("Switch the session's billing wallet to a named auth profile."),
5412
+ route: z.object({
5413
+ gateway: z.string().min(1).describe("Endpoint/gateway id (anthropic|moonshot|\u2026)."),
5414
+ baseUrl: z.string().url().optional().describe("Explicit base URL for a custom gateway.")
5415
+ }).optional().describe("Override the endpoint/gateway rail on restart (access is downstream)."),
5416
+ posture: z.union([
5417
+ z.enum(["default", "plan", "accept-edits", "bypass", "read-only"]),
5418
+ z.object({ harnessModeId: z.string().min(1) })
5419
+ ]).optional().describe("Override the posture (what the agent may DO) on restart."),
5420
+ contextProfile: z.string().min(1).optional().describe("Override what enters context (full|lean|\u2026) on restart."),
5421
+ mode: z.string().min(1).optional().describe("Legacy AIP-45 mode id override, forwarded verbatim to the driver at spawn.")
4806
5422
  },
4807
5423
  async (input) => {
4808
5424
  const prev = registry.findByIdOrName(input.idOrName);
@@ -4818,7 +5434,10 @@ function registerSessionTools(rawServer, opts) {
4818
5434
  };
4819
5435
  }
4820
5436
  if (callerScope) {
4821
- const subtree = collectSubtree(callerScope.ownerSessionId, registry.list());
5437
+ const subtree = collectSubtree(
5438
+ callerScope.ownerSessionId,
5439
+ registry.list({ includeArchived: true })
5440
+ );
4822
5441
  if (!subtree.has(prev.id)) {
4823
5442
  return {
4824
5443
  content: [
@@ -4836,6 +5455,84 @@ function registerSessionTools(rawServer, opts) {
4836
5455
  };
4837
5456
  }
4838
5457
  }
5458
+ const overrides = {
5459
+ ...input.model !== void 0 ? { model: input.model } : {},
5460
+ ...input.effort !== void 0 ? { effort: input.effort } : {},
5461
+ ...input.access !== void 0 ? { access: input.access } : {},
5462
+ ...input.route !== void 0 ? { route: input.route } : {},
5463
+ ...input.posture !== void 0 ? { posture: input.posture } : {},
5464
+ ...input.contextProfile !== void 0 ? { contextProfile: input.contextProfile } : {},
5465
+ ...input.mode !== void 0 ? { mode: input.mode } : {}
5466
+ };
5467
+ if (Object.keys(overrides).length > 0) {
5468
+ if (!prev.adapterSlug || !resolveAgentAdapter) {
5469
+ return {
5470
+ content: [
5471
+ {
5472
+ type: "text",
5473
+ text: JSON.stringify({
5474
+ error: "restart_override_invalid",
5475
+ status: 400,
5476
+ message: "session_restart: restart-with-override only applies to agent-cli sessions (a PTY/command session has no config axes to override).",
5477
+ ok: false,
5478
+ sessionId: prev.id
5479
+ })
5480
+ }
5481
+ ],
5482
+ isError: true
5483
+ };
5484
+ }
5485
+ try {
5486
+ const restarted = await restartAgentSession(registry, resolveAgentAdapter, prev, {
5487
+ forceAgentResume: true,
5488
+ overrides
5489
+ });
5490
+ return {
5491
+ content: [
5492
+ {
5493
+ type: "text",
5494
+ text: JSON.stringify(
5495
+ {
5496
+ ...restarted.desc,
5497
+ resumedFrom: restarted.resumedFrom,
5498
+ resumeVia: restarted.resumeVia,
5499
+ ...restarted.resumeFallback ? { resumeFallback: true } : {}
5500
+ },
5501
+ null,
5502
+ 2
5503
+ )
5504
+ }
5505
+ ]
5506
+ };
5507
+ } catch (err) {
5508
+ if (err instanceof RestartOverrideError) {
5509
+ return {
5510
+ content: [
5511
+ {
5512
+ type: "text",
5513
+ text: JSON.stringify({
5514
+ error: err.code,
5515
+ status: err.status,
5516
+ message: err.message,
5517
+ ok: false,
5518
+ sessionId: prev.id
5519
+ })
5520
+ }
5521
+ ],
5522
+ isError: true
5523
+ };
5524
+ }
5525
+ return {
5526
+ content: [
5527
+ {
5528
+ type: "text",
5529
+ text: `session_restart: ${err instanceof Error ? err.message : String(err)}`
5530
+ }
5531
+ ],
5532
+ isError: true
5533
+ };
5534
+ }
5535
+ }
4839
5536
  const augmented = await augmentWithFsResume(prev);
4840
5537
  const strategy = decideRestartStrategy(augmented);
4841
5538
  if (strategy.kind === "unsupported") {
@@ -4867,17 +5564,15 @@ function registerSessionTools(rawServer, opts) {
4867
5564
  cols: input.cols ?? 80,
4868
5565
  rows: input.rows ?? 24,
4869
5566
  ...prev.name ? { name: prev.name } : {},
4870
- ...prev.label ? { label: prev.label } : {}
5567
+ ...prev.label ? { label: prev.label } : {},
5568
+ resumedFrom: prev.id,
5569
+ resumeVia: describeResumePath(augmented)
4871
5570
  });
4872
5571
  return {
4873
5572
  content: [
4874
5573
  {
4875
5574
  type: "text",
4876
- text: JSON.stringify(
4877
- { ...desc, resumedFrom: prev.id, resumeVia: describeResumePath(augmented) },
4878
- null,
4879
- 2
4880
- )
5575
+ text: JSON.stringify(desc, null, 2)
4881
5576
  }
4882
5577
  ]
4883
5578
  };
@@ -4920,14 +5615,220 @@ function registerSessionTools(rawServer, opts) {
4920
5615
  2
4921
5616
  )
4922
5617
  }
4923
- ]
5618
+ ]
5619
+ };
5620
+ } catch (err) {
5621
+ return {
5622
+ content: [
5623
+ {
5624
+ type: "text",
5625
+ text: `session_restart: ${err instanceof Error ? err.message : String(err)}`
5626
+ }
5627
+ ],
5628
+ isError: true
5629
+ };
5630
+ }
5631
+ }
5632
+ );
5633
+ server.tool(
5634
+ "session_archive",
5635
+ "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.",
5636
+ {
5637
+ idOrName: z.string().min(1).describe(
5638
+ "Session id or name to archive \u2014 from `session_list`, must be terminal-status."
5639
+ )
5640
+ },
5641
+ async (input) => {
5642
+ const prev = registry.findByIdOrName(input.idOrName);
5643
+ if (!prev) {
5644
+ return {
5645
+ content: [
5646
+ {
5647
+ type: "text",
5648
+ text: JSON.stringify({ error: `no session "${input.idOrName}" found` })
5649
+ }
5650
+ ],
5651
+ isError: true
5652
+ };
5653
+ }
5654
+ if (callerScope) {
5655
+ const subtree = collectSubtree(
5656
+ callerScope.ownerSessionId,
5657
+ registry.list({ includeArchived: true })
5658
+ );
5659
+ if (!subtree.has(prev.id)) {
5660
+ return {
5661
+ content: [
5662
+ {
5663
+ type: "text",
5664
+ text: JSON.stringify({
5665
+ error: "orchestrator_session_out_of_scope",
5666
+ message: `session_archive: session "${prev.id}" is not in your subtree \u2014 a scoped orchestrator can only archive sessions it (transitively) spawned.`,
5667
+ ok: false,
5668
+ sessionId: prev.id
5669
+ })
5670
+ }
5671
+ ],
5672
+ isError: true
5673
+ };
5674
+ }
5675
+ }
5676
+ try {
5677
+ const desc = registry.archiveSession(prev.id);
5678
+ return {
5679
+ content: [{ type: "text", text: JSON.stringify(desc, null, 2) }]
5680
+ };
5681
+ } catch (err) {
5682
+ return {
5683
+ content: [
5684
+ {
5685
+ type: "text",
5686
+ text: `session_archive: ${err instanceof Error ? err.message : String(err)}`
5687
+ }
5688
+ ],
5689
+ isError: true
5690
+ };
5691
+ }
5692
+ }
5693
+ );
5694
+ server.tool(
5695
+ "session_unarchive",
5696
+ "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.",
5697
+ {
5698
+ idOrName: z.string().min(1).describe(
5699
+ "Session id or name to unarchive \u2014 find it via `session_list({ includeArchived: true })`."
5700
+ )
5701
+ },
5702
+ async (input) => {
5703
+ const prev = registry.findByIdOrName(input.idOrName);
5704
+ if (!prev) {
5705
+ return {
5706
+ content: [
5707
+ {
5708
+ type: "text",
5709
+ text: JSON.stringify({ error: `no session "${input.idOrName}" found` })
5710
+ }
5711
+ ],
5712
+ isError: true
5713
+ };
5714
+ }
5715
+ if (callerScope) {
5716
+ const subtree = collectSubtree(
5717
+ callerScope.ownerSessionId,
5718
+ registry.list({ includeArchived: true })
5719
+ );
5720
+ if (!subtree.has(prev.id)) {
5721
+ return {
5722
+ content: [
5723
+ {
5724
+ type: "text",
5725
+ text: JSON.stringify({
5726
+ error: "orchestrator_session_out_of_scope",
5727
+ message: `session_unarchive: session "${prev.id}" is not in your subtree \u2014 a scoped orchestrator can only unarchive sessions it (transitively) spawned.`,
5728
+ ok: false,
5729
+ sessionId: prev.id
5730
+ })
5731
+ }
5732
+ ],
5733
+ isError: true
5734
+ };
5735
+ }
5736
+ }
5737
+ try {
5738
+ const desc = registry.unarchiveSession(prev.id);
5739
+ return {
5740
+ content: [{ type: "text", text: JSON.stringify(desc, null, 2) }]
5741
+ };
5742
+ } catch (err) {
5743
+ return {
5744
+ content: [
5745
+ {
5746
+ type: "text",
5747
+ text: `session_unarchive: ${err instanceof Error ? err.message : String(err)}`
5748
+ }
5749
+ ],
5750
+ isError: true
5751
+ };
5752
+ }
5753
+ }
5754
+ );
5755
+ server.tool(
5756
+ "session_rename",
5757
+ "Set or clear a session's user-facing name \u2014 the label the sessions tree, transcript header, and tab show. `label` out-ranks `title` in that display chain, so a user rename should write `label` (the default a UI picks) to be sure it shows; `title` is the auto-derived first-sentence fallback. For EACH of `title`/`label`: a non-empty string sets it (trimmed + length-capped), an empty string clears it (reverting to the derived title / a friendly `adapter \xB7 id` fallback), and omitting it leaves that field untouched. Persists across daemon restarts. Does NOT rename the adapter-native session or touch the running agent.",
5758
+ {
5759
+ idOrName: z.string().min(1).describe("Session id or name to rename \u2014 from `session_list`."),
5760
+ label: z.string().optional().describe(
5761
+ "New label (the winning display field). Empty string clears it. Omit to leave the label untouched."
5762
+ ),
5763
+ title: z.string().optional().describe(
5764
+ "New title (the auto-derived fallback slot). Empty string clears it, reverting to the first-sentence derivation. Omit to leave it untouched."
5765
+ )
5766
+ },
5767
+ async (input) => {
5768
+ const prev = registry.findByIdOrName(input.idOrName);
5769
+ if (!prev) {
5770
+ return {
5771
+ content: [
5772
+ {
5773
+ type: "text",
5774
+ text: JSON.stringify({ error: `no session "${input.idOrName}" found` })
5775
+ }
5776
+ ],
5777
+ isError: true
5778
+ };
5779
+ }
5780
+ if (callerScope) {
5781
+ const subtree = collectSubtree(
5782
+ callerScope.ownerSessionId,
5783
+ registry.list({ includeArchived: true })
5784
+ );
5785
+ if (!subtree.has(prev.id)) {
5786
+ return {
5787
+ content: [
5788
+ {
5789
+ type: "text",
5790
+ text: JSON.stringify({
5791
+ error: "orchestrator_session_out_of_scope",
5792
+ message: `session_rename: session "${prev.id}" is not in your subtree \u2014 a scoped orchestrator can only rename sessions it (transitively) spawned.`,
5793
+ ok: false,
5794
+ sessionId: prev.id
5795
+ })
5796
+ }
5797
+ ],
5798
+ isError: true
5799
+ };
5800
+ }
5801
+ }
5802
+ if (input.title === void 0 && input.label === void 0) {
5803
+ return {
5804
+ content: [
5805
+ {
5806
+ type: "text",
5807
+ text: JSON.stringify({
5808
+ error: "nothing_to_rename",
5809
+ message: "session_rename: supply at least one of `title` or `label`.",
5810
+ ok: false,
5811
+ sessionId: prev.id
5812
+ })
5813
+ }
5814
+ ],
5815
+ isError: true
5816
+ };
5817
+ }
5818
+ try {
5819
+ const desc = registry.renameSession(prev.id, {
5820
+ ...input.title !== void 0 ? { title: input.title } : {},
5821
+ ...input.label !== void 0 ? { label: input.label } : {}
5822
+ });
5823
+ return {
5824
+ content: [{ type: "text", text: JSON.stringify(desc, null, 2) }]
4924
5825
  };
4925
5826
  } catch (err) {
4926
5827
  return {
4927
5828
  content: [
4928
5829
  {
4929
5830
  type: "text",
4930
- text: `session_restart: ${err instanceof Error ? err.message : String(err)}`
5831
+ text: `session_rename: ${err instanceof Error ? err.message : String(err)}`
4931
5832
  }
4932
5833
  ],
4933
5834
  isError: true
@@ -4996,7 +5897,16 @@ function registerSessionTools(rawServer, opts) {
4996
5897
  cols: input.cols ?? 80,
4997
5898
  rows: input.rows ?? 24,
4998
5899
  ...input.name ? { name: input.name } : {},
4999
- ...input.label ? { label: input.label } : {}
5900
+ ...input.label ? { label: input.label } : {},
5901
+ // Parent attribution + depth (orchestrator WP4) — same rule as
5902
+ // `agent_start` (session-spawn.ts): a spawn through a scoped
5903
+ // sub-gateway is attributed to the owning orchestrator so
5904
+ // `session_tree` shows the PTY as its child. Depth caps and
5905
+ // child quotas stay agent_start-only for now.
5906
+ ...callerScope?.ownerSessionId ? {
5907
+ parentSessionId: callerScope.ownerSessionId,
5908
+ depth: callerScope.depth + 1
5909
+ } : {}
5000
5910
  });
5001
5911
  return {
5002
5912
  content: [{ type: "text", text: JSON.stringify(desc, null, 2) }]
@@ -5075,10 +5985,13 @@ function registerSessionTools(rawServer, opts) {
5075
5985
  );
5076
5986
  server.tool(
5077
5987
  "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.",
5988
+ "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
5989
  {
5080
5990
  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).")
5991
+ lastBytes: z.number().int().min(1).max(64 * 1024).optional().describe("Max bytes from the tail. Default: full ring buffer (~64 KiB)."),
5992
+ clean: mcpBool2.optional().describe(
5993
+ "Strip ANSI codes, returning human-readable text (as `text` instead of `b64`). Default false = raw base64."
5994
+ )
5082
5995
  },
5083
5996
  async (input) => {
5084
5997
  if (!ptyEnabled) return ptyNotConfigured("terminal_output");
@@ -5118,7 +6031,7 @@ function registerSessionTools(rawServer, opts) {
5118
6031
  sessionId: desc.id,
5119
6032
  status: desc.status,
5120
6033
  bytes: buf.byteLength,
5121
- b64: buf.toString("base64")
6034
+ ...input.clean ? { text: stripAnsi(buf.toString("utf8")) } : { b64: buf.toString("base64") }
5122
6035
  },
5123
6036
  null,
5124
6037
  2
@@ -7935,6 +8848,9 @@ function filterSessionObserver(inner, shouldObserve) {
7935
8848
  // src/sessions.ts
7936
8849
  init_tool_presenter();
7937
8850
  init_transcript_writer();
8851
+
8852
+ // src/conversation-index.ts
8853
+ init_conversation_store();
7938
8854
  var DEFAULT_BUCKET = "default";
7939
8855
  var BUCKETS_ROOT = () => resolve(homedir(), ".agentproto", "workspaces");
7940
8856
  var LEGACY_SESSIONS_FILE = () => resolve(homedir(), ".agentproto", "sessions.json");
@@ -8058,6 +8974,110 @@ function readBucketRows(root, slug) {
8058
8974
  return [];
8059
8975
  }
8060
8976
  }
8977
+ var rowId = (row) => row && typeof row === "object" && "id" in row && typeof row.id === "string" ? row.id : void 0;
8978
+ function mergeBucketRows(onDisk, rows, everHeldIds) {
8979
+ const incomingIds = new Set(rows.map(rowId).filter((id) => id !== void 0));
8980
+ const preserved = onDisk.filter((row) => {
8981
+ const id = rowId(row);
8982
+ return id !== void 0 && !incomingIds.has(id) && !everHeldIds.has(id);
8983
+ });
8984
+ return [...rows, ...preserved];
8985
+ }
8986
+
8987
+ // src/conversation-index.ts
8988
+ function conversationIndexPath(bucketsRoot, slug) {
8989
+ return join(bucketDir(bucketsRoot, slug), "conversations.jsonl");
8990
+ }
8991
+ async function listClaudeSubagents(projectDir, adapterSessionId) {
8992
+ const dir = join(projectDir, adapterSessionId, "subagents");
8993
+ let entries;
8994
+ try {
8995
+ entries = await promises.readdir(dir);
8996
+ } catch {
8997
+ return [];
8998
+ }
8999
+ return entries.filter((e) => e.startsWith("agent-") && e.endsWith(".jsonl")).map((e) => join(dir, e)).sort();
9000
+ }
9001
+ async function resolveNativeLink(input) {
9002
+ const { cwd, adapterSlug, adapterSessionId } = input;
9003
+ if (adapterSlug === "claude-code") {
9004
+ const dir = claudeCodeProjectDir(cwd);
9005
+ const path = join(dir, `${adapterSessionId}.jsonl`);
9006
+ const subagents = await listClaudeSubagents(dir, adapterSessionId);
9007
+ return { kind: "claude-jsonl", path, subagents };
9008
+ }
9009
+ if (adapterSlug === "hermes") {
9010
+ return {
9011
+ kind: "hermes-sqlite",
9012
+ dbPath: join(homedir(), ".hermes", "state.db"),
9013
+ rowId: adapterSessionId
9014
+ };
9015
+ }
9016
+ return void 0;
9017
+ }
9018
+ async function appendConversationRecord(bucketsRoot, slug, record) {
9019
+ const path = conversationIndexPath(bucketsRoot, slug);
9020
+ await promises.mkdir(dirname(path), { recursive: true });
9021
+ await promises.appendFile(path, JSON.stringify(record) + "\n", "utf8");
9022
+ }
9023
+ function isConversationIndexRecord(value) {
9024
+ if (!value || typeof value !== "object") return false;
9025
+ const r = value;
9026
+ 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";
9027
+ }
9028
+ async function readConversationIndex(bucketsRoot, slug) {
9029
+ const path = conversationIndexPath(bucketsRoot, slug);
9030
+ let raw;
9031
+ try {
9032
+ raw = await promises.readFile(path, "utf8");
9033
+ } catch {
9034
+ return [];
9035
+ }
9036
+ const bySession = /* @__PURE__ */ new Map();
9037
+ for (const line of raw.split("\n")) {
9038
+ const trimmed = line.trim();
9039
+ if (!trimmed) continue;
9040
+ let parsed;
9041
+ try {
9042
+ parsed = JSON.parse(trimmed);
9043
+ } catch {
9044
+ continue;
9045
+ }
9046
+ if (!isConversationIndexRecord(parsed)) continue;
9047
+ bySession.set(parsed.sessionId, parsed);
9048
+ }
9049
+ return Array.from(bySession.values());
9050
+ }
9051
+ async function findConversationRecord(bucketsRoot, slug, sessionId) {
9052
+ const rows = await readConversationIndex(bucketsRoot, slug);
9053
+ return rows.find((r) => r.sessionId === sessionId);
9054
+ }
9055
+ async function locateConversationBySessionId(bucketsRoot, listBuckets2, sessionId) {
9056
+ for (const slug of listBuckets2()) {
9057
+ const record = await findConversationRecord(bucketsRoot, slug, sessionId);
9058
+ if (record) return { workspace: slug, record };
9059
+ }
9060
+ return void 0;
9061
+ }
9062
+ async function locateConversationByNativePath(bucketsRoot, listBuckets2, nativePath) {
9063
+ const target = resolve(nativePath);
9064
+ for (const slug of listBuckets2()) {
9065
+ const rows = await readConversationIndex(bucketsRoot, slug);
9066
+ for (const record of rows) {
9067
+ if (!record.native || record.native.kind !== "claude-jsonl") continue;
9068
+ if (resolve(record.native.path) === target) {
9069
+ return { workspace: slug, record };
9070
+ }
9071
+ const matchedSubagentPath = record.native.subagents.find(
9072
+ (p) => resolve(p) === target
9073
+ );
9074
+ if (matchedSubagentPath) {
9075
+ return { workspace: slug, record, matchedSubagentPath };
9076
+ }
9077
+ }
9078
+ }
9079
+ return void 0;
9080
+ }
8061
9081
 
8062
9082
  // src/terminal-transcript-writer.ts
8063
9083
  init_transcript_writer();
@@ -8108,60 +9128,6 @@ function createTerminalTranscriptWriter(opts) {
8108
9128
  }
8109
9129
  };
8110
9130
  }
8111
- var markerSchema = z.object({ worktreeId: z.string() });
8112
- function statOrUndefined(path) {
8113
- try {
8114
- return statSync(path);
8115
- } catch {
8116
- return void 0;
8117
- }
8118
- }
8119
- function readWorktreeGitDir(dir) {
8120
- const link = (() => {
8121
- try {
8122
- return readFileSync(join(dir, ".git"), "utf8");
8123
- } catch {
8124
- return void 0;
8125
- }
8126
- })();
8127
- if (link === void 0) return void 0;
8128
- const target = /^gitdir:\s*(.+)$/m.exec(link)?.[1]?.trim();
8129
- if (!target) return void 0;
8130
- const gitDir = isAbsolute(target) ? target : resolve(dir, target);
8131
- return statOrUndefined(join(gitDir, "gitdir"))?.isFile() === true ? gitDir : void 0;
8132
- }
8133
- function readWorktreeId(gitDir) {
8134
- let raw;
8135
- try {
8136
- raw = readFileSync(join(gitDir, "agentproto-worktree.json"), "utf8");
8137
- } catch {
8138
- return void 0;
8139
- }
8140
- let parsed;
8141
- try {
8142
- parsed = JSON.parse(raw);
8143
- } catch {
8144
- return void 0;
8145
- }
8146
- const result = markerSchema.safeParse(parsed);
8147
- return result.success ? result.data.worktreeId : void 0;
8148
- }
8149
- function resolveWorktreeIdentity(cwd) {
8150
- let dir = resolve(cwd);
8151
- for (; ; ) {
8152
- const dotGit = statOrUndefined(join(dir, ".git"));
8153
- if (dotGit) {
8154
- if (!dotGit.isFile()) return void 0;
8155
- const gitDir = readWorktreeGitDir(dir);
8156
- if (gitDir === void 0) return void 0;
8157
- const worktreeId = readWorktreeId(gitDir);
8158
- return worktreeId === void 0 ? { worktreePath: dir } : { worktreePath: dir, worktreeId };
8159
- }
8160
- const parent = dirname(dir);
8161
- if (parent === dir) return void 0;
8162
- dir = parent;
8163
- }
8164
- }
8165
9131
  function normalizeAgentPromptOptions(raw) {
8166
9132
  if (!Array.isArray(raw)) return void 0;
8167
9133
  const labels = raw.map((o) => {
@@ -8235,6 +9201,15 @@ var SessionNotAliveError = class extends Error {
8235
9201
  var RECENT_LINES_CAP = 500;
8236
9202
  var RECENT_BYTES_CAP = 64 * 1024;
8237
9203
  var PERSIST_DEBOUNCE_MS = 1500;
9204
+ var EMPTY_ID_SET = /* @__PURE__ */ new Set();
9205
+ function markHeldId(map, slug, id) {
9206
+ let set = map.get(slug);
9207
+ if (!set) {
9208
+ set = /* @__PURE__ */ new Set();
9209
+ map.set(slug, set);
9210
+ }
9211
+ set.add(id);
9212
+ }
8238
9213
  var HISTORY_CAP = 200;
8239
9214
  var INTERRUPT_SETTLE_TIMEOUT_MS = 6e4;
8240
9215
  function stampProcessAlive(desc) {
@@ -8259,6 +9234,11 @@ function findPriorCommandSessionId(liveSessions, cwd) {
8259
9234
  }
8260
9235
  return best?.desc.id;
8261
9236
  }
9237
+ function currentRouteOf(desc) {
9238
+ if (desc.route?.gateway) return desc.route.gateway;
9239
+ if (desc.model) return tryParseModelRef(desc.model)?.route;
9240
+ return void 0;
9241
+ }
8262
9242
  function worktreeFields(cwd) {
8263
9243
  const identity = resolveWorktreeIdentity(cwd);
8264
9244
  if (!identity) return {};
@@ -8309,6 +9289,8 @@ function createSessionsRegistry(opts) {
8309
9289
  let nextSubId = 1;
8310
9290
  let shutdownDone = false;
8311
9291
  const knownBuckets = /* @__PURE__ */ new Set();
9292
+ const sourceBucketOf = /* @__PURE__ */ new Map();
9293
+ const heldIdsByBucket = /* @__PURE__ */ new Map();
8312
9294
  if (persist) {
8313
9295
  if (partitioned) {
8314
9296
  migrateLegacySessionsFile({
@@ -8321,13 +9303,17 @@ function createSessionsRegistry(opts) {
8321
9303
  loadHistorySnapshot(
8322
9304
  bucketSessionsFile(bucketsRoot, slug),
8323
9305
  sessions,
8324
- sessionEvents
9306
+ sessionEvents,
9307
+ slug,
9308
+ sourceBucketOf,
9309
+ heldIdsByBucket
8325
9310
  );
8326
9311
  }
8327
9312
  } else {
8328
9313
  loadHistorySnapshot(legacyPath, sessions, sessionEvents);
8329
9314
  }
8330
9315
  }
9316
+ const bootLoadedBuckets = new Set(knownBuckets);
8331
9317
  const onProcessExit = () => {
8332
9318
  shutdownImpl();
8333
9319
  };
@@ -8411,6 +9397,39 @@ function createSessionsRegistry(opts) {
8411
9397
  }
8412
9398
  delete rt.desc.awaitingPermission;
8413
9399
  };
9400
+ const recordConversationLink = (rt, adapterSessionIdOverride) => {
9401
+ if (!persist || !partitioned) return;
9402
+ const desc = rt.desc;
9403
+ if (desc.kind !== "agent-cli") return;
9404
+ const adapterSlug = desc.adapterSlug;
9405
+ const adapterSessionId = adapterSessionIdOverride ?? desc.adapterSessionId;
9406
+ const cwd = desc.cwd;
9407
+ if (!adapterSlug || !adapterSessionId || !cwd) return;
9408
+ void (async () => {
9409
+ try {
9410
+ const native = await resolveNativeLink({ cwd, adapterSlug, adapterSessionId });
9411
+ const registered = readRegisteredSlugs(workspacesConfigPath);
9412
+ const slug = resolveBucketSlug(desc.workspaceSlug, registered);
9413
+ const record = {
9414
+ sessionId: desc.id,
9415
+ workspace: slug,
9416
+ cwd,
9417
+ adapterSlug,
9418
+ adapterSessionId,
9419
+ ...native ? { native } : {},
9420
+ agentprotoTranscript: sessionEventsPath(desc.id, transcriptBaseDir),
9421
+ ...desc.title ? { title: desc.title } : {},
9422
+ startedAt: desc.startedAt,
9423
+ ...desc.endedAt ? { endedAt: desc.endedAt } : {}
9424
+ };
9425
+ await appendConversationRecord(bucketsRoot, slug, record);
9426
+ } catch (err) {
9427
+ console.warn(
9428
+ `[sessions] conversation-index write failed for ${desc.id}: ${err instanceof Error ? err.message : String(err)}`
9429
+ );
9430
+ }
9431
+ })();
9432
+ };
8414
9433
  const schedulePersist = () => {
8415
9434
  if (!persist) return;
8416
9435
  if (persistTimer) clearTimeout(persistTimer);
@@ -8427,7 +9446,8 @@ function createSessionsRegistry(opts) {
8427
9446
  const groups = /* @__PURE__ */ new Map();
8428
9447
  for (const slug of knownBuckets) groups.set(slug, []);
8429
9448
  for (const desc of snapshotRows()) {
8430
- const slug = resolveBucketSlug(desc.workspaceSlug, registered);
9449
+ const slug = sourceBucketOf.get(desc.id) ?? resolveBucketSlug(desc.workspaceSlug, registered);
9450
+ markHeldId(heldIdsByBucket, slug, desc.id);
8431
9451
  const list = groups.get(slug);
8432
9452
  if (list) list.push(desc);
8433
9453
  else groups.set(slug, [desc]);
@@ -8435,6 +9455,11 @@ function createSessionsRegistry(opts) {
8435
9455
  for (const slug of groups.keys()) knownBuckets.add(slug);
8436
9456
  return groups;
8437
9457
  };
9458
+ const rowsToWrite = (slug, rows) => bootLoadedBuckets.has(slug) ? rows : mergeBucketRows(
9459
+ readBucketRows(bucketsRoot, slug),
9460
+ rows,
9461
+ heldIdsByBucket.get(slug) ?? EMPTY_ID_SET
9462
+ );
8438
9463
  const persistSnapshot = async () => {
8439
9464
  try {
8440
9465
  const savedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -8442,7 +9467,7 @@ function createSessionsRegistry(opts) {
8442
9467
  for (const [slug, rows] of groupRowsByBucket()) {
8443
9468
  await writeBucketSnapshot(bucketsRoot, slug, {
8444
9469
  savedAt,
8445
- sessions: rows
9470
+ sessions: rowsToWrite(slug, rows)
8446
9471
  });
8447
9472
  }
8448
9473
  return;
@@ -8477,6 +9502,7 @@ function createSessionsRegistry(opts) {
8477
9502
  [strategy.storeAs]: m[1]
8478
9503
  };
8479
9504
  schedulePersist();
9505
+ recordConversationLink(rt, m[1]);
8480
9506
  }
8481
9507
  };
8482
9508
  const appendBytes = (rt, chunk) => {
@@ -8754,6 +9780,7 @@ function createSessionsRegistry(opts) {
8754
9780
  rt.emitter.emit("status", rt.desc.status);
8755
9781
  }
8756
9782
  schedulePersist();
9783
+ recordConversationLink(rt);
8757
9784
  } catch (err) {
8758
9785
  const msg = err instanceof Error ? err.message : String(err);
8759
9786
  appendLine(rt, `[error] resume failed: ${msg}`, "stderr");
@@ -9098,11 +10125,25 @@ function createSessionsRegistry(opts) {
9098
10125
  ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
9099
10126
  depth: input.depth ?? 0,
9100
10127
  ...input.model ? { model: input.model } : {},
10128
+ ...input.mode ? { mode: input.mode } : {},
9101
10129
  ...input.auth ? { auth: input.auth } : {},
10130
+ // Decomposed config-axis echoes (SPEC §3.7), same optional-spread
10131
+ // shape as `model`/`mode`/`auth` above.
10132
+ ...input.effort ? { effort: input.effort } : {},
10133
+ ...input.posture !== void 0 ? { posture: input.posture } : {},
10134
+ ...input.route ? { route: input.route } : {},
10135
+ ...input.contextProfile ? { contextProfile: input.contextProfile } : {},
10136
+ ...input.accessProfile ? { accessProfile: input.accessProfile } : {},
9102
10137
  ...priorCommandSessionId ? { priorCommandSessionId } : {},
9103
10138
  ...input.remote ? { remote: true } : {},
9104
10139
  ...input.sandboxId ? { sandboxId: input.sandboxId } : {},
9105
- ...input.sandboxTeardown ? { sandboxTeardown: input.sandboxTeardown } : {}
10140
+ ...input.sandboxTeardown ? { sandboxTeardown: input.sandboxTeardown } : {},
10141
+ // Restart lineage (see SessionDescriptor.resumedFrom's doc). `resumeVia`
10142
+ // can legitimately be "" (a fresh fallback spawn with no continuity),
10143
+ // so it's gated on `!== undefined` rather than truthiness — a truthy
10144
+ // gate would silently drop the empty-string case.
10145
+ ...input.resumedFrom ? { resumedFrom: input.resumedFrom } : {},
10146
+ ...input.resumeVia !== void 0 ? { resumeVia: input.resumeVia } : {}
9106
10147
  };
9107
10148
  if (input.trace ?? opts?.langfuseTracingDefault ?? false) {
9108
10149
  tracedSessions.add(id);
@@ -9130,6 +10171,7 @@ function createSessionsRegistry(opts) {
9130
10171
  "stdout"
9131
10172
  );
9132
10173
  schedulePersist();
10174
+ recordConversationLink(rt);
9133
10175
  if (input.initialPrompt) {
9134
10176
  void runAgentTurn(rt, input.initialPrompt).catch((err) => {
9135
10177
  appendLine(
@@ -9189,7 +10231,16 @@ function createSessionsRegistry(opts) {
9189
10231
  ...worktreeFields(input.cwd),
9190
10232
  ...input.name ? { name: input.name } : {},
9191
10233
  ...input.label ? { label: input.label } : {},
9192
- ...priorCommandSessionId ? { priorCommandSessionId } : {}
10234
+ ...priorCommandSessionId ? { priorCommandSessionId } : {},
10235
+ // Parent attribution + depth (orchestrator WP4) — same recording
10236
+ // rule as spawnAgent above: depth always set so subtree/depth
10237
+ // logic never distinguishes "absent" from "root".
10238
+ ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
10239
+ depth: input.depth ?? 0,
10240
+ // Restart lineage — same gating rule as spawnAgent above (`resumeVia`
10241
+ // can legitimately be "").
10242
+ ...input.resumedFrom ? { resumedFrom: input.resumedFrom } : {},
10243
+ ...input.resumeVia !== void 0 ? { resumeVia: input.resumeVia } : {}
9193
10244
  };
9194
10245
  const rt = {
9195
10246
  desc,
@@ -9355,14 +10406,126 @@ function createSessionsRegistry(opts) {
9355
10406
  await interruptInFlightTurn(rt, id, "interruptSession");
9356
10407
  return { wasBusy: true };
9357
10408
  },
10409
+ async setModel(id, modelId) {
10410
+ const rt = sessions.get(id);
10411
+ if (!rt) throw new Error(`setModel: no session "${id}"`);
10412
+ if (rt.desc.kind !== "agent-cli" || !rt.agentSession) {
10413
+ throw new Error(`setModel: session "${id}" is not an agent-cli session`);
10414
+ }
10415
+ const targetRoute = tryParseModelRef(modelId)?.route;
10416
+ const currentRoute = currentRouteOf(rt.desc);
10417
+ if (targetRoute && currentRoute && targetRoute !== currentRoute) {
10418
+ return {
10419
+ applied: false,
10420
+ reason: "requires-restart",
10421
+ suggestedOverride: { route: { gateway: targetRoute }, model: modelId }
10422
+ };
10423
+ }
10424
+ if (!rt.agentSession.setModel) {
10425
+ return { applied: false, reason: "not-supported" };
10426
+ }
10427
+ const result = await rt.agentSession.setModel(modelId);
10428
+ if (result.applied) {
10429
+ rt.desc.model = result.model ?? modelId;
10430
+ schedulePersist();
10431
+ if (sessionEvents) {
10432
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
10433
+ sessionEvents.emit({
10434
+ type: "session:config-changed",
10435
+ sessionId: id,
10436
+ axis: "model",
10437
+ value: rt.desc.model,
10438
+ label: rt.desc.label,
10439
+ ts
10440
+ });
10441
+ sessionEvents.emit({
10442
+ type: "session:model-changed",
10443
+ sessionId: id,
10444
+ model: rt.desc.model,
10445
+ label: rt.desc.label,
10446
+ ts
10447
+ });
10448
+ }
10449
+ }
10450
+ return result;
10451
+ },
10452
+ emitConfigChanged(ev) {
10453
+ sessionEvents?.emit(ev);
10454
+ },
10455
+ async setEffort(id, effort) {
10456
+ const rt = sessions.get(id);
10457
+ if (!rt) throw new Error(`setEffort: no session "${id}"`);
10458
+ if (rt.desc.kind !== "agent-cli" || !rt.agentSession) {
10459
+ throw new Error(`setEffort: session "${id}" is not an agent-cli session`);
10460
+ }
10461
+ if (!rt.agentSession.setEffort) {
10462
+ return { applied: false, reason: "not-supported" };
10463
+ }
10464
+ const result = await rt.agentSession.setEffort(effort);
10465
+ if (result.applied) {
10466
+ rt.desc.effort = result.effort ?? effort;
10467
+ schedulePersist();
10468
+ if (sessionEvents) {
10469
+ sessionEvents.emit({
10470
+ type: "session:config-changed",
10471
+ sessionId: id,
10472
+ axis: "effort",
10473
+ value: rt.desc.effort,
10474
+ label: rt.desc.label,
10475
+ ts: (/* @__PURE__ */ new Date()).toISOString()
10476
+ });
10477
+ }
10478
+ }
10479
+ return result;
10480
+ },
10481
+ async setPosture(id, posture) {
10482
+ const rt = sessions.get(id);
10483
+ if (!rt) throw new Error(`setPosture: no session "${id}"`);
10484
+ if (rt.desc.kind !== "agent-cli" || !rt.agentSession) {
10485
+ throw new Error(`setPosture: session "${id}" is not an agent-cli session`);
10486
+ }
10487
+ const resolution = resolvePosture(posture, rt.agentSession.availableModes ?? []);
10488
+ if (resolution.kind !== "native") {
10489
+ return {
10490
+ applied: false,
10491
+ reason: "requires-restart",
10492
+ resolution: resolution.kind
10493
+ };
10494
+ }
10495
+ if (!rt.agentSession.setSessionMode) {
10496
+ return { applied: false, reason: "not-supported", resolution: "native" };
10497
+ }
10498
+ const result = await rt.agentSession.setSessionMode(resolution.mode.id);
10499
+ if (!result.applied) {
10500
+ return {
10501
+ applied: false,
10502
+ resolution: "native",
10503
+ ...result.reason ? { reason: result.reason } : {}
10504
+ };
10505
+ }
10506
+ rt.desc.posture = posture;
10507
+ schedulePersist();
10508
+ if (sessionEvents) {
10509
+ sessionEvents.emit({
10510
+ type: "session:config-changed",
10511
+ sessionId: id,
10512
+ axis: "posture",
10513
+ value: posture,
10514
+ label: rt.desc.label,
10515
+ ts: (/* @__PURE__ */ new Date()).toISOString()
10516
+ });
10517
+ }
10518
+ return { applied: true, posture, modeId: resolution.mode.id, resolution: "native" };
10519
+ },
9358
10520
  pulseActivity(id) {
9359
10521
  const rt = sessions.get(id);
9360
10522
  if (!rt) return;
9361
10523
  rt.desc.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
9362
10524
  schedulePersist();
9363
10525
  },
9364
- list() {
9365
- return Array.from(sessions.values()).map((s) => s.desc).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).map((desc) => {
10526
+ list(opts2) {
10527
+ const includeArchived = opts2?.includeArchived ?? false;
10528
+ 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
10529
  stampProcessAlive(desc);
9367
10530
  return desc;
9368
10531
  });
@@ -9478,6 +10641,55 @@ function createSessionsRegistry(opts) {
9478
10641
  emitExited(rt);
9479
10642
  return true;
9480
10643
  },
10644
+ archiveSession(id) {
10645
+ const rt = sessions.get(id);
10646
+ if (!rt) throw new Error(`archiveSession: no session "${id}"`);
10647
+ const isAlive = rt.desc.status === "running" || rt.desc.status === "starting";
10648
+ if (isAlive) {
10649
+ throw new Error(
10650
+ `archiveSession: session "${id}" is still ${rt.desc.status} \u2014 only a terminal-status session (exited/killed/error) can be archived.`
10651
+ );
10652
+ }
10653
+ rt.desc.archived = true;
10654
+ schedulePersist();
10655
+ stampProcessAlive(rt.desc);
10656
+ return rt.desc;
10657
+ },
10658
+ unarchiveSession(id) {
10659
+ const rt = sessions.get(id);
10660
+ if (!rt) throw new Error(`unarchiveSession: no session "${id}"`);
10661
+ rt.desc.archived = false;
10662
+ schedulePersist();
10663
+ stampProcessAlive(rt.desc);
10664
+ return rt.desc;
10665
+ },
10666
+ renameSession(id, patch) {
10667
+ const rt = sessions.get(id);
10668
+ if (!rt) throw new Error(`renameSession: no session "${id}"`);
10669
+ const apply = (field) => {
10670
+ const raw = patch[field];
10671
+ if (raw === void 0) return;
10672
+ const trimmed = raw === null ? "" : raw.trim();
10673
+ if (trimmed === "") {
10674
+ rt.desc[field] = void 0;
10675
+ return;
10676
+ }
10677
+ const points = Array.from(trimmed);
10678
+ rt.desc[field] = points.length > MAX_LENGTH ? points.slice(0, MAX_LENGTH).join("") : trimmed;
10679
+ };
10680
+ apply("title");
10681
+ apply("label");
10682
+ schedulePersist();
10683
+ sessionEvents?.emit({
10684
+ type: "session:renamed",
10685
+ sessionId: id,
10686
+ ...rt.desc.title !== void 0 ? { title: rt.desc.title } : {},
10687
+ ...rt.desc.label !== void 0 ? { label: rt.desc.label } : {},
10688
+ ts: (/* @__PURE__ */ new Date()).toISOString()
10689
+ });
10690
+ stampProcessAlive(rt.desc);
10691
+ return rt.desc;
10692
+ },
9481
10693
  listPendingPermissions(filter) {
9482
10694
  const all = Array.from(pendingPermissions.values());
9483
10695
  const scoped = filter?.sessionId ? all.filter((p) => p.sessionId === filter.sessionId) : all;
@@ -9602,7 +10814,7 @@ function createSessionsRegistry(opts) {
9602
10814
  for (const [slug, rows] of groupRowsByBucket()) {
9603
10815
  writeBucketSnapshotSync(bucketsRoot, slug, {
9604
10816
  savedAt: nowIso,
9605
- sessions: rows
10817
+ sessions: rowsToWrite(slug, rows)
9606
10818
  });
9607
10819
  }
9608
10820
  } else {
@@ -9624,7 +10836,7 @@ function clearInFlightFlags(desc) {
9624
10836
  desc.blockedOn = void 0;
9625
10837
  desc.pendingToolCallId = void 0;
9626
10838
  }
9627
- function loadHistorySnapshot(persistPath, sessions, sessionEvents) {
10839
+ function loadHistorySnapshot(persistPath, sessions, sessionEvents, bucketSlug, sourceBucketOf, heldIdsByBucket) {
9628
10840
  let raw;
9629
10841
  try {
9630
10842
  raw = readFileSync(persistPath, "utf8");
@@ -9676,6 +10888,10 @@ function loadHistorySnapshot(persistPath, sessions, sessionEvents) {
9676
10888
  };
9677
10889
  rt.emitter.setMaxListeners(50);
9678
10890
  sessions.set(desc.id, rt);
10891
+ if (bucketSlug !== void 0) {
10892
+ sourceBucketOf?.set(desc.id, bucketSlug);
10893
+ if (heldIdsByBucket) markHeldId(heldIdsByBucket, bucketSlug, desc.id);
10894
+ }
9679
10895
  if (wasAlive) {
9680
10896
  sessionEvents?.emit({
9681
10897
  type: "session:exited",
@@ -10551,7 +11767,7 @@ function registerOrchestrationTools(rawServer, opts) {
10551
11767
  const server = opts.toolSubset ? withToolSubset(rawServer, opts.toolSubset) : rawServer;
10552
11768
  const { registry, sessionEvents, eventRing, callerScope, inboundWatcher } = opts;
10553
11769
  const isPolicyInSubtree = (policy, ownerId) => {
10554
- const subtree = collectSubtree(ownerId, registry.list());
11770
+ const subtree = collectSubtree(ownerId, registry.list({ includeArchived: true }));
10555
11771
  const ids = policy.sessionIds.length > 0 ? policy.sessionIds : [policy.sessionId];
10556
11772
  return ids.every((id) => subtree.has(id));
10557
11773
  };
@@ -10583,7 +11799,10 @@ function registerOrchestrationTools(rawServer, opts) {
10583
11799
  const isSessionInScope = (sessionId) => {
10584
11800
  if (!callerScope) return true;
10585
11801
  if (!callerScope.ownerSessionId) return false;
10586
- return collectSubtree(callerScope.ownerSessionId, registry.list()).has(sessionId);
11802
+ return collectSubtree(
11803
+ callerScope.ownerSessionId,
11804
+ registry.list({ includeArchived: true })
11805
+ ).has(sessionId);
10587
11806
  };
10588
11807
  const enrichPermission2 = (p) => {
10589
11808
  const desc = registry.get(p.sessionId);
@@ -10761,6 +11980,12 @@ function registerOrchestrationTools(rawServer, opts) {
10761
11980
  "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
11981
  ),
10763
11982
  cacheable: z.boolean().optional().describe("Cache this step's output under the run's cacheKey (opt-in; for idempotent/pure steps only)."),
11983
+ sandbox: z.union([
11984
+ z.string().min(1).describe("Sandbox provider slug from `list_sandbox_providers` (e.g. 'local', 'e2b')."),
11985
+ z.object({ provider: z.string().min(1) }).passthrough().describe("Inline AIP-36 SandboxSpec ({ provider, config?, env?, \u2026 }).")
11986
+ ]).optional().describe(
11987
+ "Run this step's session inside a sandbox instead of on the host \u2014 same semantics as `agent_start.sandbox`. Only meaningful with `adapter`."
11988
+ ),
10764
11989
  policy: z.discriminatedUnion("awaiting", [
10765
11990
  z.object({ awaiting: z.literal("auto-allow"), prompt: z.string() }),
10766
11991
  z.object({
@@ -10982,7 +12207,10 @@ function registerOrchestrationTools(rawServer, opts) {
10982
12207
  };
10983
12208
  }
10984
12209
  const targetIds = input.sessionIds && input.sessionIds.length > 0 ? input.sessionIds : input.sessionId ? [input.sessionId] : [];
10985
- const subtree = collectSubtree(callerScope.ownerSessionId, registry.list());
12210
+ const subtree = collectSubtree(
12211
+ callerScope.ownerSessionId,
12212
+ registry.list({ includeArchived: true })
12213
+ );
10986
12214
  const outside = targetIds.filter((id) => !subtree.has(id));
10987
12215
  if (outside.length > 0) {
10988
12216
  return {
@@ -11874,22 +13102,136 @@ async function startHttpServer(opts) {
11874
13102
  );
11875
13103
  if (handled) return;
11876
13104
  }
11877
- if (path === "/workspaces" && req.method === "GET") {
13105
+ if (path === "/workspaces" && req.method === "GET") {
13106
+ try {
13107
+ const config = await loadWorkspacesConfig();
13108
+ res.writeHead(200, { "content-type": "application/json" });
13109
+ res.end(JSON.stringify(config));
13110
+ } catch (err) {
13111
+ res.writeHead(500, { "content-type": "application/json" });
13112
+ res.end(
13113
+ JSON.stringify({
13114
+ error: "workspaces_load_failed",
13115
+ message: err instanceof Error ? err.message : String(err)
13116
+ })
13117
+ );
13118
+ }
13119
+ return;
13120
+ }
13121
+ if (path === "/workspaces" && req.method === "POST") {
13122
+ const gate = checkSessionsToken(req);
13123
+ if (gate !== "ok") {
13124
+ rejectUnauthorizedSession(req, res, gate);
13125
+ return;
13126
+ }
13127
+ const body = await readJsonBody(req);
13128
+ if (!body || typeof body.path !== "string" || !body.path) {
13129
+ res.writeHead(400, { "content-type": "application/json" });
13130
+ res.end(JSON.stringify({ error: "missing_path" }));
13131
+ return;
13132
+ }
13133
+ if (!isAbsolute(body.path)) {
13134
+ res.writeHead(400, { "content-type": "application/json" });
13135
+ res.end(
13136
+ JSON.stringify({
13137
+ error: "workspace_path_not_absolute",
13138
+ message: `path must be absolute, got "${body.path}".`
13139
+ })
13140
+ );
13141
+ return;
13142
+ }
13143
+ try {
13144
+ await stat(body.path);
13145
+ } catch {
13146
+ res.writeHead(400, { "content-type": "application/json" });
13147
+ res.end(
13148
+ JSON.stringify({
13149
+ error: "workspace_path_not_found",
13150
+ message: `"${body.path}" doesn't exist.`
13151
+ })
13152
+ );
13153
+ return;
13154
+ }
13155
+ if (body.slug !== void 0 && typeof body.slug !== "string") {
13156
+ res.writeHead(400, { "content-type": "application/json" });
13157
+ res.end(JSON.stringify({ error: "invalid_slug" }));
13158
+ return;
13159
+ }
13160
+ if (body.label !== void 0 && typeof body.label !== "string") {
13161
+ res.writeHead(400, { "content-type": "application/json" });
13162
+ res.end(JSON.stringify({ error: "invalid_label" }));
13163
+ return;
13164
+ }
13165
+ try {
13166
+ const config = await loadWorkspacesConfig();
13167
+ const next = addWorkspace(config, {
13168
+ slug: body.slug || basename(body.path),
13169
+ path: body.path,
13170
+ ...body.label ? { label: body.label } : {}
13171
+ });
13172
+ await saveWorkspacesConfig(next);
13173
+ res.writeHead(201, { "content-type": "application/json" });
13174
+ res.end(JSON.stringify(next));
13175
+ } catch (err) {
13176
+ res.writeHead(400, { "content-type": "application/json" });
13177
+ res.end(
13178
+ JSON.stringify({
13179
+ error: "workspace_add_failed",
13180
+ message: err instanceof Error ? err.message : String(err)
13181
+ })
13182
+ );
13183
+ }
13184
+ return;
13185
+ }
13186
+ if (path === "/workspaces/active" && req.method === "PUT") {
13187
+ const gate = checkSessionsToken(req);
13188
+ if (gate !== "ok") {
13189
+ rejectUnauthorizedSession(req, res, gate);
13190
+ return;
13191
+ }
13192
+ const body = await readJsonBody(req);
13193
+ if (!body || typeof body.slug !== "string" || !body.slug) {
13194
+ res.writeHead(400, { "content-type": "application/json" });
13195
+ res.end(JSON.stringify({ error: "missing_slug" }));
13196
+ return;
13197
+ }
11878
13198
  try {
11879
13199
  const config = await loadWorkspacesConfig();
13200
+ const next = setActiveWorkspace(config, body.slug);
13201
+ await saveWorkspacesConfig(next);
11880
13202
  res.writeHead(200, { "content-type": "application/json" });
11881
- res.end(JSON.stringify(config));
13203
+ res.end(JSON.stringify(next));
11882
13204
  } catch (err) {
11883
- res.writeHead(500, { "content-type": "application/json" });
13205
+ res.writeHead(404, { "content-type": "application/json" });
11884
13206
  res.end(
11885
13207
  JSON.stringify({
11886
- error: "workspaces_load_failed",
13208
+ error: "workspace_not_found",
11887
13209
  message: err instanceof Error ? err.message : String(err)
11888
13210
  })
11889
13211
  );
11890
13212
  }
11891
13213
  return;
11892
13214
  }
13215
+ const workspaceSlugMatch = path.match(/^\/workspaces\/([^/]+)$/);
13216
+ if (workspaceSlugMatch && req.method === "DELETE") {
13217
+ const gate = checkSessionsToken(req);
13218
+ if (gate !== "ok") {
13219
+ rejectUnauthorizedSession(req, res, gate);
13220
+ return;
13221
+ }
13222
+ const slug = decodeURIComponent(workspaceSlugMatch[1] ?? "");
13223
+ const config = await loadWorkspacesConfig();
13224
+ if (!findWorkspace(config, slug)) {
13225
+ res.writeHead(404, { "content-type": "application/json" });
13226
+ res.end(JSON.stringify({ error: "workspace_not_found", slug }));
13227
+ return;
13228
+ }
13229
+ const next = removeWorkspace(config, slug);
13230
+ await saveWorkspacesConfig(next);
13231
+ res.writeHead(200, { "content-type": "application/json" });
13232
+ res.end(JSON.stringify(next));
13233
+ return;
13234
+ }
11893
13235
  if (path === "/mcps/imports" && req.method === "GET") {
11894
13236
  const config = await loadImportedMcps();
11895
13237
  res.writeHead(200, { "content-type": "application/json" });
@@ -12045,6 +13387,41 @@ async function startHttpServer(opts) {
12045
13387
  }
12046
13388
  return;
12047
13389
  }
13390
+ if (path === "/catalog/models" && req.method === "GET") {
13391
+ if (!opts.listCatalogModels) {
13392
+ res.writeHead(501, { "content-type": "application/json" });
13393
+ res.end(
13394
+ JSON.stringify({
13395
+ error: "lister_not_configured",
13396
+ message: "Daemon was started without `listCatalogModels` \u2014 see `buildCatalogModels` in `catalog-models.ts`."
13397
+ })
13398
+ );
13399
+ return;
13400
+ }
13401
+ try {
13402
+ const qs = new URLSearchParams(
13403
+ url.includes("?") ? url.slice(url.indexOf("?") + 1) : ""
13404
+ );
13405
+ const runnableOnlyParam = qs.get("runnableOnly");
13406
+ const catalog = await opts.listCatalogModels({
13407
+ ...qs.get("adapter") ? { adapter: qs.get("adapter") } : {},
13408
+ ...qs.get("vendor") ? { vendor: qs.get("vendor") } : {},
13409
+ ...qs.get("route") ? { route: qs.get("route") } : {},
13410
+ ...runnableOnlyParam === "true" ? { runnableOnly: true } : {}
13411
+ });
13412
+ res.writeHead(200, { "content-type": "application/json" });
13413
+ res.end(JSON.stringify(catalog));
13414
+ } catch (err) {
13415
+ res.writeHead(500, { "content-type": "application/json" });
13416
+ res.end(
13417
+ JSON.stringify({
13418
+ error: "list_failed",
13419
+ message: err instanceof Error ? err.message : String(err)
13420
+ })
13421
+ );
13422
+ }
13423
+ return;
13424
+ }
12048
13425
  if (path === "/presets" && req.method === "GET") {
12049
13426
  const handled = await handlePresets(req, res, path);
12050
13427
  if (handled) return;
@@ -12400,7 +13777,10 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
12400
13777
  res.end(JSON.stringify(body));
12401
13778
  };
12402
13779
  if (path === "/sessions" && req.method === "GET") {
12403
- json(200, { sessions: registry.list() });
13780
+ const reqUrl = req.url ?? "";
13781
+ const queryString = reqUrl.includes("?") ? reqUrl.slice(reqUrl.indexOf("?") + 1) : "";
13782
+ const includeArchived = new URLSearchParams(queryString).get("includeArchived") === "true";
13783
+ json(200, { sessions: registry.list({ includeArchived }) });
12404
13784
  return true;
12405
13785
  }
12406
13786
  if (path === "/sessions/agent" && req.method === "POST") {
@@ -12448,6 +13828,9 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
12448
13828
  })() : {},
12449
13829
  ...typeof b.prompt === "string" ? { prompt: b.prompt } : {},
12450
13830
  ...typeof b.label === "string" ? { label: b.label } : {},
13831
+ // Explicit title override (SPEC-3 FIX C, `--title`) — wins over the
13832
+ // first-sentence derivation from the prompt (see session-spawn.ts).
13833
+ ...typeof b.title === "string" ? { title: b.title } : {},
12451
13834
  ...typeof b.idempotencyKey === "string" && b.idempotencyKey.length > 0 ? { idempotencyKey: b.idempotencyKey } : {},
12452
13835
  ...typeof b.role === "string" && b.role.length > 0 ? { role: b.role } : {},
12453
13836
  ...typeof b.promptAppend === "string" ? { promptAppend: b.promptAppend } : {},
@@ -12485,7 +13868,7 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
12485
13868
  }
12486
13869
  );
12487
13870
  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;
13871
+ 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
13872
  json(status, {
12490
13873
  error: result.code,
12491
13874
  message: result.message,
@@ -12704,6 +14087,197 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
12704
14087
  }
12705
14088
  return true;
12706
14089
  }
14090
+ const terminalInputMatch = path.match(/^\/sessions\/([^/]+)\/terminal\/input$/);
14091
+ if (terminalInputMatch && req.method === "POST") {
14092
+ const id2 = terminalInputMatch[1];
14093
+ if (!id2) return false;
14094
+ if (!ptyEnabled) {
14095
+ json(501, {
14096
+ error: "pty_not_configured",
14097
+ message: "POST /sessions/:id/terminal/input needs the host to inject `spawnPty` into createGateway (node-pty optional dep \u2014 install in @agentproto/cli)."
14098
+ });
14099
+ return true;
14100
+ }
14101
+ const desc = registry.get(id2);
14102
+ if (!desc) {
14103
+ json(404, { error: "no_session", message: `no session "${id2}"` });
14104
+ return true;
14105
+ }
14106
+ const body = await readJsonBody(req);
14107
+ const text6 = body?.text;
14108
+ if (typeof text6 !== "string") {
14109
+ json(400, { error: "missing_text", message: "Body `text` must be a string." });
14110
+ return true;
14111
+ }
14112
+ if (desc.kind !== "terminal" || desc.pty !== true) {
14113
+ json(400, {
14114
+ error: "not_a_pty",
14115
+ message: `session "${id2}" is not a live PTY (kind=${desc.kind})`
14116
+ });
14117
+ return true;
14118
+ }
14119
+ const enter = body?.enter !== false;
14120
+ let ok = true;
14121
+ if (text6.length > 0) ok = registry.writeTerminalInput(id2, text6) && ok;
14122
+ if (enter) ok = registry.writeTerminalInput(id2, "\r") && ok;
14123
+ if (!ok) {
14124
+ json(400, {
14125
+ error: "not_a_pty",
14126
+ message: `session "${id2}" has no live PTY to write to`
14127
+ });
14128
+ return true;
14129
+ }
14130
+ json(200, { ok: true });
14131
+ return true;
14132
+ }
14133
+ const modelMatch = path.match(/^\/sessions\/([^/]+)\/model$/);
14134
+ if (modelMatch && req.method === "POST") {
14135
+ const id2 = modelMatch[1];
14136
+ if (!id2) return false;
14137
+ const body = await readJsonBody(req);
14138
+ const model = body && typeof body === "object" && typeof body.model === "string" ? body.model : void 0;
14139
+ if (!model) {
14140
+ json(400, { error: "missing_model" });
14141
+ return true;
14142
+ }
14143
+ try {
14144
+ const result = await registry.setModel(id2, model);
14145
+ json(200, { ok: true, id: id2, ...result });
14146
+ } catch (err) {
14147
+ const msg = err instanceof Error ? err.message : String(err);
14148
+ const status = msg.includes("no session") ? 404 : msg.includes("not an agent-cli session") ? 400 : 500;
14149
+ json(status, { error: "set_model_failed", message: msg });
14150
+ }
14151
+ return true;
14152
+ }
14153
+ const restartMatch = path.match(/^\/sessions\/([^/]+)\/restart$/);
14154
+ if (restartMatch && req.method === "POST") {
14155
+ const id2 = restartMatch[1];
14156
+ if (!id2) return false;
14157
+ if (!resolveAgentAdapter) {
14158
+ json(501, {
14159
+ error: "restart_not_enabled",
14160
+ message: "POST /sessions/:id/restart needs the host to inject `resolveAgentAdapter`."
14161
+ });
14162
+ return true;
14163
+ }
14164
+ const prev = registry.findByIdOrName(id2);
14165
+ if (!prev) {
14166
+ json(404, { error: "no_session", message: `no session "${id2}" found` });
14167
+ return true;
14168
+ }
14169
+ if (!prev.adapterSlug) {
14170
+ json(400, {
14171
+ error: "restart_override_invalid",
14172
+ message: "restart-with-override only applies to agent-cli sessions (a PTY/command session has no config axes to override)."
14173
+ });
14174
+ return true;
14175
+ }
14176
+ const body = await readJsonBody(req);
14177
+ const b = body && typeof body === "object" ? body : {};
14178
+ const str = (v) => typeof v === "string" ? v : void 0;
14179
+ const overrides = {
14180
+ ...str(b.model) !== void 0 ? { model: str(b.model) } : {},
14181
+ ...str(b.effort) !== void 0 ? { effort: str(b.effort) } : {},
14182
+ ...b.access && typeof b.access === "object" && str(b.access.profileRef) !== void 0 ? { access: { profileRef: str(b.access.profileRef) } } : {},
14183
+ ...b.route && typeof b.route === "object" && str(b.route.gateway) !== void 0 ? {
14184
+ route: {
14185
+ gateway: str(b.route.gateway),
14186
+ ...str(b.route.baseUrl) !== void 0 ? { baseUrl: str(b.route.baseUrl) } : {}
14187
+ }
14188
+ } : {},
14189
+ ...b.posture !== void 0 ? { posture: b.posture } : {},
14190
+ ...str(b.contextProfile) !== void 0 ? { contextProfile: str(b.contextProfile) } : {},
14191
+ ...str(b.mode) !== void 0 ? { mode: str(b.mode) } : {}
14192
+ };
14193
+ try {
14194
+ const restarted = await restartAgentSession(registry, resolveAgentAdapter, prev, {
14195
+ forceAgentResume: true,
14196
+ overrides
14197
+ });
14198
+ json(200, {
14199
+ ...restarted.desc,
14200
+ resumedFrom: restarted.resumedFrom,
14201
+ resumeVia: restarted.resumeVia,
14202
+ ...restarted.resumeFallback ? { resumeFallback: true } : {}
14203
+ });
14204
+ } catch (err) {
14205
+ if (err instanceof RestartOverrideError) {
14206
+ json(err.status, { error: err.code, message: err.message, sessionId: prev.id });
14207
+ return true;
14208
+ }
14209
+ json(500, {
14210
+ error: "restart_failed",
14211
+ message: err instanceof Error ? err.message : String(err)
14212
+ });
14213
+ }
14214
+ return true;
14215
+ }
14216
+ const effortMatch = path.match(/^\/sessions\/([^/]+)\/effort$/);
14217
+ if (effortMatch && req.method === "POST") {
14218
+ const id2 = effortMatch[1];
14219
+ if (!id2) return false;
14220
+ const body = await readJsonBody(req);
14221
+ const effort = body && typeof body === "object" && typeof body.effort === "string" ? body.effort : void 0;
14222
+ if (!effort) {
14223
+ json(400, { error: "missing_effort" });
14224
+ return true;
14225
+ }
14226
+ try {
14227
+ const result = await registry.setEffort(id2, effort);
14228
+ json(200, { ok: true, id: id2, ...result });
14229
+ } catch (err) {
14230
+ const msg = err instanceof Error ? err.message : String(err);
14231
+ const status = msg.includes("no session") ? 404 : msg.includes("not an agent-cli session") ? 400 : 500;
14232
+ json(status, { error: "set_effort_failed", message: msg });
14233
+ }
14234
+ return true;
14235
+ }
14236
+ const postureMatch = path.match(/^\/sessions\/([^/]+)\/posture$/);
14237
+ if (postureMatch && req.method === "POST") {
14238
+ const id2 = postureMatch[1];
14239
+ if (!id2) return false;
14240
+ const body = await readJsonBody(req);
14241
+ const postureRaw = body && typeof body === "object" && typeof body.posture === "string" ? body.posture : void 0;
14242
+ if (!postureRaw) {
14243
+ json(400, { error: "missing_posture" });
14244
+ return true;
14245
+ }
14246
+ try {
14247
+ const result = await registry.setPosture(id2, parsePostureInput(postureRaw));
14248
+ json(200, { ok: true, id: id2, ...result });
14249
+ } catch (err) {
14250
+ const msg = err instanceof Error ? err.message : String(err);
14251
+ const status = msg.includes("no session") ? 404 : msg.includes("not an agent-cli session") ? 400 : 500;
14252
+ json(status, { error: "set_posture_failed", message: msg });
14253
+ }
14254
+ return true;
14255
+ }
14256
+ const renameMatch = path.match(/^\/sessions\/([^/]+)$/);
14257
+ if (renameMatch && req.method === "PATCH") {
14258
+ const rawIdOrName2 = renameMatch[1];
14259
+ if (!rawIdOrName2) return false;
14260
+ const resolved = registry.findByIdOrName(rawIdOrName2);
14261
+ if (!resolved) {
14262
+ json(404, { error: "session_not_found", id: rawIdOrName2 });
14263
+ return true;
14264
+ }
14265
+ const body = await readJsonBody(req);
14266
+ const b = body && typeof body === "object" ? body : {};
14267
+ const field = (v) => typeof v === "string" ? v : v === null ? null : void 0;
14268
+ const patch = {
14269
+ ..."title" in b ? { title: field(b.title) } : {},
14270
+ ..."label" in b ? { label: field(b.label) } : {}
14271
+ };
14272
+ try {
14273
+ const desc = registry.renameSession(resolved.id, patch);
14274
+ json(200, desc);
14275
+ } catch (err) {
14276
+ const msg = err instanceof Error ? err.message : String(err);
14277
+ json(msg.includes("no session") ? 404 : 500, { error: "rename_failed", message: msg });
14278
+ }
14279
+ return true;
14280
+ }
12707
14281
  if (path === "/sessions" && req.method === "POST") {
12708
14282
  const body = await readJsonBody(req);
12709
14283
  if (!body || typeof body !== "object") {
@@ -14544,8 +16118,6 @@ function createRoutineRunner(opts) {
14544
16118
  }
14545
16119
  };
14546
16120
  }
14547
-
14548
- // src/sessions-registry-agent-host.ts
14549
16121
  init_transcript_export();
14550
16122
  var SessionsRegistryAgentHost = class {
14551
16123
  constructor(registry, sessionEvents, resolveAgentAdapter, opts) {
@@ -14560,10 +16132,45 @@ var SessionsRegistryAgentHost = class {
14560
16132
  opts;
14561
16133
  sessionsByLabel = /* @__PURE__ */ new Map();
14562
16134
  async spawn(adapter, opts) {
14563
- const resolved = await this.resolveAgentAdapter(adapter);
14564
- if (!resolved) throw new Error(`adapter '${adapter}' not found`);
14565
16135
  const workspaceSlug = opts.workspaceSlug ?? this.opts?.workspaceSlug ?? "default";
14566
16136
  const cwd = opts.cwd ?? this.opts?.cwd ?? process.cwd();
16137
+ if (opts.sandbox !== void 0) {
16138
+ let sandbox;
16139
+ if (typeof opts.sandbox === "string") {
16140
+ sandbox = opts.sandbox;
16141
+ } else {
16142
+ const parsed = SandboxSpecSchema.safeParse({ config: {}, ...opts.sandbox });
16143
+ if (!parsed.success) {
16144
+ throw new Error(
16145
+ `agent step sandbox spec invalid (provider "${opts.sandbox.provider}"): ${parsed.error.message}`
16146
+ );
16147
+ }
16148
+ sandbox = parsed.data;
16149
+ }
16150
+ const result = await spawnAgentSession(
16151
+ {
16152
+ registry: this.registry,
16153
+ resolveAgentAdapter: this.resolveAgentAdapter,
16154
+ ...this.opts?.resolveSandboxProvider ? { resolveSandboxProvider: this.opts.resolveSandboxProvider } : {}
16155
+ },
16156
+ {
16157
+ adapter,
16158
+ cwd,
16159
+ workspaceSlug,
16160
+ sandbox,
16161
+ label: `agent-step:${adapter}`
16162
+ }
16163
+ );
16164
+ if (!result.ok) {
16165
+ throw new Error(`agent step sandbox spawn failed (${result.code}): ${result.message}`);
16166
+ }
16167
+ if (opts.stepId) {
16168
+ this.sessionsByLabel.set(opts.stepId, result.descriptor.id);
16169
+ }
16170
+ return result.descriptor.id;
16171
+ }
16172
+ const resolved = await this.resolveAgentAdapter(adapter);
16173
+ if (!resolved) throw new Error(`adapter '${adapter}' not found`);
14567
16174
  const agentSession = await resolved.startSession({ cwd });
14568
16175
  const desc = this.registry.spawnAgent({
14569
16176
  workspaceSlug,
@@ -14658,7 +16265,14 @@ var SessionsRegistryAgentHost = class {
14658
16265
  };
14659
16266
  unsubs.push(
14660
16267
  this.sessionEvents.on("session:turn-end", (ev) => {
14661
- if (ev.sessionId === sessionId) done();
16268
+ if (ev.sessionId !== sessionId) return;
16269
+ if (ev.empty === true) {
16270
+ fail(
16271
+ `session ${sessionId} produced an empty turn \u2014 no assistant output or tool call (commonly an auth failure or an invalid model id)`
16272
+ );
16273
+ } else {
16274
+ done();
16275
+ }
14662
16276
  })
14663
16277
  );
14664
16278
  unsubs.push(
@@ -14739,6 +16353,7 @@ function translateStages(stages, workflowId) {
14739
16353
  id: step.label,
14740
16354
  ...step.adapter !== void 0 ? { adapter: step.adapter } : {},
14741
16355
  ...step.sessionRef !== void 0 ? { sessionRef: step.sessionRef } : {},
16356
+ ...step.sandbox !== void 0 ? { sandbox: step.sandbox } : {},
14742
16357
  ...step.cacheable ? { cacheable: true } : {},
14743
16358
  prompt: () => step.prompt ?? "",
14744
16359
  policy: step.policy ?? { awaiting: "fail" }
@@ -14756,6 +16371,45 @@ function translateStages(stages, workflowId) {
14756
16371
  steps
14757
16372
  };
14758
16373
  }
16374
+ function collectAgentSteps(steps) {
16375
+ const collected = [];
16376
+ for (const step of steps) {
16377
+ if (step.kind === "agent") {
16378
+ const adapter = typeof step.adapter === "string" ? step.adapter : void 0;
16379
+ collected.push({ id: step.id, adapter, sessionRef: step.sessionRef });
16380
+ } else if (step.kind === "parallel") {
16381
+ for (const branch of step.branches) collected.push(...collectAgentSteps(branch.steps));
16382
+ } else if (step.kind === "group") {
16383
+ collected.push(...collectAgentSteps(step.steps));
16384
+ } else if (step.kind === "map") ; else if (step.kind === "pipeline") {
16385
+ for (const stage of step.stages) {
16386
+ }
16387
+ } else if (step.kind === "branch") {
16388
+ collected.push(...collectAgentSteps(step.then));
16389
+ if (step.otherwise) collected.push(...collectAgentSteps(step.otherwise));
16390
+ } else if (step.kind === "loop") {
16391
+ collected.push(...collectAgentSteps(step.body));
16392
+ } else if (step.kind === "subworkflow") {
16393
+ collected.push(...collectAgentSteps(step.workflow.steps));
16394
+ }
16395
+ }
16396
+ return collected;
16397
+ }
16398
+ function runtimeWorkflowToStages(workflow) {
16399
+ const agents = collectAgentSteps(workflow.steps);
16400
+ if (agents.length === 0) {
16401
+ return [{ steps: [{ label: "workflow" }] }];
16402
+ }
16403
+ return [
16404
+ {
16405
+ steps: agents.map((a) => ({
16406
+ label: a.id,
16407
+ ...a.adapter !== void 0 ? { adapter: a.adapter } : {},
16408
+ ...a.sessionRef !== void 0 ? { sessionRef: a.sessionRef } : {}
16409
+ }))
16410
+ }
16411
+ ];
16412
+ }
14759
16413
  var DEFAULT_PERSIST_PATH2 = () => join(homedir(), ".agentproto", "workflow-runs.json");
14760
16414
  function loadRuns2(persistPath) {
14761
16415
  const result = /* @__PURE__ */ new Map();
@@ -14813,13 +16467,10 @@ function fireNotifyUrl(run) {
14813
16467
  }).catch(() => void 0);
14814
16468
  }
14815
16469
  function resolveStepSessionId(step, agents) {
14816
- if (step.adapter) {
14817
- return agents.resolveByLabel(step.label);
14818
- }
14819
16470
  if (step.sessionRef) {
14820
16471
  return agents.resolveByLabel(step.sessionRef);
14821
16472
  }
14822
- return void 0;
16473
+ return agents.resolveByLabel(step.label);
14823
16474
  }
14824
16475
  function fillStepStates(stages, defs, agents) {
14825
16476
  const sessionIds = [];
@@ -14879,6 +16530,8 @@ async function executeRunWorkflow(state, runtimeWf, agents, signal, cache, cache
14879
16530
  }
14880
16531
  }
14881
16532
  }
16533
+ const sessionIds = fillStepStates(state.run.stages, state.stages, agents);
16534
+ if (sessionIds.length > 0) state.run.result = { sessionIds };
14882
16535
  }
14883
16536
  }
14884
16537
  fireNotifyUrl(state.run);
@@ -14930,7 +16583,8 @@ function createWorkflowRunner(opts) {
14930
16583
  {
14931
16584
  workspaceSlug: input.workspaceSlug,
14932
16585
  cwd: input.cwd,
14933
- notifyUrl: input.notifyUrl
16586
+ notifyUrl: input.notifyUrl,
16587
+ ...opts.resolveSandboxProvider ? { resolveSandboxProvider: opts.resolveSandboxProvider } : {}
14934
16588
  }
14935
16589
  );
14936
16590
  const cache = input.cacheKey ? createFileStepCache(input.cacheKey) : void 0;
@@ -14947,26 +16601,30 @@ function createWorkflowRunner(opts) {
14947
16601
  }
14948
16602
  const handle = await loadWorkflowHandle(args.path);
14949
16603
  const workflow = await compileWorkflow2(handle);
16604
+ const fileStages = runtimeWorkflowToStages(workflow);
14950
16605
  const runId = `wfrun_${randomUUID()}`;
14951
16606
  const run = {
14952
16607
  runId,
14953
16608
  workflowId: handle.id,
14954
16609
  status: "running",
14955
16610
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
14956
- stages: [
14957
- {
14958
- index: 0,
14959
- status: "pending",
14960
- steps: [{ index: 0, label: "workflow", status: "pending" }]
14961
- }
14962
- ]
16611
+ stages: fileStages.map((stage, si) => ({
16612
+ index: si,
16613
+ ...stage.label !== void 0 ? { label: stage.label } : {},
16614
+ status: "pending",
16615
+ steps: stage.steps.map((s, i) => ({
16616
+ index: i,
16617
+ label: s.label,
16618
+ status: "pending"
16619
+ }))
16620
+ }))
14963
16621
  };
14964
16622
  const abort = new AbortController();
14965
16623
  const state = {
14966
16624
  run,
14967
16625
  cancelled: false,
14968
16626
  abort,
14969
- stages: [],
16627
+ stages: fileStages,
14970
16628
  ...args.cwd !== void 0 ? { cwd: args.cwd } : {},
14971
16629
  ...args.workspaceSlug !== void 0 ? { workspaceSlug: args.workspaceSlug } : {}
14972
16630
  };
@@ -14978,7 +16636,8 @@ function createWorkflowRunner(opts) {
14978
16636
  resolveAgentAdapter,
14979
16637
  {
14980
16638
  workspaceSlug: args.workspaceSlug,
14981
- cwd: args.cwd
16639
+ cwd: args.cwd,
16640
+ ...opts.resolveSandboxProvider ? { resolveSandboxProvider: opts.resolveSandboxProvider } : {}
14982
16641
  }
14983
16642
  );
14984
16643
  const cache = args.cacheKey ? createFileStepCache(args.cacheKey) : void 0;
@@ -15743,6 +17402,216 @@ function createOrchestratorInjector(deps2) {
15743
17402
  return { entry, scope, bindLifecycle };
15744
17403
  };
15745
17404
  }
17405
+ var WIDENING_ROUTES = ["openrouter", "requesty", "huggingface"];
17406
+ function normalizeRouterPrefixedId(id) {
17407
+ const firstSlash = id.indexOf("/");
17408
+ if (firstSlash === -1) return id;
17409
+ const head = id.slice(0, firstSlash);
17410
+ if (!WIDENING_ROUTES.includes(head)) return id;
17411
+ const remainder = id.slice(firstSlash + 1);
17412
+ if (!remainder.includes("/") || remainder.includes("@")) return id;
17413
+ return `${remainder}@${head}`;
17414
+ }
17415
+ function tryResolveLlmModelRoute(id) {
17416
+ try {
17417
+ return resolveLlmModelRoute(id);
17418
+ } catch {
17419
+ return void 0;
17420
+ }
17421
+ }
17422
+ function vendorFromIdPrefix(bareId) {
17423
+ if (/^claude[-/]/.test(bareId)) return "anthropic";
17424
+ if (/^(gpt[-/]|o[1-9](-|$)|chatgpt)/.test(bareId)) return "openai";
17425
+ if (/^gemini[-/]/.test(bareId)) return "google";
17426
+ if (/^grok[-/]/.test(bareId)) return "x-ai";
17427
+ if (/^deepseek[-/]/.test(bareId)) return "deepseek";
17428
+ return void 0;
17429
+ }
17430
+ function resolveModelId(id) {
17431
+ const normalized = normalizeRouterPrefixedId(id);
17432
+ const resolved = tryResolveLlmModelRoute(normalized);
17433
+ if (resolved) {
17434
+ return {
17435
+ vendor: resolved.vendor,
17436
+ product: resolved.product,
17437
+ directRoute: resolved.route,
17438
+ ref: formatModelRef(resolved.ref),
17439
+ baseUrl: resolved.transport.baseUrl ?? null,
17440
+ pricing: {
17441
+ inPer1M: resolved.pricing.inputPer1M,
17442
+ outPer1M: resolved.pricing.outputPer1M
17443
+ }
17444
+ };
17445
+ }
17446
+ const parsed = tryParseModelRef(normalized);
17447
+ if (parsed) {
17448
+ return {
17449
+ vendor: parsed.vendor,
17450
+ product: parsed.product,
17451
+ directRoute: parsed.route,
17452
+ ref: formatModelRef(parsed),
17453
+ baseUrl: null,
17454
+ pricing: null
17455
+ };
17456
+ }
17457
+ const vendor = vendorFromIdPrefix(id) ?? "unknown";
17458
+ return {
17459
+ vendor,
17460
+ product: id,
17461
+ directRoute: vendor,
17462
+ ref: `${vendor}/${id}`,
17463
+ baseUrl: null,
17464
+ pricing: null
17465
+ };
17466
+ }
17467
+ function methodsForDirect(descriptor) {
17468
+ const methods = [];
17469
+ if (descriptor?.authSubscription) methods.push("oauth-bearer");
17470
+ if (descriptor?.provider) methods.push("api-key");
17471
+ return methods;
17472
+ }
17473
+ function isDirectRoute(mode, resolved) {
17474
+ return mode === void 0 && resolved.directRoute === resolved.vendor;
17475
+ }
17476
+ function curatedContributions(adapters) {
17477
+ const out = [];
17478
+ for (const adapter of adapters) {
17479
+ for (const model of adapter.models) {
17480
+ const resolved = resolveModelId(model.id);
17481
+ const route = model.mode ?? resolved.directRoute;
17482
+ const methods = isDirectRoute(model.mode, resolved) ? methodsForDirect(adapter.authDescriptor) : ["api-key"];
17483
+ out.push({
17484
+ vendor: resolved.vendor,
17485
+ product: resolved.product,
17486
+ route,
17487
+ ref: resolved.ref,
17488
+ baseUrl: resolved.baseUrl,
17489
+ pricing: resolved.pricing,
17490
+ curated: true,
17491
+ adapterSlug: adapter.slug,
17492
+ ...model.mode ? { adapterMode: model.mode } : {},
17493
+ methods
17494
+ });
17495
+ }
17496
+ }
17497
+ return out;
17498
+ }
17499
+ function widenedContributions(curated) {
17500
+ const seenProducts = /* @__PURE__ */ new Map();
17501
+ for (const c of curated) {
17502
+ const key = `${c.vendor}/${c.product}`;
17503
+ const routes = seenProducts.get(key) ?? /* @__PURE__ */ new Set();
17504
+ routes.add(c.route);
17505
+ seenProducts.set(key, routes);
17506
+ }
17507
+ const out = [];
17508
+ for (const [key, existingRoutes] of seenProducts) {
17509
+ const [vendor, product] = key.split("/", 2);
17510
+ for (const router of WIDENING_ROUTES) {
17511
+ if (existingRoutes.has(router)) continue;
17512
+ const resolved = resolveLlmModelRoute(`${vendor}/${product}@${router}`);
17513
+ if (!resolved) continue;
17514
+ out.push({
17515
+ vendor,
17516
+ product,
17517
+ route: router,
17518
+ ref: formatModelRef(resolved.ref),
17519
+ baseUrl: resolved.transport.baseUrl ?? null,
17520
+ pricing: {
17521
+ inPer1M: resolved.pricing.inputPer1M,
17522
+ outPer1M: resolved.pricing.outputPer1M
17523
+ },
17524
+ curated: false,
17525
+ methods: ["api-key"]
17526
+ });
17527
+ }
17528
+ }
17529
+ return out;
17530
+ }
17531
+ function mergeContributions(contributions) {
17532
+ const rows = /* @__PURE__ */ new Map();
17533
+ for (const c of contributions) {
17534
+ const key = `${c.vendor}\0${c.product}\0${c.route}`;
17535
+ const existing = rows.get(key);
17536
+ if (!existing) {
17537
+ rows.set(key, {
17538
+ vendor: c.vendor,
17539
+ product: c.product,
17540
+ route: c.route,
17541
+ ref: c.ref,
17542
+ baseUrl: c.baseUrl,
17543
+ pricing: c.pricing,
17544
+ curated: c.curated,
17545
+ adapters: c.adapterSlug ? [c.adapterSlug] : [],
17546
+ adapterModes: c.adapterMode ? [c.adapterMode] : [],
17547
+ methods: [...c.methods]
17548
+ });
17549
+ continue;
17550
+ }
17551
+ existing.curated = existing.curated || c.curated;
17552
+ existing.baseUrl = existing.baseUrl ?? c.baseUrl;
17553
+ existing.pricing = existing.pricing ?? c.pricing;
17554
+ if (c.adapterSlug && !existing.adapters.includes(c.adapterSlug)) {
17555
+ existing.adapters.push(c.adapterSlug);
17556
+ }
17557
+ if (c.adapterMode && !existing.adapterModes.includes(c.adapterMode)) {
17558
+ existing.adapterModes.push(c.adapterMode);
17559
+ }
17560
+ for (const m of c.methods) {
17561
+ if (!existing.methods.includes(m)) existing.methods.push(m);
17562
+ }
17563
+ }
17564
+ return [...rows.values()];
17565
+ }
17566
+ function billedVendor(vendor, route) {
17567
+ return route === vendor ? vendor : route;
17568
+ }
17569
+ function buildCatalogModels(input) {
17570
+ const contributions = [
17571
+ ...curatedContributions(input.adapters),
17572
+ ...widenedContributions(curatedContributions(input.adapters))
17573
+ ];
17574
+ const merged = mergeContributions(contributions);
17575
+ const query = input.query ?? {};
17576
+ const vendors = /* @__PURE__ */ new Map();
17577
+ for (const row of merged) {
17578
+ if (query.vendor && row.vendor !== query.vendor) continue;
17579
+ if (query.route && row.route !== query.route) continue;
17580
+ if (query.adapter && !row.adapters.includes(query.adapter)) continue;
17581
+ const manifest = {
17582
+ id: `${row.vendor}/${row.product}@${row.route}`,
17583
+ vendorByRoute: { [row.route]: billedVendor(row.vendor, row.route) },
17584
+ methodsByRoute: { [row.route]: row.methods }
17585
+ };
17586
+ const eligible = eligibleProfiles(input.profiles, manifest, row.route);
17587
+ const runnable = eligible.length > 0;
17588
+ if (query.runnableOnly && !runnable) continue;
17589
+ const route = {
17590
+ route: row.route,
17591
+ ref: row.ref,
17592
+ baseUrl: row.baseUrl,
17593
+ pricing: row.pricing,
17594
+ runnable,
17595
+ eligibleProfiles: eligible.map((p) => p.id),
17596
+ adapterModes: row.adapterModes,
17597
+ adapters: row.adapters,
17598
+ curated: row.curated
17599
+ };
17600
+ const products = vendors.get(row.vendor) ?? /* @__PURE__ */ new Map();
17601
+ vendors.set(row.vendor, products);
17602
+ const routes = products.get(row.product) ?? [];
17603
+ products.set(row.product, routes);
17604
+ routes.push(route);
17605
+ }
17606
+ const result = [...vendors.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([vendor, products]) => ({
17607
+ vendor,
17608
+ products: [...products.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([product, routes]) => ({
17609
+ product,
17610
+ routes: [...routes].sort((a, b) => a.route.localeCompare(b.route))
17611
+ }))
17612
+ }));
17613
+ return { vendors: result };
17614
+ }
15746
17615
  function makeBrowserHandle(entry, resolve14) {
15747
17616
  return {
15748
17617
  slug: entry.id,
@@ -17642,7 +19511,7 @@ var WorkspacePathError = class extends Error {
17642
19511
  };
17643
19512
  function createWorkspaceFs(opts) {
17644
19513
  const root = resolve(opts.workspace);
17645
- function resolvePath4(path) {
19514
+ function resolvePath5(path) {
17646
19515
  if (typeof path !== "string" || path.length === 0) {
17647
19516
  throw new WorkspacePathError("path must be a non-empty string");
17648
19517
  }
@@ -17662,18 +19531,18 @@ function createWorkspaceFs(opts) {
17662
19531
  }
17663
19532
  return {
17664
19533
  async readFile(path) {
17665
- const abs = resolvePath4(path);
19534
+ const abs = resolvePath5(path);
17666
19535
  const buf = await readFile(abs);
17667
19536
  return buf.toString("utf8");
17668
19537
  },
17669
19538
  async writeFile(path, content) {
17670
- const abs = resolvePath4(path);
19539
+ const abs = resolvePath5(path);
17671
19540
  await mkdir(dirname(abs), { recursive: true });
17672
19541
  await writeFile(abs, content);
17673
19542
  },
17674
19543
  async exists(path) {
17675
19544
  try {
17676
- const abs = resolvePath4(path);
19545
+ const abs = resolvePath5(path);
17677
19546
  return existsSync(abs);
17678
19547
  } catch {
17679
19548
  return false;
@@ -18031,6 +19900,39 @@ function isEnoent(err) {
18031
19900
  function errMsg(err) {
18032
19901
  return err instanceof Error ? err.message : String(err);
18033
19902
  }
19903
+ var POSTURE_MODE_VALUES = {
19904
+ default: "default",
19905
+ plan: "plan",
19906
+ "accept-edits": "accept-edits",
19907
+ "bypass-permissions": "bypass",
19908
+ "read-only": "read-only",
19909
+ "full-access": "bypass",
19910
+ build: "default"
19911
+ };
19912
+ function decomposeMode(modes, modeId) {
19913
+ const declared = modes.find((mode) => mode.id === modeId);
19914
+ const kind = declared?.kind ?? inferLegacyModeKind(modeId);
19915
+ if (kind === "route") return { route: { gateway: modeId } };
19916
+ if (kind === "posture") return { posture: POSTURE_MODE_VALUES[modeId] ?? "default" };
19917
+ return { contextProfile: modeId };
19918
+ }
19919
+ function decomposedAxisMatches(cfg, decomposed) {
19920
+ if (decomposed.route) return cfg.route?.gateway === decomposed.route.gateway;
19921
+ if (decomposed.posture !== void 0) return cfg.posture === decomposed.posture;
19922
+ if (decomposed.contextProfile !== void 0) {
19923
+ return cfg.contextProfile === decomposed.contextProfile;
19924
+ }
19925
+ return false;
19926
+ }
19927
+ function composeMode(cfg, modes) {
19928
+ for (const mode of modes) {
19929
+ if (decomposedAxisMatches(cfg, decomposeMode(modes, mode.id))) return mode.id;
19930
+ }
19931
+ return void 0;
19932
+ }
19933
+
19934
+ // src/index.ts
19935
+ init_conversation_store();
18034
19936
  async function isAgentCliAuthConfigured(slug, descriptor, model) {
18035
19937
  const config = await loadConfig();
18036
19938
  const spawnDefaults = resolveSpawnDefaults(config.defaults, slug, {});
@@ -18203,6 +20105,10 @@ async function createGateway(opts) {
18203
20105
  sessionEvents,
18204
20106
  resolveAgentAdapter: opts.resolveAgentAdapter,
18205
20107
  persist,
20108
+ // Sandbox-capable agent steps (`AgentStep.sandbox` / workflow_start's
20109
+ // step `sandbox`) resolve providers through the same resolver
20110
+ // `agent_start.sandbox` uses.
20111
+ resolveSandboxProvider: resolveSandboxProviderResolved,
18206
20112
  // Compile a loaded WORKFLOW.md handle into a runnable RuntimeWorkflow
18207
20113
  // for `workflow_run_file` / `startFromFile`. The daemon's workflow
18208
20114
  // surface is agent-step based (like the stage primitive), so no tool/
@@ -18266,7 +20172,8 @@ async function createGateway(opts) {
18266
20172
  resolveSandboxProvider: resolveSandboxProviderResolved,
18267
20173
  ...opts.provisionWorktree ? { provisionWorktree: opts.provisionWorktree } : {},
18268
20174
  ...opts.resolveAgentAdapter ? { resolveAgentAdapter: opts.resolveAgentAdapter } : {},
18269
- ...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {}
20175
+ ...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {},
20176
+ ...opts.listCatalogModels ? { listCatalogModels: opts.listCatalogModels } : {}
18270
20177
  });
18271
20178
  registerBrowserTools(server, {
18272
20179
  registry: sessions,
@@ -18422,6 +20329,7 @@ async function createGateway(opts) {
18422
20329
  daemonMcpUrl,
18423
20330
  ...opts.provisionWorktree ? { provisionWorktree: opts.provisionWorktree } : {},
18424
20331
  ...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {},
20332
+ ...opts.listCatalogModels ? { listCatalogModels: opts.listCatalogModels } : {},
18425
20333
  ...opts.resolveBrowserAdapter ? { resolveBrowserAdapter: opts.resolveBrowserAdapter } : {},
18426
20334
  ...opts.listBrowserAdapters ? { listBrowserAdapters: opts.listBrowserAdapters } : {},
18427
20335
  meta: { workspace, registered, startedAt },
@@ -18508,6 +20416,6 @@ var export_providersPath = providers_store_exports.providersPath;
18508
20416
  var export_removeProviderKey = providers_store_exports.removeProviderKey;
18509
20417
  var export_setProviderKey = providers_store_exports.setProviderKey;
18510
20418
 
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 };
20419
+ 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
20420
  //# sourceMappingURL=index.mjs.map
18513
20421
  //# sourceMappingURL=index.mjs.map