@agentproto/runtime 1.0.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 +33 -2
- package/dist/index.mjs +294 -62
- package/dist/index.mjs.map +1 -1
- 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 +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -316,7 +316,7 @@ declare function composeMode(cfg: Partial<SessionConfig>, modes: readonly Declar
|
|
|
316
316
|
* and session_monitor MCP tool (long-poll multiplexed).
|
|
317
317
|
*/
|
|
318
318
|
|
|
319
|
-
type SessionEventType = "session:turn-end" | "session:awaiting-input" | "session:permission-request" | "session:permission-resolved" | "session:exited" | "session:command-done" | "session:model-changed" | "session:config-changed" | "policy:passed" | "policy:failed" | "policy:commit-ready" | "policy:committed" | "cron:fired" | "cron:succeeded" | "cron:failed";
|
|
319
|
+
type SessionEventType = "session:turn-end" | "session:awaiting-input" | "session:permission-request" | "session:permission-resolved" | "session:exited" | "session:command-done" | "session:model-changed" | "session:config-changed" | "session:renamed" | "policy:passed" | "policy:failed" | "policy:commit-ready" | "policy:committed" | "cron:fired" | "cron:succeeded" | "cron:failed";
|
|
320
320
|
/**
|
|
321
321
|
* Structured detail on why a session is awaiting input, when derivable.
|
|
322
322
|
* `source: "structured"` — a driver-reported ACP-style prompt (e.g. a tool
|
|
@@ -470,6 +470,24 @@ type SessionConfigChangedEvent = {
|
|
|
470
470
|
ts: string;
|
|
471
471
|
};
|
|
472
472
|
}[SessionConfigAxis];
|
|
473
|
+
/**
|
|
474
|
+
* Emitted when an operator sets or clears a session's user-facing name
|
|
475
|
+
* (`PATCH /sessions/:id`, the `session_rename` MCP verb). Unlike
|
|
476
|
+
* `session:config-changed`, a name is NOT a `SessionConfig` axis — it never
|
|
477
|
+
* touches the live agent, only the descriptor's display fields — so it rides
|
|
478
|
+
* its own event. `title`/`label` carry the values now on the descriptor
|
|
479
|
+
* (absent when that field was cleared). Same bus distribution as every other
|
|
480
|
+
* lifecycle event: `session_events_poll`, the webhook notifier, the routine
|
|
481
|
+
* engine — which is how a live UI (the VS Code tree / transcript header/tab)
|
|
482
|
+
* learns to repaint the name without waiting for its next snapshot poll.
|
|
483
|
+
*/
|
|
484
|
+
interface SessionRenamedEvent {
|
|
485
|
+
type: "session:renamed";
|
|
486
|
+
sessionId: string;
|
|
487
|
+
title?: string;
|
|
488
|
+
label?: string;
|
|
489
|
+
ts: string;
|
|
490
|
+
}
|
|
473
491
|
/** Emitted by the supervisor when a completion policy's gate passes. */
|
|
474
492
|
interface PolicyPassedEvent {
|
|
475
493
|
type: "policy:passed";
|
|
@@ -534,7 +552,7 @@ interface CronFailedEvent {
|
|
|
534
552
|
error: string;
|
|
535
553
|
ts: string;
|
|
536
554
|
}
|
|
537
|
-
type SessionEvent = SessionTurnEndEvent | SessionAwaitingInputEvent | SessionPermissionRequestEvent | SessionPermissionResolvedEvent | SessionExitedEvent | SessionCommandDoneEvent | SessionModelChangedEvent | SessionConfigChangedEvent | PolicyPassedEvent | PolicyFailedEvent | PolicyCommitReadyEvent | PolicyCommittedEvent | CronFiredEvent | CronSucceededEvent | CronFailedEvent;
|
|
555
|
+
type SessionEvent = SessionTurnEndEvent | SessionAwaitingInputEvent | SessionPermissionRequestEvent | SessionPermissionResolvedEvent | SessionExitedEvent | SessionCommandDoneEvent | SessionModelChangedEvent | SessionConfigChangedEvent | SessionRenamedEvent | PolicyPassedEvent | PolicyFailedEvent | PolicyCommitReadyEvent | PolicyCommittedEvent | CronFiredEvent | CronSucceededEvent | CronFailedEvent;
|
|
538
556
|
interface SessionEventBus {
|
|
539
557
|
emit(ev: SessionEvent): void;
|
|
540
558
|
/** Subscribe to a specific event type. Returns an unsubscribe fn. */
|
|
@@ -1391,6 +1409,19 @@ interface SessionsRegistry {
|
|
|
1391
1409
|
* there is nothing to re-validate). Throws only when the id is
|
|
1392
1410
|
* unknown. */
|
|
1393
1411
|
unarchiveSession(id: string): SessionDescriptor;
|
|
1412
|
+
/** Set or clear a session's user-facing name (`PATCH /sessions/:id`, the
|
|
1413
|
+
* `session_rename` MCP verb). Each of `title`/`label`: a non-empty string
|
|
1414
|
+
* sets that field (trimmed, capped to the derivation's `MAX_LENGTH` by
|
|
1415
|
+
* code point); an empty/whitespace-only string or `null` CLEARS it (the
|
|
1416
|
+
* UI reverts to the derived title / friendly fallback); `undefined`
|
|
1417
|
+
* leaves it untouched. Persists via the same `schedulePersist` every
|
|
1418
|
+
* descriptor mutation uses, and emits `session:renamed` so live UIs
|
|
1419
|
+
* repaint. Pure display state — never touches the live agent. Throws when
|
|
1420
|
+
* the id is unknown. */
|
|
1421
|
+
renameSession(id: string, patch: {
|
|
1422
|
+
title?: string | null;
|
|
1423
|
+
label?: string | null;
|
|
1424
|
+
}): SessionDescriptor;
|
|
1394
1425
|
/** Subscribe to a session's output. Returns an unsubscribe fn.
|
|
1395
1426
|
* Initial backfill: synchronously invokes `onLine` once for each
|
|
1396
1427
|
* line currently in the ring buffer so attaches show context. */
|
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';
|
|
@@ -1955,9 +1955,19 @@ function setActiveWorkspace(config, slug) {
|
|
|
1955
1955
|
function findWorkspace(config, slug) {
|
|
1956
1956
|
return config.workspaces.find((w) => w.slug === sanitizeSlug(slug));
|
|
1957
1957
|
}
|
|
1958
|
+
function canonical(p) {
|
|
1959
|
+
try {
|
|
1960
|
+
return realpathSync(resolve(p));
|
|
1961
|
+
} catch {
|
|
1962
|
+
return resolve(p);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1958
1965
|
function findWorkspaceByPath(config, dir) {
|
|
1959
|
-
const resolved =
|
|
1960
|
-
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);
|
|
1961
1971
|
return candidates[0];
|
|
1962
1972
|
}
|
|
1963
1973
|
function getActiveWorkspace(config) {
|
|
@@ -2043,6 +2053,76 @@ async function loadConfig(path) {
|
|
|
2043
2053
|
return {};
|
|
2044
2054
|
}
|
|
2045
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
|
+
}
|
|
2046
2126
|
|
|
2047
2127
|
// src/providers-store.ts
|
|
2048
2128
|
var providers_store_exports = {};
|
|
@@ -2613,15 +2693,38 @@ async function spawnAgentSession(deps2, input) {
|
|
|
2613
2693
|
try {
|
|
2614
2694
|
const config = await loadWorkspacesConfig();
|
|
2615
2695
|
if (!cwd) {
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
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
|
+
}
|
|
2620
2717
|
}
|
|
2621
2718
|
} else if (!resolvedSlug) {
|
|
2622
2719
|
const ws = findWorkspaceByPath(config, cwd);
|
|
2623
2720
|
if (ws) {
|
|
2624
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
|
+
}
|
|
2625
2728
|
}
|
|
2626
2729
|
}
|
|
2627
2730
|
} catch {
|
|
@@ -2827,7 +2930,8 @@ async function spawnAgentSession(deps2, input) {
|
|
|
2827
2930
|
const effectivePrompt = input.prompt ? `${composeRoleContext(role, input.promptAppend, roleRegistry)}
|
|
2828
2931
|
|
|
2829
2932
|
${input.prompt}` : input.prompt;
|
|
2830
|
-
const
|
|
2933
|
+
const explicitTitle = input.title?.trim() ? input.title.trim() : void 0;
|
|
2934
|
+
const initialTitle = explicitTitle ?? (input.prompt ? deriveSessionTitle(input.prompt) : void 0);
|
|
2831
2935
|
let settleClaim;
|
|
2832
2936
|
if (input.idempotencyKey) {
|
|
2833
2937
|
const claims = claimsFor(registry);
|
|
@@ -5648,6 +5752,90 @@ function registerSessionTools(rawServer, opts) {
|
|
|
5648
5752
|
}
|
|
5649
5753
|
}
|
|
5650
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) }]
|
|
5825
|
+
};
|
|
5826
|
+
} catch (err) {
|
|
5827
|
+
return {
|
|
5828
|
+
content: [
|
|
5829
|
+
{
|
|
5830
|
+
type: "text",
|
|
5831
|
+
text: `session_rename: ${err instanceof Error ? err.message : String(err)}`
|
|
5832
|
+
}
|
|
5833
|
+
],
|
|
5834
|
+
isError: true
|
|
5835
|
+
};
|
|
5836
|
+
}
|
|
5837
|
+
}
|
|
5838
|
+
);
|
|
5651
5839
|
server.tool(
|
|
5652
5840
|
"terminal_start",
|
|
5653
5841
|
"Spawn a process under a real PTY (node-pty) on the host. Bytes (including ANSI escapes, alt-screen sequences) flow through the daemon's byte ring buffer; subscribers attach via the WS at /sessions/:id/pty. Use for interactive TUIs (claude, vim, htop) or to orchestrate shells from another agent. Returns the session descriptor.",
|
|
@@ -8940,60 +9128,6 @@ function createTerminalTranscriptWriter(opts) {
|
|
|
8940
9128
|
}
|
|
8941
9129
|
};
|
|
8942
9130
|
}
|
|
8943
|
-
var markerSchema = z.object({ worktreeId: z.string() });
|
|
8944
|
-
function statOrUndefined(path) {
|
|
8945
|
-
try {
|
|
8946
|
-
return statSync(path);
|
|
8947
|
-
} catch {
|
|
8948
|
-
return void 0;
|
|
8949
|
-
}
|
|
8950
|
-
}
|
|
8951
|
-
function readWorktreeGitDir(dir) {
|
|
8952
|
-
const link = (() => {
|
|
8953
|
-
try {
|
|
8954
|
-
return readFileSync(join(dir, ".git"), "utf8");
|
|
8955
|
-
} catch {
|
|
8956
|
-
return void 0;
|
|
8957
|
-
}
|
|
8958
|
-
})();
|
|
8959
|
-
if (link === void 0) return void 0;
|
|
8960
|
-
const target = /^gitdir:\s*(.+)$/m.exec(link)?.[1]?.trim();
|
|
8961
|
-
if (!target) return void 0;
|
|
8962
|
-
const gitDir = isAbsolute(target) ? target : resolve(dir, target);
|
|
8963
|
-
return statOrUndefined(join(gitDir, "gitdir"))?.isFile() === true ? gitDir : void 0;
|
|
8964
|
-
}
|
|
8965
|
-
function readWorktreeId(gitDir) {
|
|
8966
|
-
let raw;
|
|
8967
|
-
try {
|
|
8968
|
-
raw = readFileSync(join(gitDir, "agentproto-worktree.json"), "utf8");
|
|
8969
|
-
} catch {
|
|
8970
|
-
return void 0;
|
|
8971
|
-
}
|
|
8972
|
-
let parsed;
|
|
8973
|
-
try {
|
|
8974
|
-
parsed = JSON.parse(raw);
|
|
8975
|
-
} catch {
|
|
8976
|
-
return void 0;
|
|
8977
|
-
}
|
|
8978
|
-
const result = markerSchema.safeParse(parsed);
|
|
8979
|
-
return result.success ? result.data.worktreeId : void 0;
|
|
8980
|
-
}
|
|
8981
|
-
function resolveWorktreeIdentity(cwd) {
|
|
8982
|
-
let dir = resolve(cwd);
|
|
8983
|
-
for (; ; ) {
|
|
8984
|
-
const dotGit = statOrUndefined(join(dir, ".git"));
|
|
8985
|
-
if (dotGit) {
|
|
8986
|
-
if (!dotGit.isFile()) return void 0;
|
|
8987
|
-
const gitDir = readWorktreeGitDir(dir);
|
|
8988
|
-
if (gitDir === void 0) return void 0;
|
|
8989
|
-
const worktreeId = readWorktreeId(gitDir);
|
|
8990
|
-
return worktreeId === void 0 ? { worktreePath: dir } : { worktreePath: dir, worktreeId };
|
|
8991
|
-
}
|
|
8992
|
-
const parent = dirname(dir);
|
|
8993
|
-
if (parent === dir) return void 0;
|
|
8994
|
-
dir = parent;
|
|
8995
|
-
}
|
|
8996
|
-
}
|
|
8997
9131
|
function normalizeAgentPromptOptions(raw) {
|
|
8998
9132
|
if (!Array.isArray(raw)) return void 0;
|
|
8999
9133
|
const labels = raw.map((o) => {
|
|
@@ -10529,6 +10663,33 @@ function createSessionsRegistry(opts) {
|
|
|
10529
10663
|
stampProcessAlive(rt.desc);
|
|
10530
10664
|
return rt.desc;
|
|
10531
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
|
+
},
|
|
10532
10693
|
listPendingPermissions(filter) {
|
|
10533
10694
|
const all = Array.from(pendingPermissions.values());
|
|
10534
10695
|
const scoped = filter?.sessionId ? all.filter((p) => p.sessionId === filter.sessionId) : all;
|
|
@@ -13667,6 +13828,9 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
|
|
|
13667
13828
|
})() : {},
|
|
13668
13829
|
...typeof b.prompt === "string" ? { prompt: b.prompt } : {},
|
|
13669
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 } : {},
|
|
13670
13834
|
...typeof b.idempotencyKey === "string" && b.idempotencyKey.length > 0 ? { idempotencyKey: b.idempotencyKey } : {},
|
|
13671
13835
|
...typeof b.role === "string" && b.role.length > 0 ? { role: b.role } : {},
|
|
13672
13836
|
...typeof b.promptAppend === "string" ? { promptAppend: b.promptAppend } : {},
|
|
@@ -13923,6 +14087,49 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
|
|
|
13923
14087
|
}
|
|
13924
14088
|
return true;
|
|
13925
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
|
+
}
|
|
13926
14133
|
const modelMatch = path.match(/^\/sessions\/([^/]+)\/model$/);
|
|
13927
14134
|
if (modelMatch && req.method === "POST") {
|
|
13928
14135
|
const id2 = modelMatch[1];
|
|
@@ -14046,6 +14253,31 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
|
|
|
14046
14253
|
}
|
|
14047
14254
|
return true;
|
|
14048
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
|
+
}
|
|
14049
14281
|
if (path === "/sessions" && req.method === "POST") {
|
|
14050
14282
|
const body = await readJsonBody(req);
|
|
14051
14283
|
if (!body || typeof body !== "object") {
|