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