@agentproto/runtime 0.7.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/config-BRKy_SAF.d.ts +569 -0
- package/dist/config.d.ts +1 -272
- package/dist/config.mjs.map +1 -1
- package/dist/index.d.ts +1173 -79
- package/dist/index.mjs +2176 -320
- package/dist/index.mjs.map +1 -1
- package/dist/resume-strategies.mjs +42 -13
- package/dist/resume-strategies.mjs.map +1 -1
- package/package.json +14 -12
- package/dist/spawn-defaults-DAbADRd4.d.ts +0 -256
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createReadStream, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, renameSync,
|
|
1
|
+
import { createReadStream, existsSync, promises, readdirSync, readFileSync, mkdirSync, writeFileSync, renameSync, 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
|
|
705
|
+
const encoded = claudeProjectSlug(cwd);
|
|
553
706
|
const filePath = join(homedir(), ".claude", "projects", encoded, `${adapterSessionId}.jsonl`);
|
|
554
707
|
let stream;
|
|
555
708
|
try {
|
|
@@ -1057,6 +1210,7 @@ var init_transcript_export = __esm({
|
|
|
1057
1210
|
"src/transcript-export.ts"() {
|
|
1058
1211
|
init_tool_presenter();
|
|
1059
1212
|
init_transcript_writer();
|
|
1213
|
+
init_conversation_store();
|
|
1060
1214
|
ROLE_ICON = {
|
|
1061
1215
|
user: "\u{1F9D1} User",
|
|
1062
1216
|
assistant: "\u{1F916} Assistant",
|
|
@@ -1743,6 +1897,61 @@ function loadWorkspacesConfigSync(path = DEFAULT_CONFIG_PATH()) {
|
|
|
1743
1897
|
}
|
|
1744
1898
|
return normalizeConfig(parsed);
|
|
1745
1899
|
}
|
|
1900
|
+
async function saveWorkspacesConfig(config, path = DEFAULT_CONFIG_PATH()) {
|
|
1901
|
+
const normalized = normalizeConfig(config);
|
|
1902
|
+
await promises.mkdir(dirname(path), { recursive: true });
|
|
1903
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
1904
|
+
await promises.writeFile(tmp, JSON.stringify(normalized, null, 2) + "\n", "utf8");
|
|
1905
|
+
await promises.rename(tmp, path);
|
|
1906
|
+
}
|
|
1907
|
+
function addWorkspace(config, input) {
|
|
1908
|
+
if (!isAbsolute(input.path)) {
|
|
1909
|
+
throw new Error(
|
|
1910
|
+
`addWorkspace: path must be absolute, got "${input.path}".`
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
const slug = sanitizeSlug(input.slug);
|
|
1914
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1915
|
+
const existingIdx = config.workspaces.findIndex((w) => w.slug === slug);
|
|
1916
|
+
const next = existingIdx >= 0 ? {
|
|
1917
|
+
...config.workspaces[existingIdx],
|
|
1918
|
+
path: input.path,
|
|
1919
|
+
...input.label !== void 0 ? { label: input.label } : {},
|
|
1920
|
+
updatedAt: now
|
|
1921
|
+
} : {
|
|
1922
|
+
slug,
|
|
1923
|
+
path: input.path,
|
|
1924
|
+
...input.label ? { label: input.label } : {},
|
|
1925
|
+
addedAt: now,
|
|
1926
|
+
updatedAt: now
|
|
1927
|
+
};
|
|
1928
|
+
const workspaces = [...config.workspaces];
|
|
1929
|
+
if (existingIdx >= 0) workspaces[existingIdx] = next;
|
|
1930
|
+
else workspaces.push(next);
|
|
1931
|
+
const active = config.active ?? slug;
|
|
1932
|
+
return { ...config, workspaces, active };
|
|
1933
|
+
}
|
|
1934
|
+
function removeWorkspace(config, slug) {
|
|
1935
|
+
const sanitised = sanitizeSlug(slug);
|
|
1936
|
+
const workspaces = config.workspaces.filter((w) => w.slug !== sanitised);
|
|
1937
|
+
let active = config.active;
|
|
1938
|
+
if (active === sanitised) {
|
|
1939
|
+
active = workspaces[0]?.slug;
|
|
1940
|
+
}
|
|
1941
|
+
const next = { ...config, workspaces };
|
|
1942
|
+
if (active !== void 0) next.active = active;
|
|
1943
|
+
else delete next.active;
|
|
1944
|
+
return next;
|
|
1945
|
+
}
|
|
1946
|
+
function setActiveWorkspace(config, slug) {
|
|
1947
|
+
const sanitised = sanitizeSlug(slug);
|
|
1948
|
+
if (!config.workspaces.some((w) => w.slug === sanitised)) {
|
|
1949
|
+
throw new Error(
|
|
1950
|
+
`setActiveWorkspace: no workspace registered with slug "${sanitised}". Run \`agentproto workspace add <path> --slug ${sanitised}\` first.`
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
return { ...config, active: sanitised };
|
|
1954
|
+
}
|
|
1746
1955
|
function findWorkspace(config, slug) {
|
|
1747
1956
|
return config.workspaces.find((w) => w.slug === sanitizeSlug(slug));
|
|
1748
1957
|
}
|
|
@@ -1809,7 +2018,7 @@ function sanitizeAcpAgents(raw, target) {
|
|
|
1809
2018
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
1810
2019
|
}
|
|
1811
2020
|
async function loadConfig(path) {
|
|
1812
|
-
const target = CONFIG_FILE_PATH();
|
|
2021
|
+
const target = path ?? CONFIG_FILE_PATH();
|
|
1813
2022
|
try {
|
|
1814
2023
|
const raw = await promises.readFile(target, "utf8");
|
|
1815
2024
|
const parsed = JSON.parse(raw);
|
|
@@ -1900,6 +2109,7 @@ function resolveAuthSpec(input) {
|
|
|
1900
2109
|
const subCredAvailable = input.subscriptionCredential !== void 0;
|
|
1901
2110
|
const apiCredAvailable = input.apiKeyConfigCredential !== void 0 || input.apiKeyStoreCredential !== void 0;
|
|
1902
2111
|
let mode;
|
|
2112
|
+
let neitherConfigured = false;
|
|
1903
2113
|
if (input.requestedMode) {
|
|
1904
2114
|
if (input.requestedMode === "subscription" && !supportsSub) {
|
|
1905
2115
|
throw new AuthResolutionError(
|
|
@@ -1909,7 +2119,11 @@ function resolveAuthSpec(input) {
|
|
|
1909
2119
|
mode = input.requestedMode;
|
|
1910
2120
|
} else {
|
|
1911
2121
|
const preference = supportsSub ? ["subscription", "api-key"] : ["api-key"];
|
|
1912
|
-
|
|
2122
|
+
const available = preference.find(
|
|
2123
|
+
(m) => m === "subscription" ? subCredAvailable : apiCredAvailable
|
|
2124
|
+
);
|
|
2125
|
+
mode = available ?? preference[0];
|
|
2126
|
+
neitherConfigured = available === void 0;
|
|
1913
2127
|
}
|
|
1914
2128
|
let setEnv;
|
|
1915
2129
|
let credential;
|
|
@@ -1945,7 +2159,8 @@ function resolveAuthSpec(input) {
|
|
|
1945
2159
|
setEnv,
|
|
1946
2160
|
unsetEnv,
|
|
1947
2161
|
explicit: input.explicit,
|
|
1948
|
-
enforce
|
|
2162
|
+
enforce,
|
|
2163
|
+
...neitherConfigured ? { neitherConfigured } : {}
|
|
1949
2164
|
};
|
|
1950
2165
|
const echo = {
|
|
1951
2166
|
provider,
|
|
@@ -2209,6 +2424,8 @@ function getMcpCredentialDeps() {
|
|
|
2209
2424
|
// src/sandbox-agent-session-proxy.ts
|
|
2210
2425
|
var MAX_POLL_MS = 49e3;
|
|
2211
2426
|
var MAX_OUTPUT_LINES = 500;
|
|
2427
|
+
var MAX_CONSECUTIVE_POLL_FAILURES = 6;
|
|
2428
|
+
var POLL_RETRY_DELAY_MS = 5e3;
|
|
2212
2429
|
function extractPromptText(message) {
|
|
2213
2430
|
if (typeof message === "string") return message;
|
|
2214
2431
|
if (message && typeof message === "object" && "text" in message) {
|
|
@@ -2226,29 +2443,63 @@ function createSandboxAgentSessionProxy(opts) {
|
|
|
2226
2443
|
async *send(message) {
|
|
2227
2444
|
const prompt = extractPromptText(message);
|
|
2228
2445
|
lastPrompt = prompt;
|
|
2229
|
-
await host.prompt(remoteSessionId, prompt);
|
|
2230
2446
|
let seenLength = 0;
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2447
|
+
try {
|
|
2448
|
+
await host.prompt(remoteSessionId, prompt);
|
|
2449
|
+
let consecutivePollFailures = 0;
|
|
2450
|
+
for (; ; ) {
|
|
2451
|
+
let result;
|
|
2452
|
+
try {
|
|
2453
|
+
result = await host.waitForAny([remoteSessionId], {
|
|
2454
|
+
event: "any",
|
|
2455
|
+
timeoutMs: MAX_POLL_MS
|
|
2456
|
+
});
|
|
2457
|
+
consecutivePollFailures = 0;
|
|
2458
|
+
} catch (pollErr) {
|
|
2459
|
+
consecutivePollFailures++;
|
|
2460
|
+
if (consecutivePollFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
|
|
2461
|
+
throw new Error(
|
|
2462
|
+
`sandbox proxy: ${consecutivePollFailures} consecutive poll failures against the box daemon (session "${remoteSessionId}") \u2014 giving up. Last error: ${pollErr instanceof Error ? pollErr.message : String(pollErr)}`
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
await new Promise((resolve14) => setTimeout(resolve14, POLL_RETRY_DELAY_MS));
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
if (result.timedOut) continue;
|
|
2469
|
+
let tail;
|
|
2470
|
+
try {
|
|
2471
|
+
tail = await host.output(remoteSessionId, MAX_OUTPUT_LINES);
|
|
2472
|
+
} catch {
|
|
2473
|
+
try {
|
|
2474
|
+
tail = await host.output(remoteSessionId, MAX_OUTPUT_LINES);
|
|
2475
|
+
} catch {
|
|
2476
|
+
tail = void 0;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
if (tail !== void 0 && tail.length > seenLength) {
|
|
2480
|
+
yield { kind: "text-delta", text: tail.slice(seenLength) };
|
|
2481
|
+
}
|
|
2482
|
+
if (tail !== void 0) seenLength = tail.length;
|
|
2483
|
+
if (result.event === "exited") {
|
|
2484
|
+
throw new Error(
|
|
2485
|
+
`sandbox proxy: remote session "${remoteSessionId}" exited \u2014 the box's own agentproto daemon ended this session (crash, OOM, or an out-of-band kill).`
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2488
|
+
yield {
|
|
2489
|
+
kind: "turn-end",
|
|
2490
|
+
reason: result.event === "awaiting-input" ? "awaiting-input" : "completed"
|
|
2491
|
+
};
|
|
2492
|
+
return;
|
|
2240
2493
|
}
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2494
|
+
} catch (err) {
|
|
2495
|
+
try {
|
|
2496
|
+
const tail = await host.output(remoteSessionId, MAX_OUTPUT_LINES);
|
|
2497
|
+
if (tail.length > seenLength) {
|
|
2498
|
+
yield { kind: "text-delta", text: tail.slice(seenLength) };
|
|
2499
|
+
}
|
|
2500
|
+
} catch {
|
|
2246
2501
|
}
|
|
2247
|
-
|
|
2248
|
-
kind: "turn-end",
|
|
2249
|
-
reason: result.event === "awaiting-input" ? "awaiting-input" : "completed"
|
|
2250
|
-
};
|
|
2251
|
-
return;
|
|
2502
|
+
throw err;
|
|
2252
2503
|
}
|
|
2253
2504
|
},
|
|
2254
2505
|
async cancel() {
|
|
@@ -2271,6 +2522,50 @@ function createSandboxAgentSessionProxy(opts) {
|
|
|
2271
2522
|
};
|
|
2272
2523
|
}
|
|
2273
2524
|
|
|
2525
|
+
// src/worktree-isolation.ts
|
|
2526
|
+
var WORKTREE_ISOLATION_ENV = "AGENTPROTO_WORKTREES_ISOLATION";
|
|
2527
|
+
var DEFAULT_WORKTREE_ISOLATION = "on-request";
|
|
2528
|
+
function normalizeWorktreeField(field) {
|
|
2529
|
+
if (field === void 0 || field === false) return void 0;
|
|
2530
|
+
if (field === true) return {};
|
|
2531
|
+
const request = {};
|
|
2532
|
+
if (field.slug !== void 0) request.slug = field.slug;
|
|
2533
|
+
if (field.base !== void 0) request.base = field.base;
|
|
2534
|
+
return request;
|
|
2535
|
+
}
|
|
2536
|
+
function decideWorktreeIsolation(input) {
|
|
2537
|
+
const request = normalizeWorktreeField(input.field);
|
|
2538
|
+
if (input.depth > 0) return { action: "spawn-in-place" };
|
|
2539
|
+
switch (input.mode) {
|
|
2540
|
+
case "never":
|
|
2541
|
+
if (request !== void 0) {
|
|
2542
|
+
return {
|
|
2543
|
+
action: "reject",
|
|
2544
|
+
message: `agent_start: \`worktree\` was requested but this daemon's \`worktrees.isolation\` policy is set to "never". Remove the \`worktree\` field, or change the policy (config \`worktrees.isolation\` / env ${WORKTREE_ISOLATION_ENV}).`
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
return { action: "spawn-in-place" };
|
|
2548
|
+
case "on-request":
|
|
2549
|
+
return request !== void 0 ? { action: "provision", request } : { action: "spawn-in-place" };
|
|
2550
|
+
case "always":
|
|
2551
|
+
return { action: "provision", request: request ?? {} };
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
function parseWorktreeIsolationMode(raw) {
|
|
2555
|
+
return raw === "always" || raw === "on-request" || raw === "never" ? raw : void 0;
|
|
2556
|
+
}
|
|
2557
|
+
async function loadWorktreeIsolation(loadCfg = loadConfig) {
|
|
2558
|
+
const fromEnv = parseWorktreeIsolationMode(process.env[WORKTREE_ISOLATION_ENV]);
|
|
2559
|
+
if (fromEnv) return fromEnv;
|
|
2560
|
+
try {
|
|
2561
|
+
const cfg = await loadCfg();
|
|
2562
|
+
const fromCfg = parseWorktreeIsolationMode(cfg.worktrees?.isolation);
|
|
2563
|
+
if (fromCfg) return fromCfg;
|
|
2564
|
+
} catch {
|
|
2565
|
+
}
|
|
2566
|
+
return DEFAULT_WORKTREE_ISOLATION;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2274
2569
|
// src/session-spawn.ts
|
|
2275
2570
|
var SPAWN_CLAIM_WINDOW_MS = 3e4;
|
|
2276
2571
|
var spawnClaimsByRegistry = /* @__PURE__ */ new WeakMap();
|
|
@@ -2298,8 +2593,20 @@ async function spawnAgentSession(deps2, input) {
|
|
|
2298
2593
|
callerScope,
|
|
2299
2594
|
webhookNotifier,
|
|
2300
2595
|
loadDefaultsConfig,
|
|
2301
|
-
resolveSandboxProvider: resolveSandboxProvider2
|
|
2596
|
+
resolveSandboxProvider: resolveSandboxProvider2,
|
|
2597
|
+
provisionWorktree,
|
|
2598
|
+
resolveWorktreeIsolation
|
|
2302
2599
|
} = deps2;
|
|
2600
|
+
const explicitCwd = input.cwd !== void 0;
|
|
2601
|
+
const explicitWorkspaceSlug = input.workspaceSlug !== void 0;
|
|
2602
|
+
const childDepth = callerScope ? callerScope.depth + 1 : 0;
|
|
2603
|
+
if (childDepth === 0 && !explicitCwd && !explicitWorkspaceSlug && normalizeWorktreeField(input.worktree) !== void 0) {
|
|
2604
|
+
return {
|
|
2605
|
+
ok: false,
|
|
2606
|
+
code: "worktree_requires_explicit_repo",
|
|
2607
|
+
message: "agent_start: `worktree` isolation was requested but neither `cwd` nor `workspaceSlug` was passed \u2014 refusing to guess the base repo from the daemon's active workspace. Pass `cwd` (an explicit path inside the repo to worktree from) or `workspaceSlug` (a slug from `agentproto workspace list`) to `agent_start`."
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2303
2610
|
let cwd = input.cwd;
|
|
2304
2611
|
let resolvedSlug = input.workspaceSlug;
|
|
2305
2612
|
if (!cwd || !resolvedSlug) {
|
|
@@ -2336,7 +2643,6 @@ async function spawnAgentSession(deps2, input) {
|
|
|
2336
2643
|
message: `agent_start: adapter "${input.adapter}" could not be resolved. If it was working a moment ago, something may be mid-rebuild \u2014 wait and retry. If it has never been installed, run \`agentproto install ${input.adapter}\` first.`
|
|
2337
2644
|
};
|
|
2338
2645
|
}
|
|
2339
|
-
const childDepth = callerScope ? callerScope.depth + 1 : 0;
|
|
2340
2646
|
const parentSessionId = callerScope?.ownerSessionId;
|
|
2341
2647
|
if (callerScope) {
|
|
2342
2648
|
if (childDepth > callerScope.maxDepth) {
|
|
@@ -2366,6 +2672,35 @@ async function spawnAgentSession(deps2, input) {
|
|
|
2366
2672
|
};
|
|
2367
2673
|
}
|
|
2368
2674
|
}
|
|
2675
|
+
let worktreeRequest;
|
|
2676
|
+
if (input.sandbox === void 0) {
|
|
2677
|
+
const mode = resolveWorktreeIsolation ? await resolveWorktreeIsolation() : await loadWorktreeIsolation();
|
|
2678
|
+
const decision = decideWorktreeIsolation({
|
|
2679
|
+
mode,
|
|
2680
|
+
field: input.worktree,
|
|
2681
|
+
depth: childDepth
|
|
2682
|
+
});
|
|
2683
|
+
if (decision.action === "reject") {
|
|
2684
|
+
return { ok: false, code: "worktree_disabled", message: decision.message };
|
|
2685
|
+
}
|
|
2686
|
+
if (decision.action === "provision") {
|
|
2687
|
+
if (!explicitCwd && !explicitWorkspaceSlug) {
|
|
2688
|
+
return {
|
|
2689
|
+
ok: false,
|
|
2690
|
+
code: "worktree_requires_explicit_repo",
|
|
2691
|
+
message: "agent_start: `worktree` isolation was requested but neither `cwd` nor `workspaceSlug` was passed \u2014 refusing to guess the base repo from the daemon's active workspace. Pass `cwd` (an explicit path inside the repo to worktree from) or `workspaceSlug` (a slug from `agentproto workspace list`) to `agent_start`."
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2694
|
+
if (!provisionWorktree) {
|
|
2695
|
+
return {
|
|
2696
|
+
ok: false,
|
|
2697
|
+
code: "worktree_provisioner_not_enabled",
|
|
2698
|
+
message: `agent_start: \`worktree\` isolation is required (by request or the "${mode}" policy) but this daemon has no worktree provisioner wired (createGateway needs \`provisionWorktree\`, injected by the CLI over @agentproto/worktree).`
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
worktreeRequest = decision.request;
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2369
2704
|
const configDefaults = loadDefaultsConfig ? await loadDefaultsConfig() : (await loadConfig()).defaults;
|
|
2370
2705
|
const roleRegistry = deps2.loadRoleRegistry ? await deps2.loadRoleRegistry() : await loadDefaultRoleRegistry();
|
|
2371
2706
|
let role;
|
|
@@ -2443,9 +2778,10 @@ async function spawnAgentSession(deps2, input) {
|
|
|
2443
2778
|
options: input.options,
|
|
2444
2779
|
auth: input.auth
|
|
2445
2780
|
});
|
|
2781
|
+
const hasGatewayBaseUrlOption = typeof spawnDefaults.options?.base_url === "string" && spawnDefaults.options.base_url.length > 0;
|
|
2446
2782
|
let authSpec;
|
|
2447
2783
|
let authEcho;
|
|
2448
|
-
if (resolved && input.sandbox === void 0 && resolved.authDescriptor) {
|
|
2784
|
+
if (resolved && input.sandbox === void 0 && resolved.authDescriptor && !hasGatewayBaseUrlOption) {
|
|
2449
2785
|
const authModel = input.model ?? resolved.defaultModel;
|
|
2450
2786
|
const pinnedProvider = spawnDefaults.auth.provider;
|
|
2451
2787
|
const resolvedProvider = pinnedProvider ?? resolved.authDescriptor.provider ?? (authModel ? getModelProvider(authModel) : void 0);
|
|
@@ -2476,6 +2812,12 @@ async function spawnAgentSession(deps2, input) {
|
|
|
2476
2812
|
}
|
|
2477
2813
|
throw err;
|
|
2478
2814
|
}
|
|
2815
|
+
if (authSpec && authSpec.enforce === "always" && authSpec.credential === void 0 && !spawnDefaults.auth.explicit && resolvedProvider !== void 0) {
|
|
2816
|
+
const ignored = await (0, providers_store_exports.getProviderKey)(resolvedProvider);
|
|
2817
|
+
if (ignored !== void 0) {
|
|
2818
|
+
authSpec = { ...authSpec, ignoredApiKeyInStore: true };
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2479
2821
|
}
|
|
2480
2822
|
const effectiveOptions = normalizeSkillsOption(
|
|
2481
2823
|
spawnDefaults.skills,
|
|
@@ -2520,6 +2862,24 @@ ${input.prompt}` : input.prompt;
|
|
|
2520
2862
|
return result;
|
|
2521
2863
|
};
|
|
2522
2864
|
try {
|
|
2865
|
+
if (worktreeRequest && provisionWorktree) {
|
|
2866
|
+
let outcome;
|
|
2867
|
+
try {
|
|
2868
|
+
outcome = await provisionWorktree({
|
|
2869
|
+
cwd,
|
|
2870
|
+
...worktreeRequest.slug ? { slug: worktreeRequest.slug } : {},
|
|
2871
|
+
...worktreeRequest.base ? { base: worktreeRequest.base } : {},
|
|
2872
|
+
...input.label ? { labelHint: input.label } : {}
|
|
2873
|
+
});
|
|
2874
|
+
} catch (err) {
|
|
2875
|
+
return finish({
|
|
2876
|
+
ok: false,
|
|
2877
|
+
code: "worktree_provision_failed",
|
|
2878
|
+
message: `agent_start: worktree provisioning failed \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
if (outcome.isolated) cwd = outcome.cwd;
|
|
2882
|
+
}
|
|
2523
2883
|
const resolvedMcpServers = await resolveMcpCredentialHeaders(mcpServers);
|
|
2524
2884
|
let liveSessionId;
|
|
2525
2885
|
let agentSession;
|
|
@@ -2544,7 +2904,14 @@ ${input.prompt}` : input.prompt;
|
|
|
2544
2904
|
...resolvedMcpServers ? { mcpServers: resolvedMcpServers } : {},
|
|
2545
2905
|
...input.model ? { model: input.model } : {},
|
|
2546
2906
|
...input.effort ? { effort: input.effort } : {},
|
|
2547
|
-
...input.label ? { label: input.label } : {}
|
|
2907
|
+
...input.label ? { label: input.label } : {},
|
|
2908
|
+
// Explicit billing-auth for the box's OWN agent_start. A fresh box
|
|
2909
|
+
// has no ~/.agentproto/config.json and claude-code never inherits
|
|
2910
|
+
// subscription auth from the shell env — the credential must ride the
|
|
2911
|
+
// spawn call itself. Only the caller's EXPLICIT `auth` is forwarded
|
|
2912
|
+
// (host config defaults stay host-scoped; the box resolves its own
|
|
2913
|
+
// defaults otherwise).
|
|
2914
|
+
...input.auth ? { auth: input.auth } : {}
|
|
2548
2915
|
});
|
|
2549
2916
|
if (!booted.ok) return booted;
|
|
2550
2917
|
agentSession = booted.agentSession;
|
|
@@ -2578,6 +2945,7 @@ ${input.prompt}` : input.prompt;
|
|
|
2578
2945
|
agentSession,
|
|
2579
2946
|
adapterSlug: input.adapter,
|
|
2580
2947
|
...input.model ? { model: input.model } : {},
|
|
2948
|
+
...input.mode ? { mode: input.mode } : {},
|
|
2581
2949
|
...input.wait && effectivePrompt ? {} : effectivePrompt ? { initialPrompt: effectivePrompt } : {},
|
|
2582
2950
|
...input.label ? { label: input.label } : {},
|
|
2583
2951
|
...resolvedMcpServers ? { mcpServers: resolvedMcpServers } : {},
|
|
@@ -2728,7 +3096,8 @@ async function bootSandboxAgentSession(opts) {
|
|
|
2728
3096
|
...opts.mcpServers ? { mcpServers: toMcpServerMounts(opts.mcpServers) } : {},
|
|
2729
3097
|
...opts.model ? { model: opts.model } : {},
|
|
2730
3098
|
...opts.effort ? { effort: opts.effort } : {},
|
|
2731
|
-
...opts.label ? { label: opts.label } : {}
|
|
3099
|
+
...opts.label ? { label: opts.label } : {},
|
|
3100
|
+
...opts.auth ? { auth: opts.auth } : {}
|
|
2732
3101
|
});
|
|
2733
3102
|
remoteSessionId = remoteDesc.id;
|
|
2734
3103
|
} catch (err) {
|
|
@@ -2759,6 +3128,60 @@ async function resolveSandboxSecret(slug) {
|
|
|
2759
3128
|
return null;
|
|
2760
3129
|
}
|
|
2761
3130
|
}
|
|
3131
|
+
|
|
3132
|
+
// src/canonical-posture.ts
|
|
3133
|
+
var POSTURE_PREAMBLES = {
|
|
3134
|
+
plan: "You are in PLAN mode. Investigate and propose a concrete plan for the user to approve; do NOT edit files, run commands, or make any other changes until the plan is explicitly approved. Reading and searching are fine.",
|
|
3135
|
+
"accept-edits": "You are in ACCEPT-EDITS mode. File edits are auto-approved, so apply them directly without pausing for confirmation on each one; commands and other actions still warrant the usual care.",
|
|
3136
|
+
bypass: "You are in BYPASS-PERMISSIONS mode. No approval prompts will interrupt you \u2014 every file edit and command runs without confirmation. Be deliberate and careful: there is no safety prompt between you and a destructive action.",
|
|
3137
|
+
"read-only": "You are in READ-ONLY mode. You may read, search, and analyze, but you must NOT edit files, run commands that mutate state, or make any other changes. Answer and advise only."
|
|
3138
|
+
};
|
|
3139
|
+
var POSTURE_NATIVE_ALIASES = {
|
|
3140
|
+
default: ["default", "build", "normal", "standard"],
|
|
3141
|
+
plan: ["plan", "planning", "plan-mode"],
|
|
3142
|
+
"accept-edits": ["accept-edits", "auto-accept", "auto-edit"],
|
|
3143
|
+
bypass: ["bypass", "bypass-permissions", "full-access", "yolo", "dangerously-skip-permissions"],
|
|
3144
|
+
"read-only": ["read-only", "chat", "ask"]
|
|
3145
|
+
};
|
|
3146
|
+
var CANONICAL_POSTURES = Object.keys(
|
|
3147
|
+
POSTURE_NATIVE_ALIASES
|
|
3148
|
+
);
|
|
3149
|
+
var CANONICAL_POSTURE_SET = new Set(CANONICAL_POSTURES);
|
|
3150
|
+
function isCanonicalPosture(value) {
|
|
3151
|
+
return CANONICAL_POSTURE_SET.has(value);
|
|
3152
|
+
}
|
|
3153
|
+
function parsePostureInput(raw) {
|
|
3154
|
+
return isCanonicalPosture(raw) ? raw : { harnessModeId: raw };
|
|
3155
|
+
}
|
|
3156
|
+
function normalizeModeId(id) {
|
|
3157
|
+
return id.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3158
|
+
}
|
|
3159
|
+
var CANONICAL_BY_NORMALIZED_ALIAS = (() => {
|
|
3160
|
+
const index = /* @__PURE__ */ new Map();
|
|
3161
|
+
for (const [posture, aliases] of Object.entries(POSTURE_NATIVE_ALIASES)) {
|
|
3162
|
+
for (const alias of aliases) index.set(normalizeModeId(alias), posture);
|
|
3163
|
+
}
|
|
3164
|
+
return index;
|
|
3165
|
+
})();
|
|
3166
|
+
function canonicalForModeId(modeId) {
|
|
3167
|
+
return CANONICAL_BY_NORMALIZED_ALIAS.get(normalizeModeId(modeId));
|
|
3168
|
+
}
|
|
3169
|
+
function findNativeMode(posture, availableModes) {
|
|
3170
|
+
if (typeof posture === "object") {
|
|
3171
|
+
const target = normalizeModeId(posture.harnessModeId);
|
|
3172
|
+
return availableModes.find((mode) => normalizeModeId(mode.id) === target);
|
|
3173
|
+
}
|
|
3174
|
+
return availableModes.find((mode) => canonicalForModeId(mode.id) === posture);
|
|
3175
|
+
}
|
|
3176
|
+
function resolvePosture(posture, availableModes) {
|
|
3177
|
+
const native = findNativeMode(posture, availableModes);
|
|
3178
|
+
if (native) return { kind: "native", mode: native };
|
|
3179
|
+
if (typeof posture === "object") {
|
|
3180
|
+
return { kind: "unavailable", requestedModeId: posture.harnessModeId };
|
|
3181
|
+
}
|
|
3182
|
+
if (posture === "default") return { kind: "noop", posture: "default" };
|
|
3183
|
+
return { kind: "prompt", posture, preamble: POSTURE_PREAMBLES[posture] };
|
|
3184
|
+
}
|
|
2762
3185
|
var sandboxSpecWithReuseSchema = z.object({
|
|
2763
3186
|
...SandboxSpecSchema.shape,
|
|
2764
3187
|
reuse: z.string().min(1).optional().describe(
|
|
@@ -2799,12 +3222,15 @@ function registerAgentTools(server, opts) {
|
|
|
2799
3222
|
registry,
|
|
2800
3223
|
resolveAgentAdapter,
|
|
2801
3224
|
listAgentAdapters,
|
|
3225
|
+
listCatalogModels,
|
|
2802
3226
|
buildOrchestratorMcp,
|
|
2803
3227
|
callerScope,
|
|
2804
3228
|
webhookNotifier,
|
|
2805
3229
|
daemonMcpUrl,
|
|
2806
3230
|
loadRoleRegistry: loadRoleRegistry2,
|
|
2807
|
-
resolveSandboxProvider: resolveSandboxProvider2
|
|
3231
|
+
resolveSandboxProvider: resolveSandboxProvider2,
|
|
3232
|
+
provisionWorktree,
|
|
3233
|
+
resolveWorktreeIsolation
|
|
2808
3234
|
} = opts;
|
|
2809
3235
|
server.tool(
|
|
2810
3236
|
"agent_start",
|
|
@@ -2919,6 +3345,22 @@ function registerAgentTools(server, opts) {
|
|
|
2919
3345
|
])
|
|
2920
3346
|
).optional().describe(
|
|
2921
3347
|
"Run this session inside a sandbox instead of on the host \u2014 pass a provider slug (see `list_sandbox_providers`) or an inline AIP-36 SandboxDefinition object. The daemon boots the sandbox, spawns `adapter` on the box's OWN agentproto daemon, and proxies the conversation back onto this session \u2014 `agent_prompt`/`agent_output`/`agent_kill` behave exactly as they do for a local spawn, and the transcript stays readable here even after the box is torn down. Omit to run locally (default). Pass an inline spec with `reuse: \"<sandboxId>\"` (from a prior session's `sandboxId`) to reconnect to an existing box instead \u2014 by default such a box is PAUSED (not killed) on session close so it stays reusable; set `lifecycle.destroy_on` to always kill it."
|
|
3348
|
+
),
|
|
3349
|
+
worktree: jsonTolerant(
|
|
3350
|
+
z.union([
|
|
3351
|
+
mcpBool,
|
|
3352
|
+
z.object({
|
|
3353
|
+
slug: z.string().regex(
|
|
3354
|
+
/^[a-z0-9][a-z0-9-]*$/,
|
|
3355
|
+
"slug must be lowercase kebab-case (letters, digits, hyphens)"
|
|
3356
|
+
).optional().describe(
|
|
3357
|
+
"Pin the worktree's slug (names its branch `wt/<slug>` and its directory). Omit to auto-mint a collision-free one from the label."
|
|
3358
|
+
),
|
|
3359
|
+
base: z.string().min(1).optional().describe("Git ref the worktree branch is cut from. Default 'origin/main'.")
|
|
3360
|
+
}).strict()
|
|
3361
|
+
])
|
|
3362
|
+
).optional().describe(
|
|
3363
|
+
"Isolate this session in its OWN git worktree instead of spawning directly in `cwd` \u2014 so a parallel agent can't collide on the working tree. `true` provisions a worktree on a fresh branch `wt/<slug>` cut from origin/main (slug auto-minted from `label`); pass `{ slug, base }` to pin either. The daemon boots the worktree (git worktree add + the repo's agentproto.json setup hooks) and spawns `adapter` THERE; the session's cwd, and every path it edits, live inside the worktree. Honoured only for a ROOT spawn (a spawn made THROUGH an orchestrator inherits its parent's tree \u2014 no second worktree) and only when `cwd` is inside a git repo (nothing to isolate otherwise \u21D2 spawns plain, no error). The daemon's `worktrees.isolation` policy may force this ON for every root spawn (`always`) or OFF (`never`, which REJECTS an explicit `worktree`). Ignored for a `sandbox` spawn (the box already isolates). The worktree is NOT auto-removed on session close \u2014 it holds the agent's work; tear it down with `agentproto worktree rm|archive|gc`."
|
|
2922
3364
|
)
|
|
2923
3365
|
},
|
|
2924
3366
|
async (input) => {
|
|
@@ -2942,7 +3384,9 @@ function registerAgentTools(server, opts) {
|
|
|
2942
3384
|
callerScope,
|
|
2943
3385
|
webhookNotifier,
|
|
2944
3386
|
loadRoleRegistry: loadRoleRegistry2,
|
|
2945
|
-
resolveSandboxProvider: resolveSandboxProvider2
|
|
3387
|
+
resolveSandboxProvider: resolveSandboxProvider2,
|
|
3388
|
+
...provisionWorktree ? { provisionWorktree } : {},
|
|
3389
|
+
...resolveWorktreeIsolation ? { resolveWorktreeIsolation } : {}
|
|
2946
3390
|
},
|
|
2947
3391
|
input
|
|
2948
3392
|
);
|
|
@@ -3091,7 +3535,7 @@ function registerAgentTools(server, opts) {
|
|
|
3091
3535
|
if (callerScope) {
|
|
3092
3536
|
const subtree = collectSubtree(
|
|
3093
3537
|
callerScope.ownerSessionId,
|
|
3094
|
-
registry.list()
|
|
3538
|
+
registry.list({ includeArchived: true })
|
|
3095
3539
|
);
|
|
3096
3540
|
if (!subtree.has(sessionId)) {
|
|
3097
3541
|
return {
|
|
@@ -3159,35 +3603,142 @@ function registerAgentTools(server, opts) {
|
|
|
3159
3603
|
}
|
|
3160
3604
|
);
|
|
3161
3605
|
server.tool(
|
|
3162
|
-
"
|
|
3163
|
-
"
|
|
3606
|
+
"agent_set_model",
|
|
3607
|
+
"Switch the model on a LIVE agent-cli session without restarting it \u2014 the mid-session counterpart to picking a model at `agent_start` time. Dispatches on the adapter's own apply strategy: a session whose adapter selects models via ACP session config or a `/model` control turn switches live; one that takes its model as a spawn-time CLI argument (e.g. codex) can't, and reports `{applied:false, reason:\"requires-restart\"}` instead of failing. Never throws on a rejected switch \u2014 check `applied` in the result.",
|
|
3164
3608
|
{
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
)
|
|
3168
|
-
onlyAlive: z.boolean().optional().describe("When true, only running/starting sessions. Default false."),
|
|
3169
|
-
status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive).")
|
|
3609
|
+
sessionId: sessionIdField,
|
|
3610
|
+
id: sessionIdAliasField,
|
|
3611
|
+
model: z.string().describe("Model id to switch to.")
|
|
3170
3612
|
},
|
|
3171
3613
|
async (input) => {
|
|
3172
|
-
|
|
3173
|
-
if (
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3614
|
+
const sessionId = resolveSessionIdArg(input);
|
|
3615
|
+
if (!sessionId) return missingSessionIdError("agent_set_model");
|
|
3616
|
+
try {
|
|
3617
|
+
const result = await registry.setModel(sessionId, input.model);
|
|
3618
|
+
return {
|
|
3619
|
+
content: [
|
|
3620
|
+
{
|
|
3621
|
+
type: "text",
|
|
3622
|
+
text: JSON.stringify({ ok: true, sessionId, ...result }, null, 2)
|
|
3623
|
+
}
|
|
3624
|
+
]
|
|
3625
|
+
};
|
|
3626
|
+
} catch (err) {
|
|
3627
|
+
return {
|
|
3628
|
+
content: [
|
|
3629
|
+
{
|
|
3630
|
+
type: "text",
|
|
3631
|
+
text: `agent_set_model: ${err instanceof Error ? err.message : String(err)}`
|
|
3632
|
+
}
|
|
3633
|
+
],
|
|
3634
|
+
isError: true
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
);
|
|
3639
|
+
server.tool(
|
|
3640
|
+
"agent_set_effort",
|
|
3641
|
+
"Switch the reasoning/compute budget (effort) on a LIVE agent-cli session without restarting it \u2014 the effort-axis counterpart to `agent_set_model`. Applied via the adapter's ACP session config. Effort is model-dependent: the same label means a different budget across models and some labels are model-gated (opus offers `ultracode`, haiku doesn't), so a label the current model rejects reports `{applied:false, reason}` instead of failing. Never throws on a rejected switch \u2014 check `applied` in the result.",
|
|
3642
|
+
{
|
|
3643
|
+
sessionId: sessionIdField,
|
|
3644
|
+
id: sessionIdAliasField,
|
|
3645
|
+
effort: z.string().describe(
|
|
3646
|
+
"Effort label to switch to (e.g. low/medium/high/xhigh/max/ultracode; the accepted set is model-dependent)."
|
|
3647
|
+
)
|
|
3648
|
+
},
|
|
3649
|
+
async (input) => {
|
|
3650
|
+
const sessionId = resolveSessionIdArg(input);
|
|
3651
|
+
if (!sessionId) return missingSessionIdError("agent_set_effort");
|
|
3652
|
+
try {
|
|
3653
|
+
const result = await registry.setEffort(sessionId, input.effort);
|
|
3654
|
+
return {
|
|
3655
|
+
content: [
|
|
3656
|
+
{
|
|
3657
|
+
type: "text",
|
|
3658
|
+
text: JSON.stringify({ ok: true, sessionId, ...result }, null, 2)
|
|
3659
|
+
}
|
|
3660
|
+
]
|
|
3661
|
+
};
|
|
3662
|
+
} catch (err) {
|
|
3663
|
+
return {
|
|
3664
|
+
content: [
|
|
3665
|
+
{
|
|
3666
|
+
type: "text",
|
|
3667
|
+
text: `agent_set_effort: ${err instanceof Error ? err.message : String(err)}`
|
|
3668
|
+
}
|
|
3669
|
+
],
|
|
3670
|
+
isError: true
|
|
3671
|
+
};
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
);
|
|
3675
|
+
server.tool(
|
|
3676
|
+
"agent_set_posture",
|
|
3677
|
+
'Switch the posture (what the agent may DO \u2014 plan / accept-edits / bypass / read-only, or a raw harness mode id) on a LIVE agent-cli session. When the posture maps to a NATIVE mode the harness advertises, it switches live (`applied:true`). When there is no native mode (the posture would have to be prompt-injected or applied at spawn), it is NOT forced live \u2014 the result is `{applied:false, reason:"requires-restart"}` so the caller can re-apply it through a session restart instead. Never throws on a rejected switch \u2014 check `applied`.',
|
|
3678
|
+
{
|
|
3679
|
+
sessionId: sessionIdField,
|
|
3680
|
+
id: sessionIdAliasField,
|
|
3681
|
+
posture: z.string().describe(
|
|
3682
|
+
"Posture to switch to: a canonical value (default/plan/accept-edits/bypass/read-only) or a raw harness mode id."
|
|
3683
|
+
)
|
|
3684
|
+
},
|
|
3685
|
+
async (input) => {
|
|
3686
|
+
const sessionId = resolveSessionIdArg(input);
|
|
3687
|
+
if (!sessionId) return missingSessionIdError("agent_set_posture");
|
|
3688
|
+
try {
|
|
3689
|
+
const result = await registry.setPosture(sessionId, parsePostureInput(input.posture));
|
|
3690
|
+
return {
|
|
3691
|
+
content: [
|
|
3692
|
+
{
|
|
3693
|
+
type: "text",
|
|
3694
|
+
text: JSON.stringify({ ok: true, sessionId, ...result }, null, 2)
|
|
3695
|
+
}
|
|
3696
|
+
]
|
|
3697
|
+
};
|
|
3698
|
+
} catch (err) {
|
|
3699
|
+
return {
|
|
3700
|
+
content: [
|
|
3701
|
+
{
|
|
3702
|
+
type: "text",
|
|
3703
|
+
text: `agent_set_posture: ${err instanceof Error ? err.message : String(err)}`
|
|
3704
|
+
}
|
|
3705
|
+
],
|
|
3706
|
+
isError: true
|
|
3707
|
+
};
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
);
|
|
3711
|
+
server.tool(
|
|
3712
|
+
"agent_sessions_list",
|
|
3713
|
+
"List agent-CLI sessions tracked by the daemon. Equivalent to `session_list({kind: 'agent-cli'})`. Each entry includes `kind`, `status`, age, etc. Use this when you only want the agent-CLI subset.",
|
|
3714
|
+
{
|
|
3715
|
+
kind: z.enum(["terminal", "agent-cli", "command", "all"]).optional().describe(
|
|
3716
|
+
"Optional override of the default `agent-cli` filter. `all` returns every kind."
|
|
3717
|
+
),
|
|
3718
|
+
onlyAlive: z.boolean().optional().describe("When true, only running/starting sessions. Default false."),
|
|
3719
|
+
status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive).")
|
|
3720
|
+
},
|
|
3721
|
+
async (input) => {
|
|
3722
|
+
let rows = registry.list({ includeArchived: true });
|
|
3723
|
+
if (callerScope) {
|
|
3724
|
+
const subtree = collectSubtree(callerScope.ownerSessionId, rows);
|
|
3725
|
+
rows = rows.filter((s) => subtree.has(s.id));
|
|
3726
|
+
}
|
|
3727
|
+
rows = rows.filter((s) => !s.archived);
|
|
3728
|
+
const kind = input.kind ?? "agent-cli";
|
|
3729
|
+
if (kind !== "all") {
|
|
3730
|
+
rows = rows.filter((s) => s.kind === kind);
|
|
3731
|
+
}
|
|
3732
|
+
if (input.status) {
|
|
3733
|
+
rows = rows.filter((s) => s.status === input.status);
|
|
3734
|
+
} else if (input.onlyAlive) {
|
|
3735
|
+
rows = rows.filter(
|
|
3736
|
+
(s) => s.status === "running" || s.status === "starting"
|
|
3737
|
+
);
|
|
3738
|
+
}
|
|
3739
|
+
return {
|
|
3740
|
+
content: [
|
|
3741
|
+
{ type: "text", text: JSON.stringify({ sessions: rows }, null, 2) }
|
|
3191
3742
|
]
|
|
3192
3743
|
};
|
|
3193
3744
|
}
|
|
@@ -3226,6 +3777,50 @@ function registerAgentTools(server, opts) {
|
|
|
3226
3777
|
}
|
|
3227
3778
|
}
|
|
3228
3779
|
);
|
|
3780
|
+
server.tool(
|
|
3781
|
+
"catalog_models",
|
|
3782
|
+
"Read-only vendor/product/route catalog (SPEC \xA75) \u2014 every model this host can reach, widened beyond any one adapter's model list via OpenRouter/Requesty/HuggingFace routing, with a profile-aware `runnable` flag per route. Use before `agent_start` to see what's actually spawnable given the auth profiles configured on this host.",
|
|
3783
|
+
{
|
|
3784
|
+
adapter: z.string().optional().describe("Keep only routes reachable via this adapter slug."),
|
|
3785
|
+
vendor: z.string().optional().describe("Keep only this vendor's entry."),
|
|
3786
|
+
route: z.string().optional().describe("Keep only routes with this route id."),
|
|
3787
|
+
runnableOnly: mcpBool.optional().describe("Drop every route with runnable:false.")
|
|
3788
|
+
},
|
|
3789
|
+
async ({ adapter, vendor, route, runnableOnly }) => {
|
|
3790
|
+
if (!listCatalogModels) {
|
|
3791
|
+
return {
|
|
3792
|
+
content: [
|
|
3793
|
+
{
|
|
3794
|
+
type: "text",
|
|
3795
|
+
text: "catalog_models is not enabled \u2014 the daemon was started without a catalog lister. Wire `buildCatalogModels` via `createGateway({ listCatalogModels })`."
|
|
3796
|
+
}
|
|
3797
|
+
],
|
|
3798
|
+
isError: true
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3801
|
+
try {
|
|
3802
|
+
const catalog = await listCatalogModels({
|
|
3803
|
+
...adapter ? { adapter } : {},
|
|
3804
|
+
...vendor ? { vendor } : {},
|
|
3805
|
+
...route ? { route } : {},
|
|
3806
|
+
...runnableOnly ? { runnableOnly: true } : {}
|
|
3807
|
+
});
|
|
3808
|
+
return {
|
|
3809
|
+
content: [{ type: "text", text: JSON.stringify(catalog, null, 2) }]
|
|
3810
|
+
};
|
|
3811
|
+
} catch (err) {
|
|
3812
|
+
return {
|
|
3813
|
+
content: [
|
|
3814
|
+
{
|
|
3815
|
+
type: "text",
|
|
3816
|
+
text: `catalog_models failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3817
|
+
}
|
|
3818
|
+
],
|
|
3819
|
+
isError: true
|
|
3820
|
+
};
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
);
|
|
3229
3824
|
server.tool(
|
|
3230
3825
|
"role_list",
|
|
3231
3826
|
"Enumerate every spawn-time role known to the daemon \u2014 the two built-ins (executor, supervisor) plus any custom role installed as a role pack. Read-only: pure visibility into the same registry `agent_start`'s `role` field and privilege-lattice spawn gate use \u2014 this tool never itself grants or denies a spawn. Use before `agent_start` with `orchestrator` to discover which roles this session may in turn spawn.",
|
|
@@ -3537,151 +4132,9 @@ function stringifyValues(raw) {
|
|
|
3537
4132
|
}
|
|
3538
4133
|
return out;
|
|
3539
4134
|
}
|
|
3540
|
-
function claudeCodeProjectDir(cwd) {
|
|
3541
|
-
const encoded = cwd.replace(/\//g, "-");
|
|
3542
|
-
return resolve(homedir(), ".claude", "projects", encoded);
|
|
3543
|
-
}
|
|
3544
|
-
function extractFirstText(content) {
|
|
3545
|
-
if (typeof content === "string") {
|
|
3546
|
-
const t = content.trim();
|
|
3547
|
-
return t || void 0;
|
|
3548
|
-
}
|
|
3549
|
-
if (Array.isArray(content)) {
|
|
3550
|
-
for (const block of content) {
|
|
3551
|
-
if (block && typeof block === "object" && block.type === "text" && typeof block.text === "string") {
|
|
3552
|
-
const t = block.text.trim();
|
|
3553
|
-
if (t) return t;
|
|
3554
|
-
}
|
|
3555
|
-
}
|
|
3556
|
-
}
|
|
3557
|
-
return void 0;
|
|
3558
|
-
}
|
|
3559
|
-
async function scanClaudeJsonl(filePath) {
|
|
3560
|
-
const stream = createReadStream(filePath, { encoding: "utf8" });
|
|
3561
|
-
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
3562
|
-
let startedAt;
|
|
3563
|
-
let lastActivityAt;
|
|
3564
|
-
let messageCount = 0;
|
|
3565
|
-
let preview2;
|
|
3566
|
-
let lastWriter;
|
|
3567
|
-
for await (const line of rl) {
|
|
3568
|
-
const trimmed = line.trim();
|
|
3569
|
-
if (!trimmed) continue;
|
|
3570
|
-
let entry;
|
|
3571
|
-
try {
|
|
3572
|
-
entry = JSON.parse(trimmed);
|
|
3573
|
-
} catch {
|
|
3574
|
-
continue;
|
|
3575
|
-
}
|
|
3576
|
-
if (typeof entry.timestamp === "string") {
|
|
3577
|
-
if (!startedAt) startedAt = entry.timestamp;
|
|
3578
|
-
lastActivityAt = entry.timestamp;
|
|
3579
|
-
}
|
|
3580
|
-
if (typeof entry.entrypoint === "string") {
|
|
3581
|
-
lastWriter = entry.entrypoint;
|
|
3582
|
-
}
|
|
3583
|
-
if (entry.type === "user" || entry.type === "assistant") {
|
|
3584
|
-
messageCount += 1;
|
|
3585
|
-
if (preview2 === void 0 && entry.type === "user") {
|
|
3586
|
-
const text6 = extractFirstText(entry.message?.content);
|
|
3587
|
-
if (text6 !== void 0) {
|
|
3588
|
-
preview2 = text6.length > 120 ? text6.slice(0, 120) : text6;
|
|
3589
|
-
}
|
|
3590
|
-
}
|
|
3591
|
-
}
|
|
3592
|
-
}
|
|
3593
|
-
return { startedAt, lastActivityAt, messageCount, preview: preview2, lastWriter };
|
|
3594
|
-
}
|
|
3595
|
-
async function buildClaudeCandidate(filePath, conversationId) {
|
|
3596
|
-
const scanned = await scanClaudeJsonl(filePath);
|
|
3597
|
-
return { conversationId, ...scanned };
|
|
3598
|
-
}
|
|
3599
|
-
function claudeEntrypointFor(mode) {
|
|
3600
|
-
return mode === "native" ? "cli" : "sdk-ts";
|
|
3601
|
-
}
|
|
3602
|
-
async function discoverClaudeCode(input) {
|
|
3603
|
-
const { cwd, since, until, attachmentMode, expectedId } = input;
|
|
3604
|
-
const dir = claudeCodeProjectDir(cwd);
|
|
3605
|
-
if (expectedId) {
|
|
3606
|
-
const filePath = join(dir, `${expectedId}.jsonl`);
|
|
3607
|
-
try {
|
|
3608
|
-
await promises.stat(filePath);
|
|
3609
|
-
} catch {
|
|
3610
|
-
return [];
|
|
3611
|
-
}
|
|
3612
|
-
return [await buildClaudeCandidate(filePath, expectedId)];
|
|
3613
|
-
}
|
|
3614
|
-
let entries;
|
|
3615
|
-
try {
|
|
3616
|
-
entries = await promises.readdir(dir);
|
|
3617
|
-
} catch {
|
|
3618
|
-
return [];
|
|
3619
|
-
}
|
|
3620
|
-
const jsonlFiles = entries.filter((e) => e.endsWith(".jsonl"));
|
|
3621
|
-
if (jsonlFiles.length === 0) return [];
|
|
3622
|
-
const sinceMs = since ? Date.parse(since) : NaN;
|
|
3623
|
-
const untilMs = until ? Date.parse(until) : NaN;
|
|
3624
|
-
const wantEntrypoint = attachmentMode ? claudeEntrypointFor(attachmentMode) : void 0;
|
|
3625
|
-
const scored = [];
|
|
3626
|
-
for (const f of jsonlFiles) {
|
|
3627
|
-
const filePath = join(dir, f);
|
|
3628
|
-
let mtimeMs;
|
|
3629
|
-
try {
|
|
3630
|
-
mtimeMs = (await promises.stat(filePath)).mtimeMs;
|
|
3631
|
-
} catch {
|
|
3632
|
-
continue;
|
|
3633
|
-
}
|
|
3634
|
-
if (Number.isFinite(sinceMs) && mtimeMs < sinceMs - 1e3) continue;
|
|
3635
|
-
const conversationId = f.replace(/\.jsonl$/, "");
|
|
3636
|
-
const candidate = await buildClaudeCandidate(filePath, conversationId);
|
|
3637
|
-
if (Number.isFinite(untilMs) && candidate.startedAt !== void 0) {
|
|
3638
|
-
const startedMs = Date.parse(candidate.startedAt);
|
|
3639
|
-
if (Number.isFinite(startedMs) && startedMs > untilMs) continue;
|
|
3640
|
-
}
|
|
3641
|
-
if (wantEntrypoint !== void 0 && candidate.lastWriter !== void 0 && candidate.lastWriter !== wantEntrypoint) {
|
|
3642
|
-
continue;
|
|
3643
|
-
}
|
|
3644
|
-
scored.push({ candidate, mtimeMs });
|
|
3645
|
-
}
|
|
3646
|
-
scored.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
3647
|
-
return scored.map((s) => s.candidate);
|
|
3648
|
-
}
|
|
3649
|
-
async function readClaudeCode(conversationId, cwd) {
|
|
3650
|
-
const { exportClaudeCodeSession: exportClaudeCodeSession2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
|
|
3651
|
-
return exportClaudeCodeSession2(conversationId, cwd);
|
|
3652
|
-
}
|
|
3653
|
-
async function discoverHermes(input) {
|
|
3654
|
-
const { discoverHermesSessions: discoverHermesSessions2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
|
|
3655
|
-
return discoverHermesSessions2(input.cwd, input.since, input.expectedId);
|
|
3656
|
-
}
|
|
3657
|
-
async function readHermes(conversationId) {
|
|
3658
|
-
const { exportHermesSession: exportHermesSession2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
|
|
3659
|
-
return exportHermesSession2(conversationId);
|
|
3660
|
-
}
|
|
3661
|
-
var CONVERSATION_STORES = {
|
|
3662
|
-
"claude-code": {
|
|
3663
|
-
storeAs: "claudeResumeId",
|
|
3664
|
-
// Printed by claude on graceful exit when session persistence is on
|
|
3665
|
-
// (default). Example: `claude --resume 0e483f81-1a44-4bec-9667-b37158450296`
|
|
3666
|
-
outputHint: /claude\s+--resume\s+([0-9a-f-]{8,})/i,
|
|
3667
|
-
attachArgv: (conversationId) => ["claude", "--resume", conversationId],
|
|
3668
|
-
discover: discoverClaudeCode,
|
|
3669
|
-
read: readClaudeCode
|
|
3670
|
-
},
|
|
3671
|
-
hermes: {
|
|
3672
|
-
storeAs: "hermesResumeId",
|
|
3673
|
-
// `hermes acp` is the ACP arm (bin_args in adapters/hermes/src/index.ts);
|
|
3674
|
-
// `--resume SESSION --tui` is the native TUI resume path — same binary,
|
|
3675
|
-
// different flags, unlike claude-code where the two arms are different
|
|
3676
|
-
// binaries. Verified via `hermes --help`: `--resume SESSION, -r` =
|
|
3677
|
-
// "Resume a previous session by ID or title", `--tui` = the real TUI.
|
|
3678
|
-
attachArgv: (conversationId) => ["hermes", "--resume", conversationId, "--tui"],
|
|
3679
|
-
discover: discoverHermes,
|
|
3680
|
-
read: readHermes
|
|
3681
|
-
}
|
|
3682
|
-
};
|
|
3683
4135
|
|
|
3684
4136
|
// src/resume-strategies.ts
|
|
4137
|
+
init_conversation_store();
|
|
3685
4138
|
var claudeCodeStore = CONVERSATION_STORES["claude-code"];
|
|
3686
4139
|
var RESUME_STRATEGIES = {
|
|
3687
4140
|
"claude-code": {
|
|
@@ -3789,6 +4242,46 @@ function tokenizeCommand(s) {
|
|
|
3789
4242
|
if (buf) out.push(buf);
|
|
3790
4243
|
return out;
|
|
3791
4244
|
}
|
|
4245
|
+
var RestartOverrideError = class extends Error {
|
|
4246
|
+
code = "restart_override_invalid";
|
|
4247
|
+
status = 400;
|
|
4248
|
+
constructor(message) {
|
|
4249
|
+
super(message);
|
|
4250
|
+
this.name = "RestartOverrideError";
|
|
4251
|
+
}
|
|
4252
|
+
};
|
|
4253
|
+
async function resolveAccessProfileFromStore(profileRef) {
|
|
4254
|
+
const profile = await getAuthProfile(profileRef);
|
|
4255
|
+
if (!profile) return void 0;
|
|
4256
|
+
const stored = await new KeychainStore().read({ path: profile.credentialRef });
|
|
4257
|
+
return { profile, ...stored?.value !== void 0 ? { credential: stored.value } : {} };
|
|
4258
|
+
}
|
|
4259
|
+
function methodToMode(method) {
|
|
4260
|
+
return method === "oauth-bearer" ? "subscription" : "api-key";
|
|
4261
|
+
}
|
|
4262
|
+
function directMethods(descriptor) {
|
|
4263
|
+
const methods = [];
|
|
4264
|
+
if (descriptor?.authSubscription) methods.push("oauth-bearer");
|
|
4265
|
+
if (descriptor?.provider) methods.push("api-key");
|
|
4266
|
+
return methods;
|
|
4267
|
+
}
|
|
4268
|
+
function eligibilityManifest(adapterSlug, descriptor, route, model) {
|
|
4269
|
+
const baseVendor = descriptor?.provider ?? (model ? getModelProvider(model) : void 0);
|
|
4270
|
+
const gateway = route?.gateway;
|
|
4271
|
+
const routeId = gateway ?? baseVendor;
|
|
4272
|
+
if (routeId === void 0) return void 0;
|
|
4273
|
+
const isDirect = baseVendor !== void 0 && routeId === baseVendor;
|
|
4274
|
+
const billedVendor2 = isDirect ? baseVendor : routeId;
|
|
4275
|
+
const methods = isDirect ? directMethods(descriptor) : ["api-key"];
|
|
4276
|
+
return {
|
|
4277
|
+
manifest: {
|
|
4278
|
+
id: adapterSlug,
|
|
4279
|
+
vendorByRoute: { [routeId]: billedVendor2 },
|
|
4280
|
+
methodsByRoute: { [routeId]: methods }
|
|
4281
|
+
},
|
|
4282
|
+
routeId
|
|
4283
|
+
};
|
|
4284
|
+
}
|
|
3792
4285
|
async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {}) {
|
|
3793
4286
|
const augmented = opts.forceAgentResume ? prev : await augmentWithFsResume(prev);
|
|
3794
4287
|
const strategy = opts.forceAgentResume ? { kind: "agent", resumeSessionId: prev.adapterSessionId } : decideRestartStrategy(augmented);
|
|
@@ -3810,15 +4303,80 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
|
|
|
3810
4303
|
let cwd = prev.cwd;
|
|
3811
4304
|
if (!cwd) console.warn(`[restartAgentSession] no cwd on prior descriptor ${prev.id} \u2014 falling back to daemon's cwd ${process.cwd()}`);
|
|
3812
4305
|
cwd ??= process.cwd();
|
|
4306
|
+
const overrides = opts.overrides ?? {};
|
|
4307
|
+
const effModel = overrides.model ?? prev.model;
|
|
4308
|
+
const effEffort = overrides.effort ?? prev.effort;
|
|
4309
|
+
const effRoute = overrides.route ?? prev.route;
|
|
4310
|
+
const effPosture = overrides.posture ?? prev.posture;
|
|
4311
|
+
const effContextProfile = overrides.contextProfile ?? prev.contextProfile;
|
|
4312
|
+
const effMode = overrides.mode ?? prev.mode;
|
|
4313
|
+
const accessOverrideRef = overrides.access?.profileRef;
|
|
3813
4314
|
let authSpec;
|
|
3814
4315
|
let authEcho;
|
|
3815
|
-
|
|
4316
|
+
let accessProfileEcho = prev.accessProfile;
|
|
4317
|
+
if (accessOverrideRef !== void 0) {
|
|
4318
|
+
const resolveProfile = opts.resolveAccessProfile ?? resolveAccessProfileFromStore;
|
|
4319
|
+
const found = await resolveProfile(accessOverrideRef);
|
|
4320
|
+
if (!found) {
|
|
4321
|
+
throw new RestartOverrideError(
|
|
4322
|
+
`restart access override: no auth profile "${accessOverrideRef}" found.`
|
|
4323
|
+
);
|
|
4324
|
+
}
|
|
4325
|
+
const { profile, credential } = found;
|
|
4326
|
+
if (!resolved.authDescriptor) {
|
|
4327
|
+
throw new RestartOverrideError(
|
|
4328
|
+
`restart access override: adapter "${adapterSlug}" presents no billing-auth, so profile "${profile.id}" cannot be attached.`
|
|
4329
|
+
);
|
|
4330
|
+
}
|
|
4331
|
+
const projected = eligibilityManifest(
|
|
4332
|
+
adapterSlug,
|
|
4333
|
+
resolved.authDescriptor,
|
|
4334
|
+
effRoute,
|
|
4335
|
+
effModel ?? resolved.defaultModel
|
|
4336
|
+
);
|
|
4337
|
+
if (!projected) {
|
|
4338
|
+
throw new RestartOverrideError(
|
|
4339
|
+
`restart access override: cannot resolve a billing vendor for adapter "${adapterSlug}" (no fixed provider, no model) \u2014 profile "${profile.id}" eligibility is unverifiable, refusing to spawn.`
|
|
4340
|
+
);
|
|
4341
|
+
}
|
|
4342
|
+
const { manifest, routeId } = projected;
|
|
4343
|
+
if (eligibleProfiles([profile], manifest, routeId).length === 0) {
|
|
4344
|
+
const billed = manifest.vendorByRoute[routeId];
|
|
4345
|
+
const methods = manifest.methodsByRoute[routeId] ?? [];
|
|
4346
|
+
throw new RestartOverrideError(
|
|
4347
|
+
`restart access override: profile "${profile.id}" (${profile.vendor}/${profile.method}) is not eligible for adapter "${adapterSlug}" on route "${routeId}" \u2014 that endpoint bills "${billed}" via [${methods.join(", ") || "no presentable methods"}]. Attach a profile whose vendor + method match the route.`
|
|
4348
|
+
);
|
|
4349
|
+
}
|
|
4350
|
+
const mode = methodToMode(profile.method);
|
|
4351
|
+
const result = resolveAuthSpec({
|
|
4352
|
+
descriptor: resolved.authDescriptor,
|
|
4353
|
+
...effModel ? { model: effModel } : {},
|
|
4354
|
+
requestedProvider: profile.vendor,
|
|
4355
|
+
requestedMode: mode,
|
|
4356
|
+
// Attaching a named profile is always an EXPLICIT billing choice — so a
|
|
4357
|
+
// missing credential fails loud (driver `missing_auth_credential`) rather
|
|
4358
|
+
// than falling back to ambient env or the prior credential.
|
|
4359
|
+
explicit: true,
|
|
4360
|
+
...mode === "subscription" && credential !== void 0 ? { subscriptionCredential: credential } : {},
|
|
4361
|
+
...mode === "api-key" && credential !== void 0 ? { apiKeyConfigCredential: credential } : {}
|
|
4362
|
+
});
|
|
4363
|
+
if (result) {
|
|
4364
|
+
authSpec = result.spec;
|
|
4365
|
+
authEcho = result.echo;
|
|
4366
|
+
}
|
|
4367
|
+
accessProfileEcho = {
|
|
4368
|
+
profileRef: profile.id,
|
|
4369
|
+
...profile.label !== void 0 ? { label: profile.label } : {},
|
|
4370
|
+
vendor: profile.vendor,
|
|
4371
|
+
method: profile.method
|
|
4372
|
+
};
|
|
4373
|
+
} else if (resolved.authDescriptor) {
|
|
3816
4374
|
const configDefaults = opts.loadDefaultsConfig ? await opts.loadDefaultsConfig() : (await loadConfig()).defaults;
|
|
3817
4375
|
const explicitAuthInput = prev.auth ? { mode: prev.auth.mode } : void 0;
|
|
3818
4376
|
const spawnDefaults = resolveSpawnDefaults(configDefaults, adapterSlug, {
|
|
3819
4377
|
auth: explicitAuthInput
|
|
3820
4378
|
});
|
|
3821
|
-
const authModel =
|
|
4379
|
+
const authModel = effModel ?? resolved.defaultModel;
|
|
3822
4380
|
const pinnedProvider = spawnDefaults.auth.provider;
|
|
3823
4381
|
const resolvedProvider = pinnedProvider ?? resolved.authDescriptor.provider ?? (authModel ? getModelProvider(authModel) : void 0);
|
|
3824
4382
|
const apiKeyStoreCredential = resolvedProvider && spawnDefaults.auth.explicit && spawnDefaults.auth.apiKeyCredential === void 0 ? await (0, providers_store_exports.getProviderKey)(resolvedProvider) : void 0;
|
|
@@ -3836,19 +4394,32 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
|
|
|
3836
4394
|
authSpec = result.spec;
|
|
3837
4395
|
authEcho = result.echo;
|
|
3838
4396
|
}
|
|
4397
|
+
if (authSpec && authSpec.enforce === "always" && authSpec.credential === void 0 && !spawnDefaults.auth.explicit && resolvedProvider !== void 0) {
|
|
4398
|
+
const ignored = await (0, providers_store_exports.getProviderKey)(resolvedProvider);
|
|
4399
|
+
if (ignored !== void 0) {
|
|
4400
|
+
authSpec = { ...authSpec, ignoredApiKeyInStore: true };
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
3839
4403
|
}
|
|
3840
4404
|
const spawnWithResume = async (resumeSessionId) => {
|
|
3841
4405
|
let liveSessionId;
|
|
3842
4406
|
const agentSession = await resolved.startSession({
|
|
3843
4407
|
cwd,
|
|
3844
4408
|
...resumeSessionId ? { resumeSessionId } : {},
|
|
3845
|
-
...
|
|
4409
|
+
...effModel ? { model: effModel } : {},
|
|
4410
|
+
...effEffort ? { effort: effEffort } : {},
|
|
4411
|
+
// Legacy AIP-45 mode override only — the decomposed route/posture/
|
|
4412
|
+
// contextProfile spawn-env apply-path rides the driver's mode
|
|
4413
|
+
// decomposition (build step 2), out of scope here; they still round-trip
|
|
4414
|
+
// on the descriptor below.
|
|
4415
|
+
...overrides.mode ? { mode: overrides.mode } : {},
|
|
3846
4416
|
...prev.mcpServers ? { mcpServers: prev.mcpServers } : {},
|
|
3847
4417
|
...authSpec ? { auth: authSpec } : {},
|
|
3848
4418
|
onActivity: () => {
|
|
3849
4419
|
if (liveSessionId) registry.pulseActivity(liveSessionId);
|
|
3850
4420
|
}
|
|
3851
4421
|
});
|
|
4422
|
+
const resumeVia = !resumeSessionId ? "" : opts.forceAgentResume ? "resumed via ACP" : describeResumePath(augmented);
|
|
3852
4423
|
const desc2 = registry.spawnAgent({
|
|
3853
4424
|
workspaceSlug: prev.workspaceSlug,
|
|
3854
4425
|
cwd,
|
|
@@ -3856,7 +4427,17 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
|
|
|
3856
4427
|
adapterSlug,
|
|
3857
4428
|
...prev.label ? { label: prev.label } : {},
|
|
3858
4429
|
...prev.mcpServers ? { mcpServers: prev.mcpServers } : {},
|
|
3859
|
-
...
|
|
4430
|
+
...effModel ? { model: effModel } : {},
|
|
4431
|
+
// Decomposed config-axis echoes (SPEC §3.7) — carried forward from `prev`
|
|
4432
|
+
// and overlaid with any override so every axis round-trips onto the fresh
|
|
4433
|
+
// descriptor and the picker re-opens on it (SPEC §3.8), even the axes
|
|
4434
|
+
// whose spawn-env apply-path isn't wired here yet (route/posture/context).
|
|
4435
|
+
...effEffort ? { effort: effEffort } : {},
|
|
4436
|
+
...effPosture !== void 0 ? { posture: effPosture } : {},
|
|
4437
|
+
...effRoute ? { route: effRoute } : {},
|
|
4438
|
+
...effContextProfile ? { contextProfile: effContextProfile } : {},
|
|
4439
|
+
...accessProfileEcho ? { accessProfile: accessProfileEcho } : {},
|
|
4440
|
+
...effMode ? { mode: effMode } : {},
|
|
3860
4441
|
...resolved.commandPreview ? { commandPreview: resolved.commandPreview } : {},
|
|
3861
4442
|
// Verifiability echo (never the credential) — see the auth
|
|
3862
4443
|
// resolution block above. Absent when no credential resolved,
|
|
@@ -3869,31 +4450,53 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
|
|
|
3869
4450
|
credentialSource: authEcho.credentialSource,
|
|
3870
4451
|
setEnv: authEcho.setEnv
|
|
3871
4452
|
}
|
|
3872
|
-
} : {}
|
|
4453
|
+
} : {},
|
|
4454
|
+
resumedFrom: prev.id,
|
|
4455
|
+
resumeVia
|
|
3873
4456
|
});
|
|
3874
4457
|
liveSessionId = desc2.id;
|
|
3875
4458
|
return desc2;
|
|
3876
4459
|
};
|
|
3877
4460
|
let desc;
|
|
3878
4461
|
let resumeFallback = false;
|
|
3879
|
-
let usedResumeSessionId = strategy.resumeSessionId;
|
|
3880
4462
|
try {
|
|
3881
4463
|
desc = await spawnWithResume(strategy.resumeSessionId);
|
|
3882
4464
|
} catch (err) {
|
|
3883
4465
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3884
4466
|
if (strategy.resumeSessionId && /not found|Resource not found/i.test(msg)) {
|
|
3885
4467
|
desc = await spawnWithResume(void 0);
|
|
3886
|
-
usedResumeSessionId = void 0;
|
|
3887
4468
|
resumeFallback = true;
|
|
3888
4469
|
} else {
|
|
3889
4470
|
throw err;
|
|
3890
4471
|
}
|
|
3891
4472
|
}
|
|
3892
|
-
const
|
|
4473
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
4474
|
+
const emit = (axis, value) => {
|
|
4475
|
+
registry.emitConfigChanged({
|
|
4476
|
+
type: "session:config-changed",
|
|
4477
|
+
sessionId: desc.id,
|
|
4478
|
+
axis,
|
|
4479
|
+
value,
|
|
4480
|
+
...desc.label ? { label: desc.label } : {},
|
|
4481
|
+
ts
|
|
4482
|
+
});
|
|
4483
|
+
};
|
|
4484
|
+
if (overrides.model !== void 0) emit("model", overrides.model);
|
|
4485
|
+
if (overrides.effort !== void 0) emit("effort", overrides.effort);
|
|
4486
|
+
if (accessOverrideRef !== void 0) emit("access", { profileRef: accessOverrideRef });
|
|
4487
|
+
if (overrides.route !== void 0) emit("route", overrides.route);
|
|
4488
|
+
if (overrides.posture !== void 0) emit("posture", overrides.posture);
|
|
4489
|
+
if (overrides.contextProfile !== void 0) emit("contextProfile", overrides.contextProfile);
|
|
3893
4490
|
return {
|
|
3894
4491
|
desc,
|
|
3895
4492
|
resumedFrom: prev.id,
|
|
3896
|
-
|
|
4493
|
+
// `spawnWithResume` already computed + persisted this onto `desc` (the
|
|
4494
|
+
// fix this module carries — see `SessionDescriptor.resumedFrom`'s doc);
|
|
4495
|
+
// reading it back here rather than recomputing keeps the RESULT and the
|
|
4496
|
+
// STORED descriptor from ever being able to diverge. Never actually
|
|
4497
|
+
// undefined — every `spawnWithResume` call sets it, `?? ""` is just
|
|
4498
|
+
// satisfying the optional field's type.
|
|
4499
|
+
resumeVia: desc.resumeVia ?? "",
|
|
3897
4500
|
...resumeFallback ? { resumeFallback: true } : {}
|
|
3898
4501
|
};
|
|
3899
4502
|
}
|
|
@@ -4037,6 +4640,9 @@ function withToolExclusion(server, excluded) {
|
|
|
4037
4640
|
}
|
|
4038
4641
|
});
|
|
4039
4642
|
}
|
|
4643
|
+
|
|
4644
|
+
// src/conversation-read.ts
|
|
4645
|
+
init_conversation_store();
|
|
4040
4646
|
init_transcript_export();
|
|
4041
4647
|
var BINARY_TO_STORE_KEY = {
|
|
4042
4648
|
claude: "claude-code",
|
|
@@ -4216,7 +4822,7 @@ function buildSessionTree(sessions) {
|
|
|
4216
4822
|
});
|
|
4217
4823
|
return sessions.filter((s) => !s.parentSessionId || !idSet.has(s.parentSessionId)).sort((a, b) => a.startedAt.localeCompare(b.startedAt)).map(toNode);
|
|
4218
4824
|
}
|
|
4219
|
-
z.preprocess(
|
|
4825
|
+
var mcpBool2 = z.preprocess(
|
|
4220
4826
|
(v) => v === "true" ? true : v === "false" ? false : v,
|
|
4221
4827
|
z.boolean()
|
|
4222
4828
|
);
|
|
@@ -4238,14 +4844,20 @@ function registerSessionTools(rawServer, opts) {
|
|
|
4238
4844
|
"Filter by session kind. `all` (default) returns every kind. Use `terminal` to list only PTY sessions, `agent-cli` for structured ACP agents."
|
|
4239
4845
|
),
|
|
4240
4846
|
onlyAlive: z.boolean().optional().describe("When true, only running/starting sessions. Default false."),
|
|
4241
|
-
status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive).")
|
|
4847
|
+
status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive)."),
|
|
4848
|
+
includeArchived: z.boolean().optional().describe(
|
|
4849
|
+
"When true, also include archived sessions (hidden from every other view by `session_archive`). Default false."
|
|
4850
|
+
)
|
|
4242
4851
|
},
|
|
4243
4852
|
async (input) => {
|
|
4244
|
-
let rows = registry.list();
|
|
4853
|
+
let rows = registry.list({ includeArchived: true });
|
|
4245
4854
|
if (callerScope) {
|
|
4246
4855
|
const subtree = collectSubtree(callerScope.ownerSessionId, rows);
|
|
4247
4856
|
rows = rows.filter((s) => subtree.has(s.id));
|
|
4248
4857
|
}
|
|
4858
|
+
if (!input.includeArchived) {
|
|
4859
|
+
rows = rows.filter((s) => !s.archived);
|
|
4860
|
+
}
|
|
4249
4861
|
if (input.kind && input.kind !== "all") {
|
|
4250
4862
|
rows = rows.filter((s) => s.kind === input.kind);
|
|
4251
4863
|
}
|
|
@@ -4283,7 +4895,10 @@ function registerSessionTools(rawServer, opts) {
|
|
|
4283
4895
|
};
|
|
4284
4896
|
}
|
|
4285
4897
|
if (callerScope) {
|
|
4286
|
-
const subtree = collectSubtree(
|
|
4898
|
+
const subtree = collectSubtree(
|
|
4899
|
+
callerScope.ownerSessionId,
|
|
4900
|
+
registry.list({ includeArchived: true })
|
|
4901
|
+
);
|
|
4287
4902
|
if (!subtree.has(desc.id)) {
|
|
4288
4903
|
return {
|
|
4289
4904
|
content: [
|
|
@@ -4319,11 +4934,12 @@ function registerSessionTools(rawServer, opts) {
|
|
|
4319
4934
|
status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive).")
|
|
4320
4935
|
},
|
|
4321
4936
|
async (input) => {
|
|
4322
|
-
let rows = registry.list();
|
|
4937
|
+
let rows = registry.list({ includeArchived: true });
|
|
4323
4938
|
if (callerScope) {
|
|
4324
4939
|
const subtree = collectSubtree(callerScope.ownerSessionId, rows);
|
|
4325
4940
|
rows = rows.filter((s) => subtree.has(s.id));
|
|
4326
4941
|
}
|
|
4942
|
+
rows = rows.filter((s) => !s.archived);
|
|
4327
4943
|
const kind = input.kind ?? "terminal";
|
|
4328
4944
|
if (kind !== "all") {
|
|
4329
4945
|
rows = rows.filter((s) => s.kind === kind);
|
|
@@ -4353,11 +4969,12 @@ function registerSessionTools(rawServer, opts) {
|
|
|
4353
4969
|
status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive).")
|
|
4354
4970
|
},
|
|
4355
4971
|
async (input) => {
|
|
4356
|
-
let rows = registry.list();
|
|
4972
|
+
let rows = registry.list({ includeArchived: true });
|
|
4357
4973
|
if (callerScope) {
|
|
4358
4974
|
const subtree = collectSubtree(callerScope.ownerSessionId, rows);
|
|
4359
4975
|
rows = rows.filter((s) => subtree.has(s.id));
|
|
4360
4976
|
}
|
|
4977
|
+
rows = rows.filter((s) => !s.archived);
|
|
4361
4978
|
const kind = input.kind ?? "command";
|
|
4362
4979
|
if (kind !== "all") {
|
|
4363
4980
|
rows = rows.filter((s) => s.kind === kind);
|
|
@@ -4641,11 +5258,12 @@ function registerSessionTools(rawServer, opts) {
|
|
|
4641
5258
|
)
|
|
4642
5259
|
},
|
|
4643
5260
|
async (input) => {
|
|
4644
|
-
let rows = registry.list();
|
|
5261
|
+
let rows = registry.list({ includeArchived: true });
|
|
4645
5262
|
if (callerScope) {
|
|
4646
5263
|
const subtree = collectSubtree(callerScope.ownerSessionId, rows);
|
|
4647
5264
|
rows = rows.filter((s) => subtree.has(s.id));
|
|
4648
5265
|
}
|
|
5266
|
+
rows = rows.filter((s) => !s.archived);
|
|
4649
5267
|
if (input.onlyAlive) {
|
|
4650
5268
|
rows = rows.filter(
|
|
4651
5269
|
(s) => s.status === "running" || s.status === "starting"
|
|
@@ -4678,7 +5296,25 @@ function registerSessionTools(rawServer, opts) {
|
|
|
4678
5296
|
cols: z.number().int().min(1).max(500).optional().describe(
|
|
4679
5297
|
"PTY cols \u2014 only used when the restart resolves to a provider-native or plain PTY resume. Default 80."
|
|
4680
5298
|
),
|
|
4681
|
-
rows: z.number().int().min(1).max(200).optional().describe("PTY rows \u2014 same case as `cols`. Default 24.")
|
|
5299
|
+
rows: z.number().int().min(1).max(200).optional().describe("PTY rows \u2014 same case as `cols`. Default 24."),
|
|
5300
|
+
// ── Restart-with-override axes (SPEC §4.3, step 6) — the single path
|
|
5301
|
+
// for all four restart-only axes. Each optional; an omitted axis is
|
|
5302
|
+
// carried forward from the prior session, an axis set here wins.
|
|
5303
|
+
model: z.string().min(1).optional().describe("Override the model on restart (route-identity ref)."),
|
|
5304
|
+
effort: z.enum(["low", "medium", "high", "xhigh", "max", "ultracode"]).optional().describe("Override the reasoning-effort level on restart."),
|
|
5305
|
+
access: z.object({
|
|
5306
|
+
profileRef: z.string().min(1).describe("Attach this NAMED auth profile (SPEC \xA71c). Rejected 400 if it's not eligible for the resolved (adapter \xD7 route).")
|
|
5307
|
+
}).optional().describe("Switch the session's billing wallet to a named auth profile."),
|
|
5308
|
+
route: z.object({
|
|
5309
|
+
gateway: z.string().min(1).describe("Endpoint/gateway id (anthropic|moonshot|\u2026)."),
|
|
5310
|
+
baseUrl: z.string().url().optional().describe("Explicit base URL for a custom gateway.")
|
|
5311
|
+
}).optional().describe("Override the endpoint/gateway rail on restart (access is downstream)."),
|
|
5312
|
+
posture: z.union([
|
|
5313
|
+
z.enum(["default", "plan", "accept-edits", "bypass", "read-only"]),
|
|
5314
|
+
z.object({ harnessModeId: z.string().min(1) })
|
|
5315
|
+
]).optional().describe("Override the posture (what the agent may DO) on restart."),
|
|
5316
|
+
contextProfile: z.string().min(1).optional().describe("Override what enters context (full|lean|\u2026) on restart."),
|
|
5317
|
+
mode: z.string().min(1).optional().describe("Legacy AIP-45 mode id override, forwarded verbatim to the driver at spawn.")
|
|
4682
5318
|
},
|
|
4683
5319
|
async (input) => {
|
|
4684
5320
|
const prev = registry.findByIdOrName(input.idOrName);
|
|
@@ -4694,7 +5330,10 @@ function registerSessionTools(rawServer, opts) {
|
|
|
4694
5330
|
};
|
|
4695
5331
|
}
|
|
4696
5332
|
if (callerScope) {
|
|
4697
|
-
const subtree = collectSubtree(
|
|
5333
|
+
const subtree = collectSubtree(
|
|
5334
|
+
callerScope.ownerSessionId,
|
|
5335
|
+
registry.list({ includeArchived: true })
|
|
5336
|
+
);
|
|
4698
5337
|
if (!subtree.has(prev.id)) {
|
|
4699
5338
|
return {
|
|
4700
5339
|
content: [
|
|
@@ -4712,6 +5351,84 @@ function registerSessionTools(rawServer, opts) {
|
|
|
4712
5351
|
};
|
|
4713
5352
|
}
|
|
4714
5353
|
}
|
|
5354
|
+
const overrides = {
|
|
5355
|
+
...input.model !== void 0 ? { model: input.model } : {},
|
|
5356
|
+
...input.effort !== void 0 ? { effort: input.effort } : {},
|
|
5357
|
+
...input.access !== void 0 ? { access: input.access } : {},
|
|
5358
|
+
...input.route !== void 0 ? { route: input.route } : {},
|
|
5359
|
+
...input.posture !== void 0 ? { posture: input.posture } : {},
|
|
5360
|
+
...input.contextProfile !== void 0 ? { contextProfile: input.contextProfile } : {},
|
|
5361
|
+
...input.mode !== void 0 ? { mode: input.mode } : {}
|
|
5362
|
+
};
|
|
5363
|
+
if (Object.keys(overrides).length > 0) {
|
|
5364
|
+
if (!prev.adapterSlug || !resolveAgentAdapter) {
|
|
5365
|
+
return {
|
|
5366
|
+
content: [
|
|
5367
|
+
{
|
|
5368
|
+
type: "text",
|
|
5369
|
+
text: JSON.stringify({
|
|
5370
|
+
error: "restart_override_invalid",
|
|
5371
|
+
status: 400,
|
|
5372
|
+
message: "session_restart: restart-with-override only applies to agent-cli sessions (a PTY/command session has no config axes to override).",
|
|
5373
|
+
ok: false,
|
|
5374
|
+
sessionId: prev.id
|
|
5375
|
+
})
|
|
5376
|
+
}
|
|
5377
|
+
],
|
|
5378
|
+
isError: true
|
|
5379
|
+
};
|
|
5380
|
+
}
|
|
5381
|
+
try {
|
|
5382
|
+
const restarted = await restartAgentSession(registry, resolveAgentAdapter, prev, {
|
|
5383
|
+
forceAgentResume: true,
|
|
5384
|
+
overrides
|
|
5385
|
+
});
|
|
5386
|
+
return {
|
|
5387
|
+
content: [
|
|
5388
|
+
{
|
|
5389
|
+
type: "text",
|
|
5390
|
+
text: JSON.stringify(
|
|
5391
|
+
{
|
|
5392
|
+
...restarted.desc,
|
|
5393
|
+
resumedFrom: restarted.resumedFrom,
|
|
5394
|
+
resumeVia: restarted.resumeVia,
|
|
5395
|
+
...restarted.resumeFallback ? { resumeFallback: true } : {}
|
|
5396
|
+
},
|
|
5397
|
+
null,
|
|
5398
|
+
2
|
|
5399
|
+
)
|
|
5400
|
+
}
|
|
5401
|
+
]
|
|
5402
|
+
};
|
|
5403
|
+
} catch (err) {
|
|
5404
|
+
if (err instanceof RestartOverrideError) {
|
|
5405
|
+
return {
|
|
5406
|
+
content: [
|
|
5407
|
+
{
|
|
5408
|
+
type: "text",
|
|
5409
|
+
text: JSON.stringify({
|
|
5410
|
+
error: err.code,
|
|
5411
|
+
status: err.status,
|
|
5412
|
+
message: err.message,
|
|
5413
|
+
ok: false,
|
|
5414
|
+
sessionId: prev.id
|
|
5415
|
+
})
|
|
5416
|
+
}
|
|
5417
|
+
],
|
|
5418
|
+
isError: true
|
|
5419
|
+
};
|
|
5420
|
+
}
|
|
5421
|
+
return {
|
|
5422
|
+
content: [
|
|
5423
|
+
{
|
|
5424
|
+
type: "text",
|
|
5425
|
+
text: `session_restart: ${err instanceof Error ? err.message : String(err)}`
|
|
5426
|
+
}
|
|
5427
|
+
],
|
|
5428
|
+
isError: true
|
|
5429
|
+
};
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
4715
5432
|
const augmented = await augmentWithFsResume(prev);
|
|
4716
5433
|
const strategy = decideRestartStrategy(augmented);
|
|
4717
5434
|
if (strategy.kind === "unsupported") {
|
|
@@ -4743,17 +5460,15 @@ function registerSessionTools(rawServer, opts) {
|
|
|
4743
5460
|
cols: input.cols ?? 80,
|
|
4744
5461
|
rows: input.rows ?? 24,
|
|
4745
5462
|
...prev.name ? { name: prev.name } : {},
|
|
4746
|
-
...prev.label ? { label: prev.label } : {}
|
|
5463
|
+
...prev.label ? { label: prev.label } : {},
|
|
5464
|
+
resumedFrom: prev.id,
|
|
5465
|
+
resumeVia: describeResumePath(augmented)
|
|
4747
5466
|
});
|
|
4748
5467
|
return {
|
|
4749
5468
|
content: [
|
|
4750
5469
|
{
|
|
4751
5470
|
type: "text",
|
|
4752
|
-
text: JSON.stringify(
|
|
4753
|
-
{ ...desc, resumedFrom: prev.id, resumeVia: describeResumePath(augmented) },
|
|
4754
|
-
null,
|
|
4755
|
-
2
|
|
4756
|
-
)
|
|
5471
|
+
text: JSON.stringify(desc, null, 2)
|
|
4757
5472
|
}
|
|
4758
5473
|
]
|
|
4759
5474
|
};
|
|
@@ -4774,36 +5489,158 @@ function registerSessionTools(rawServer, opts) {
|
|
|
4774
5489
|
content: [
|
|
4775
5490
|
{
|
|
4776
5491
|
type: "text",
|
|
4777
|
-
text: "session_restart: agent_start is not enabled \u2014 the daemon was started without an adapter resolver."
|
|
5492
|
+
text: "session_restart: agent_start is not enabled \u2014 the daemon was started without an adapter resolver."
|
|
5493
|
+
}
|
|
5494
|
+
],
|
|
5495
|
+
isError: true
|
|
5496
|
+
};
|
|
5497
|
+
}
|
|
5498
|
+
const restarted = await restartAgentSession(registry, resolveAgentAdapter, prev);
|
|
5499
|
+
return {
|
|
5500
|
+
content: [
|
|
5501
|
+
{
|
|
5502
|
+
type: "text",
|
|
5503
|
+
text: JSON.stringify(
|
|
5504
|
+
{
|
|
5505
|
+
...restarted.desc,
|
|
5506
|
+
resumedFrom: restarted.resumedFrom,
|
|
5507
|
+
resumeVia: restarted.resumeVia,
|
|
5508
|
+
...restarted.resumeFallback ? { resumeFallback: true } : {}
|
|
5509
|
+
},
|
|
5510
|
+
null,
|
|
5511
|
+
2
|
|
5512
|
+
)
|
|
5513
|
+
}
|
|
5514
|
+
]
|
|
5515
|
+
};
|
|
5516
|
+
} catch (err) {
|
|
5517
|
+
return {
|
|
5518
|
+
content: [
|
|
5519
|
+
{
|
|
5520
|
+
type: "text",
|
|
5521
|
+
text: `session_restart: ${err instanceof Error ? err.message : String(err)}`
|
|
5522
|
+
}
|
|
5523
|
+
],
|
|
5524
|
+
isError: true
|
|
5525
|
+
};
|
|
5526
|
+
}
|
|
5527
|
+
}
|
|
5528
|
+
);
|
|
5529
|
+
server.tool(
|
|
5530
|
+
"session_archive",
|
|
5531
|
+
"Archive a terminal-status session (exited/killed/error) so it drops out of `session_list`'s / `GET /sessions`'s default view \u2014 a housekeeping flag, not a daemon action: the session's history and transcript are untouched and stay fully readable (`session_usage`, `agent_export`, or `session_list({ includeArchived: true })`). Refuses a still-alive session (running/starting) \u2014 archiving one would hide it from view while it keeps working unattended. Use `session_unarchive` to restore visibility.",
|
|
5532
|
+
{
|
|
5533
|
+
idOrName: z.string().min(1).describe(
|
|
5534
|
+
"Session id or name to archive \u2014 from `session_list`, must be terminal-status."
|
|
5535
|
+
)
|
|
5536
|
+
},
|
|
5537
|
+
async (input) => {
|
|
5538
|
+
const prev = registry.findByIdOrName(input.idOrName);
|
|
5539
|
+
if (!prev) {
|
|
5540
|
+
return {
|
|
5541
|
+
content: [
|
|
5542
|
+
{
|
|
5543
|
+
type: "text",
|
|
5544
|
+
text: JSON.stringify({ error: `no session "${input.idOrName}" found` })
|
|
5545
|
+
}
|
|
5546
|
+
],
|
|
5547
|
+
isError: true
|
|
5548
|
+
};
|
|
5549
|
+
}
|
|
5550
|
+
if (callerScope) {
|
|
5551
|
+
const subtree = collectSubtree(
|
|
5552
|
+
callerScope.ownerSessionId,
|
|
5553
|
+
registry.list({ includeArchived: true })
|
|
5554
|
+
);
|
|
5555
|
+
if (!subtree.has(prev.id)) {
|
|
5556
|
+
return {
|
|
5557
|
+
content: [
|
|
5558
|
+
{
|
|
5559
|
+
type: "text",
|
|
5560
|
+
text: JSON.stringify({
|
|
5561
|
+
error: "orchestrator_session_out_of_scope",
|
|
5562
|
+
message: `session_archive: session "${prev.id}" is not in your subtree \u2014 a scoped orchestrator can only archive sessions it (transitively) spawned.`,
|
|
5563
|
+
ok: false,
|
|
5564
|
+
sessionId: prev.id
|
|
5565
|
+
})
|
|
5566
|
+
}
|
|
5567
|
+
],
|
|
5568
|
+
isError: true
|
|
5569
|
+
};
|
|
5570
|
+
}
|
|
5571
|
+
}
|
|
5572
|
+
try {
|
|
5573
|
+
const desc = registry.archiveSession(prev.id);
|
|
5574
|
+
return {
|
|
5575
|
+
content: [{ type: "text", text: JSON.stringify(desc, null, 2) }]
|
|
5576
|
+
};
|
|
5577
|
+
} catch (err) {
|
|
5578
|
+
return {
|
|
5579
|
+
content: [
|
|
5580
|
+
{
|
|
5581
|
+
type: "text",
|
|
5582
|
+
text: `session_archive: ${err instanceof Error ? err.message : String(err)}`
|
|
5583
|
+
}
|
|
5584
|
+
],
|
|
5585
|
+
isError: true
|
|
5586
|
+
};
|
|
5587
|
+
}
|
|
5588
|
+
}
|
|
5589
|
+
);
|
|
5590
|
+
server.tool(
|
|
5591
|
+
"session_unarchive",
|
|
5592
|
+
"Restore an archived session to `session_list`'s default view \u2014 the inverse of `session_archive`. No status guard: archiving never touches daemon state, so there is nothing to re-validate \u2014 any archived session can be unarchived at any time.",
|
|
5593
|
+
{
|
|
5594
|
+
idOrName: z.string().min(1).describe(
|
|
5595
|
+
"Session id or name to unarchive \u2014 find it via `session_list({ includeArchived: true })`."
|
|
5596
|
+
)
|
|
5597
|
+
},
|
|
5598
|
+
async (input) => {
|
|
5599
|
+
const prev = registry.findByIdOrName(input.idOrName);
|
|
5600
|
+
if (!prev) {
|
|
5601
|
+
return {
|
|
5602
|
+
content: [
|
|
5603
|
+
{
|
|
5604
|
+
type: "text",
|
|
5605
|
+
text: JSON.stringify({ error: `no session "${input.idOrName}" found` })
|
|
5606
|
+
}
|
|
5607
|
+
],
|
|
5608
|
+
isError: true
|
|
5609
|
+
};
|
|
5610
|
+
}
|
|
5611
|
+
if (callerScope) {
|
|
5612
|
+
const subtree = collectSubtree(
|
|
5613
|
+
callerScope.ownerSessionId,
|
|
5614
|
+
registry.list({ includeArchived: true })
|
|
5615
|
+
);
|
|
5616
|
+
if (!subtree.has(prev.id)) {
|
|
5617
|
+
return {
|
|
5618
|
+
content: [
|
|
5619
|
+
{
|
|
5620
|
+
type: "text",
|
|
5621
|
+
text: JSON.stringify({
|
|
5622
|
+
error: "orchestrator_session_out_of_scope",
|
|
5623
|
+
message: `session_unarchive: session "${prev.id}" is not in your subtree \u2014 a scoped orchestrator can only unarchive sessions it (transitively) spawned.`,
|
|
5624
|
+
ok: false,
|
|
5625
|
+
sessionId: prev.id
|
|
5626
|
+
})
|
|
4778
5627
|
}
|
|
4779
5628
|
],
|
|
4780
5629
|
isError: true
|
|
4781
5630
|
};
|
|
4782
5631
|
}
|
|
4783
|
-
|
|
5632
|
+
}
|
|
5633
|
+
try {
|
|
5634
|
+
const desc = registry.unarchiveSession(prev.id);
|
|
4784
5635
|
return {
|
|
4785
|
-
content: [
|
|
4786
|
-
{
|
|
4787
|
-
type: "text",
|
|
4788
|
-
text: JSON.stringify(
|
|
4789
|
-
{
|
|
4790
|
-
...restarted.desc,
|
|
4791
|
-
resumedFrom: restarted.resumedFrom,
|
|
4792
|
-
resumeVia: restarted.resumeVia,
|
|
4793
|
-
...restarted.resumeFallback ? { resumeFallback: true } : {}
|
|
4794
|
-
},
|
|
4795
|
-
null,
|
|
4796
|
-
2
|
|
4797
|
-
)
|
|
4798
|
-
}
|
|
4799
|
-
]
|
|
5636
|
+
content: [{ type: "text", text: JSON.stringify(desc, null, 2) }]
|
|
4800
5637
|
};
|
|
4801
5638
|
} catch (err) {
|
|
4802
5639
|
return {
|
|
4803
5640
|
content: [
|
|
4804
5641
|
{
|
|
4805
5642
|
type: "text",
|
|
4806
|
-
text: `
|
|
5643
|
+
text: `session_unarchive: ${err instanceof Error ? err.message : String(err)}`
|
|
4807
5644
|
}
|
|
4808
5645
|
],
|
|
4809
5646
|
isError: true
|
|
@@ -4872,7 +5709,16 @@ function registerSessionTools(rawServer, opts) {
|
|
|
4872
5709
|
cols: input.cols ?? 80,
|
|
4873
5710
|
rows: input.rows ?? 24,
|
|
4874
5711
|
...input.name ? { name: input.name } : {},
|
|
4875
|
-
...input.label ? { label: input.label } : {}
|
|
5712
|
+
...input.label ? { label: input.label } : {},
|
|
5713
|
+
// Parent attribution + depth (orchestrator WP4) — same rule as
|
|
5714
|
+
// `agent_start` (session-spawn.ts): a spawn through a scoped
|
|
5715
|
+
// sub-gateway is attributed to the owning orchestrator so
|
|
5716
|
+
// `session_tree` shows the PTY as its child. Depth caps and
|
|
5717
|
+
// child quotas stay agent_start-only for now.
|
|
5718
|
+
...callerScope?.ownerSessionId ? {
|
|
5719
|
+
parentSessionId: callerScope.ownerSessionId,
|
|
5720
|
+
depth: callerScope.depth + 1
|
|
5721
|
+
} : {}
|
|
4876
5722
|
});
|
|
4877
5723
|
return {
|
|
4878
5724
|
content: [{ type: "text", text: JSON.stringify(desc, null, 2) }]
|
|
@@ -4951,10 +5797,13 @@ function registerSessionTools(rawServer, opts) {
|
|
|
4951
5797
|
);
|
|
4952
5798
|
server.tool(
|
|
4953
5799
|
"terminal_output",
|
|
4954
|
-
"Snapshot the recent byte buffer of a PTY session. Returns base64-encoded bytes (the buffer is RAW including ANSI escapes
|
|
5800
|
+
"Snapshot the recent byte buffer of a PTY session. Returns base64-encoded bytes (the buffer is RAW including ANSI escapes) by default; pass `clean: true` for ANSI-stripped plain text instead. `lastBytes` caps the read from the tail.",
|
|
4955
5801
|
{
|
|
4956
5802
|
sessionId: z.string().describe("Session id OR name from terminal_start."),
|
|
4957
|
-
lastBytes: z.number().int().min(1).max(64 * 1024).optional().describe("Max bytes from the tail. Default: full ring buffer (~64 KiB).")
|
|
5803
|
+
lastBytes: z.number().int().min(1).max(64 * 1024).optional().describe("Max bytes from the tail. Default: full ring buffer (~64 KiB)."),
|
|
5804
|
+
clean: mcpBool2.optional().describe(
|
|
5805
|
+
"Strip ANSI codes, returning human-readable text (as `text` instead of `b64`). Default false = raw base64."
|
|
5806
|
+
)
|
|
4958
5807
|
},
|
|
4959
5808
|
async (input) => {
|
|
4960
5809
|
if (!ptyEnabled) return ptyNotConfigured("terminal_output");
|
|
@@ -4994,7 +5843,7 @@ function registerSessionTools(rawServer, opts) {
|
|
|
4994
5843
|
sessionId: desc.id,
|
|
4995
5844
|
status: desc.status,
|
|
4996
5845
|
bytes: buf.byteLength,
|
|
4997
|
-
b64: buf.toString("base64")
|
|
5846
|
+
...input.clean ? { text: stripAnsi(buf.toString("utf8")) } : { b64: buf.toString("base64") }
|
|
4998
5847
|
},
|
|
4999
5848
|
null,
|
|
5000
5849
|
2
|
|
@@ -7811,6 +8660,9 @@ function filterSessionObserver(inner, shouldObserve) {
|
|
|
7811
8660
|
// src/sessions.ts
|
|
7812
8661
|
init_tool_presenter();
|
|
7813
8662
|
init_transcript_writer();
|
|
8663
|
+
|
|
8664
|
+
// src/conversation-index.ts
|
|
8665
|
+
init_conversation_store();
|
|
7814
8666
|
var DEFAULT_BUCKET = "default";
|
|
7815
8667
|
var BUCKETS_ROOT = () => resolve(homedir(), ".agentproto", "workspaces");
|
|
7816
8668
|
var LEGACY_SESSIONS_FILE = () => resolve(homedir(), ".agentproto", "sessions.json");
|
|
@@ -7934,6 +8786,110 @@ function readBucketRows(root, slug) {
|
|
|
7934
8786
|
return [];
|
|
7935
8787
|
}
|
|
7936
8788
|
}
|
|
8789
|
+
var rowId = (row) => row && typeof row === "object" && "id" in row && typeof row.id === "string" ? row.id : void 0;
|
|
8790
|
+
function mergeBucketRows(onDisk, rows, everHeldIds) {
|
|
8791
|
+
const incomingIds = new Set(rows.map(rowId).filter((id) => id !== void 0));
|
|
8792
|
+
const preserved = onDisk.filter((row) => {
|
|
8793
|
+
const id = rowId(row);
|
|
8794
|
+
return id !== void 0 && !incomingIds.has(id) && !everHeldIds.has(id);
|
|
8795
|
+
});
|
|
8796
|
+
return [...rows, ...preserved];
|
|
8797
|
+
}
|
|
8798
|
+
|
|
8799
|
+
// src/conversation-index.ts
|
|
8800
|
+
function conversationIndexPath(bucketsRoot, slug) {
|
|
8801
|
+
return join(bucketDir(bucketsRoot, slug), "conversations.jsonl");
|
|
8802
|
+
}
|
|
8803
|
+
async function listClaudeSubagents(projectDir, adapterSessionId) {
|
|
8804
|
+
const dir = join(projectDir, adapterSessionId, "subagents");
|
|
8805
|
+
let entries;
|
|
8806
|
+
try {
|
|
8807
|
+
entries = await promises.readdir(dir);
|
|
8808
|
+
} catch {
|
|
8809
|
+
return [];
|
|
8810
|
+
}
|
|
8811
|
+
return entries.filter((e) => e.startsWith("agent-") && e.endsWith(".jsonl")).map((e) => join(dir, e)).sort();
|
|
8812
|
+
}
|
|
8813
|
+
async function resolveNativeLink(input) {
|
|
8814
|
+
const { cwd, adapterSlug, adapterSessionId } = input;
|
|
8815
|
+
if (adapterSlug === "claude-code") {
|
|
8816
|
+
const dir = claudeCodeProjectDir(cwd);
|
|
8817
|
+
const path = join(dir, `${adapterSessionId}.jsonl`);
|
|
8818
|
+
const subagents = await listClaudeSubagents(dir, adapterSessionId);
|
|
8819
|
+
return { kind: "claude-jsonl", path, subagents };
|
|
8820
|
+
}
|
|
8821
|
+
if (adapterSlug === "hermes") {
|
|
8822
|
+
return {
|
|
8823
|
+
kind: "hermes-sqlite",
|
|
8824
|
+
dbPath: join(homedir(), ".hermes", "state.db"),
|
|
8825
|
+
rowId: adapterSessionId
|
|
8826
|
+
};
|
|
8827
|
+
}
|
|
8828
|
+
return void 0;
|
|
8829
|
+
}
|
|
8830
|
+
async function appendConversationRecord(bucketsRoot, slug, record) {
|
|
8831
|
+
const path = conversationIndexPath(bucketsRoot, slug);
|
|
8832
|
+
await promises.mkdir(dirname(path), { recursive: true });
|
|
8833
|
+
await promises.appendFile(path, JSON.stringify(record) + "\n", "utf8");
|
|
8834
|
+
}
|
|
8835
|
+
function isConversationIndexRecord(value) {
|
|
8836
|
+
if (!value || typeof value !== "object") return false;
|
|
8837
|
+
const r = value;
|
|
8838
|
+
return typeof r.sessionId === "string" && typeof r.workspace === "string" && typeof r.cwd === "string" && typeof r.adapterSlug === "string" && typeof r.adapterSessionId === "string" && typeof r.agentprotoTranscript === "string" && typeof r.startedAt === "string";
|
|
8839
|
+
}
|
|
8840
|
+
async function readConversationIndex(bucketsRoot, slug) {
|
|
8841
|
+
const path = conversationIndexPath(bucketsRoot, slug);
|
|
8842
|
+
let raw;
|
|
8843
|
+
try {
|
|
8844
|
+
raw = await promises.readFile(path, "utf8");
|
|
8845
|
+
} catch {
|
|
8846
|
+
return [];
|
|
8847
|
+
}
|
|
8848
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
8849
|
+
for (const line of raw.split("\n")) {
|
|
8850
|
+
const trimmed = line.trim();
|
|
8851
|
+
if (!trimmed) continue;
|
|
8852
|
+
let parsed;
|
|
8853
|
+
try {
|
|
8854
|
+
parsed = JSON.parse(trimmed);
|
|
8855
|
+
} catch {
|
|
8856
|
+
continue;
|
|
8857
|
+
}
|
|
8858
|
+
if (!isConversationIndexRecord(parsed)) continue;
|
|
8859
|
+
bySession.set(parsed.sessionId, parsed);
|
|
8860
|
+
}
|
|
8861
|
+
return Array.from(bySession.values());
|
|
8862
|
+
}
|
|
8863
|
+
async function findConversationRecord(bucketsRoot, slug, sessionId) {
|
|
8864
|
+
const rows = await readConversationIndex(bucketsRoot, slug);
|
|
8865
|
+
return rows.find((r) => r.sessionId === sessionId);
|
|
8866
|
+
}
|
|
8867
|
+
async function locateConversationBySessionId(bucketsRoot, listBuckets2, sessionId) {
|
|
8868
|
+
for (const slug of listBuckets2()) {
|
|
8869
|
+
const record = await findConversationRecord(bucketsRoot, slug, sessionId);
|
|
8870
|
+
if (record) return { workspace: slug, record };
|
|
8871
|
+
}
|
|
8872
|
+
return void 0;
|
|
8873
|
+
}
|
|
8874
|
+
async function locateConversationByNativePath(bucketsRoot, listBuckets2, nativePath) {
|
|
8875
|
+
const target = resolve(nativePath);
|
|
8876
|
+
for (const slug of listBuckets2()) {
|
|
8877
|
+
const rows = await readConversationIndex(bucketsRoot, slug);
|
|
8878
|
+
for (const record of rows) {
|
|
8879
|
+
if (!record.native || record.native.kind !== "claude-jsonl") continue;
|
|
8880
|
+
if (resolve(record.native.path) === target) {
|
|
8881
|
+
return { workspace: slug, record };
|
|
8882
|
+
}
|
|
8883
|
+
const matchedSubagentPath = record.native.subagents.find(
|
|
8884
|
+
(p) => resolve(p) === target
|
|
8885
|
+
);
|
|
8886
|
+
if (matchedSubagentPath) {
|
|
8887
|
+
return { workspace: slug, record, matchedSubagentPath };
|
|
8888
|
+
}
|
|
8889
|
+
}
|
|
8890
|
+
}
|
|
8891
|
+
return void 0;
|
|
8892
|
+
}
|
|
7937
8893
|
|
|
7938
8894
|
// src/terminal-transcript-writer.ts
|
|
7939
8895
|
init_transcript_writer();
|
|
@@ -8111,6 +9067,15 @@ var SessionNotAliveError = class extends Error {
|
|
|
8111
9067
|
var RECENT_LINES_CAP = 500;
|
|
8112
9068
|
var RECENT_BYTES_CAP = 64 * 1024;
|
|
8113
9069
|
var PERSIST_DEBOUNCE_MS = 1500;
|
|
9070
|
+
var EMPTY_ID_SET = /* @__PURE__ */ new Set();
|
|
9071
|
+
function markHeldId(map, slug, id) {
|
|
9072
|
+
let set = map.get(slug);
|
|
9073
|
+
if (!set) {
|
|
9074
|
+
set = /* @__PURE__ */ new Set();
|
|
9075
|
+
map.set(slug, set);
|
|
9076
|
+
}
|
|
9077
|
+
set.add(id);
|
|
9078
|
+
}
|
|
8114
9079
|
var HISTORY_CAP = 200;
|
|
8115
9080
|
var INTERRUPT_SETTLE_TIMEOUT_MS = 6e4;
|
|
8116
9081
|
function stampProcessAlive(desc) {
|
|
@@ -8135,6 +9100,11 @@ function findPriorCommandSessionId(liveSessions, cwd) {
|
|
|
8135
9100
|
}
|
|
8136
9101
|
return best?.desc.id;
|
|
8137
9102
|
}
|
|
9103
|
+
function currentRouteOf(desc) {
|
|
9104
|
+
if (desc.route?.gateway) return desc.route.gateway;
|
|
9105
|
+
if (desc.model) return tryParseModelRef(desc.model)?.route;
|
|
9106
|
+
return void 0;
|
|
9107
|
+
}
|
|
8138
9108
|
function worktreeFields(cwd) {
|
|
8139
9109
|
const identity = resolveWorktreeIdentity(cwd);
|
|
8140
9110
|
if (!identity) return {};
|
|
@@ -8185,6 +9155,8 @@ function createSessionsRegistry(opts) {
|
|
|
8185
9155
|
let nextSubId = 1;
|
|
8186
9156
|
let shutdownDone = false;
|
|
8187
9157
|
const knownBuckets = /* @__PURE__ */ new Set();
|
|
9158
|
+
const sourceBucketOf = /* @__PURE__ */ new Map();
|
|
9159
|
+
const heldIdsByBucket = /* @__PURE__ */ new Map();
|
|
8188
9160
|
if (persist) {
|
|
8189
9161
|
if (partitioned) {
|
|
8190
9162
|
migrateLegacySessionsFile({
|
|
@@ -8197,13 +9169,17 @@ function createSessionsRegistry(opts) {
|
|
|
8197
9169
|
loadHistorySnapshot(
|
|
8198
9170
|
bucketSessionsFile(bucketsRoot, slug),
|
|
8199
9171
|
sessions,
|
|
8200
|
-
sessionEvents
|
|
9172
|
+
sessionEvents,
|
|
9173
|
+
slug,
|
|
9174
|
+
sourceBucketOf,
|
|
9175
|
+
heldIdsByBucket
|
|
8201
9176
|
);
|
|
8202
9177
|
}
|
|
8203
9178
|
} else {
|
|
8204
9179
|
loadHistorySnapshot(legacyPath, sessions, sessionEvents);
|
|
8205
9180
|
}
|
|
8206
9181
|
}
|
|
9182
|
+
const bootLoadedBuckets = new Set(knownBuckets);
|
|
8207
9183
|
const onProcessExit = () => {
|
|
8208
9184
|
shutdownImpl();
|
|
8209
9185
|
};
|
|
@@ -8287,6 +9263,39 @@ function createSessionsRegistry(opts) {
|
|
|
8287
9263
|
}
|
|
8288
9264
|
delete rt.desc.awaitingPermission;
|
|
8289
9265
|
};
|
|
9266
|
+
const recordConversationLink = (rt, adapterSessionIdOverride) => {
|
|
9267
|
+
if (!persist || !partitioned) return;
|
|
9268
|
+
const desc = rt.desc;
|
|
9269
|
+
if (desc.kind !== "agent-cli") return;
|
|
9270
|
+
const adapterSlug = desc.adapterSlug;
|
|
9271
|
+
const adapterSessionId = adapterSessionIdOverride ?? desc.adapterSessionId;
|
|
9272
|
+
const cwd = desc.cwd;
|
|
9273
|
+
if (!adapterSlug || !adapterSessionId || !cwd) return;
|
|
9274
|
+
void (async () => {
|
|
9275
|
+
try {
|
|
9276
|
+
const native = await resolveNativeLink({ cwd, adapterSlug, adapterSessionId });
|
|
9277
|
+
const registered = readRegisteredSlugs(workspacesConfigPath);
|
|
9278
|
+
const slug = resolveBucketSlug(desc.workspaceSlug, registered);
|
|
9279
|
+
const record = {
|
|
9280
|
+
sessionId: desc.id,
|
|
9281
|
+
workspace: slug,
|
|
9282
|
+
cwd,
|
|
9283
|
+
adapterSlug,
|
|
9284
|
+
adapterSessionId,
|
|
9285
|
+
...native ? { native } : {},
|
|
9286
|
+
agentprotoTranscript: sessionEventsPath(desc.id, transcriptBaseDir),
|
|
9287
|
+
...desc.title ? { title: desc.title } : {},
|
|
9288
|
+
startedAt: desc.startedAt,
|
|
9289
|
+
...desc.endedAt ? { endedAt: desc.endedAt } : {}
|
|
9290
|
+
};
|
|
9291
|
+
await appendConversationRecord(bucketsRoot, slug, record);
|
|
9292
|
+
} catch (err) {
|
|
9293
|
+
console.warn(
|
|
9294
|
+
`[sessions] conversation-index write failed for ${desc.id}: ${err instanceof Error ? err.message : String(err)}`
|
|
9295
|
+
);
|
|
9296
|
+
}
|
|
9297
|
+
})();
|
|
9298
|
+
};
|
|
8290
9299
|
const schedulePersist = () => {
|
|
8291
9300
|
if (!persist) return;
|
|
8292
9301
|
if (persistTimer) clearTimeout(persistTimer);
|
|
@@ -8303,7 +9312,8 @@ function createSessionsRegistry(opts) {
|
|
|
8303
9312
|
const groups = /* @__PURE__ */ new Map();
|
|
8304
9313
|
for (const slug of knownBuckets) groups.set(slug, []);
|
|
8305
9314
|
for (const desc of snapshotRows()) {
|
|
8306
|
-
const slug = resolveBucketSlug(desc.workspaceSlug, registered);
|
|
9315
|
+
const slug = sourceBucketOf.get(desc.id) ?? resolveBucketSlug(desc.workspaceSlug, registered);
|
|
9316
|
+
markHeldId(heldIdsByBucket, slug, desc.id);
|
|
8307
9317
|
const list = groups.get(slug);
|
|
8308
9318
|
if (list) list.push(desc);
|
|
8309
9319
|
else groups.set(slug, [desc]);
|
|
@@ -8311,6 +9321,11 @@ function createSessionsRegistry(opts) {
|
|
|
8311
9321
|
for (const slug of groups.keys()) knownBuckets.add(slug);
|
|
8312
9322
|
return groups;
|
|
8313
9323
|
};
|
|
9324
|
+
const rowsToWrite = (slug, rows) => bootLoadedBuckets.has(slug) ? rows : mergeBucketRows(
|
|
9325
|
+
readBucketRows(bucketsRoot, slug),
|
|
9326
|
+
rows,
|
|
9327
|
+
heldIdsByBucket.get(slug) ?? EMPTY_ID_SET
|
|
9328
|
+
);
|
|
8314
9329
|
const persistSnapshot = async () => {
|
|
8315
9330
|
try {
|
|
8316
9331
|
const savedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -8318,7 +9333,7 @@ function createSessionsRegistry(opts) {
|
|
|
8318
9333
|
for (const [slug, rows] of groupRowsByBucket()) {
|
|
8319
9334
|
await writeBucketSnapshot(bucketsRoot, slug, {
|
|
8320
9335
|
savedAt,
|
|
8321
|
-
sessions: rows
|
|
9336
|
+
sessions: rowsToWrite(slug, rows)
|
|
8322
9337
|
});
|
|
8323
9338
|
}
|
|
8324
9339
|
return;
|
|
@@ -8353,6 +9368,7 @@ function createSessionsRegistry(opts) {
|
|
|
8353
9368
|
[strategy.storeAs]: m[1]
|
|
8354
9369
|
};
|
|
8355
9370
|
schedulePersist();
|
|
9371
|
+
recordConversationLink(rt, m[1]);
|
|
8356
9372
|
}
|
|
8357
9373
|
};
|
|
8358
9374
|
const appendBytes = (rt, chunk) => {
|
|
@@ -8630,6 +9646,7 @@ function createSessionsRegistry(opts) {
|
|
|
8630
9646
|
rt.emitter.emit("status", rt.desc.status);
|
|
8631
9647
|
}
|
|
8632
9648
|
schedulePersist();
|
|
9649
|
+
recordConversationLink(rt);
|
|
8633
9650
|
} catch (err) {
|
|
8634
9651
|
const msg = err instanceof Error ? err.message : String(err);
|
|
8635
9652
|
appendLine(rt, `[error] resume failed: ${msg}`, "stderr");
|
|
@@ -8974,11 +9991,25 @@ function createSessionsRegistry(opts) {
|
|
|
8974
9991
|
...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
|
|
8975
9992
|
depth: input.depth ?? 0,
|
|
8976
9993
|
...input.model ? { model: input.model } : {},
|
|
9994
|
+
...input.mode ? { mode: input.mode } : {},
|
|
8977
9995
|
...input.auth ? { auth: input.auth } : {},
|
|
9996
|
+
// Decomposed config-axis echoes (SPEC §3.7), same optional-spread
|
|
9997
|
+
// shape as `model`/`mode`/`auth` above.
|
|
9998
|
+
...input.effort ? { effort: input.effort } : {},
|
|
9999
|
+
...input.posture !== void 0 ? { posture: input.posture } : {},
|
|
10000
|
+
...input.route ? { route: input.route } : {},
|
|
10001
|
+
...input.contextProfile ? { contextProfile: input.contextProfile } : {},
|
|
10002
|
+
...input.accessProfile ? { accessProfile: input.accessProfile } : {},
|
|
8978
10003
|
...priorCommandSessionId ? { priorCommandSessionId } : {},
|
|
8979
10004
|
...input.remote ? { remote: true } : {},
|
|
8980
10005
|
...input.sandboxId ? { sandboxId: input.sandboxId } : {},
|
|
8981
|
-
...input.sandboxTeardown ? { sandboxTeardown: input.sandboxTeardown } : {}
|
|
10006
|
+
...input.sandboxTeardown ? { sandboxTeardown: input.sandboxTeardown } : {},
|
|
10007
|
+
// Restart lineage (see SessionDescriptor.resumedFrom's doc). `resumeVia`
|
|
10008
|
+
// can legitimately be "" (a fresh fallback spawn with no continuity),
|
|
10009
|
+
// so it's gated on `!== undefined` rather than truthiness — a truthy
|
|
10010
|
+
// gate would silently drop the empty-string case.
|
|
10011
|
+
...input.resumedFrom ? { resumedFrom: input.resumedFrom } : {},
|
|
10012
|
+
...input.resumeVia !== void 0 ? { resumeVia: input.resumeVia } : {}
|
|
8982
10013
|
};
|
|
8983
10014
|
if (input.trace ?? opts?.langfuseTracingDefault ?? false) {
|
|
8984
10015
|
tracedSessions.add(id);
|
|
@@ -9006,6 +10037,7 @@ function createSessionsRegistry(opts) {
|
|
|
9006
10037
|
"stdout"
|
|
9007
10038
|
);
|
|
9008
10039
|
schedulePersist();
|
|
10040
|
+
recordConversationLink(rt);
|
|
9009
10041
|
if (input.initialPrompt) {
|
|
9010
10042
|
void runAgentTurn(rt, input.initialPrompt).catch((err) => {
|
|
9011
10043
|
appendLine(
|
|
@@ -9065,7 +10097,16 @@ function createSessionsRegistry(opts) {
|
|
|
9065
10097
|
...worktreeFields(input.cwd),
|
|
9066
10098
|
...input.name ? { name: input.name } : {},
|
|
9067
10099
|
...input.label ? { label: input.label } : {},
|
|
9068
|
-
...priorCommandSessionId ? { priorCommandSessionId } : {}
|
|
10100
|
+
...priorCommandSessionId ? { priorCommandSessionId } : {},
|
|
10101
|
+
// Parent attribution + depth (orchestrator WP4) — same recording
|
|
10102
|
+
// rule as spawnAgent above: depth always set so subtree/depth
|
|
10103
|
+
// logic never distinguishes "absent" from "root".
|
|
10104
|
+
...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
|
|
10105
|
+
depth: input.depth ?? 0,
|
|
10106
|
+
// Restart lineage — same gating rule as spawnAgent above (`resumeVia`
|
|
10107
|
+
// can legitimately be "").
|
|
10108
|
+
...input.resumedFrom ? { resumedFrom: input.resumedFrom } : {},
|
|
10109
|
+
...input.resumeVia !== void 0 ? { resumeVia: input.resumeVia } : {}
|
|
9069
10110
|
};
|
|
9070
10111
|
const rt = {
|
|
9071
10112
|
desc,
|
|
@@ -9231,14 +10272,126 @@ function createSessionsRegistry(opts) {
|
|
|
9231
10272
|
await interruptInFlightTurn(rt, id, "interruptSession");
|
|
9232
10273
|
return { wasBusy: true };
|
|
9233
10274
|
},
|
|
10275
|
+
async setModel(id, modelId) {
|
|
10276
|
+
const rt = sessions.get(id);
|
|
10277
|
+
if (!rt) throw new Error(`setModel: no session "${id}"`);
|
|
10278
|
+
if (rt.desc.kind !== "agent-cli" || !rt.agentSession) {
|
|
10279
|
+
throw new Error(`setModel: session "${id}" is not an agent-cli session`);
|
|
10280
|
+
}
|
|
10281
|
+
const targetRoute = tryParseModelRef(modelId)?.route;
|
|
10282
|
+
const currentRoute = currentRouteOf(rt.desc);
|
|
10283
|
+
if (targetRoute && currentRoute && targetRoute !== currentRoute) {
|
|
10284
|
+
return {
|
|
10285
|
+
applied: false,
|
|
10286
|
+
reason: "requires-restart",
|
|
10287
|
+
suggestedOverride: { route: { gateway: targetRoute }, model: modelId }
|
|
10288
|
+
};
|
|
10289
|
+
}
|
|
10290
|
+
if (!rt.agentSession.setModel) {
|
|
10291
|
+
return { applied: false, reason: "not-supported" };
|
|
10292
|
+
}
|
|
10293
|
+
const result = await rt.agentSession.setModel(modelId);
|
|
10294
|
+
if (result.applied) {
|
|
10295
|
+
rt.desc.model = result.model ?? modelId;
|
|
10296
|
+
schedulePersist();
|
|
10297
|
+
if (sessionEvents) {
|
|
10298
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
10299
|
+
sessionEvents.emit({
|
|
10300
|
+
type: "session:config-changed",
|
|
10301
|
+
sessionId: id,
|
|
10302
|
+
axis: "model",
|
|
10303
|
+
value: rt.desc.model,
|
|
10304
|
+
label: rt.desc.label,
|
|
10305
|
+
ts
|
|
10306
|
+
});
|
|
10307
|
+
sessionEvents.emit({
|
|
10308
|
+
type: "session:model-changed",
|
|
10309
|
+
sessionId: id,
|
|
10310
|
+
model: rt.desc.model,
|
|
10311
|
+
label: rt.desc.label,
|
|
10312
|
+
ts
|
|
10313
|
+
});
|
|
10314
|
+
}
|
|
10315
|
+
}
|
|
10316
|
+
return result;
|
|
10317
|
+
},
|
|
10318
|
+
emitConfigChanged(ev) {
|
|
10319
|
+
sessionEvents?.emit(ev);
|
|
10320
|
+
},
|
|
10321
|
+
async setEffort(id, effort) {
|
|
10322
|
+
const rt = sessions.get(id);
|
|
10323
|
+
if (!rt) throw new Error(`setEffort: no session "${id}"`);
|
|
10324
|
+
if (rt.desc.kind !== "agent-cli" || !rt.agentSession) {
|
|
10325
|
+
throw new Error(`setEffort: session "${id}" is not an agent-cli session`);
|
|
10326
|
+
}
|
|
10327
|
+
if (!rt.agentSession.setEffort) {
|
|
10328
|
+
return { applied: false, reason: "not-supported" };
|
|
10329
|
+
}
|
|
10330
|
+
const result = await rt.agentSession.setEffort(effort);
|
|
10331
|
+
if (result.applied) {
|
|
10332
|
+
rt.desc.effort = result.effort ?? effort;
|
|
10333
|
+
schedulePersist();
|
|
10334
|
+
if (sessionEvents) {
|
|
10335
|
+
sessionEvents.emit({
|
|
10336
|
+
type: "session:config-changed",
|
|
10337
|
+
sessionId: id,
|
|
10338
|
+
axis: "effort",
|
|
10339
|
+
value: rt.desc.effort,
|
|
10340
|
+
label: rt.desc.label,
|
|
10341
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
10342
|
+
});
|
|
10343
|
+
}
|
|
10344
|
+
}
|
|
10345
|
+
return result;
|
|
10346
|
+
},
|
|
10347
|
+
async setPosture(id, posture) {
|
|
10348
|
+
const rt = sessions.get(id);
|
|
10349
|
+
if (!rt) throw new Error(`setPosture: no session "${id}"`);
|
|
10350
|
+
if (rt.desc.kind !== "agent-cli" || !rt.agentSession) {
|
|
10351
|
+
throw new Error(`setPosture: session "${id}" is not an agent-cli session`);
|
|
10352
|
+
}
|
|
10353
|
+
const resolution = resolvePosture(posture, rt.agentSession.availableModes ?? []);
|
|
10354
|
+
if (resolution.kind !== "native") {
|
|
10355
|
+
return {
|
|
10356
|
+
applied: false,
|
|
10357
|
+
reason: "requires-restart",
|
|
10358
|
+
resolution: resolution.kind
|
|
10359
|
+
};
|
|
10360
|
+
}
|
|
10361
|
+
if (!rt.agentSession.setSessionMode) {
|
|
10362
|
+
return { applied: false, reason: "not-supported", resolution: "native" };
|
|
10363
|
+
}
|
|
10364
|
+
const result = await rt.agentSession.setSessionMode(resolution.mode.id);
|
|
10365
|
+
if (!result.applied) {
|
|
10366
|
+
return {
|
|
10367
|
+
applied: false,
|
|
10368
|
+
resolution: "native",
|
|
10369
|
+
...result.reason ? { reason: result.reason } : {}
|
|
10370
|
+
};
|
|
10371
|
+
}
|
|
10372
|
+
rt.desc.posture = posture;
|
|
10373
|
+
schedulePersist();
|
|
10374
|
+
if (sessionEvents) {
|
|
10375
|
+
sessionEvents.emit({
|
|
10376
|
+
type: "session:config-changed",
|
|
10377
|
+
sessionId: id,
|
|
10378
|
+
axis: "posture",
|
|
10379
|
+
value: posture,
|
|
10380
|
+
label: rt.desc.label,
|
|
10381
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
10382
|
+
});
|
|
10383
|
+
}
|
|
10384
|
+
return { applied: true, posture, modeId: resolution.mode.id, resolution: "native" };
|
|
10385
|
+
},
|
|
9234
10386
|
pulseActivity(id) {
|
|
9235
10387
|
const rt = sessions.get(id);
|
|
9236
10388
|
if (!rt) return;
|
|
9237
10389
|
rt.desc.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9238
10390
|
schedulePersist();
|
|
9239
10391
|
},
|
|
9240
|
-
list() {
|
|
9241
|
-
|
|
10392
|
+
list(opts2) {
|
|
10393
|
+
const includeArchived = opts2?.includeArchived ?? false;
|
|
10394
|
+
return Array.from(sessions.values()).map((s) => s.desc).filter((desc) => includeArchived || !desc.archived).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).map((desc) => {
|
|
9242
10395
|
stampProcessAlive(desc);
|
|
9243
10396
|
return desc;
|
|
9244
10397
|
});
|
|
@@ -9354,6 +10507,28 @@ function createSessionsRegistry(opts) {
|
|
|
9354
10507
|
emitExited(rt);
|
|
9355
10508
|
return true;
|
|
9356
10509
|
},
|
|
10510
|
+
archiveSession(id) {
|
|
10511
|
+
const rt = sessions.get(id);
|
|
10512
|
+
if (!rt) throw new Error(`archiveSession: no session "${id}"`);
|
|
10513
|
+
const isAlive = rt.desc.status === "running" || rt.desc.status === "starting";
|
|
10514
|
+
if (isAlive) {
|
|
10515
|
+
throw new Error(
|
|
10516
|
+
`archiveSession: session "${id}" is still ${rt.desc.status} \u2014 only a terminal-status session (exited/killed/error) can be archived.`
|
|
10517
|
+
);
|
|
10518
|
+
}
|
|
10519
|
+
rt.desc.archived = true;
|
|
10520
|
+
schedulePersist();
|
|
10521
|
+
stampProcessAlive(rt.desc);
|
|
10522
|
+
return rt.desc;
|
|
10523
|
+
},
|
|
10524
|
+
unarchiveSession(id) {
|
|
10525
|
+
const rt = sessions.get(id);
|
|
10526
|
+
if (!rt) throw new Error(`unarchiveSession: no session "${id}"`);
|
|
10527
|
+
rt.desc.archived = false;
|
|
10528
|
+
schedulePersist();
|
|
10529
|
+
stampProcessAlive(rt.desc);
|
|
10530
|
+
return rt.desc;
|
|
10531
|
+
},
|
|
9357
10532
|
listPendingPermissions(filter) {
|
|
9358
10533
|
const all = Array.from(pendingPermissions.values());
|
|
9359
10534
|
const scoped = filter?.sessionId ? all.filter((p) => p.sessionId === filter.sessionId) : all;
|
|
@@ -9478,7 +10653,7 @@ function createSessionsRegistry(opts) {
|
|
|
9478
10653
|
for (const [slug, rows] of groupRowsByBucket()) {
|
|
9479
10654
|
writeBucketSnapshotSync(bucketsRoot, slug, {
|
|
9480
10655
|
savedAt: nowIso,
|
|
9481
|
-
sessions: rows
|
|
10656
|
+
sessions: rowsToWrite(slug, rows)
|
|
9482
10657
|
});
|
|
9483
10658
|
}
|
|
9484
10659
|
} else {
|
|
@@ -9500,7 +10675,7 @@ function clearInFlightFlags(desc) {
|
|
|
9500
10675
|
desc.blockedOn = void 0;
|
|
9501
10676
|
desc.pendingToolCallId = void 0;
|
|
9502
10677
|
}
|
|
9503
|
-
function loadHistorySnapshot(persistPath, sessions, sessionEvents) {
|
|
10678
|
+
function loadHistorySnapshot(persistPath, sessions, sessionEvents, bucketSlug, sourceBucketOf, heldIdsByBucket) {
|
|
9504
10679
|
let raw;
|
|
9505
10680
|
try {
|
|
9506
10681
|
raw = readFileSync(persistPath, "utf8");
|
|
@@ -9552,6 +10727,10 @@ function loadHistorySnapshot(persistPath, sessions, sessionEvents) {
|
|
|
9552
10727
|
};
|
|
9553
10728
|
rt.emitter.setMaxListeners(50);
|
|
9554
10729
|
sessions.set(desc.id, rt);
|
|
10730
|
+
if (bucketSlug !== void 0) {
|
|
10731
|
+
sourceBucketOf?.set(desc.id, bucketSlug);
|
|
10732
|
+
if (heldIdsByBucket) markHeldId(heldIdsByBucket, bucketSlug, desc.id);
|
|
10733
|
+
}
|
|
9555
10734
|
if (wasAlive) {
|
|
9556
10735
|
sessionEvents?.emit({
|
|
9557
10736
|
type: "session:exited",
|
|
@@ -10427,7 +11606,7 @@ function registerOrchestrationTools(rawServer, opts) {
|
|
|
10427
11606
|
const server = opts.toolSubset ? withToolSubset(rawServer, opts.toolSubset) : rawServer;
|
|
10428
11607
|
const { registry, sessionEvents, eventRing, callerScope, inboundWatcher } = opts;
|
|
10429
11608
|
const isPolicyInSubtree = (policy, ownerId) => {
|
|
10430
|
-
const subtree = collectSubtree(ownerId, registry.list());
|
|
11609
|
+
const subtree = collectSubtree(ownerId, registry.list({ includeArchived: true }));
|
|
10431
11610
|
const ids = policy.sessionIds.length > 0 ? policy.sessionIds : [policy.sessionId];
|
|
10432
11611
|
return ids.every((id) => subtree.has(id));
|
|
10433
11612
|
};
|
|
@@ -10459,7 +11638,10 @@ function registerOrchestrationTools(rawServer, opts) {
|
|
|
10459
11638
|
const isSessionInScope = (sessionId) => {
|
|
10460
11639
|
if (!callerScope) return true;
|
|
10461
11640
|
if (!callerScope.ownerSessionId) return false;
|
|
10462
|
-
return collectSubtree(
|
|
11641
|
+
return collectSubtree(
|
|
11642
|
+
callerScope.ownerSessionId,
|
|
11643
|
+
registry.list({ includeArchived: true })
|
|
11644
|
+
).has(sessionId);
|
|
10463
11645
|
};
|
|
10464
11646
|
const enrichPermission2 = (p) => {
|
|
10465
11647
|
const desc = registry.get(p.sessionId);
|
|
@@ -10637,6 +11819,12 @@ function registerOrchestrationTools(rawServer, opts) {
|
|
|
10637
11819
|
"Reuse the session spawned by an earlier step (any prior stage), by that step's `label`. Ignored if `adapter` is set. Lets a later-stage step act on an earlier stage's output (e.g. a 'verify' step reusing a 'produce' step's session)."
|
|
10638
11820
|
),
|
|
10639
11821
|
cacheable: z.boolean().optional().describe("Cache this step's output under the run's cacheKey (opt-in; for idempotent/pure steps only)."),
|
|
11822
|
+
sandbox: z.union([
|
|
11823
|
+
z.string().min(1).describe("Sandbox provider slug from `list_sandbox_providers` (e.g. 'local', 'e2b')."),
|
|
11824
|
+
z.object({ provider: z.string().min(1) }).passthrough().describe("Inline AIP-36 SandboxSpec ({ provider, config?, env?, \u2026 }).")
|
|
11825
|
+
]).optional().describe(
|
|
11826
|
+
"Run this step's session inside a sandbox instead of on the host \u2014 same semantics as `agent_start.sandbox`. Only meaningful with `adapter`."
|
|
11827
|
+
),
|
|
10640
11828
|
policy: z.discriminatedUnion("awaiting", [
|
|
10641
11829
|
z.object({ awaiting: z.literal("auto-allow"), prompt: z.string() }),
|
|
10642
11830
|
z.object({
|
|
@@ -10858,7 +12046,10 @@ function registerOrchestrationTools(rawServer, opts) {
|
|
|
10858
12046
|
};
|
|
10859
12047
|
}
|
|
10860
12048
|
const targetIds = input.sessionIds && input.sessionIds.length > 0 ? input.sessionIds : input.sessionId ? [input.sessionId] : [];
|
|
10861
|
-
const subtree = collectSubtree(
|
|
12049
|
+
const subtree = collectSubtree(
|
|
12050
|
+
callerScope.ownerSessionId,
|
|
12051
|
+
registry.list({ includeArchived: true })
|
|
12052
|
+
);
|
|
10862
12053
|
const outside = targetIds.filter((id) => !subtree.has(id));
|
|
10863
12054
|
if (outside.length > 0) {
|
|
10864
12055
|
return {
|
|
@@ -11716,55 +12907,170 @@ async function startHttpServer(opts) {
|
|
|
11716
12907
|
await handleHeartbeatTick(req, res);
|
|
11717
12908
|
return;
|
|
11718
12909
|
}
|
|
11719
|
-
if (path === "/files/upload" && req.method === "POST") {
|
|
12910
|
+
if (path === "/files/upload" && req.method === "POST") {
|
|
12911
|
+
const gate = checkSessionsToken(req);
|
|
12912
|
+
if (gate !== "ok") {
|
|
12913
|
+
rejectUnauthorizedSession(req, res, gate);
|
|
12914
|
+
return;
|
|
12915
|
+
}
|
|
12916
|
+
await handleFileUpload(req, res, url);
|
|
12917
|
+
return;
|
|
12918
|
+
}
|
|
12919
|
+
if (opts.sessions && path.startsWith("/sessions")) {
|
|
12920
|
+
if (isMutatingSessionsRoute(req.method ?? "GET", path)) {
|
|
12921
|
+
const gate = checkSessionsToken(req);
|
|
12922
|
+
if (gate !== "ok") {
|
|
12923
|
+
rejectUnauthorizedSession(req, res, gate);
|
|
12924
|
+
return;
|
|
12925
|
+
}
|
|
12926
|
+
}
|
|
12927
|
+
const handled = await handleSessions(
|
|
12928
|
+
req,
|
|
12929
|
+
res,
|
|
12930
|
+
path,
|
|
12931
|
+
opts.sessions,
|
|
12932
|
+
opts.resolveAgentAdapter,
|
|
12933
|
+
opts.ptyEnabled === true,
|
|
12934
|
+
opts.resolveBrowserAdapter,
|
|
12935
|
+
opts.listBrowserAdapters,
|
|
12936
|
+
opts.sessionEvents,
|
|
12937
|
+
opts.eventRing,
|
|
12938
|
+
opts.buildOrchestratorMcp,
|
|
12939
|
+
opts.daemonMcpUrl,
|
|
12940
|
+
opts.provisionWorktree
|
|
12941
|
+
);
|
|
12942
|
+
if (handled) return;
|
|
12943
|
+
}
|
|
12944
|
+
if (path === "/workspaces" && req.method === "GET") {
|
|
12945
|
+
try {
|
|
12946
|
+
const config = await loadWorkspacesConfig();
|
|
12947
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
12948
|
+
res.end(JSON.stringify(config));
|
|
12949
|
+
} catch (err) {
|
|
12950
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
12951
|
+
res.end(
|
|
12952
|
+
JSON.stringify({
|
|
12953
|
+
error: "workspaces_load_failed",
|
|
12954
|
+
message: err instanceof Error ? err.message : String(err)
|
|
12955
|
+
})
|
|
12956
|
+
);
|
|
12957
|
+
}
|
|
12958
|
+
return;
|
|
12959
|
+
}
|
|
12960
|
+
if (path === "/workspaces" && req.method === "POST") {
|
|
12961
|
+
const gate = checkSessionsToken(req);
|
|
12962
|
+
if (gate !== "ok") {
|
|
12963
|
+
rejectUnauthorizedSession(req, res, gate);
|
|
12964
|
+
return;
|
|
12965
|
+
}
|
|
12966
|
+
const body = await readJsonBody(req);
|
|
12967
|
+
if (!body || typeof body.path !== "string" || !body.path) {
|
|
12968
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
12969
|
+
res.end(JSON.stringify({ error: "missing_path" }));
|
|
12970
|
+
return;
|
|
12971
|
+
}
|
|
12972
|
+
if (!isAbsolute(body.path)) {
|
|
12973
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
12974
|
+
res.end(
|
|
12975
|
+
JSON.stringify({
|
|
12976
|
+
error: "workspace_path_not_absolute",
|
|
12977
|
+
message: `path must be absolute, got "${body.path}".`
|
|
12978
|
+
})
|
|
12979
|
+
);
|
|
12980
|
+
return;
|
|
12981
|
+
}
|
|
12982
|
+
try {
|
|
12983
|
+
await stat(body.path);
|
|
12984
|
+
} catch {
|
|
12985
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
12986
|
+
res.end(
|
|
12987
|
+
JSON.stringify({
|
|
12988
|
+
error: "workspace_path_not_found",
|
|
12989
|
+
message: `"${body.path}" doesn't exist.`
|
|
12990
|
+
})
|
|
12991
|
+
);
|
|
12992
|
+
return;
|
|
12993
|
+
}
|
|
12994
|
+
if (body.slug !== void 0 && typeof body.slug !== "string") {
|
|
12995
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
12996
|
+
res.end(JSON.stringify({ error: "invalid_slug" }));
|
|
12997
|
+
return;
|
|
12998
|
+
}
|
|
12999
|
+
if (body.label !== void 0 && typeof body.label !== "string") {
|
|
13000
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
13001
|
+
res.end(JSON.stringify({ error: "invalid_label" }));
|
|
13002
|
+
return;
|
|
13003
|
+
}
|
|
13004
|
+
try {
|
|
13005
|
+
const config = await loadWorkspacesConfig();
|
|
13006
|
+
const next = addWorkspace(config, {
|
|
13007
|
+
slug: body.slug || basename(body.path),
|
|
13008
|
+
path: body.path,
|
|
13009
|
+
...body.label ? { label: body.label } : {}
|
|
13010
|
+
});
|
|
13011
|
+
await saveWorkspacesConfig(next);
|
|
13012
|
+
res.writeHead(201, { "content-type": "application/json" });
|
|
13013
|
+
res.end(JSON.stringify(next));
|
|
13014
|
+
} catch (err) {
|
|
13015
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
13016
|
+
res.end(
|
|
13017
|
+
JSON.stringify({
|
|
13018
|
+
error: "workspace_add_failed",
|
|
13019
|
+
message: err instanceof Error ? err.message : String(err)
|
|
13020
|
+
})
|
|
13021
|
+
);
|
|
13022
|
+
}
|
|
13023
|
+
return;
|
|
13024
|
+
}
|
|
13025
|
+
if (path === "/workspaces/active" && req.method === "PUT") {
|
|
11720
13026
|
const gate = checkSessionsToken(req);
|
|
11721
13027
|
if (gate !== "ok") {
|
|
11722
13028
|
rejectUnauthorizedSession(req, res, gate);
|
|
11723
13029
|
return;
|
|
11724
13030
|
}
|
|
11725
|
-
await
|
|
11726
|
-
|
|
11727
|
-
|
|
11728
|
-
|
|
11729
|
-
|
|
11730
|
-
const gate = checkSessionsToken(req);
|
|
11731
|
-
if (gate !== "ok") {
|
|
11732
|
-
rejectUnauthorizedSession(req, res, gate);
|
|
11733
|
-
return;
|
|
11734
|
-
}
|
|
13031
|
+
const body = await readJsonBody(req);
|
|
13032
|
+
if (!body || typeof body.slug !== "string" || !body.slug) {
|
|
13033
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
13034
|
+
res.end(JSON.stringify({ error: "missing_slug" }));
|
|
13035
|
+
return;
|
|
11735
13036
|
}
|
|
11736
|
-
const handled = await handleSessions(
|
|
11737
|
-
req,
|
|
11738
|
-
res,
|
|
11739
|
-
path,
|
|
11740
|
-
opts.sessions,
|
|
11741
|
-
opts.resolveAgentAdapter,
|
|
11742
|
-
opts.ptyEnabled === true,
|
|
11743
|
-
opts.resolveBrowserAdapter,
|
|
11744
|
-
opts.listBrowserAdapters,
|
|
11745
|
-
opts.sessionEvents,
|
|
11746
|
-
opts.eventRing,
|
|
11747
|
-
opts.buildOrchestratorMcp,
|
|
11748
|
-
opts.daemonMcpUrl
|
|
11749
|
-
);
|
|
11750
|
-
if (handled) return;
|
|
11751
|
-
}
|
|
11752
|
-
if (path === "/workspaces" && req.method === "GET") {
|
|
11753
13037
|
try {
|
|
11754
13038
|
const config = await loadWorkspacesConfig();
|
|
13039
|
+
const next = setActiveWorkspace(config, body.slug);
|
|
13040
|
+
await saveWorkspacesConfig(next);
|
|
11755
13041
|
res.writeHead(200, { "content-type": "application/json" });
|
|
11756
|
-
res.end(JSON.stringify(
|
|
13042
|
+
res.end(JSON.stringify(next));
|
|
11757
13043
|
} catch (err) {
|
|
11758
|
-
res.writeHead(
|
|
13044
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
11759
13045
|
res.end(
|
|
11760
13046
|
JSON.stringify({
|
|
11761
|
-
error: "
|
|
13047
|
+
error: "workspace_not_found",
|
|
11762
13048
|
message: err instanceof Error ? err.message : String(err)
|
|
11763
13049
|
})
|
|
11764
13050
|
);
|
|
11765
13051
|
}
|
|
11766
13052
|
return;
|
|
11767
13053
|
}
|
|
13054
|
+
const workspaceSlugMatch = path.match(/^\/workspaces\/([^/]+)$/);
|
|
13055
|
+
if (workspaceSlugMatch && req.method === "DELETE") {
|
|
13056
|
+
const gate = checkSessionsToken(req);
|
|
13057
|
+
if (gate !== "ok") {
|
|
13058
|
+
rejectUnauthorizedSession(req, res, gate);
|
|
13059
|
+
return;
|
|
13060
|
+
}
|
|
13061
|
+
const slug = decodeURIComponent(workspaceSlugMatch[1] ?? "");
|
|
13062
|
+
const config = await loadWorkspacesConfig();
|
|
13063
|
+
if (!findWorkspace(config, slug)) {
|
|
13064
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
13065
|
+
res.end(JSON.stringify({ error: "workspace_not_found", slug }));
|
|
13066
|
+
return;
|
|
13067
|
+
}
|
|
13068
|
+
const next = removeWorkspace(config, slug);
|
|
13069
|
+
await saveWorkspacesConfig(next);
|
|
13070
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
13071
|
+
res.end(JSON.stringify(next));
|
|
13072
|
+
return;
|
|
13073
|
+
}
|
|
11768
13074
|
if (path === "/mcps/imports" && req.method === "GET") {
|
|
11769
13075
|
const config = await loadImportedMcps();
|
|
11770
13076
|
res.writeHead(200, { "content-type": "application/json" });
|
|
@@ -11920,6 +13226,41 @@ async function startHttpServer(opts) {
|
|
|
11920
13226
|
}
|
|
11921
13227
|
return;
|
|
11922
13228
|
}
|
|
13229
|
+
if (path === "/catalog/models" && req.method === "GET") {
|
|
13230
|
+
if (!opts.listCatalogModels) {
|
|
13231
|
+
res.writeHead(501, { "content-type": "application/json" });
|
|
13232
|
+
res.end(
|
|
13233
|
+
JSON.stringify({
|
|
13234
|
+
error: "lister_not_configured",
|
|
13235
|
+
message: "Daemon was started without `listCatalogModels` \u2014 see `buildCatalogModels` in `catalog-models.ts`."
|
|
13236
|
+
})
|
|
13237
|
+
);
|
|
13238
|
+
return;
|
|
13239
|
+
}
|
|
13240
|
+
try {
|
|
13241
|
+
const qs = new URLSearchParams(
|
|
13242
|
+
url.includes("?") ? url.slice(url.indexOf("?") + 1) : ""
|
|
13243
|
+
);
|
|
13244
|
+
const runnableOnlyParam = qs.get("runnableOnly");
|
|
13245
|
+
const catalog = await opts.listCatalogModels({
|
|
13246
|
+
...qs.get("adapter") ? { adapter: qs.get("adapter") } : {},
|
|
13247
|
+
...qs.get("vendor") ? { vendor: qs.get("vendor") } : {},
|
|
13248
|
+
...qs.get("route") ? { route: qs.get("route") } : {},
|
|
13249
|
+
...runnableOnlyParam === "true" ? { runnableOnly: true } : {}
|
|
13250
|
+
});
|
|
13251
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
13252
|
+
res.end(JSON.stringify(catalog));
|
|
13253
|
+
} catch (err) {
|
|
13254
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
13255
|
+
res.end(
|
|
13256
|
+
JSON.stringify({
|
|
13257
|
+
error: "list_failed",
|
|
13258
|
+
message: err instanceof Error ? err.message : String(err)
|
|
13259
|
+
})
|
|
13260
|
+
);
|
|
13261
|
+
}
|
|
13262
|
+
return;
|
|
13263
|
+
}
|
|
11923
13264
|
if (path === "/presets" && req.method === "GET") {
|
|
11924
13265
|
const handled = await handlePresets(req, res, path);
|
|
11925
13266
|
if (handled) return;
|
|
@@ -12203,6 +13544,22 @@ function parseOrchestratorField(raw) {
|
|
|
12203
13544
|
}
|
|
12204
13545
|
return void 0;
|
|
12205
13546
|
}
|
|
13547
|
+
function parseWorktreeField(raw) {
|
|
13548
|
+
const value = typeof raw === "string" ? tryParseJson(raw) ?? raw : raw;
|
|
13549
|
+
if (typeof value === "boolean") return value;
|
|
13550
|
+
if (value === "true") return true;
|
|
13551
|
+
if (value === "false") return false;
|
|
13552
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
13553
|
+
const obj = value;
|
|
13554
|
+
const slug = typeof obj.slug === "string" ? obj.slug : void 0;
|
|
13555
|
+
const base = typeof obj.base === "string" ? obj.base : void 0;
|
|
13556
|
+
return {
|
|
13557
|
+
...slug !== void 0 ? { slug } : {},
|
|
13558
|
+
...base !== void 0 ? { base } : {}
|
|
13559
|
+
};
|
|
13560
|
+
}
|
|
13561
|
+
return void 0;
|
|
13562
|
+
}
|
|
12206
13563
|
function parseMcpServersField(raw) {
|
|
12207
13564
|
const value = typeof raw === "string" ? tryParseJson(raw) : raw;
|
|
12208
13565
|
if (!Array.isArray(value)) return void 0;
|
|
@@ -12253,13 +13610,16 @@ async function resolveSlugFromCwd(cwd) {
|
|
|
12253
13610
|
return void 0;
|
|
12254
13611
|
}
|
|
12255
13612
|
}
|
|
12256
|
-
async function handleSessions(req, res, path, registry, resolveAgentAdapter, ptyEnabled = false, resolveBrowserAdapter, listBrowserAdapters, sessionEvents, eventRing, buildOrchestratorMcp, daemonMcpUrl) {
|
|
13613
|
+
async function handleSessions(req, res, path, registry, resolveAgentAdapter, ptyEnabled = false, resolveBrowserAdapter, listBrowserAdapters, sessionEvents, eventRing, buildOrchestratorMcp, daemonMcpUrl, provisionWorktree) {
|
|
12257
13614
|
const json = (status, body) => {
|
|
12258
13615
|
res.writeHead(status, { "content-type": "application/json" });
|
|
12259
13616
|
res.end(JSON.stringify(body));
|
|
12260
13617
|
};
|
|
12261
13618
|
if (path === "/sessions" && req.method === "GET") {
|
|
12262
|
-
|
|
13619
|
+
const reqUrl = req.url ?? "";
|
|
13620
|
+
const queryString = reqUrl.includes("?") ? reqUrl.slice(reqUrl.indexOf("?") + 1) : "";
|
|
13621
|
+
const includeArchived = new URLSearchParams(queryString).get("includeArchived") === "true";
|
|
13622
|
+
json(200, { sessions: registry.list({ includeArchived }) });
|
|
12263
13623
|
return true;
|
|
12264
13624
|
}
|
|
12265
13625
|
if (path === "/sessions/agent" && req.method === "POST") {
|
|
@@ -12282,7 +13642,13 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
|
|
|
12282
13642
|
return true;
|
|
12283
13643
|
}
|
|
12284
13644
|
const result = await spawnAgentSession(
|
|
12285
|
-
{
|
|
13645
|
+
{
|
|
13646
|
+
registry,
|
|
13647
|
+
resolveAgentAdapter,
|
|
13648
|
+
buildOrchestratorMcp,
|
|
13649
|
+
daemonMcpUrl,
|
|
13650
|
+
...provisionWorktree ? { provisionWorktree } : {}
|
|
13651
|
+
},
|
|
12286
13652
|
{
|
|
12287
13653
|
adapter,
|
|
12288
13654
|
...typeof b.cwd === "string" && b.cwd.length > 0 ? { cwd: b.cwd } : {},
|
|
@@ -12326,11 +13692,19 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
|
|
|
12326
13692
|
...b.permissionHold !== void 0 ? (() => {
|
|
12327
13693
|
const h = typeof b.permissionHold === "boolean" ? b.permissionHold : b.permissionHold === "true" ? true : b.permissionHold === "false" ? false : void 0;
|
|
12328
13694
|
return h ? { permissionHold: true } : {};
|
|
13695
|
+
})() : {},
|
|
13696
|
+
// Worktree isolation — the HTTP twin of the MCP `agent_start` tool's
|
|
13697
|
+
// `worktree` field. Same `spawnAgentSession` core resolves the
|
|
13698
|
+
// `worktrees.isolation` policy, so `always` bites here too and there's
|
|
13699
|
+
// no policy-bypassing spawn path.
|
|
13700
|
+
...b.worktree !== void 0 ? (() => {
|
|
13701
|
+
const parsed = parseWorktreeField(b.worktree);
|
|
13702
|
+
return parsed !== void 0 ? { worktree: parsed } : {};
|
|
12329
13703
|
})() : {}
|
|
12330
13704
|
}
|
|
12331
13705
|
);
|
|
12332
13706
|
if (!result.ok) {
|
|
12333
|
-
const status = result.code === "adapter_not_found" || result.code === "no_cwd" ? 404 : result.code === "orchestrator_not_enabled" ? 501 : result.code === "orchestrator_max_depth_exceeded" || result.code === "orchestrator_child_quota_exceeded" || result.code === "role_spawn_denied" ? 409 : result.code === "invalid_role" ? 400 : 500;
|
|
13707
|
+
const status = result.code === "adapter_not_found" || result.code === "no_cwd" ? 404 : result.code === "orchestrator_not_enabled" ? 501 : result.code === "orchestrator_max_depth_exceeded" || result.code === "orchestrator_child_quota_exceeded" || result.code === "role_spawn_denied" ? 409 : result.code === "invalid_role" || result.code === "worktree_requires_explicit_repo" ? 400 : 500;
|
|
12334
13708
|
json(status, {
|
|
12335
13709
|
error: result.code,
|
|
12336
13710
|
message: result.message,
|
|
@@ -12549,6 +13923,129 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
|
|
|
12549
13923
|
}
|
|
12550
13924
|
return true;
|
|
12551
13925
|
}
|
|
13926
|
+
const modelMatch = path.match(/^\/sessions\/([^/]+)\/model$/);
|
|
13927
|
+
if (modelMatch && req.method === "POST") {
|
|
13928
|
+
const id2 = modelMatch[1];
|
|
13929
|
+
if (!id2) return false;
|
|
13930
|
+
const body = await readJsonBody(req);
|
|
13931
|
+
const model = body && typeof body === "object" && typeof body.model === "string" ? body.model : void 0;
|
|
13932
|
+
if (!model) {
|
|
13933
|
+
json(400, { error: "missing_model" });
|
|
13934
|
+
return true;
|
|
13935
|
+
}
|
|
13936
|
+
try {
|
|
13937
|
+
const result = await registry.setModel(id2, model);
|
|
13938
|
+
json(200, { ok: true, id: id2, ...result });
|
|
13939
|
+
} catch (err) {
|
|
13940
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13941
|
+
const status = msg.includes("no session") ? 404 : msg.includes("not an agent-cli session") ? 400 : 500;
|
|
13942
|
+
json(status, { error: "set_model_failed", message: msg });
|
|
13943
|
+
}
|
|
13944
|
+
return true;
|
|
13945
|
+
}
|
|
13946
|
+
const restartMatch = path.match(/^\/sessions\/([^/]+)\/restart$/);
|
|
13947
|
+
if (restartMatch && req.method === "POST") {
|
|
13948
|
+
const id2 = restartMatch[1];
|
|
13949
|
+
if (!id2) return false;
|
|
13950
|
+
if (!resolveAgentAdapter) {
|
|
13951
|
+
json(501, {
|
|
13952
|
+
error: "restart_not_enabled",
|
|
13953
|
+
message: "POST /sessions/:id/restart needs the host to inject `resolveAgentAdapter`."
|
|
13954
|
+
});
|
|
13955
|
+
return true;
|
|
13956
|
+
}
|
|
13957
|
+
const prev = registry.findByIdOrName(id2);
|
|
13958
|
+
if (!prev) {
|
|
13959
|
+
json(404, { error: "no_session", message: `no session "${id2}" found` });
|
|
13960
|
+
return true;
|
|
13961
|
+
}
|
|
13962
|
+
if (!prev.adapterSlug) {
|
|
13963
|
+
json(400, {
|
|
13964
|
+
error: "restart_override_invalid",
|
|
13965
|
+
message: "restart-with-override only applies to agent-cli sessions (a PTY/command session has no config axes to override)."
|
|
13966
|
+
});
|
|
13967
|
+
return true;
|
|
13968
|
+
}
|
|
13969
|
+
const body = await readJsonBody(req);
|
|
13970
|
+
const b = body && typeof body === "object" ? body : {};
|
|
13971
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
13972
|
+
const overrides = {
|
|
13973
|
+
...str(b.model) !== void 0 ? { model: str(b.model) } : {},
|
|
13974
|
+
...str(b.effort) !== void 0 ? { effort: str(b.effort) } : {},
|
|
13975
|
+
...b.access && typeof b.access === "object" && str(b.access.profileRef) !== void 0 ? { access: { profileRef: str(b.access.profileRef) } } : {},
|
|
13976
|
+
...b.route && typeof b.route === "object" && str(b.route.gateway) !== void 0 ? {
|
|
13977
|
+
route: {
|
|
13978
|
+
gateway: str(b.route.gateway),
|
|
13979
|
+
...str(b.route.baseUrl) !== void 0 ? { baseUrl: str(b.route.baseUrl) } : {}
|
|
13980
|
+
}
|
|
13981
|
+
} : {},
|
|
13982
|
+
...b.posture !== void 0 ? { posture: b.posture } : {},
|
|
13983
|
+
...str(b.contextProfile) !== void 0 ? { contextProfile: str(b.contextProfile) } : {},
|
|
13984
|
+
...str(b.mode) !== void 0 ? { mode: str(b.mode) } : {}
|
|
13985
|
+
};
|
|
13986
|
+
try {
|
|
13987
|
+
const restarted = await restartAgentSession(registry, resolveAgentAdapter, prev, {
|
|
13988
|
+
forceAgentResume: true,
|
|
13989
|
+
overrides
|
|
13990
|
+
});
|
|
13991
|
+
json(200, {
|
|
13992
|
+
...restarted.desc,
|
|
13993
|
+
resumedFrom: restarted.resumedFrom,
|
|
13994
|
+
resumeVia: restarted.resumeVia,
|
|
13995
|
+
...restarted.resumeFallback ? { resumeFallback: true } : {}
|
|
13996
|
+
});
|
|
13997
|
+
} catch (err) {
|
|
13998
|
+
if (err instanceof RestartOverrideError) {
|
|
13999
|
+
json(err.status, { error: err.code, message: err.message, sessionId: prev.id });
|
|
14000
|
+
return true;
|
|
14001
|
+
}
|
|
14002
|
+
json(500, {
|
|
14003
|
+
error: "restart_failed",
|
|
14004
|
+
message: err instanceof Error ? err.message : String(err)
|
|
14005
|
+
});
|
|
14006
|
+
}
|
|
14007
|
+
return true;
|
|
14008
|
+
}
|
|
14009
|
+
const effortMatch = path.match(/^\/sessions\/([^/]+)\/effort$/);
|
|
14010
|
+
if (effortMatch && req.method === "POST") {
|
|
14011
|
+
const id2 = effortMatch[1];
|
|
14012
|
+
if (!id2) return false;
|
|
14013
|
+
const body = await readJsonBody(req);
|
|
14014
|
+
const effort = body && typeof body === "object" && typeof body.effort === "string" ? body.effort : void 0;
|
|
14015
|
+
if (!effort) {
|
|
14016
|
+
json(400, { error: "missing_effort" });
|
|
14017
|
+
return true;
|
|
14018
|
+
}
|
|
14019
|
+
try {
|
|
14020
|
+
const result = await registry.setEffort(id2, effort);
|
|
14021
|
+
json(200, { ok: true, id: id2, ...result });
|
|
14022
|
+
} catch (err) {
|
|
14023
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14024
|
+
const status = msg.includes("no session") ? 404 : msg.includes("not an agent-cli session") ? 400 : 500;
|
|
14025
|
+
json(status, { error: "set_effort_failed", message: msg });
|
|
14026
|
+
}
|
|
14027
|
+
return true;
|
|
14028
|
+
}
|
|
14029
|
+
const postureMatch = path.match(/^\/sessions\/([^/]+)\/posture$/);
|
|
14030
|
+
if (postureMatch && req.method === "POST") {
|
|
14031
|
+
const id2 = postureMatch[1];
|
|
14032
|
+
if (!id2) return false;
|
|
14033
|
+
const body = await readJsonBody(req);
|
|
14034
|
+
const postureRaw = body && typeof body === "object" && typeof body.posture === "string" ? body.posture : void 0;
|
|
14035
|
+
if (!postureRaw) {
|
|
14036
|
+
json(400, { error: "missing_posture" });
|
|
14037
|
+
return true;
|
|
14038
|
+
}
|
|
14039
|
+
try {
|
|
14040
|
+
const result = await registry.setPosture(id2, parsePostureInput(postureRaw));
|
|
14041
|
+
json(200, { ok: true, id: id2, ...result });
|
|
14042
|
+
} catch (err) {
|
|
14043
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14044
|
+
const status = msg.includes("no session") ? 404 : msg.includes("not an agent-cli session") ? 400 : 500;
|
|
14045
|
+
json(status, { error: "set_posture_failed", message: msg });
|
|
14046
|
+
}
|
|
14047
|
+
return true;
|
|
14048
|
+
}
|
|
12552
14049
|
if (path === "/sessions" && req.method === "POST") {
|
|
12553
14050
|
const body = await readJsonBody(req);
|
|
12554
14051
|
if (!body || typeof body !== "object") {
|
|
@@ -14389,8 +15886,6 @@ function createRoutineRunner(opts) {
|
|
|
14389
15886
|
}
|
|
14390
15887
|
};
|
|
14391
15888
|
}
|
|
14392
|
-
|
|
14393
|
-
// src/sessions-registry-agent-host.ts
|
|
14394
15889
|
init_transcript_export();
|
|
14395
15890
|
var SessionsRegistryAgentHost = class {
|
|
14396
15891
|
constructor(registry, sessionEvents, resolveAgentAdapter, opts) {
|
|
@@ -14405,10 +15900,45 @@ var SessionsRegistryAgentHost = class {
|
|
|
14405
15900
|
opts;
|
|
14406
15901
|
sessionsByLabel = /* @__PURE__ */ new Map();
|
|
14407
15902
|
async spawn(adapter, opts) {
|
|
14408
|
-
const resolved = await this.resolveAgentAdapter(adapter);
|
|
14409
|
-
if (!resolved) throw new Error(`adapter '${adapter}' not found`);
|
|
14410
15903
|
const workspaceSlug = opts.workspaceSlug ?? this.opts?.workspaceSlug ?? "default";
|
|
14411
15904
|
const cwd = opts.cwd ?? this.opts?.cwd ?? process.cwd();
|
|
15905
|
+
if (opts.sandbox !== void 0) {
|
|
15906
|
+
let sandbox;
|
|
15907
|
+
if (typeof opts.sandbox === "string") {
|
|
15908
|
+
sandbox = opts.sandbox;
|
|
15909
|
+
} else {
|
|
15910
|
+
const parsed = SandboxSpecSchema.safeParse({ config: {}, ...opts.sandbox });
|
|
15911
|
+
if (!parsed.success) {
|
|
15912
|
+
throw new Error(
|
|
15913
|
+
`agent step sandbox spec invalid (provider "${opts.sandbox.provider}"): ${parsed.error.message}`
|
|
15914
|
+
);
|
|
15915
|
+
}
|
|
15916
|
+
sandbox = parsed.data;
|
|
15917
|
+
}
|
|
15918
|
+
const result = await spawnAgentSession(
|
|
15919
|
+
{
|
|
15920
|
+
registry: this.registry,
|
|
15921
|
+
resolveAgentAdapter: this.resolveAgentAdapter,
|
|
15922
|
+
...this.opts?.resolveSandboxProvider ? { resolveSandboxProvider: this.opts.resolveSandboxProvider } : {}
|
|
15923
|
+
},
|
|
15924
|
+
{
|
|
15925
|
+
adapter,
|
|
15926
|
+
cwd,
|
|
15927
|
+
workspaceSlug,
|
|
15928
|
+
sandbox,
|
|
15929
|
+
label: `agent-step:${adapter}`
|
|
15930
|
+
}
|
|
15931
|
+
);
|
|
15932
|
+
if (!result.ok) {
|
|
15933
|
+
throw new Error(`agent step sandbox spawn failed (${result.code}): ${result.message}`);
|
|
15934
|
+
}
|
|
15935
|
+
if (opts.stepId) {
|
|
15936
|
+
this.sessionsByLabel.set(opts.stepId, result.descriptor.id);
|
|
15937
|
+
}
|
|
15938
|
+
return result.descriptor.id;
|
|
15939
|
+
}
|
|
15940
|
+
const resolved = await this.resolveAgentAdapter(adapter);
|
|
15941
|
+
if (!resolved) throw new Error(`adapter '${adapter}' not found`);
|
|
14412
15942
|
const agentSession = await resolved.startSession({ cwd });
|
|
14413
15943
|
const desc = this.registry.spawnAgent({
|
|
14414
15944
|
workspaceSlug,
|
|
@@ -14503,7 +16033,14 @@ var SessionsRegistryAgentHost = class {
|
|
|
14503
16033
|
};
|
|
14504
16034
|
unsubs.push(
|
|
14505
16035
|
this.sessionEvents.on("session:turn-end", (ev) => {
|
|
14506
|
-
if (ev.sessionId
|
|
16036
|
+
if (ev.sessionId !== sessionId) return;
|
|
16037
|
+
if (ev.empty === true) {
|
|
16038
|
+
fail(
|
|
16039
|
+
`session ${sessionId} produced an empty turn \u2014 no assistant output or tool call (commonly an auth failure or an invalid model id)`
|
|
16040
|
+
);
|
|
16041
|
+
} else {
|
|
16042
|
+
done();
|
|
16043
|
+
}
|
|
14507
16044
|
})
|
|
14508
16045
|
);
|
|
14509
16046
|
unsubs.push(
|
|
@@ -14584,6 +16121,7 @@ function translateStages(stages, workflowId) {
|
|
|
14584
16121
|
id: step.label,
|
|
14585
16122
|
...step.adapter !== void 0 ? { adapter: step.adapter } : {},
|
|
14586
16123
|
...step.sessionRef !== void 0 ? { sessionRef: step.sessionRef } : {},
|
|
16124
|
+
...step.sandbox !== void 0 ? { sandbox: step.sandbox } : {},
|
|
14587
16125
|
...step.cacheable ? { cacheable: true } : {},
|
|
14588
16126
|
prompt: () => step.prompt ?? "",
|
|
14589
16127
|
policy: step.policy ?? { awaiting: "fail" }
|
|
@@ -14601,6 +16139,45 @@ function translateStages(stages, workflowId) {
|
|
|
14601
16139
|
steps
|
|
14602
16140
|
};
|
|
14603
16141
|
}
|
|
16142
|
+
function collectAgentSteps(steps) {
|
|
16143
|
+
const collected = [];
|
|
16144
|
+
for (const step of steps) {
|
|
16145
|
+
if (step.kind === "agent") {
|
|
16146
|
+
const adapter = typeof step.adapter === "string" ? step.adapter : void 0;
|
|
16147
|
+
collected.push({ id: step.id, adapter, sessionRef: step.sessionRef });
|
|
16148
|
+
} else if (step.kind === "parallel") {
|
|
16149
|
+
for (const branch of step.branches) collected.push(...collectAgentSteps(branch.steps));
|
|
16150
|
+
} else if (step.kind === "group") {
|
|
16151
|
+
collected.push(...collectAgentSteps(step.steps));
|
|
16152
|
+
} else if (step.kind === "map") ; else if (step.kind === "pipeline") {
|
|
16153
|
+
for (const stage of step.stages) {
|
|
16154
|
+
}
|
|
16155
|
+
} else if (step.kind === "branch") {
|
|
16156
|
+
collected.push(...collectAgentSteps(step.then));
|
|
16157
|
+
if (step.otherwise) collected.push(...collectAgentSteps(step.otherwise));
|
|
16158
|
+
} else if (step.kind === "loop") {
|
|
16159
|
+
collected.push(...collectAgentSteps(step.body));
|
|
16160
|
+
} else if (step.kind === "subworkflow") {
|
|
16161
|
+
collected.push(...collectAgentSteps(step.workflow.steps));
|
|
16162
|
+
}
|
|
16163
|
+
}
|
|
16164
|
+
return collected;
|
|
16165
|
+
}
|
|
16166
|
+
function runtimeWorkflowToStages(workflow) {
|
|
16167
|
+
const agents = collectAgentSteps(workflow.steps);
|
|
16168
|
+
if (agents.length === 0) {
|
|
16169
|
+
return [{ steps: [{ label: "workflow" }] }];
|
|
16170
|
+
}
|
|
16171
|
+
return [
|
|
16172
|
+
{
|
|
16173
|
+
steps: agents.map((a) => ({
|
|
16174
|
+
label: a.id,
|
|
16175
|
+
...a.adapter !== void 0 ? { adapter: a.adapter } : {},
|
|
16176
|
+
...a.sessionRef !== void 0 ? { sessionRef: a.sessionRef } : {}
|
|
16177
|
+
}))
|
|
16178
|
+
}
|
|
16179
|
+
];
|
|
16180
|
+
}
|
|
14604
16181
|
var DEFAULT_PERSIST_PATH2 = () => join(homedir(), ".agentproto", "workflow-runs.json");
|
|
14605
16182
|
function loadRuns2(persistPath) {
|
|
14606
16183
|
const result = /* @__PURE__ */ new Map();
|
|
@@ -14658,13 +16235,10 @@ function fireNotifyUrl(run) {
|
|
|
14658
16235
|
}).catch(() => void 0);
|
|
14659
16236
|
}
|
|
14660
16237
|
function resolveStepSessionId(step, agents) {
|
|
14661
|
-
if (step.adapter) {
|
|
14662
|
-
return agents.resolveByLabel(step.label);
|
|
14663
|
-
}
|
|
14664
16238
|
if (step.sessionRef) {
|
|
14665
16239
|
return agents.resolveByLabel(step.sessionRef);
|
|
14666
16240
|
}
|
|
14667
|
-
return
|
|
16241
|
+
return agents.resolveByLabel(step.label);
|
|
14668
16242
|
}
|
|
14669
16243
|
function fillStepStates(stages, defs, agents) {
|
|
14670
16244
|
const sessionIds = [];
|
|
@@ -14724,6 +16298,8 @@ async function executeRunWorkflow(state, runtimeWf, agents, signal, cache, cache
|
|
|
14724
16298
|
}
|
|
14725
16299
|
}
|
|
14726
16300
|
}
|
|
16301
|
+
const sessionIds = fillStepStates(state.run.stages, state.stages, agents);
|
|
16302
|
+
if (sessionIds.length > 0) state.run.result = { sessionIds };
|
|
14727
16303
|
}
|
|
14728
16304
|
}
|
|
14729
16305
|
fireNotifyUrl(state.run);
|
|
@@ -14775,7 +16351,8 @@ function createWorkflowRunner(opts) {
|
|
|
14775
16351
|
{
|
|
14776
16352
|
workspaceSlug: input.workspaceSlug,
|
|
14777
16353
|
cwd: input.cwd,
|
|
14778
|
-
notifyUrl: input.notifyUrl
|
|
16354
|
+
notifyUrl: input.notifyUrl,
|
|
16355
|
+
...opts.resolveSandboxProvider ? { resolveSandboxProvider: opts.resolveSandboxProvider } : {}
|
|
14779
16356
|
}
|
|
14780
16357
|
);
|
|
14781
16358
|
const cache = input.cacheKey ? createFileStepCache(input.cacheKey) : void 0;
|
|
@@ -14792,26 +16369,30 @@ function createWorkflowRunner(opts) {
|
|
|
14792
16369
|
}
|
|
14793
16370
|
const handle = await loadWorkflowHandle(args.path);
|
|
14794
16371
|
const workflow = await compileWorkflow2(handle);
|
|
16372
|
+
const fileStages = runtimeWorkflowToStages(workflow);
|
|
14795
16373
|
const runId = `wfrun_${randomUUID()}`;
|
|
14796
16374
|
const run = {
|
|
14797
16375
|
runId,
|
|
14798
16376
|
workflowId: handle.id,
|
|
14799
16377
|
status: "running",
|
|
14800
16378
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14801
|
-
stages:
|
|
14802
|
-
|
|
14803
|
-
|
|
14804
|
-
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
|
|
16379
|
+
stages: fileStages.map((stage, si) => ({
|
|
16380
|
+
index: si,
|
|
16381
|
+
...stage.label !== void 0 ? { label: stage.label } : {},
|
|
16382
|
+
status: "pending",
|
|
16383
|
+
steps: stage.steps.map((s, i) => ({
|
|
16384
|
+
index: i,
|
|
16385
|
+
label: s.label,
|
|
16386
|
+
status: "pending"
|
|
16387
|
+
}))
|
|
16388
|
+
}))
|
|
14808
16389
|
};
|
|
14809
16390
|
const abort = new AbortController();
|
|
14810
16391
|
const state = {
|
|
14811
16392
|
run,
|
|
14812
16393
|
cancelled: false,
|
|
14813
16394
|
abort,
|
|
14814
|
-
stages:
|
|
16395
|
+
stages: fileStages,
|
|
14815
16396
|
...args.cwd !== void 0 ? { cwd: args.cwd } : {},
|
|
14816
16397
|
...args.workspaceSlug !== void 0 ? { workspaceSlug: args.workspaceSlug } : {}
|
|
14817
16398
|
};
|
|
@@ -14823,7 +16404,8 @@ function createWorkflowRunner(opts) {
|
|
|
14823
16404
|
resolveAgentAdapter,
|
|
14824
16405
|
{
|
|
14825
16406
|
workspaceSlug: args.workspaceSlug,
|
|
14826
|
-
cwd: args.cwd
|
|
16407
|
+
cwd: args.cwd,
|
|
16408
|
+
...opts.resolveSandboxProvider ? { resolveSandboxProvider: opts.resolveSandboxProvider } : {}
|
|
14827
16409
|
}
|
|
14828
16410
|
);
|
|
14829
16411
|
const cache = args.cacheKey ? createFileStepCache(args.cacheKey) : void 0;
|
|
@@ -15588,6 +17170,216 @@ function createOrchestratorInjector(deps2) {
|
|
|
15588
17170
|
return { entry, scope, bindLifecycle };
|
|
15589
17171
|
};
|
|
15590
17172
|
}
|
|
17173
|
+
var WIDENING_ROUTES = ["openrouter", "requesty", "huggingface"];
|
|
17174
|
+
function normalizeRouterPrefixedId(id) {
|
|
17175
|
+
const firstSlash = id.indexOf("/");
|
|
17176
|
+
if (firstSlash === -1) return id;
|
|
17177
|
+
const head = id.slice(0, firstSlash);
|
|
17178
|
+
if (!WIDENING_ROUTES.includes(head)) return id;
|
|
17179
|
+
const remainder = id.slice(firstSlash + 1);
|
|
17180
|
+
if (!remainder.includes("/") || remainder.includes("@")) return id;
|
|
17181
|
+
return `${remainder}@${head}`;
|
|
17182
|
+
}
|
|
17183
|
+
function tryResolveLlmModelRoute(id) {
|
|
17184
|
+
try {
|
|
17185
|
+
return resolveLlmModelRoute(id);
|
|
17186
|
+
} catch {
|
|
17187
|
+
return void 0;
|
|
17188
|
+
}
|
|
17189
|
+
}
|
|
17190
|
+
function vendorFromIdPrefix(bareId) {
|
|
17191
|
+
if (/^claude[-/]/.test(bareId)) return "anthropic";
|
|
17192
|
+
if (/^(gpt[-/]|o[1-9](-|$)|chatgpt)/.test(bareId)) return "openai";
|
|
17193
|
+
if (/^gemini[-/]/.test(bareId)) return "google";
|
|
17194
|
+
if (/^grok[-/]/.test(bareId)) return "x-ai";
|
|
17195
|
+
if (/^deepseek[-/]/.test(bareId)) return "deepseek";
|
|
17196
|
+
return void 0;
|
|
17197
|
+
}
|
|
17198
|
+
function resolveModelId(id) {
|
|
17199
|
+
const normalized = normalizeRouterPrefixedId(id);
|
|
17200
|
+
const resolved = tryResolveLlmModelRoute(normalized);
|
|
17201
|
+
if (resolved) {
|
|
17202
|
+
return {
|
|
17203
|
+
vendor: resolved.vendor,
|
|
17204
|
+
product: resolved.product,
|
|
17205
|
+
directRoute: resolved.route,
|
|
17206
|
+
ref: formatModelRef(resolved.ref),
|
|
17207
|
+
baseUrl: resolved.transport.baseUrl ?? null,
|
|
17208
|
+
pricing: {
|
|
17209
|
+
inPer1M: resolved.pricing.inputPer1M,
|
|
17210
|
+
outPer1M: resolved.pricing.outputPer1M
|
|
17211
|
+
}
|
|
17212
|
+
};
|
|
17213
|
+
}
|
|
17214
|
+
const parsed = tryParseModelRef(normalized);
|
|
17215
|
+
if (parsed) {
|
|
17216
|
+
return {
|
|
17217
|
+
vendor: parsed.vendor,
|
|
17218
|
+
product: parsed.product,
|
|
17219
|
+
directRoute: parsed.route,
|
|
17220
|
+
ref: formatModelRef(parsed),
|
|
17221
|
+
baseUrl: null,
|
|
17222
|
+
pricing: null
|
|
17223
|
+
};
|
|
17224
|
+
}
|
|
17225
|
+
const vendor = vendorFromIdPrefix(id) ?? "unknown";
|
|
17226
|
+
return {
|
|
17227
|
+
vendor,
|
|
17228
|
+
product: id,
|
|
17229
|
+
directRoute: vendor,
|
|
17230
|
+
ref: `${vendor}/${id}`,
|
|
17231
|
+
baseUrl: null,
|
|
17232
|
+
pricing: null
|
|
17233
|
+
};
|
|
17234
|
+
}
|
|
17235
|
+
function methodsForDirect(descriptor) {
|
|
17236
|
+
const methods = [];
|
|
17237
|
+
if (descriptor?.authSubscription) methods.push("oauth-bearer");
|
|
17238
|
+
if (descriptor?.provider) methods.push("api-key");
|
|
17239
|
+
return methods;
|
|
17240
|
+
}
|
|
17241
|
+
function isDirectRoute(mode, resolved) {
|
|
17242
|
+
return mode === void 0 && resolved.directRoute === resolved.vendor;
|
|
17243
|
+
}
|
|
17244
|
+
function curatedContributions(adapters) {
|
|
17245
|
+
const out = [];
|
|
17246
|
+
for (const adapter of adapters) {
|
|
17247
|
+
for (const model of adapter.models) {
|
|
17248
|
+
const resolved = resolveModelId(model.id);
|
|
17249
|
+
const route = model.mode ?? resolved.directRoute;
|
|
17250
|
+
const methods = isDirectRoute(model.mode, resolved) ? methodsForDirect(adapter.authDescriptor) : ["api-key"];
|
|
17251
|
+
out.push({
|
|
17252
|
+
vendor: resolved.vendor,
|
|
17253
|
+
product: resolved.product,
|
|
17254
|
+
route,
|
|
17255
|
+
ref: resolved.ref,
|
|
17256
|
+
baseUrl: resolved.baseUrl,
|
|
17257
|
+
pricing: resolved.pricing,
|
|
17258
|
+
curated: true,
|
|
17259
|
+
adapterSlug: adapter.slug,
|
|
17260
|
+
...model.mode ? { adapterMode: model.mode } : {},
|
|
17261
|
+
methods
|
|
17262
|
+
});
|
|
17263
|
+
}
|
|
17264
|
+
}
|
|
17265
|
+
return out;
|
|
17266
|
+
}
|
|
17267
|
+
function widenedContributions(curated) {
|
|
17268
|
+
const seenProducts = /* @__PURE__ */ new Map();
|
|
17269
|
+
for (const c of curated) {
|
|
17270
|
+
const key = `${c.vendor}/${c.product}`;
|
|
17271
|
+
const routes = seenProducts.get(key) ?? /* @__PURE__ */ new Set();
|
|
17272
|
+
routes.add(c.route);
|
|
17273
|
+
seenProducts.set(key, routes);
|
|
17274
|
+
}
|
|
17275
|
+
const out = [];
|
|
17276
|
+
for (const [key, existingRoutes] of seenProducts) {
|
|
17277
|
+
const [vendor, product] = key.split("/", 2);
|
|
17278
|
+
for (const router of WIDENING_ROUTES) {
|
|
17279
|
+
if (existingRoutes.has(router)) continue;
|
|
17280
|
+
const resolved = resolveLlmModelRoute(`${vendor}/${product}@${router}`);
|
|
17281
|
+
if (!resolved) continue;
|
|
17282
|
+
out.push({
|
|
17283
|
+
vendor,
|
|
17284
|
+
product,
|
|
17285
|
+
route: router,
|
|
17286
|
+
ref: formatModelRef(resolved.ref),
|
|
17287
|
+
baseUrl: resolved.transport.baseUrl ?? null,
|
|
17288
|
+
pricing: {
|
|
17289
|
+
inPer1M: resolved.pricing.inputPer1M,
|
|
17290
|
+
outPer1M: resolved.pricing.outputPer1M
|
|
17291
|
+
},
|
|
17292
|
+
curated: false,
|
|
17293
|
+
methods: ["api-key"]
|
|
17294
|
+
});
|
|
17295
|
+
}
|
|
17296
|
+
}
|
|
17297
|
+
return out;
|
|
17298
|
+
}
|
|
17299
|
+
function mergeContributions(contributions) {
|
|
17300
|
+
const rows = /* @__PURE__ */ new Map();
|
|
17301
|
+
for (const c of contributions) {
|
|
17302
|
+
const key = `${c.vendor}\0${c.product}\0${c.route}`;
|
|
17303
|
+
const existing = rows.get(key);
|
|
17304
|
+
if (!existing) {
|
|
17305
|
+
rows.set(key, {
|
|
17306
|
+
vendor: c.vendor,
|
|
17307
|
+
product: c.product,
|
|
17308
|
+
route: c.route,
|
|
17309
|
+
ref: c.ref,
|
|
17310
|
+
baseUrl: c.baseUrl,
|
|
17311
|
+
pricing: c.pricing,
|
|
17312
|
+
curated: c.curated,
|
|
17313
|
+
adapters: c.adapterSlug ? [c.adapterSlug] : [],
|
|
17314
|
+
adapterModes: c.adapterMode ? [c.adapterMode] : [],
|
|
17315
|
+
methods: [...c.methods]
|
|
17316
|
+
});
|
|
17317
|
+
continue;
|
|
17318
|
+
}
|
|
17319
|
+
existing.curated = existing.curated || c.curated;
|
|
17320
|
+
existing.baseUrl = existing.baseUrl ?? c.baseUrl;
|
|
17321
|
+
existing.pricing = existing.pricing ?? c.pricing;
|
|
17322
|
+
if (c.adapterSlug && !existing.adapters.includes(c.adapterSlug)) {
|
|
17323
|
+
existing.adapters.push(c.adapterSlug);
|
|
17324
|
+
}
|
|
17325
|
+
if (c.adapterMode && !existing.adapterModes.includes(c.adapterMode)) {
|
|
17326
|
+
existing.adapterModes.push(c.adapterMode);
|
|
17327
|
+
}
|
|
17328
|
+
for (const m of c.methods) {
|
|
17329
|
+
if (!existing.methods.includes(m)) existing.methods.push(m);
|
|
17330
|
+
}
|
|
17331
|
+
}
|
|
17332
|
+
return [...rows.values()];
|
|
17333
|
+
}
|
|
17334
|
+
function billedVendor(vendor, route) {
|
|
17335
|
+
return route === vendor ? vendor : route;
|
|
17336
|
+
}
|
|
17337
|
+
function buildCatalogModels(input) {
|
|
17338
|
+
const contributions = [
|
|
17339
|
+
...curatedContributions(input.adapters),
|
|
17340
|
+
...widenedContributions(curatedContributions(input.adapters))
|
|
17341
|
+
];
|
|
17342
|
+
const merged = mergeContributions(contributions);
|
|
17343
|
+
const query = input.query ?? {};
|
|
17344
|
+
const vendors = /* @__PURE__ */ new Map();
|
|
17345
|
+
for (const row of merged) {
|
|
17346
|
+
if (query.vendor && row.vendor !== query.vendor) continue;
|
|
17347
|
+
if (query.route && row.route !== query.route) continue;
|
|
17348
|
+
if (query.adapter && !row.adapters.includes(query.adapter)) continue;
|
|
17349
|
+
const manifest = {
|
|
17350
|
+
id: `${row.vendor}/${row.product}@${row.route}`,
|
|
17351
|
+
vendorByRoute: { [row.route]: billedVendor(row.vendor, row.route) },
|
|
17352
|
+
methodsByRoute: { [row.route]: row.methods }
|
|
17353
|
+
};
|
|
17354
|
+
const eligible = eligibleProfiles(input.profiles, manifest, row.route);
|
|
17355
|
+
const runnable = eligible.length > 0;
|
|
17356
|
+
if (query.runnableOnly && !runnable) continue;
|
|
17357
|
+
const route = {
|
|
17358
|
+
route: row.route,
|
|
17359
|
+
ref: row.ref,
|
|
17360
|
+
baseUrl: row.baseUrl,
|
|
17361
|
+
pricing: row.pricing,
|
|
17362
|
+
runnable,
|
|
17363
|
+
eligibleProfiles: eligible.map((p) => p.id),
|
|
17364
|
+
adapterModes: row.adapterModes,
|
|
17365
|
+
adapters: row.adapters,
|
|
17366
|
+
curated: row.curated
|
|
17367
|
+
};
|
|
17368
|
+
const products = vendors.get(row.vendor) ?? /* @__PURE__ */ new Map();
|
|
17369
|
+
vendors.set(row.vendor, products);
|
|
17370
|
+
const routes = products.get(row.product) ?? [];
|
|
17371
|
+
products.set(row.product, routes);
|
|
17372
|
+
routes.push(route);
|
|
17373
|
+
}
|
|
17374
|
+
const result = [...vendors.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([vendor, products]) => ({
|
|
17375
|
+
vendor,
|
|
17376
|
+
products: [...products.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([product, routes]) => ({
|
|
17377
|
+
product,
|
|
17378
|
+
routes: [...routes].sort((a, b) => a.route.localeCompare(b.route))
|
|
17379
|
+
}))
|
|
17380
|
+
}));
|
|
17381
|
+
return { vendors: result };
|
|
17382
|
+
}
|
|
15591
17383
|
function makeBrowserHandle(entry, resolve14) {
|
|
15592
17384
|
return {
|
|
15593
17385
|
slug: entry.id,
|
|
@@ -17487,7 +19279,7 @@ var WorkspacePathError = class extends Error {
|
|
|
17487
19279
|
};
|
|
17488
19280
|
function createWorkspaceFs(opts) {
|
|
17489
19281
|
const root = resolve(opts.workspace);
|
|
17490
|
-
function
|
|
19282
|
+
function resolvePath5(path) {
|
|
17491
19283
|
if (typeof path !== "string" || path.length === 0) {
|
|
17492
19284
|
throw new WorkspacePathError("path must be a non-empty string");
|
|
17493
19285
|
}
|
|
@@ -17507,18 +19299,18 @@ function createWorkspaceFs(opts) {
|
|
|
17507
19299
|
}
|
|
17508
19300
|
return {
|
|
17509
19301
|
async readFile(path) {
|
|
17510
|
-
const abs =
|
|
19302
|
+
const abs = resolvePath5(path);
|
|
17511
19303
|
const buf = await readFile(abs);
|
|
17512
19304
|
return buf.toString("utf8");
|
|
17513
19305
|
},
|
|
17514
19306
|
async writeFile(path, content) {
|
|
17515
|
-
const abs =
|
|
19307
|
+
const abs = resolvePath5(path);
|
|
17516
19308
|
await mkdir(dirname(abs), { recursive: true });
|
|
17517
19309
|
await writeFile(abs, content);
|
|
17518
19310
|
},
|
|
17519
19311
|
async exists(path) {
|
|
17520
19312
|
try {
|
|
17521
|
-
const abs =
|
|
19313
|
+
const abs = resolvePath5(path);
|
|
17522
19314
|
return existsSync(abs);
|
|
17523
19315
|
} catch {
|
|
17524
19316
|
return false;
|
|
@@ -17876,6 +19668,62 @@ function isEnoent(err) {
|
|
|
17876
19668
|
function errMsg(err) {
|
|
17877
19669
|
return err instanceof Error ? err.message : String(err);
|
|
17878
19670
|
}
|
|
19671
|
+
var POSTURE_MODE_VALUES = {
|
|
19672
|
+
default: "default",
|
|
19673
|
+
plan: "plan",
|
|
19674
|
+
"accept-edits": "accept-edits",
|
|
19675
|
+
"bypass-permissions": "bypass",
|
|
19676
|
+
"read-only": "read-only",
|
|
19677
|
+
"full-access": "bypass",
|
|
19678
|
+
build: "default"
|
|
19679
|
+
};
|
|
19680
|
+
function decomposeMode(modes, modeId) {
|
|
19681
|
+
const declared = modes.find((mode) => mode.id === modeId);
|
|
19682
|
+
const kind = declared?.kind ?? inferLegacyModeKind(modeId);
|
|
19683
|
+
if (kind === "route") return { route: { gateway: modeId } };
|
|
19684
|
+
if (kind === "posture") return { posture: POSTURE_MODE_VALUES[modeId] ?? "default" };
|
|
19685
|
+
return { contextProfile: modeId };
|
|
19686
|
+
}
|
|
19687
|
+
function decomposedAxisMatches(cfg, decomposed) {
|
|
19688
|
+
if (decomposed.route) return cfg.route?.gateway === decomposed.route.gateway;
|
|
19689
|
+
if (decomposed.posture !== void 0) return cfg.posture === decomposed.posture;
|
|
19690
|
+
if (decomposed.contextProfile !== void 0) {
|
|
19691
|
+
return cfg.contextProfile === decomposed.contextProfile;
|
|
19692
|
+
}
|
|
19693
|
+
return false;
|
|
19694
|
+
}
|
|
19695
|
+
function composeMode(cfg, modes) {
|
|
19696
|
+
for (const mode of modes) {
|
|
19697
|
+
if (decomposedAxisMatches(cfg, decomposeMode(modes, mode.id))) return mode.id;
|
|
19698
|
+
}
|
|
19699
|
+
return void 0;
|
|
19700
|
+
}
|
|
19701
|
+
|
|
19702
|
+
// src/index.ts
|
|
19703
|
+
init_conversation_store();
|
|
19704
|
+
async function isAgentCliAuthConfigured(slug, descriptor, model) {
|
|
19705
|
+
const config = await loadConfig();
|
|
19706
|
+
const spawnDefaults = resolveSpawnDefaults(config.defaults, slug, {});
|
|
19707
|
+
const pinnedProvider = spawnDefaults.auth.provider;
|
|
19708
|
+
const resolvedProvider = pinnedProvider ?? descriptor.provider ?? (model ? getModelProvider(model) : void 0);
|
|
19709
|
+
const apiKeyStoreCredential = resolvedProvider && spawnDefaults.auth.explicit && spawnDefaults.auth.apiKeyCredential === void 0 ? await (0, providers_store_exports.getProviderKey)(resolvedProvider) : void 0;
|
|
19710
|
+
try {
|
|
19711
|
+
const result = resolveAuthSpec({
|
|
19712
|
+
descriptor,
|
|
19713
|
+
...model ? { model } : {},
|
|
19714
|
+
...pinnedProvider ? { requestedProvider: pinnedProvider } : {},
|
|
19715
|
+
...spawnDefaults.auth.requestedMode ? { requestedMode: spawnDefaults.auth.requestedMode } : {},
|
|
19716
|
+
explicit: spawnDefaults.auth.explicit,
|
|
19717
|
+
...spawnDefaults.auth.subscriptionCredential !== void 0 ? { subscriptionCredential: spawnDefaults.auth.subscriptionCredential } : {},
|
|
19718
|
+
...spawnDefaults.auth.apiKeyCredential !== void 0 ? { apiKeyConfigCredential: spawnDefaults.auth.apiKeyCredential } : {},
|
|
19719
|
+
...apiKeyStoreCredential !== void 0 ? { apiKeyStoreCredential } : {}
|
|
19720
|
+
});
|
|
19721
|
+
return result?.spec.credential !== void 0;
|
|
19722
|
+
} catch (err) {
|
|
19723
|
+
if (err instanceof AuthResolutionError) return false;
|
|
19724
|
+
throw err;
|
|
19725
|
+
}
|
|
19726
|
+
}
|
|
17879
19727
|
|
|
17880
19728
|
// src/index.ts
|
|
17881
19729
|
var DEFAULT_ALWAYS_ON_TOOLS = [
|
|
@@ -18025,6 +19873,10 @@ async function createGateway(opts) {
|
|
|
18025
19873
|
sessionEvents,
|
|
18026
19874
|
resolveAgentAdapter: opts.resolveAgentAdapter,
|
|
18027
19875
|
persist,
|
|
19876
|
+
// Sandbox-capable agent steps (`AgentStep.sandbox` / workflow_start's
|
|
19877
|
+
// step `sandbox`) resolve providers through the same resolver
|
|
19878
|
+
// `agent_start.sandbox` uses.
|
|
19879
|
+
resolveSandboxProvider: resolveSandboxProviderResolved,
|
|
18028
19880
|
// Compile a loaded WORKFLOW.md handle into a runnable RuntimeWorkflow
|
|
18029
19881
|
// for `workflow_run_file` / `startFromFile`. The daemon's workflow
|
|
18030
19882
|
// surface is agent-step based (like the stage primitive), so no tool/
|
|
@@ -18086,8 +19938,10 @@ async function createGateway(opts) {
|
|
|
18086
19938
|
webhookNotifier,
|
|
18087
19939
|
daemonMcpUrl,
|
|
18088
19940
|
resolveSandboxProvider: resolveSandboxProviderResolved,
|
|
19941
|
+
...opts.provisionWorktree ? { provisionWorktree: opts.provisionWorktree } : {},
|
|
18089
19942
|
...opts.resolveAgentAdapter ? { resolveAgentAdapter: opts.resolveAgentAdapter } : {},
|
|
18090
|
-
...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {}
|
|
19943
|
+
...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {},
|
|
19944
|
+
...opts.listCatalogModels ? { listCatalogModels: opts.listCatalogModels } : {}
|
|
18091
19945
|
});
|
|
18092
19946
|
registerBrowserTools(server, {
|
|
18093
19947
|
registry: sessions,
|
|
@@ -18241,7 +20095,9 @@ async function createGateway(opts) {
|
|
|
18241
20095
|
// `agent_start` tool gets (session-spawn.ts is the shared logic).
|
|
18242
20096
|
buildOrchestratorMcp: orchestratorInjector,
|
|
18243
20097
|
daemonMcpUrl,
|
|
20098
|
+
...opts.provisionWorktree ? { provisionWorktree: opts.provisionWorktree } : {},
|
|
18244
20099
|
...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {},
|
|
20100
|
+
...opts.listCatalogModels ? { listCatalogModels: opts.listCatalogModels } : {},
|
|
18245
20101
|
...opts.resolveBrowserAdapter ? { resolveBrowserAdapter: opts.resolveBrowserAdapter } : {},
|
|
18246
20102
|
...opts.listBrowserAdapters ? { listBrowserAdapters: opts.listBrowserAdapters } : {},
|
|
18247
20103
|
meta: { workspace, registered, startedAt },
|
|
@@ -18328,6 +20184,6 @@ var export_providersPath = providers_store_exports.providersPath;
|
|
|
18328
20184
|
var export_removeProviderKey = providers_store_exports.removeProviderKey;
|
|
18329
20185
|
var export_setProviderKey = providers_store_exports.setProviderKey;
|
|
18330
20186
|
|
|
18331
|
-
export { AuthResolutionError, BUCKETS_ROOT, DEFAULT_BUCKET, DEFAULT_ORCHESTRATOR_TOOLS, LEGACY_SESSIONS_FILE, PAIRINGS_VERSION, export_PROVIDER_ENV_VARS as PROVIDER_ENV_VARS, bucketDir, bucketSessionsFile, bucketTranscriptDir, composeSessionObservers, createFileStepCache, createGateway, createOrchestratorInjector, createOrchestratorMcpServerFactory, createPairingRegistry, createScopeTokenRegistry, createWorkspaceFs, credentialFingerprint, daemonRegistryDir, declaredPresetToProviderPreset, deriveSessionUsage, fileConversationStore, formatToolCall, formatToolResult, getMcpCredentialDeps, export_getProviderKey as getProviderKey, export_injectProviderKeysIntoEnv as injectProviderKeysIntoEnv, isSafeBucketSlug, listBuckets, listPresets, export_loadProviders as loadProviders, makeBrowserAdapterLister, migrateLegacySessionsFile, migrationMarkerPath, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, normalizeSkillsOption, parseDuration, policyWatchesSession, projectSessionUsage, export_providerEnvVar as providerEnvVar, export_providersPath as providersPath, readDaemonRegistry, readRegisteredSlugs, readRuntimeMeta, registerPairingTools, export_removeProviderKey as removeProviderKey, resolveAuthSpec, resolveBucketSlug, resolveSpawnDefaults, setMcpCredentialDeps, export_setProviderKey as setProviderKey, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, writeDaemonRegistryEntry };
|
|
20187
|
+
export { AuthResolutionError, BUCKETS_ROOT, DEFAULT_BUCKET, DEFAULT_ORCHESTRATOR_TOOLS, DEFAULT_WORKTREE_ISOLATION, LEGACY_SESSIONS_FILE, PAIRINGS_VERSION, POSTURE_NATIVE_ALIASES, POSTURE_PREAMBLES, export_PROVIDER_ENV_VARS as PROVIDER_ENV_VARS, WORKTREE_ISOLATION_ENV, appendConversationRecord, bucketDir, bucketSessionsFile, bucketTranscriptDir, buildCatalogModels, canonicalForModeId, claudeProjectSlug, composeMode, composeSessionObservers, conversationIndexPath, createFileStepCache, createGateway, createOrchestratorInjector, createOrchestratorMcpServerFactory, createPairingRegistry, createScopeTokenRegistry, createWorkspaceFs, credentialFingerprint, daemonRegistryDir, decideWorktreeIsolation, declaredPresetToProviderPreset, decomposeMode, deriveSessionUsage, fileConversationStore, findConversationRecord, findNativeMode, formatToolCall, formatToolResult, getMcpCredentialDeps, export_getProviderKey as getProviderKey, export_injectProviderKeysIntoEnv as injectProviderKeysIntoEnv, isAgentCliAuthConfigured, isSafeBucketSlug, listBuckets, listPresets, export_loadProviders as loadProviders, loadWorktreeIsolation, locateConversationByNativePath, locateConversationBySessionId, makeBrowserAdapterLister, migrateLegacySessionsFile, migrationMarkerPath, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, normalizeModeId, normalizeSkillsOption, normalizeWorktreeField, parseDuration, parseWorktreeIsolationMode, policyWatchesSession, projectSessionUsage, export_providerEnvVar as providerEnvVar, export_providersPath as providersPath, readConversationIndex, readDaemonRegistry, readRegisteredSlugs, readRuntimeMeta, registerPairingTools, export_removeProviderKey as removeProviderKey, resolveAuthSpec, resolveBucketSlug, resolveNativeLink, resolvePosture, resolveSpawnDefaults, setMcpCredentialDeps, export_setProviderKey as setProviderKey, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, writeDaemonRegistryEntry };
|
|
18332
20188
|
//# sourceMappingURL=index.mjs.map
|
|
18333
20189
|
//# sourceMappingURL=index.mjs.map
|