@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 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 = resolve(dir);
1960
- const candidates = config.workspaces.filter((w) => resolved.startsWith(resolve(w.path) + "/") || resolved === resolve(w.path)).sort((a, b) => b.path.length - a.path.length);
1966
+ const resolved = canonical(dir);
1967
+ const candidates = config.workspaces.filter((w) => {
1968
+ const wPath = canonical(w.path);
1969
+ return resolved.startsWith(wPath + "/") || resolved === wPath;
1970
+ }).sort((a, b) => b.path.length - a.path.length);
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
- const ws = input.workspaceSlug ? findWorkspace(config, input.workspaceSlug) : getActiveWorkspace(config);
2617
- if (ws) {
2618
- cwd = ws.path;
2619
- resolvedSlug = ws.slug;
2696
+ if (input.workspaceSlug) {
2697
+ const ws = findWorkspace(config, input.workspaceSlug);
2698
+ if (ws) {
2699
+ cwd = ws.path;
2700
+ resolvedSlug = ws.slug;
2701
+ }
2702
+ } else if (callerScope) {
2703
+ const parentCwd = callerScope.ownerSessionId ? registry.get(callerScope.ownerSessionId)?.cwd : void 0;
2704
+ if (parentCwd) {
2705
+ cwd = parentCwd;
2706
+ const ws = findWorkspaceByPath(config, parentCwd);
2707
+ if (ws) {
2708
+ resolvedSlug = ws.slug;
2709
+ }
2710
+ }
2711
+ } else {
2712
+ const ws = getActiveWorkspace(config);
2713
+ if (ws) {
2714
+ cwd = ws.path;
2715
+ resolvedSlug = ws.slug;
2716
+ }
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 initialTitle = input.prompt ? deriveSessionTitle(input.prompt) : void 0;
2933
+ const explicitTitle = input.title?.trim() ? input.title.trim() : void 0;
2934
+ const initialTitle = explicitTitle ?? (input.prompt ? deriveSessionTitle(input.prompt) : void 0);
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") {