@agentproto/runtime 3.4.0 → 3.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -21,18 +21,18 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
21
21
  import { createServer as createServer$1 } from 'net';
22
22
  import { defineTool, catchErrors, ToolError, pageParamsShape, paginated, paginate, toolText } from '@agentproto/tool';
23
23
  import { CatalogProviderSchema, getModelsByProvider, getStaticModelProvider } from '@agentproto/model-catalog';
24
+ import { APP_UI_DISCOVERY_TOOLS, RUNNER_SELECT_SCRIPT } from '@agentproto/app-client/runner-select';
25
+ import { SESSION_CHAT_APP_ID, liveSessionApp, makeLiveSessionApp, sessionsPanelApp, agentsOverviewApp, bureauSessionsApp, sessionStoryApp, workBoardApp, makeSessionsPanelApp, makeAgentsOverviewApp, makeBureauSessionsApp, makeSessionStoryPanelApp, makeSessionChatApp, makeWorkBoardApp, sessionChatApp } from '@agentproto/apps';
24
26
  import { inferLegacyModeKind, parseModelSwitchCommand, isModelSwitchAcknowledgement } from '@agentproto/driver-agent-cli';
25
27
  import { loadSandboxConfig, resolveCommandSandbox, COMMAND_SANDBOX_MODE_ENV } from '@agentproto/command-sandbox';
26
28
  import { createBrainManager, parseKnowledgeConfig } from '@agentproto/workspace-brain';
27
29
  import { defineHttpDriver } from '@agentproto/driver-http';
28
- import { liveSessionApp, makeLiveSessionApp, sessionsPanelApp, agentsOverviewApp, bureauSessionsApp, sessionStoryApp, workBoardApp, makeSessionsPanelApp, makeAgentsOverviewApp, makeBureauSessionsApp, makeSessionStoryPanelApp, makeSessionChatApp, makeWorkBoardApp, SESSION_CHAT_APP_ID, sessionChatApp } from '@agentproto/apps';
29
30
  import matter2 from 'gray-matter';
30
31
  import { createServer } from 'http';
31
32
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
32
33
  import { WebSocketServer } from 'ws';
33
34
  import { loadAppHandle } from '@agentproto/app-kit';
34
35
  import { loadAgent } from '@agentproto/agent';
35
- import { APP_UI_DISCOVERY_TOOLS, RUNNER_SELECT_SCRIPT } from '@agentproto/app-client/runner-select';
36
36
  import { normalizeToolId } from '@agentproto/driver';
37
37
  import { createIngestionClient } from '@agentproto/telemetry-langfuse';
38
38
  import { resolveRedactor } from '@agentproto/redaction';
@@ -1666,8 +1666,8 @@ async function discoverOpenCodeSessions(cwd, since, expectedId) {
1666
1666
  const sinceMs = since ? Date.parse(since) : NaN;
1667
1667
  return rows.filter((r) => {
1668
1668
  if (!Number.isFinite(sinceMs)) return true;
1669
- const last = r.time_updated ?? r.time_created;
1670
- return last === void 0 || last >= sinceMs - 1e3;
1669
+ if (r.time_created === void 0) return false;
1670
+ return r.time_created >= sinceMs - 1e3;
1671
1671
  }).map(openCodeRowToCandidate);
1672
1672
  } finally {
1673
1673
  db.close();
@@ -2098,6 +2098,9 @@ function extractFirstText(content) {
2098
2098
  }
2099
2099
  return void 0;
2100
2100
  }
2101
+ function isClaudeControlText(text10) {
2102
+ return /^<(?:local-command-caveat|local-command-stdout|command-name|task-notification)>/i.test(text10);
2103
+ }
2101
2104
  async function scanClaudeJsonl(filePath) {
2102
2105
  const stream = createReadStream(filePath, { encoding: "utf8" });
2103
2106
  const rl = createInterface({ input: stream, crlfDelay: Infinity });
@@ -2126,7 +2129,7 @@ async function scanClaudeJsonl(filePath) {
2126
2129
  messageCount += 1;
2127
2130
  if (preview2 === void 0 && entry.type === "user") {
2128
2131
  const text10 = extractFirstText(entry.message?.content);
2129
- if (text10 !== void 0) {
2132
+ if (text10 !== void 0 && !isClaudeControlText(text10)) {
2130
2133
  preview2 = text10.length > 120 ? text10.slice(0, 120) : text10;
2131
2134
  }
2132
2135
  }
@@ -2232,7 +2235,43 @@ async function readPi(conversationId) {
2232
2235
  const { exportPiSession: exportPiSession2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
2233
2236
  return exportPiSession2(conversationId);
2234
2237
  }
2235
- var CONVERSATION_STORES;
2238
+ function argvBasename(path) {
2239
+ const idx = path.lastIndexOf("/");
2240
+ return idx === -1 ? path : path.slice(idx + 1);
2241
+ }
2242
+ function conversationTerminalSlugFor(subject) {
2243
+ if (subject.adapterSlug && (CONVERSATION_STORES[subject.adapterSlug] || NATIVE_LAUNCH_ARGV[subject.adapterSlug])) {
2244
+ return subject.adapterSlug;
2245
+ }
2246
+ const argv = subject.argv;
2247
+ if (!argv || argv.length === 0 || !argv[0]) return void 0;
2248
+ const bin = argvBasename(argv[0]);
2249
+ for (const [slug, bins] of Object.entries(BARE_LAUNCH_BINS)) {
2250
+ if (bins.some((b) => argvBasename(b) === bin)) return slug;
2251
+ }
2252
+ for (const [slug, launch] of Object.entries(NATIVE_LAUNCH_ARGV)) {
2253
+ const launchBin = launch[0];
2254
+ if (!launchBin || argvBasename(launchBin) !== bin) continue;
2255
+ if (launchBin === "npx") {
2256
+ if (launch.every((arg, i) => argv[i] === arg)) return slug;
2257
+ continue;
2258
+ }
2259
+ return slug;
2260
+ }
2261
+ return void 0;
2262
+ }
2263
+ function isConversationTerminal(subject) {
2264
+ if (subject.kind !== "terminal") return false;
2265
+ if (conversationTerminalSlugFor(subject) !== void 0) return true;
2266
+ const meta = subject.resumeMetadata;
2267
+ if (meta) {
2268
+ for (const key of CONVERSATION_RESUME_KEYS) {
2269
+ if (meta[key]) return true;
2270
+ }
2271
+ }
2272
+ return false;
2273
+ }
2274
+ var CONVERSATION_STORES, NATIVE_LAUNCH_ARGV, BARE_LAUNCH_BINS, CONVERSATION_RESUME_KEYS;
2236
2275
  var init_conversation_store = __esm({
2237
2276
  "src/conversation-store.ts"() {
2238
2277
  CONVERSATION_STORES = {
@@ -2242,6 +2281,7 @@ var init_conversation_store = __esm({
2242
2281
  // (default). Example: `claude --resume 0e483f81-1a44-4bec-9667-b37158450296`
2243
2282
  outputHint: /claude\s+--resume\s+([0-9a-f-]{8,})/i,
2244
2283
  attachArgv: (conversationId) => ["claude", "--resume", conversationId],
2284
+ transcriptPath: (cwd, conversationId, configDir) => join(claudeCodeProjectDir(cwd, configDir), `${conversationId}.jsonl`),
2245
2285
  configDirEnvVar: "CLAUDE_CONFIG_DIR",
2246
2286
  discover: discoverClaudeCode,
2247
2287
  read: readClaudeCode
@@ -2266,6 +2306,9 @@ var init_conversation_store = __esm({
2266
2306
  },
2267
2307
  opencode: {
2268
2308
  storeAs: "openCodeResumeId",
2309
+ // Match the adapter's portable npx installation: a direct `opencode`
2310
+ // binary is not guaranteed to be on the daemon's PATH on every host.
2311
+ attachArgv: (conversationId) => ["npx", "-y", "opencode-ai", "-s", conversationId],
2269
2312
  discover: discoverOpenCode,
2270
2313
  read: readOpenCode
2271
2314
  },
@@ -2280,6 +2323,36 @@ var init_conversation_store = __esm({
2280
2323
  read: readPi
2281
2324
  }
2282
2325
  };
2326
+ NATIVE_LAUNCH_ARGV = {
2327
+ "claude-code": ["claude"],
2328
+ hermes: ["hermes", "--tui"],
2329
+ // Bare arm of the same npx package the adapter spawns IS the TUI.
2330
+ opencode: ["npx", "-y", "opencode-ai"],
2331
+ mastracode: ["npx", "-y", "mastracode"],
2332
+ // The adapter bundles the codex-acp wrapper (ACP-only); the interactive
2333
+ // Codex TUI lives in @openai/codex, fetched by npx on first use.
2334
+ codex: ["npx", "-y", "@openai/codex"],
2335
+ // ACP arms add flags/subcommands to these binaries; bare is interactive.
2336
+ gemini: ["gemini"],
2337
+ "grok-cli": ["grok"],
2338
+ pi: ["pi"],
2339
+ jcode: ["jcode"],
2340
+ // `openclaw chat` = "Open a local terminal UI (alias for tui --local)".
2341
+ openclaw: ["openclaw", "chat"],
2342
+ // Generic ACP catalog CLIs whose ACP arm is a separate binary/subcommand;
2343
+ // the bare bin is the interactive TUI.
2344
+ "gemini-cli": ["gemini"],
2345
+ "qwen-code": ["qwen"],
2346
+ "mistral-vibe": ["vibe"],
2347
+ "kimi-cli": ["kimi"]
2348
+ };
2349
+ BARE_LAUNCH_BINS = {
2350
+ opencode: ["opencode"],
2351
+ mastracode: ["mastracode"]
2352
+ };
2353
+ CONVERSATION_RESUME_KEYS = new Set(
2354
+ Object.values(CONVERSATION_STORES).map((s) => s.storeAs)
2355
+ );
2283
2356
  }
2284
2357
  });
2285
2358
 
@@ -2512,6 +2585,7 @@ var RESUME_STRATEGIES = Object.fromEntries(
2512
2585
  outputHint: store.outputHint,
2513
2586
  storeAs: store.storeAs,
2514
2587
  ...store.configDirEnvVar ? { configDirEnvVar: store.configDirEnvVar } : {},
2588
+ ...store.transcriptPath ? { transcriptPath: store.transcriptPath } : {},
2515
2589
  fsProbe: async (cwd, prevStartedAt, expectedId, configDir) => {
2516
2590
  const candidates = await s.discover({
2517
2591
  cwd,
@@ -2528,7 +2602,8 @@ var RESUME_STRATEGIES = Object.fromEntries(
2528
2602
  );
2529
2603
  function decideRestartStrategy(prev, opts = {}) {
2530
2604
  const mayPreferNative = prev.pty === true || opts.preferNativeTerminal === true;
2531
- if (prev.adapterSlug && prev.nativeTerminalResume === true && mayPreferNative) {
2605
+ const hasNativeCapability = prev.nativeTerminalResume === true || prev.pty === true && prev.nativeTerminalResume === void 0;
2606
+ if (prev.adapterSlug && hasNativeCapability && mayPreferNative) {
2532
2607
  const strategy = RESUME_STRATEGIES[prev.adapterSlug];
2533
2608
  const id = strategy?.storeAs ? prev.resumeMetadata?.[strategy.storeAs] : void 0;
2534
2609
  if (strategy?.spawnArgs && id) {
@@ -2583,13 +2658,30 @@ async function augmentWithFsResume(prev) {
2583
2658
  }
2584
2659
  };
2585
2660
  }
2661
+ async function probeNativeTranscript(prev) {
2662
+ if (!prev.adapterSlug || !prev.cwd) return void 0;
2663
+ const strategy = RESUME_STRATEGIES[prev.adapterSlug];
2664
+ if (!strategy?.transcriptPath) return void 0;
2665
+ const id = prev.resumeMetadata?.[strategy.storeAs] ?? prev.adapterSessionId;
2666
+ if (!id) return void 0;
2667
+ const path = strategy.transcriptPath(prev.cwd, id, prev.adapterConfigDir);
2668
+ let exists = false;
2669
+ try {
2670
+ exists = (await promises.stat(path)).isFile();
2671
+ } catch {
2672
+ exists = false;
2673
+ }
2674
+ return { dir: dirname(path), path, exists };
2675
+ }
2586
2676
  function describeResumePath(prev, opts = {}) {
2587
2677
  const mayPreferNative = prev.pty === true || opts.preferNativeTerminal === true;
2588
- if (prev.adapterSlug && mayPreferNative) {
2678
+ const hasNativeCapability = prev.nativeTerminalResume === true || prev.pty === true && prev.nativeTerminalResume === void 0;
2679
+ if (prev.adapterSlug && hasNativeCapability && mayPreferNative) {
2589
2680
  const s = RESUME_STRATEGIES[prev.adapterSlug];
2590
2681
  if (s?.spawnArgs && s.storeAs && prev.resumeMetadata?.[s.storeAs]) {
2591
- const sample = s.spawnArgs("\u2026")[0] ?? prev.adapterSlug;
2592
- return `resumed via ${sample} --resume`;
2682
+ const sample = s.spawnArgs("\u2026");
2683
+ const idIndex = sample.indexOf("\u2026");
2684
+ return `resumed via ${idIndex > 0 ? sample.slice(0, idIndex).join(" ") : sample[0] ?? prev.adapterSlug}`;
2593
2685
  }
2594
2686
  }
2595
2687
  if (prev.adapterSlug && prev.adapterSessionId) {
@@ -2628,6 +2720,9 @@ function tokenizeCommand(s) {
2628
2720
  return out;
2629
2721
  }
2630
2722
 
2723
+ // src/sessions.ts
2724
+ init_conversation_store();
2725
+
2631
2726
  // src/command-log.ts
2632
2727
  init_transcript_writer();
2633
2728
  function writeCommandLogEntry(sessionId, entry, baseDir) {
@@ -5280,6 +5375,9 @@ async function resolveWorkspaceRules(slug, deps2 = {}) {
5280
5375
 
5281
5376
  // src/session-title.ts
5282
5377
  var MAX_LENGTH = 72;
5378
+ function isInternalDerivedTitle(title) {
5379
+ return /^<(?:local-command-caveat|local-command-stdout|command-name|task-notification)>/i.test(title);
5380
+ }
5283
5381
  function isRecord3(value) {
5284
5382
  return typeof value === "object" && value !== null;
5285
5383
  }
@@ -5306,6 +5404,7 @@ function deriveSessionTitle(message) {
5306
5404
  if (raw === void 0) return void 0;
5307
5405
  const collapsed = raw.replace(/\s+/g, " ").trim();
5308
5406
  if (collapsed === "") return void 0;
5407
+ if (isInternalDerivedTitle(collapsed)) return void 0;
5309
5408
  const sentence = collapsed.replace(/[.?!](\s.*)?$/, "").trim();
5310
5409
  if (sentence === "" || !/[\p{L}\p{N}]/u.test(sentence)) return void 0;
5311
5410
  return Array.from(sentence).length > MAX_LENGTH ? truncate2(sentence, MAX_LENGTH) : sentence;
@@ -8205,8 +8304,175 @@ var sandboxSpecWithReuseSchema = z.object({
8205
8304
  "Reconnect to an existing box instead of booting a new one. Accepts a prior session's exact `sandboxId`, a ledger `label` (exact match), or an unambiguous prefix of a ledger sandboxId \u2014 resolved against the local sandbox ledger (`~/.agentproto/sandboxes.json`, see `agentproto sandbox list`). A prefix matching more than one ledger entry fails with `sandbox_reuse_ambiguous` listing the candidates. Requires the provider to support reconnect (e.g. e2b); omit to boot fresh (default)."
8206
8305
  )
8207
8306
  }).strict();
8208
-
8209
- // src/agent-tools.ts
8307
+ function appUiToolId(appId) {
8308
+ const slug = appId.replace(/^@[^/]+\//, "").replace(/[^a-z0-9]/g, "_");
8309
+ return `app_ui_${slug}`;
8310
+ }
8311
+ var MCP_APP_BRIDGE_SCRIPT = `<script>
8312
+ (function () {
8313
+ window.__AGENTPROTO_UI_TRANSPORT__ = "mcp";
8314
+ if (window.McpApp) return;
8315
+ var nextId = 1, pending = {}, teardownCbs = [];
8316
+ function post(m) { window.parent.postMessage(m, "*"); }
8317
+ window.addEventListener("message", function (e) {
8318
+ var m = e.data;
8319
+ if (typeof m === "string") { try { m = JSON.parse(m); } catch (_) { return; } }
8320
+ if (!m || m.jsonrpc !== "2.0") return;
8321
+ if (m.method === "ui/resource-teardown") {
8322
+ for (var i = 0; i < teardownCbs.length; i++) teardownCbs[i]();
8323
+ post({ jsonrpc: "2.0", id: m.id, result: {} });
8324
+ return;
8325
+ }
8326
+ if (m.id != null && pending[m.id]) { var cb = pending[m.id]; delete pending[m.id]; cb(m.result, m.error); }
8327
+ });
8328
+ function request(method, params) {
8329
+ return new Promise(function (resolve, reject) {
8330
+ var id = nextId++;
8331
+ pending[id] = function (r, err) { if (err) reject(new Error((err && err.message) || String(err))); else resolve(r); };
8332
+ post({ jsonrpc: "2.0", id: id, method: method, params: params });
8333
+ });
8334
+ }
8335
+ window.McpApp = {
8336
+ connect: function () {
8337
+ if (window.parent === window) return Promise.reject(new Error("no host (standalone)"));
8338
+ return request("ui/initialize", {
8339
+ appInfo: { name: "agentproto-app", version: "1.0" },
8340
+ appCapabilities: { availableDisplayModes: ["inline", "fullscreen"] },
8341
+ protocolVersion: "2026-01-26"
8342
+ }).then(function () {
8343
+ post({ jsonrpc: "2.0", method: "ui/notifications/initialized", params: {} });
8344
+ return {
8345
+ callTool: function (name, args) { return request("tools/call", { name: name, arguments: args || {} }); },
8346
+ sendMessage: function (p) {
8347
+ var params = typeof p === "string" ? { content: p } : (p || {});
8348
+ var content = typeof params.content === "string" ? [{ type: "text", text: params.content }] : (params.content || []);
8349
+ return request("ui/message", { role: "user", content: content });
8350
+ },
8351
+ updateModelContext: function (p) { return request("ui/update-model-context", p || {}); },
8352
+ openLink: function (url) { return request("ui/open-link", { url: url }); },
8353
+ onTeardown: function (cb) { teardownCbs.push(cb); }
8354
+ };
8355
+ });
8356
+ }
8357
+ };
8358
+ })();
8359
+ </script>
8360
+ `;
8361
+ function injectAfterStructuralTag(html, script) {
8362
+ for (const tag of ["head", "body", "html"]) {
8363
+ const match = html.match(new RegExp(`<${tag}[^>]*>`, "i"));
8364
+ if (match?.index !== void 0) {
8365
+ const insertAt = match.index + match[0].length;
8366
+ return html.slice(0, insertAt) + script + html.slice(insertAt);
8367
+ }
8368
+ }
8369
+ return script + html;
8370
+ }
8371
+ function injectMcpAppBridge(html) {
8372
+ const hasBridge = /window\.McpApp\s*=/.test(html);
8373
+ const hasRunnerSelect = /window\.AgentprotoUI\s*=/.test(html);
8374
+ if (hasBridge && hasRunnerSelect) return html;
8375
+ const script = (hasBridge ? "" : MCP_APP_BRIDGE_SCRIPT) + (hasRunnerSelect ? "" : RUNNER_SELECT_SCRIPT);
8376
+ return injectAfterStructuralTag(html, script);
8377
+ }
8378
+ var STANDALONE_REST_BRIDGE_SCRIPT = `<script>
8379
+ (function () {
8380
+ window.__AGENTPROTO_UI_TRANSPORT__ = "http";
8381
+ if (window.McpApp) return;
8382
+ window.McpApp = {
8383
+ connect: function () {
8384
+ return Promise.resolve({
8385
+ callTool: function (name, args) {
8386
+ return fetch("./tool-call", {
8387
+ method: "POST",
8388
+ headers: { "content-type": "application/json" },
8389
+ body: JSON.stringify({ tool: name, args: args || {} })
8390
+ }).then(function (res) {
8391
+ return res.json().catch(function () {
8392
+ throw new Error("tool-call failed: HTTP " + res.status);
8393
+ }).then(function (body) {
8394
+ if (!res.ok) throw new Error((body && (body.message || body.error)) || ("tool-call failed: HTTP " + res.status));
8395
+ return body;
8396
+ });
8397
+ });
8398
+ },
8399
+ sendMessage: function () {
8400
+ return Promise.reject(new Error("sendMessage: no chat host (standalone mode)"));
8401
+ },
8402
+ updateModelContext: function () {
8403
+ return Promise.reject(new Error("updateModelContext: no host (standalone mode)"));
8404
+ },
8405
+ openLink: function (url) {
8406
+ window.open(url, "_blank");
8407
+ return Promise.resolve({});
8408
+ },
8409
+ onTeardown: function () {}
8410
+ });
8411
+ }
8412
+ };
8413
+ })();
8414
+ </script>
8415
+ `;
8416
+ function injectStandaloneAppBridge(html, baseUrl) {
8417
+ const hasRunnerSelect = /window\.AgentprotoUI\s*=/.test(html);
8418
+ const baseUrlScript = baseUrl ? `<script>window.__AGENTPROTO_BASEURL__=${JSON.stringify(baseUrl).replace(/</g, "\\u003c")};</script>` : "";
8419
+ const script = baseUrlScript + STANDALONE_REST_BRIDGE_SCRIPT + (hasRunnerSelect ? "" : RUNNER_SELECT_SCRIPT);
8420
+ return injectAfterStructuralTag(html, script);
8421
+ }
8422
+ function createUiHtmlCache() {
8423
+ const cache = /* @__PURE__ */ new Map();
8424
+ return {
8425
+ async get(path, version) {
8426
+ const cached = cache.get(path);
8427
+ if (cached && cached.version === version) return cached.html;
8428
+ const raw = await readFile(path, "utf8");
8429
+ const html = injectMcpAppBridge(raw);
8430
+ cache.set(path, { version, html });
8431
+ return html;
8432
+ }
8433
+ };
8434
+ }
8435
+ async function makeInstalledAppUiApps(appRegistry, cache, existingToolNames) {
8436
+ const apps = [];
8437
+ const seen = new Set(existingToolNames);
8438
+ for (const app of appRegistry.listApps()) {
8439
+ const ui = app.ui;
8440
+ if (!ui) continue;
8441
+ const toolId = appUiToolId(app.appId);
8442
+ if (seen.has(toolId)) {
8443
+ console.warn(
8444
+ `[app-ui-apps] skipping UI panel for app "${app.appId}": tool id "${toolId}" collides with an existing tool or another installed app's panel.`
8445
+ );
8446
+ continue;
8447
+ }
8448
+ let html;
8449
+ try {
8450
+ html = await cache.get(ui.path, app.updatedAt);
8451
+ } catch (err) {
8452
+ console.warn(
8453
+ `[app-ui-apps] skipping UI panel for app "${app.appId}": could not read "${ui.path}": ${err instanceof Error ? err.message : String(err)}`
8454
+ );
8455
+ continue;
8456
+ }
8457
+ seen.add(toolId);
8458
+ apps.push({
8459
+ id: toolId,
8460
+ title: ui.title ?? app.name ?? app.appId,
8461
+ ...ui.description ? { description: ui.description } : {},
8462
+ inputSchema: z.object({}),
8463
+ execute: async () => ({ appId: app.appId, tools: ui.tools ?? [] }),
8464
+ html,
8465
+ ...ui.csp ? {
8466
+ csp: {
8467
+ ...ui.csp.connectDomains ? { connectDomains: [...ui.csp.connectDomains] } : {},
8468
+ ...ui.csp.resourceDomains ? { resourceDomains: [...ui.csp.resourceDomains] } : {},
8469
+ ...ui.csp.frameDomains ? { frameDomains: [...ui.csp.frameDomains] } : {}
8470
+ }
8471
+ } : {}
8472
+ });
8473
+ }
8474
+ return apps;
8475
+ }
8210
8476
  function stripAnsi(s) {
8211
8477
  let result = s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
8212
8478
  result = result.replace(/\r\n/g, "\n");
@@ -8261,8 +8527,12 @@ function registerAgentTools(server, opts) {
8261
8527
  resolveSandboxProvider: resolveSandboxProvider2,
8262
8528
  provisionWorktree,
8263
8529
  resolveWorktreeIsolation,
8264
- supervisor
8530
+ supervisor,
8531
+ defaultAgentPromptInterrupt,
8532
+ isSessionChatInstalled
8265
8533
  } = opts;
8534
+ const interruptDefault = defaultAgentPromptInterrupt ?? false;
8535
+ const sessionChatResourceUri = isSessionChatInstalled?.() ? `ui://${appUiToolId(SESSION_CHAT_APP_ID)}/view` : "ui://agentproto_session_chat/view";
8266
8536
  server.registerTool(
8267
8537
  "agent_start",
8268
8538
  {
@@ -8498,14 +8768,25 @@ function registerAgentTools(server, opts) {
8498
8768
  "Isolate this session in its OWN git worktree instead of spawning directly in `cwd` \u2014 so a parallel agent can't collide on the working tree. `true` provisions a worktree on a fresh branch `wt/<slug>` cut from origin/main (slug auto-minted from `label`); pass `{ slug, base, async }` to pin either, or opt into an early return (`async`, see that field's own description). The daemon boots the worktree (git worktree add + the repo's agentproto.json setup hooks) and spawns `adapter` THERE; the session's cwd, and every path it edits, live inside the worktree. Honoured only for a ROOT spawn (a spawn made THROUGH an orchestrator inherits its parent's tree \u2014 no second worktree; an EXPLICIT `worktree` on such a nested spawn is REJECTED, not silently ignored \u2014 use `sandbox` to isolate a child) and only when `cwd` is inside a git repo (nothing to isolate otherwise \u21D2 spawns plain, no error). The daemon's `worktrees.isolation` policy may force this ON for every root spawn (`always`) or OFF (`never`, which REJECTS an explicit `worktree`). Ignored for a `sandbox` spawn (the box already isolates). The worktree is NOT auto-removed on session close \u2014 it holds the agent's work; tear it down with `agentproto worktree rm|archive|gc`."
8499
8769
  )
8500
8770
  },
8501
- // Live-session widget: rendering agent_start's result auto-mounts the
8502
- // live widget for the new session (ext-apps `_meta.ui.resourceUri` at
8503
- // the tool-definition level — same mechanism as the panel apps in
8504
- // mcp-apps-adapter.ts). `visibility:["model","app"]` keeps agent_start
8505
- // fully usable by the model AND lets the widget re-call it if needed.
8771
+ // Session-chat widget: rendering agent_start's result auto-mounts the
8772
+ // session-chat launcher for the new session (ext-apps
8773
+ // `_meta.ui.resourceUri` at the tool-definition level — same mechanism
8774
+ // as the panel apps in mcp-apps-adapter.ts). The widget reads the
8775
+ // spawned `{ id: "sess_…" }` off the host's tool-result notification
8776
+ // and deep-links the installed `@agentik/session-chat` app straight
8777
+ // into that session (apps/src/session-chat/panel.ts). The older
8778
+ // `ui://live_session/view` resource stays registered for its own
8779
+ // `live_session` tool and other consumers — only this binding moved.
8780
+ // `visibility:["model","app"]` keeps agent_start fully usable by the
8781
+ // model AND lets the widget re-call it if needed. `resourceUri` binds
8782
+ // to the native `app_ui_session_chat` MCP Apps tool once
8783
+ // `@agentik/session-chat` is installed (`isSessionChatInstalled`
8784
+ // above) — the loopback-HTTP `agentproto_session_chat` launcher this
8785
+ // otherwise binds to is unreachable under a strict-CSP host (Codex),
8786
+ // and builtin-apps.ts stops mounting it once the native tool exists.
8506
8787
  _meta: {
8507
8788
  ui: {
8508
- resourceUri: "ui://live_session/view",
8789
+ resourceUri: sessionChatResourceUri,
8509
8790
  visibility: ["model", "app"]
8510
8791
  }
8511
8792
  }
@@ -8618,7 +8899,7 @@ function registerAgentTools(server, opts) {
8618
8899
  id: sessionIdAliasField,
8619
8900
  prompt: z.string().min(1).describe("The next user turn (plain text)."),
8620
8901
  interrupt: z.boolean().optional().describe(
8621
- "When true and the session is mid-turn, cancel the in-flight turn and deliver this prompt on the same session instead of rejecting. No-op when the session is already idle. Default false (mid-turn rejects, as today)."
8902
+ "When true and the session is mid-turn, cancel the in-flight turn and deliver this prompt on the same session immediately instead of queueing it behind the current turn. No-op when the session is already idle. UNSET falls back to the daemon default `defaults.agentPromptInterrupt` in config.json (false unless an operator changed it) \u2014 so a mid-turn target queues by default; pass `interrupt: true` explicitly to cut now."
8622
8903
  ),
8623
8904
  queue: z.boolean().optional().describe(
8624
8905
  "When the session is mid-turn, queue this prompt (FIFO) and dispatch it automatically once the current turn ends instead of rejecting. Default true. Explicit false restores the old reject-when-busy behavior."
@@ -8629,8 +8910,9 @@ function registerAgentTools(server, opts) {
8629
8910
  if (!sessionId) return missingSessionIdError("agent_prompt");
8630
8911
  try {
8631
8912
  const promptSource = callerScope?.ownerSessionId ?? callerSessionId;
8632
- await registry.enqueuePrompt(sessionId, input.prompt, {
8633
- interrupt: input.interrupt,
8913
+ const effectiveInterrupt = input.interrupt ?? interruptDefault;
8914
+ const { queued } = await registry.enqueuePrompt(sessionId, input.prompt, {
8915
+ interrupt: effectiveInterrupt,
8634
8916
  // Queue by default: a mid-turn session holds the prompt in its
8635
8917
  // FIFO queue and dispatches it at turn end, so callers never
8636
8918
  // lose a prompt to the busy rejection. Explicit `queue: false`
@@ -8638,12 +8920,21 @@ function registerAgentTools(server, opts) {
8638
8920
  queue: input.queue ?? true,
8639
8921
  ...promptSource ? { source: `agent:${promptSource}` } : {}
8640
8922
  });
8923
+ const hintQueued = queued && input.interrupt === void 0;
8641
8924
  return {
8642
8925
  content: [
8643
8926
  {
8644
8927
  type: "text",
8645
8928
  text: JSON.stringify(
8646
- { ok: true, sessionId, queued: true },
8929
+ {
8930
+ ok: true,
8931
+ sessionId,
8932
+ queued: true,
8933
+ ...hintQueued ? {
8934
+ delivery: "queued-mid-turn",
8935
+ hint: "Message queued \u2014 it will only be delivered when the target's CURRENT turn ends. If it's urgent, re-send with interrupt: true (cancels the in-flight turn and redirects the session onto this prompt now)."
8936
+ } : {}
8937
+ },
8647
8938
  null,
8648
8939
  2
8649
8940
  )
@@ -8672,9 +8963,12 @@ function registerAgentTools(server, opts) {
8672
8963
  );
8673
8964
  server.tool(
8674
8965
  "message_parent",
8675
- "Report a message UP to the session that spawned you (your parent/supervisor) \u2014 a result, a progress update, or a blocker. No session id needed: the daemon resolves your recorded parent from your own session identity (also visible as the AGENTPROTO_PARENT_SESSION_ID env var). Delivered as a prompt when the parent is idle, or queued onto its next turn when it's mid-turn (never interrupts). Errors if this session has no recorded parent or the parent is gone.",
8966
+ "Report a message UP to the session that spawned you (your parent/supervisor) \u2014 a result, a progress update, or a blocker. No session id needed: the daemon resolves your recorded parent from your own session identity (also visible as the AGENTPROTO_PARENT_SESSION_ID env var). Delivered as a prompt when the parent is idle, or queued onto its next turn when it's mid-turn (never interrupts, by default). Pass `interrupt: true` to CUT a mid-turn parent immediately \u2014 cancel its in-flight turn and redirect it onto this message now (same-context cancel, like `agent_prompt`'s `interrupt`) \u2014 for a genuinely urgent report the parent must act on before it finishes what it's doing. Errors if this session has no recorded parent or the parent is gone.",
8676
8967
  {
8677
- message: z.string().min(1).describe("The message to deliver to your parent session (plain text).")
8968
+ message: z.string().min(1).describe("The message to deliver to your parent session (plain text)."),
8969
+ interrupt: z.boolean().optional().describe(
8970
+ "When true and the parent is mid-turn, cancel its in-flight turn and deliver this message immediately instead of queueing it onto the parent's next turn. No-op when the parent is idle (delivered as a normal prompt either way). UNSET falls back to the daemon default `defaults.agentPromptInterrupt` in config.json (false unless an operator changed it) \u2014 so a report never interrupts by default; pass `interrupt: true` explicitly to cut."
8971
+ )
8678
8972
  },
8679
8973
  async (input) => {
8680
8974
  const fail = (text10) => ({
@@ -8708,11 +9002,12 @@ function registerAgentTools(server, opts) {
8708
9002
  }
8709
9003
  const who = self.label ?? selfId;
8710
9004
  const notice = `[child-message] ${who} (${selfId}): ${input.message}`;
8711
- const done = (delivery) => ({
9005
+ const effectiveInterrupt = input.interrupt ?? interruptDefault;
9006
+ const done = (delivery, extra) => ({
8712
9007
  content: [
8713
9008
  {
8714
9009
  type: "text",
8715
- text: JSON.stringify({ ok: true, parentSessionId: parentId, delivery })
9010
+ text: JSON.stringify({ ok: true, parentSessionId: parentId, delivery, ...extra })
8716
9011
  }
8717
9012
  ]
8718
9013
  });
@@ -8723,12 +9018,27 @@ function registerAgentTools(server, opts) {
8723
9018
  } catch {
8724
9019
  }
8725
9020
  }
9021
+ if (effectiveInterrupt) {
9022
+ try {
9023
+ await registry.enqueuePrompt(parentId, notice, {
9024
+ interrupt: true,
9025
+ origin: `child:${who}`
9026
+ });
9027
+ return done("interrupted");
9028
+ } catch {
9029
+ }
9030
+ }
8726
9031
  if (!registry.stampPendingChildCrashNotice(parentId, notice)) {
8727
9032
  return fail(
8728
9033
  `message_parent: parent session "${parentId}" vanished mid-delivery.`
8729
9034
  );
8730
9035
  }
8731
- return done("queued-next-turn");
9036
+ return done(
9037
+ "queued-next-turn",
9038
+ input.interrupt === void 0 ? {
9039
+ hint: "Message queued \u2014 it will only reach the parent when its CURRENT turn ends. If it's urgent, re-send with interrupt: true (cancels the parent's in-flight turn and redirects it onto this message now)."
9040
+ } : void 0
9041
+ );
8732
9042
  }
8733
9043
  );
8734
9044
  server.tool(
@@ -10316,6 +10626,149 @@ function filterSessionObserver(inner, shouldObserve) {
10316
10626
  init_tool_presenter();
10317
10627
  init_transcript_writer();
10318
10628
 
10629
+ // src/session-titler.ts
10630
+ init_config();
10631
+ init_transcript_writer();
10632
+ var DEFAULT_TITLER_MODEL = "z-ai/glm-5.2@openrouter";
10633
+ var DEFAULT_LABELS = /* @__PURE__ */ new Set(["chat-starter", "chat-starter-autoprompt", "agentproto"]);
10634
+ var MIN_TURN_CHARS = 200;
10635
+ var titledSessions = /* @__PURE__ */ new Set();
10636
+ async function resolveTitlerConfig() {
10637
+ const cfg = await loadConfig();
10638
+ const t = cfg.titler;
10639
+ return { enabled: t?.enabled === true, model: t?.model ?? DEFAULT_TITLER_MODEL };
10640
+ }
10641
+ function isDefaultLabel(desc) {
10642
+ const label = desc.label?.trim() ?? "";
10643
+ if (label === "") return true;
10644
+ if (desc.title !== void 0 && label === desc.title) return true;
10645
+ return DEFAULT_LABELS.has(label);
10646
+ }
10647
+ function fallbackTitle(userText) {
10648
+ const firstLine = userText.split(/\r?\n/).find((l) => l.trim().length > 0)?.trim() ?? "";
10649
+ return firstLine.split(/\s+/).filter(Boolean).slice(0, 6).join(" ");
10650
+ }
10651
+ function sanitizeTitle(raw) {
10652
+ if (!raw) return void 0;
10653
+ const cleaned = raw.replace(/[`*_"“”'']/g, "").split(/\r?\n/)[0]?.trim();
10654
+ if (!cleaned) return void 0;
10655
+ const words = cleaned.split(/\s+/).map((w) => w.replace(/[.,;:!?]+$/u, "")).filter((w) => /[\p{L}\p{N}]/u.test(w)).slice(0, 8);
10656
+ const title = words.join(" ");
10657
+ return title.length > 0 ? title : void 0;
10658
+ }
10659
+ async function readFirstTurn(sessionId) {
10660
+ let stream;
10661
+ try {
10662
+ stream = createReadStream(sessionEventsPath(sessionId), { encoding: "utf8" });
10663
+ await new Promise((resolve31, reject) => {
10664
+ stream.once("error", reject);
10665
+ stream.once("open", resolve31);
10666
+ });
10667
+ } catch {
10668
+ return null;
10669
+ }
10670
+ let userText = "";
10671
+ let assistantText = "";
10672
+ let sawUserPrompt = false;
10673
+ try {
10674
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
10675
+ for await (const line of rl) {
10676
+ const trimmed = line.trim();
10677
+ if (!trimmed) continue;
10678
+ let rec;
10679
+ try {
10680
+ rec = JSON.parse(trimmed);
10681
+ } catch {
10682
+ continue;
10683
+ }
10684
+ if (rec.kind === "user-prompt" && typeof rec.text === "string") {
10685
+ userText = rec.text;
10686
+ sawUserPrompt = true;
10687
+ } else if (rec.kind === "text-delta" && typeof rec.text === "string") {
10688
+ assistantText += rec.text;
10689
+ } else if (rec.kind === "turn-end") {
10690
+ break;
10691
+ }
10692
+ }
10693
+ } catch {
10694
+ return null;
10695
+ }
10696
+ if (!sawUserPrompt) return null;
10697
+ return { userText, assistantText };
10698
+ }
10699
+ async function generateTitleViaLlm(model, input) {
10700
+ const apiKey = process.env.OPENROUTER_API_KEY;
10701
+ if (!apiKey) return void 0;
10702
+ const excerpt = (s, n) => s.length <= n ? s : `${s.slice(0, n)}\u2026`;
10703
+ const body = {
10704
+ model,
10705
+ max_tokens: 64,
10706
+ messages: [
10707
+ {
10708
+ role: "system",
10709
+ content: "You write short prosaic titles for agent sessions. Reply with ONLY the title: 4-8 plain words, no quotes, no punctuation at the end, describing what the session is doing."
10710
+ },
10711
+ {
10712
+ role: "user",
10713
+ content: `User asked: ${excerpt(input.userText, 2e3)}
10714
+ Assistant worked on: ${excerpt(input.assistantText, 2e3)}
10715
+ Title:`
10716
+ }
10717
+ ]
10718
+ };
10719
+ try {
10720
+ const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
10721
+ method: "POST",
10722
+ headers: {
10723
+ "content-type": "application/json",
10724
+ authorization: `Bearer ${apiKey}`
10725
+ },
10726
+ body: JSON.stringify(body),
10727
+ signal: AbortSignal.timeout(15e3)
10728
+ });
10729
+ if (!res.ok) return void 0;
10730
+ const parsed = await res.json();
10731
+ const content = parsed.choices?.[0]?.message?.content;
10732
+ return typeof content === "string" ? content : void 0;
10733
+ } catch {
10734
+ return void 0;
10735
+ }
10736
+ }
10737
+ async function maybeTitleSession(registry, sessionId, opts = {}) {
10738
+ try {
10739
+ if (titledSessions.has(sessionId)) return null;
10740
+ titledSessions.add(sessionId);
10741
+ const desc = registry.get(sessionId);
10742
+ if (!desc || desc.kind !== "agent-cli") return null;
10743
+ if (desc.renamedByUser === true) return null;
10744
+ if (!isDefaultLabel(desc)) return null;
10745
+ const generate = opts.generate ?? (async (t) => {
10746
+ const { enabled, model } = await resolveTitlerConfig();
10747
+ if (!enabled) return void 0;
10748
+ return generateTitleViaLlm(model, t);
10749
+ });
10750
+ if (!opts.generate) {
10751
+ const { enabled } = await resolveTitlerConfig();
10752
+ if (!enabled) return null;
10753
+ }
10754
+ const read = opts.readFirstTurn ?? readFirstTurn;
10755
+ const turn = await read(sessionId);
10756
+ if (!turn) return null;
10757
+ const minChars = opts.minChars ?? MIN_TURN_CHARS;
10758
+ if (turn.userText.length + turn.assistantText.length < minChars) return null;
10759
+ const generated = sanitizeTitle(await generate(turn));
10760
+ const title = generated ?? fallbackTitle(turn.userText);
10761
+ if (!title) return null;
10762
+ registry.renameSession(sessionId, { label: title });
10763
+ return title;
10764
+ } catch (err) {
10765
+ console.warn(
10766
+ `[session-titler] maybeTitleSession(${sessionId}) failed: ${err instanceof Error ? err.message : String(err)}`
10767
+ );
10768
+ return null;
10769
+ }
10770
+ }
10771
+
10319
10772
  // src/resume-context-digest.ts
10320
10773
  init_transcript_export();
10321
10774
  var DIGEST_CHAR_BUDGET = 7e3;
@@ -10915,6 +11368,9 @@ function promptOriginLabel(item) {
10915
11368
  if (code.startsWith("child:")) return `child ${code.slice("child:".length)}`;
10916
11369
  return code;
10917
11370
  }
11371
+ function isShellOnlyRow(desc) {
11372
+ return desc.kind === "command" || desc.kind === "terminal" && !isConversationTerminal(desc);
11373
+ }
10918
11374
  function toSessionSummary(desc) {
10919
11375
  return {
10920
11376
  id: desc.id,
@@ -11308,6 +11764,7 @@ function createSessionsRegistry(opts) {
11308
11764
  }
11309
11765
  const emitExited = (rt) => {
11310
11766
  if (rt.exitedEmitted) return;
11767
+ rt.linkProbeStop?.();
11311
11768
  cancelPendingPermissionsForSession(rt);
11312
11769
  if (runWorktreeAutoReclaim && rt.desc.worktreeAutoProvisioned && rt.desc.worktreePath) {
11313
11770
  const worktreePath = rt.desc.worktreePath;
@@ -11489,8 +11946,9 @@ function createSessionsRegistry(opts) {
11489
11946
  const recordConversationLink = (rt, adapterSessionIdOverride) => {
11490
11947
  if (!persist || !partitioned) return;
11491
11948
  const desc = rt.desc;
11492
- if (desc.kind !== "agent-cli") return;
11493
- const adapterSlug = desc.adapterSlug;
11949
+ const isConvTerminal = desc.kind === "terminal" && isConversationTerminal(desc);
11950
+ if (desc.kind !== "agent-cli" && !isConvTerminal) return;
11951
+ const adapterSlug = desc.adapterSlug ?? (isConvTerminal ? conversationTerminalSlugFor(desc) : void 0);
11494
11952
  const adapterSessionId = adapterSessionIdOverride ?? desc.adapterSessionId;
11495
11953
  const cwd = desc.cwd;
11496
11954
  if (!adapterSlug || !adapterSessionId || !cwd) return;
@@ -11534,6 +11992,81 @@ function createSessionsRegistry(opts) {
11534
11992
  void persistSnapshot();
11535
11993
  }, PERSIST_DEBOUNCE_MS2);
11536
11994
  };
11995
+ const conversationLinkProbeMs = opts?.conversationLinkProbeMs ?? {
11996
+ initialMs: 3e3,
11997
+ intervalMs: 15e3
11998
+ };
11999
+ const startConversationTerminalLinkProbe = (rt) => {
12000
+ const desc = rt.desc;
12001
+ const slug = desc.adapterSlug ?? conversationTerminalSlugFor(desc);
12002
+ const store = slug ? CONVERSATION_STORES[slug] : void 0;
12003
+ const cwd = desc.cwd;
12004
+ if (!store || !cwd) return;
12005
+ let inFlight = false;
12006
+ let stopped = false;
12007
+ const timers = [];
12008
+ const stop = () => {
12009
+ stopped = true;
12010
+ for (const t of timers) clearTimeout(t);
12011
+ timers.length = 0;
12012
+ };
12013
+ rt.linkProbeStop = stop;
12014
+ const tick = async () => {
12015
+ if (stopped || inFlight) return;
12016
+ if (desc.status !== "running") {
12017
+ stop();
12018
+ return;
12019
+ }
12020
+ if (!desc.lastOutputAt) return;
12021
+ if (desc.adapterSessionId) {
12022
+ stop();
12023
+ return;
12024
+ }
12025
+ inFlight = true;
12026
+ try {
12027
+ const candidates = await store.discover({
12028
+ cwd,
12029
+ since: desc.startedAt,
12030
+ // A PTY writes through the provider's native arm, never ACP.
12031
+ attachmentMode: "native"
12032
+ // No configDir on purpose: nothing threaded CLAUDE_CONFIG_DIR
12033
+ // into this PTY, so the global store is where the file is.
12034
+ });
12035
+ if (candidates.length === 1) {
12036
+ const candidate = candidates[0];
12037
+ desc.adapterSessionId = candidate.conversationId;
12038
+ desc.resumeMetadata = {
12039
+ ...desc.resumeMetadata ?? {},
12040
+ [store.storeAs]: candidate.conversationId
12041
+ };
12042
+ delete desc.linkStatus;
12043
+ if (!desc.title && candidate.preview) {
12044
+ const derived = deriveSessionTitle(candidate.preview);
12045
+ if (derived) desc.title = derived;
12046
+ }
12047
+ schedulePersist();
12048
+ recordConversationLink(rt, candidate.conversationId);
12049
+ stop();
12050
+ } else if (candidates.length > 1) {
12051
+ desc.linkStatus = "ambiguous";
12052
+ schedulePersist();
12053
+ }
12054
+ } catch {
12055
+ } finally {
12056
+ inFlight = false;
12057
+ }
12058
+ };
12059
+ const schedule = (delayMs) => {
12060
+ if (stopped) return;
12061
+ const t = setTimeout(() => {
12062
+ void tick().finally(() => schedule(conversationLinkProbeMs.intervalMs));
12063
+ }, delayMs);
12064
+ t.unref?.();
12065
+ timers.length = 0;
12066
+ timers.push(t);
12067
+ };
12068
+ schedule(conversationLinkProbeMs.initialMs);
12069
+ };
11537
12070
  const snapshotRows = () => Array.from(sessions.values()).map((s) => {
11538
12071
  const {
11539
12072
  processAlive: _processAlive,
@@ -11617,8 +12150,9 @@ function createSessionsRegistry(opts) {
11617
12150
  rt.emitter.emit("line", { line, stream });
11618
12151
  };
11619
12152
  const sniffResumeHints = (rt, line) => {
11620
- if (rt.desc.kind !== "agent-cli") return;
11621
- const strategy = rt.desc.adapterSlug ? RESUME_STRATEGIES[rt.desc.adapterSlug] : void 0;
12153
+ if (rt.desc.kind !== "agent-cli" && rt.conversationTerminal !== true) return;
12154
+ const strategySlug = rt.desc.adapterSlug ?? conversationTerminalSlugFor(rt.desc);
12155
+ const strategy = strategySlug ? RESUME_STRATEGIES[strategySlug] : void 0;
11622
12156
  if (!strategy?.outputHint) return;
11623
12157
  const plain = stripAnsiCodes(line);
11624
12158
  const m = plain.match(strategy.outputHint);
@@ -11631,6 +12165,14 @@ function createSessionsRegistry(opts) {
11631
12165
  recordConversationLink(rt, m[1]);
11632
12166
  }
11633
12167
  };
12168
+ const sniffConversationTerminalChunk = (rt, chunk) => {
12169
+ const text10 = (rt.sniffCarry ?? "") + chunk.toString("utf8");
12170
+ const lines = text10.split(/\r?\n|\r/);
12171
+ rt.sniffCarry = (lines.pop() ?? "").slice(-4096);
12172
+ for (const line of lines) {
12173
+ if (line) sniffResumeHints(rt, line);
12174
+ }
12175
+ };
11634
12176
  const appendBytes = (rt, chunk) => {
11635
12177
  terminalTranscriptWriter.appendChunk(rt.desc.id, chunk);
11636
12178
  rt.recentBytes.push(chunk);
@@ -11640,6 +12182,7 @@ function createSessionsRegistry(opts) {
11640
12182
  if (dropped) rt.recentBytesSize -= dropped.byteLength;
11641
12183
  }
11642
12184
  rt.desc.lastOutputAt = (/* @__PURE__ */ new Date()).toISOString();
12185
+ if (rt.conversationTerminal === true) sniffConversationTerminalChunk(rt, chunk);
11643
12186
  rt.emitter.emit("data", chunk);
11644
12187
  };
11645
12188
  const reconcilePtySize = (rt) => {
@@ -12506,6 +13049,9 @@ ${message}`;
12506
13049
  });
12507
13050
  }
12508
13051
  if (overBudget) emitExited(rt);
13052
+ if ((rt.desc.turnsCompleted ?? 0) === 1) {
13053
+ void maybeTitleSession(registry, rt.desc.id);
13054
+ }
12509
13055
  } else {
12510
13056
  rt.desc.turnsCompleted = (rt.desc.turnsCompleted ?? 0) + 1;
12511
13057
  if (rt.desc.lastTurnEmpty !== void 0) delete rt.desc.lastTurnEmpty;
@@ -13010,6 +13556,19 @@ ${message}`;
13010
13556
  rows: input.rows
13011
13557
  });
13012
13558
  const priorCommandSessionId = findPriorCommandSessionId(sessions, input.cwd);
13559
+ const conversationSlug = conversationTerminalSlugFor({ argv: input.argv });
13560
+ const conversationStore = conversationSlug ? CONVERSATION_STORES[conversationSlug] : void 0;
13561
+ const resumeArgvId = (() => {
13562
+ if (!conversationStore) return void 0;
13563
+ for (let i = 0; i < input.argv.length - 1; i++) {
13564
+ if (input.argv[i] !== "--resume" && input.argv[i] !== "-r" && !(conversationSlug === "opencode" && (input.argv[i] === "--session" || input.argv[i] === "-s"))) continue;
13565
+ const raw = input.argv[i + 1];
13566
+ const base = raw.slice(raw.lastIndexOf("/") + 1);
13567
+ return base.endsWith(".jsonl") ? base.slice(0, -".jsonl".length) : raw;
13568
+ }
13569
+ return void 0;
13570
+ })();
13571
+ const conversationConfigDir = conversationStore?.configDirEnvVar && input.env ? input.env[conversationStore.configDirEnvVar] : void 0;
13013
13572
  const desc = {
13014
13573
  id,
13015
13574
  kind: "terminal",
@@ -13024,6 +13583,16 @@ ${message}`;
13024
13583
  ...worktreeFields(input.cwd),
13025
13584
  ...input.name ? { name: input.name } : {},
13026
13585
  ...input.label ? { label: input.label } : {},
13586
+ // Same rule as spawnAgent: a NEW spawn's label is a slug, not a
13587
+ // human rename — let a later derived title outrank it.
13588
+ ...input.label ? { renamedByUser: false } : {},
13589
+ ...conversationSlug ? { adapterSlug: conversationSlug } : {},
13590
+ ...conversationSlug && RESUME_STRATEGIES[conversationSlug]?.spawnArgs ? { nativeTerminalResume: true } : {},
13591
+ ...conversationConfigDir ? { adapterConfigDir: conversationConfigDir } : {},
13592
+ ...resumeArgvId && conversationStore ? {
13593
+ adapterSessionId: resumeArgvId,
13594
+ resumeMetadata: { [conversationStore.storeAs]: resumeArgvId }
13595
+ } : {},
13027
13596
  ...priorCommandSessionId ? { priorCommandSessionId } : {},
13028
13597
  // Parent attribution + depth (orchestrator WP4) — same recording
13029
13598
  // rule as spawnAgent above: depth always set so subtree/depth
@@ -13050,7 +13619,8 @@ ${message}`;
13050
13619
  emitter: new EventEmitter(),
13051
13620
  busy: false,
13052
13621
  textBuf: "",
13053
- thoughtBuf: ""
13622
+ thoughtBuf: "",
13623
+ ...isConversationTerminal(desc) ? { conversationTerminal: true } : {}
13054
13624
  };
13055
13625
  rt.emitter.setMaxListeners(50);
13056
13626
  sessions.set(id, rt);
@@ -13062,6 +13632,10 @@ ${message}`;
13062
13632
  depth: desc.depth ?? 0,
13063
13633
  ts: (/* @__PURE__ */ new Date()).toISOString()
13064
13634
  });
13635
+ if (rt.conversationTerminal) {
13636
+ if (desc.adapterSessionId) recordConversationLink(rt);
13637
+ else startConversationTerminalLinkProbe(rt);
13638
+ }
13065
13639
  pty.onData((chunk) => {
13066
13640
  const buf = Buffer.from(chunk, "utf8");
13067
13641
  rt.bracketedPaste = scanBracketedPaste(
@@ -13272,14 +13846,14 @@ ${message}`;
13272
13846
  };
13273
13847
  rtPre.desc.promptQueue = opts2.force ? [item, ...rtPre.desc.promptQueue ?? []] : [...rtPre.desc.promptQueue ?? [], item];
13274
13848
  schedulePersist();
13275
- return;
13849
+ return { queued: true };
13276
13850
  }
13277
13851
  await maybeResumeAgent(rtPre);
13278
13852
  const rt = validateAgentTurn(id, "enqueuePrompt");
13279
13853
  const structuredAnswer = matchStructuredQuestionAnswer(rt, message);
13280
13854
  if (structuredAnswer) {
13281
13855
  await answerStructuredQuestion(rt, structuredAnswer);
13282
- return;
13856
+ return { queued: false };
13283
13857
  }
13284
13858
  void runAgentTurn(rt, message, opts2?.source ? { promptSource: opts2.source } : void 0).catch((err) => {
13285
13859
  appendLine(
@@ -13288,6 +13862,7 @@ ${message}`;
13288
13862
  "stderr"
13289
13863
  );
13290
13864
  });
13865
+ return { queued: false };
13291
13866
  },
13292
13867
  removeQueuedPrompt(id, queueId) {
13293
13868
  const rt = sessions.get(id);
@@ -13575,7 +14150,7 @@ ${message}`;
13575
14150
  };
13576
14151
  const all = Array.from(sessions.values()).filter((rt) => includeArchived || !rt.desc.archived).filter((rt) => {
13577
14152
  if (!lane) return true;
13578
- if (rt.desc.kind === "terminal" || rt.desc.kind === "command") return false;
14153
+ if (isShellOnlyRow(rt.desc)) return false;
13579
14154
  const machine = hasMachineLineage(rt);
13580
14155
  return lane === "auto" ? machine : !machine;
13581
14156
  }).sort((a, b) => b.desc.startedAt.localeCompare(a.desc.startedAt));
@@ -13588,7 +14163,11 @@ ${message}`;
13588
14163
  stampWatchers(desc);
13589
14164
  desc.childrenBusy = childrenBusy.get(desc.id) ?? 0;
13590
14165
  desc.queuedPrompts = desc.promptQueue?.length ?? 0;
13591
- return toSessionSummary(desc);
14166
+ const summary = toSessionSummary(desc);
14167
+ if (!isShellOnlyRow(desc)) {
14168
+ summary.lane = hasMachineLineage(rt) ? "auto" : "agents";
14169
+ }
14170
+ return summary;
13592
14171
  });
13593
14172
  return { summaries, total: all.length };
13594
14173
  },
@@ -16432,6 +17011,10 @@ var mcpBool2 = z.preprocess(
16432
17011
  (v) => v === "true" ? true : v === "false" ? false : v,
16433
17012
  z.boolean()
16434
17013
  );
17014
+ var publicSessionDescriptor = (session) => {
17015
+ const { ptyResumeEnv: _privateResumeEnv, ...publicDescriptor } = session;
17016
+ return publicDescriptor;
17017
+ };
16435
17018
  var compactSessionItem = (s) => ({
16436
17019
  id: s.id,
16437
17020
  kind: s.kind,
@@ -16568,7 +17151,7 @@ function registerSessionTools(rawServer, opts) {
16568
17151
  (s) => s.status === "running" || s.status === "starting"
16569
17152
  );
16570
17153
  }
16571
- return rows;
17154
+ return rows.map(publicSessionDescriptor);
16572
17155
  },
16573
17156
  transformers: [
16574
17157
  paginated({
@@ -17025,7 +17608,7 @@ function registerSessionTools(rawServer, opts) {
17025
17608
  (s) => s.status === "running" || s.status === "starting"
17026
17609
  );
17027
17610
  }
17028
- return rows;
17611
+ return rows.map(publicSessionDescriptor);
17029
17612
  },
17030
17613
  project: compactSessionItemWithProvenance,
17031
17614
  keyOf: (s) => s.id,
@@ -17060,7 +17643,7 @@ function registerSessionTools(rawServer, opts) {
17060
17643
  (s) => s.status === "running" || s.status === "starting"
17061
17644
  );
17062
17645
  }
17063
- return rows;
17646
+ return rows.map(publicSessionDescriptor);
17064
17647
  },
17065
17648
  project: compactSessionItemWithProvenance,
17066
17649
  keyOf: (s) => s.id,
@@ -17899,9 +18482,27 @@ function registerSessionTools(rawServer, opts) {
17899
18482
  try {
17900
18483
  if (strategy.kind === "pty-native" || strategy.kind === "pty-plain") {
17901
18484
  if (!ptyEnabled) return ptyNotConfigured("session_restart");
17902
- const argv = strategy.kind === "pty-native" ? strategy.argv : Array.isArray(prev.argv) && prev.argv.length > 0 ? [...prev.argv] : tokenizeCommand(prev.command);
18485
+ let argv = strategy.kind === "pty-native" ? strategy.argv : Array.isArray(prev.argv) && prev.argv.length > 0 ? [...prev.argv] : tokenizeCommand(prev.command);
18486
+ if (strategy.kind === "pty-native" && prev.adapterSlug) {
18487
+ const probe = await probeNativeTranscript(augmented);
18488
+ if (probe?.exists) {
18489
+ const upgraded = RESUME_STRATEGIES[prev.adapterSlug]?.spawnArgs?.(probe.path);
18490
+ if (upgraded) {
18491
+ argv = upgraded;
18492
+ console.log(
18493
+ `[session_restart] ${prev.id} pty-native resume upgraded to absolute transcript path ${probe.path}`
18494
+ );
18495
+ }
18496
+ }
18497
+ }
17903
18498
  const envVarName = strategy.kind === "pty-native" && prev.adapterSlug ? RESUME_STRATEGIES[prev.adapterSlug]?.configDirEnvVar : void 0;
17904
- const ptyEnv = strategy.kind === "pty-native" ? envVarName && augmented.adapterConfigDir ? { [envVarName]: augmented.adapterConfigDir } : void 0 : prev.ptyResumeEnv;
18499
+ const ptyEnv = strategy.kind === "pty-native" ? envVarName && augmented.adapterConfigDir ? { [envVarName]: augmented.adapterConfigDir } : (
18500
+ // A pty-native restart of a row that is ITSELF a restarted
18501
+ // PTY (conversation terminal): no adapterConfigDir, but the
18502
+ // env the earlier hop recorded still names the isolated
18503
+ // store — replay it rather than dropping to the global one.
18504
+ prev.ptyResumeEnv
18505
+ ) : prev.ptyResumeEnv;
17905
18506
  if (strategy.kind === "pty-native") {
17906
18507
  console.log(
17907
18508
  `[session_restart] ${prev.id} pty-native env: ` + (ptyEnv ? `${Object.keys(ptyEnv).join(",")} threaded from adapterConfigDir` : envVarName ? "no adapterConfigDir on descriptor \u2014 resume may miss the isolated store" : `adapter "${prev.adapterSlug}" declares no configDirEnvVar \u2014 resume uses the global store`)
@@ -17956,7 +18557,7 @@ function registerSessionTools(rawServer, opts) {
17956
18557
  content: [
17957
18558
  {
17958
18559
  type: "text",
17959
- text: JSON.stringify(desc)
18560
+ text: JSON.stringify(publicSessionDescriptor(desc))
17960
18561
  }
17961
18562
  ]
17962
18563
  };
@@ -17983,6 +18584,15 @@ function registerSessionTools(rawServer, opts) {
17983
18584
  isError: true
17984
18585
  };
17985
18586
  }
18587
+ let nativeResumeDecline;
18588
+ if (input.preferNativeTerminal === true && prev.adapterSlug && RESUME_STRATEGIES[prev.adapterSlug]?.spawnArgs) {
18589
+ if (prev.nativeTerminalResume !== true) {
18590
+ nativeResumeDecline = { reason: "capability-missing" };
18591
+ } else {
18592
+ const probe = await probeNativeTranscript(augmented);
18593
+ nativeResumeDecline = probe ? { reason: "transcript-not-found", probedDir: probe.dir } : { reason: "no-resume-id" };
18594
+ }
18595
+ }
17986
18596
  const restarted = await restartAgentSession(registry, resolveAgentAdapter, prev, {
17987
18597
  ...opts.resolveSandboxProvider ? { resolveSandboxProvider: opts.resolveSandboxProvider } : {}
17988
18598
  });
@@ -17992,10 +18602,11 @@ function registerSessionTools(rawServer, opts) {
17992
18602
  type: "text",
17993
18603
  text: JSON.stringify(
17994
18604
  {
17995
- ...restarted.desc,
18605
+ ...publicSessionDescriptor(restarted.desc),
17996
18606
  resumedFrom: restarted.resumedFrom,
17997
18607
  resumeVia: restarted.resumeVia,
17998
- ...restarted.resumeFallback ? { resumeFallback: true } : {}
18608
+ ...restarted.resumeFallback ? { resumeFallback: true } : {},
18609
+ ...nativeResumeDecline ? { nativeResumeDecline } : {}
17999
18610
  },
18000
18611
  null,
18001
18612
  2
@@ -18062,7 +18673,7 @@ function registerSessionTools(rawServer, opts) {
18062
18673
  try {
18063
18674
  const desc = registry.archiveSession(prev.id);
18064
18675
  return {
18065
- content: [{ type: "text", text: JSON.stringify(desc) }]
18676
+ content: [{ type: "text", text: JSON.stringify(publicSessionDescriptor(desc)) }]
18066
18677
  };
18067
18678
  } catch (err) {
18068
18679
  return {
@@ -18144,7 +18755,7 @@ function registerSessionTools(rawServer, opts) {
18144
18755
  try {
18145
18756
  const desc = registry.unarchiveSession(prev.id);
18146
18757
  return {
18147
- content: [{ type: "text", text: JSON.stringify(desc) }]
18758
+ content: [{ type: "text", text: JSON.stringify(publicSessionDescriptor(desc)) }]
18148
18759
  };
18149
18760
  } catch (err) {
18150
18761
  return {
@@ -18229,7 +18840,7 @@ function registerSessionTools(rawServer, opts) {
18229
18840
  reason: input.reason
18230
18841
  });
18231
18842
  return {
18232
- content: [{ type: "text", text: JSON.stringify(desc) }]
18843
+ content: [{ type: "text", text: JSON.stringify(publicSessionDescriptor(desc)) }]
18233
18844
  };
18234
18845
  } catch (err) {
18235
18846
  return {
@@ -18313,7 +18924,7 @@ function registerSessionTools(rawServer, opts) {
18313
18924
  ...input.label !== void 0 ? { label: input.label } : {}
18314
18925
  });
18315
18926
  return {
18316
- content: [{ type: "text", text: JSON.stringify(desc) }]
18927
+ content: [{ type: "text", text: JSON.stringify(publicSessionDescriptor(desc)) }]
18317
18928
  };
18318
18929
  } catch (err) {
18319
18930
  return {
@@ -18375,7 +18986,7 @@ function registerSessionTools(rawServer, opts) {
18375
18986
  try {
18376
18987
  const desc = registry.setKeepAlive(prev.id, input.keepAlive);
18377
18988
  return {
18378
- content: [{ type: "text", text: JSON.stringify(desc) }]
18989
+ content: [{ type: "text", text: JSON.stringify(publicSessionDescriptor(desc)) }]
18379
18990
  };
18380
18991
  } catch (err) {
18381
18992
  return {
@@ -18437,7 +19048,7 @@ function registerSessionTools(rawServer, opts) {
18437
19048
  try {
18438
19049
  const desc = registry.setPinned(prev.id, input.pinned);
18439
19050
  return {
18440
- content: [{ type: "text", text: JSON.stringify(desc) }]
19051
+ content: [{ type: "text", text: JSON.stringify(publicSessionDescriptor(desc)) }]
18441
19052
  };
18442
19053
  } catch (err) {
18443
19054
  return {
@@ -18553,7 +19164,7 @@ function registerSessionTools(rawServer, opts) {
18553
19164
  } : {}
18554
19165
  });
18555
19166
  return {
18556
- content: [{ type: "text", text: JSON.stringify(desc) }]
19167
+ content: [{ type: "text", text: JSON.stringify(publicSessionDescriptor(desc)) }]
18557
19168
  };
18558
19169
  } catch (err) {
18559
19170
  return {
@@ -20289,13 +20900,34 @@ function registerWebSearchTools(server, opts = {}) {
20289
20900
  transformers: [catchErrors(), enrichResolverRejections()]
20290
20901
  });
20291
20902
  }
20903
+ var embedTokens = /* @__PURE__ */ new Map();
20904
+ function mintAppEmbedToken(appId) {
20905
+ const token = randomBytes(24).toString("base64url");
20906
+ embedTokens.set(token, appId);
20907
+ return token;
20908
+ }
20909
+ var stableTokens = /* @__PURE__ */ new Map();
20910
+ function stableAppEmbedToken(appId) {
20911
+ const existing = stableTokens.get(appId);
20912
+ if (existing) return existing;
20913
+ const token = mintAppEmbedToken(appId);
20914
+ stableTokens.set(appId, token);
20915
+ return token;
20916
+ }
20917
+ function isValidAppEmbedToken(token) {
20918
+ return typeof token === "string" && token.length > 0 && embedTokens.has(token);
20919
+ }
20292
20920
 
20293
20921
  // src/mcp-apps-adapter.ts
20294
20922
  var MIME_TYPE = "text/html;profile=mcp-app";
20923
+ var EMBED_TOKEN_PLACEHOLDER = '"__AGENPROTO_EMBED_TOKEN__"';
20295
20924
  function registerMcpApps(server, apps) {
20296
20925
  for (const app of apps) {
20297
20926
  const resourceUri = `ui://${app.id}/view`;
20298
- const html = typeof app.html === "string" ? app.html : app.html({});
20927
+ let html = typeof app.html === "string" ? app.html : app.html({});
20928
+ if (html.includes(EMBED_TOKEN_PLACEHOLDER)) {
20929
+ html = html.split(EMBED_TOKEN_PLACEHOLDER).join(JSON.stringify(mintAppEmbedToken(app.id)));
20930
+ }
20299
20931
  const resourceMeta = {
20300
20932
  ui: {
20301
20933
  prefersBorder: true,
@@ -20352,6 +20984,7 @@ function registerMcpApps(server, apps) {
20352
20984
  }
20353
20985
  }
20354
20986
  function makeBuiltinPanelApps(ops) {
20987
+ const sessionChatNativeInstalled = ops.isSessionChatInstalled();
20355
20988
  return [
20356
20989
  makeSessionsPanelApp({ listSessions: ops.listSessions }),
20357
20990
  makeAgentsOverviewApp({ listSessions: ops.listSessions }),
@@ -20361,13 +20994,26 @@ function makeBuiltinPanelApps(ops) {
20361
20994
  // `agent_start` via _meta.ui.resourceUri (agent-tools.ts) so a launch
20362
20995
  // auto-renders it.
20363
20996
  makeLiveSessionApp({ httpBaseUrl: ops.httpBaseUrl }),
20364
- // Session-chat widget — thin launcher for the installed
20997
+ // Session-chat widget — thin loopback-HTTP launcher for the installed
20365
20998
  // `@agentik/session-chat` app's standalone UI (deep-linked iframe when
20366
20999
  // installed, install notice otherwise; see apps/src/session-chat).
20367
- makeSessionChatApp({
20368
- httpBaseUrl: ops.httpBaseUrl,
20369
- isSessionChatInstalled: ops.isSessionChatInstalled
20370
- }),
21000
+ // Mounted ONLY when the native app isn't installed: once it is, its own
21001
+ // MCP Apps tool (`app_ui_session_chat`, app-ui-apps.ts) is the one and
21002
+ // only launch path — a strict-CSP host (Codex) can't fetch this
21003
+ // widget's loopback HTTP calls, so offering both would just add a
21004
+ // broken option next to the working one. `agent_start`'s launch-card
21005
+ // binding (agent-tools.ts) makes the matching choice.
21006
+ ...sessionChatNativeInstalled ? [] : [
21007
+ makeSessionChatApp({
21008
+ httpBaseUrl: ops.httpBaseUrl,
21009
+ isSessionChatInstalled: ops.isSessionChatInstalled,
21010
+ // Hand every result a live embed token so a host-cached widget
21011
+ // (Claude Desktop's per-conversation srcdoc) re-arms itself
21012
+ // after a daemon restart instead of 403ing on its dead baked
21013
+ // token.
21014
+ mintEmbedToken: () => stableAppEmbedToken("agentproto_session_chat")
21015
+ })
21016
+ ],
20371
21017
  // Work-board widget — kanban over the Task ledger (see apps/src/
20372
21018
  // work-board). Read path only; writes go through task_claim/
20373
21019
  // task_update/task_create over the bridge, same as every other caller.
@@ -20420,6 +21066,39 @@ function builtinPanelCatalogEntries() {
20420
21066
  };
20421
21067
  });
20422
21068
  }
21069
+
21070
+ // src/public-origins.ts
21071
+ var HTTP_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
21072
+ var WS_PROTOCOLS = /* @__PURE__ */ new Set(["ws:", "wss:"]);
21073
+ function normalizeOrigin(value, protocols) {
21074
+ const candidate = value?.trim().replace(/\/+$/, "");
21075
+ if (!candidate) return void 0;
21076
+ try {
21077
+ const url = new URL(candidate);
21078
+ if (!protocols.has(url.protocol)) return void 0;
21079
+ if (url.username || url.password || url.search || url.hash) return void 0;
21080
+ if (url.pathname !== "/") return void 0;
21081
+ return url.origin;
21082
+ } catch {
21083
+ return void 0;
21084
+ }
21085
+ }
21086
+ function resolveRequestHttpBaseUrl(headers, configuredOrigin = process.env.AGENTPROTO_PUBLIC_HTTP_ORIGIN) {
21087
+ const configured = normalizeOrigin(configuredOrigin, HTTP_PROTOCOLS);
21088
+ if (configured) return configured;
21089
+ const forwarded = headers["x-forwarded-proto"];
21090
+ const firstForwarded = (Array.isArray(forwarded) ? forwarded[0] : forwarded)?.split(",")[0]?.trim().toLowerCase();
21091
+ const protocol = firstForwarded === "https" ? "https" : "http";
21092
+ return normalizeOrigin(`${protocol}://${headers.host ?? "127.0.0.1"}`, HTTP_PROTOCOLS) ?? `${protocol}://127.0.0.1`;
21093
+ }
21094
+ function resolvePublicAppOrigins(port, configuredHttpOrigin = process.env.AGENTPROTO_PUBLIC_HTTP_ORIGIN, configuredWsOrigin = process.env.AGENTPROTO_PUBLIC_WS_ORIGIN) {
21095
+ const httpOrigin = normalizeOrigin(configuredHttpOrigin, HTTP_PROTOCOLS) ?? `http://127.0.0.1:${port}`;
21096
+ const configuredWs = normalizeOrigin(configuredWsOrigin, WS_PROTOCOLS);
21097
+ if (configuredWs) return { httpOrigin, wsOrigin: configuredWs };
21098
+ const http = new URL(httpOrigin);
21099
+ http.protocol = http.protocol === "https:" ? "wss:" : "ws:";
21100
+ return { httpOrigin, wsOrigin: http.origin };
21101
+ }
20423
21102
  function summarizeSession(desc, lines, nowMs) {
20424
21103
  return {
20425
21104
  sessionId: desc.id,
@@ -22846,172 +23525,6 @@ function registerAppTools(server, opts) {
22846
23525
  }
22847
23526
  );
22848
23527
  }
22849
- function appUiToolId(appId) {
22850
- const slug = appId.replace(/^@[^/]+\//, "").replace(/[^a-z0-9]/g, "_");
22851
- return `app_ui_${slug}`;
22852
- }
22853
- var MCP_APP_BRIDGE_SCRIPT = `<script>
22854
- (function () {
22855
- if (window.McpApp) return;
22856
- var nextId = 1, pending = {}, teardownCbs = [];
22857
- function post(m) { window.parent.postMessage(m, "*"); }
22858
- window.addEventListener("message", function (e) {
22859
- var m = e.data;
22860
- if (typeof m === "string") { try { m = JSON.parse(m); } catch (_) { return; } }
22861
- if (!m || m.jsonrpc !== "2.0") return;
22862
- if (m.method === "ui/resource-teardown") {
22863
- for (var i = 0; i < teardownCbs.length; i++) teardownCbs[i]();
22864
- post({ jsonrpc: "2.0", id: m.id, result: {} });
22865
- return;
22866
- }
22867
- if (m.id != null && pending[m.id]) { var cb = pending[m.id]; delete pending[m.id]; cb(m.result, m.error); }
22868
- });
22869
- function request(method, params) {
22870
- return new Promise(function (resolve, reject) {
22871
- var id = nextId++;
22872
- pending[id] = function (r, err) { if (err) reject(new Error((err && err.message) || String(err))); else resolve(r); };
22873
- post({ jsonrpc: "2.0", id: id, method: method, params: params });
22874
- });
22875
- }
22876
- window.McpApp = {
22877
- connect: function () {
22878
- if (window.parent === window) return Promise.reject(new Error("no host (standalone)"));
22879
- return request("ui/initialize", {
22880
- appInfo: { name: "agentproto-app", version: "1.0" },
22881
- appCapabilities: { availableDisplayModes: ["inline", "fullscreen"] },
22882
- protocolVersion: "2026-01-26"
22883
- }).then(function () {
22884
- post({ jsonrpc: "2.0", method: "ui/notifications/initialized", params: {} });
22885
- return {
22886
- callTool: function (name, args) { return request("tools/call", { name: name, arguments: args || {} }); },
22887
- sendMessage: function (p) {
22888
- var params = typeof p === "string" ? { content: p } : (p || {});
22889
- var content = typeof params.content === "string" ? [{ type: "text", text: params.content }] : (params.content || []);
22890
- return request("ui/message", { role: "user", content: content });
22891
- },
22892
- updateModelContext: function (p) { return request("ui/update-model-context", p || {}); },
22893
- openLink: function (url) { return request("ui/open-link", { url: url }); },
22894
- onTeardown: function (cb) { teardownCbs.push(cb); }
22895
- };
22896
- });
22897
- }
22898
- };
22899
- })();
22900
- </script>
22901
- `;
22902
- function injectAfterStructuralTag(html, script) {
22903
- for (const tag of ["head", "body", "html"]) {
22904
- const match = html.match(new RegExp(`<${tag}[^>]*>`, "i"));
22905
- if (match?.index !== void 0) {
22906
- const insertAt = match.index + match[0].length;
22907
- return html.slice(0, insertAt) + script + html.slice(insertAt);
22908
- }
22909
- }
22910
- return script + html;
22911
- }
22912
- function injectMcpAppBridge(html) {
22913
- const hasBridge = /window\.McpApp\s*=/.test(html);
22914
- const hasRunnerSelect = /window\.AgentprotoUI\s*=/.test(html);
22915
- if (hasBridge && hasRunnerSelect) return html;
22916
- const script = (hasBridge ? "" : MCP_APP_BRIDGE_SCRIPT) + (hasRunnerSelect ? "" : RUNNER_SELECT_SCRIPT);
22917
- return injectAfterStructuralTag(html, script);
22918
- }
22919
- var STANDALONE_REST_BRIDGE_SCRIPT = `<script>
22920
- (function () {
22921
- if (window.McpApp) return;
22922
- window.McpApp = {
22923
- connect: function () {
22924
- return Promise.resolve({
22925
- callTool: function (name, args) {
22926
- return fetch("./tool-call", {
22927
- method: "POST",
22928
- headers: { "content-type": "application/json" },
22929
- body: JSON.stringify({ tool: name, args: args || {} })
22930
- }).then(function (res) {
22931
- return res.json().catch(function () {
22932
- throw new Error("tool-call failed: HTTP " + res.status);
22933
- }).then(function (body) {
22934
- if (!res.ok) throw new Error((body && (body.message || body.error)) || ("tool-call failed: HTTP " + res.status));
22935
- return body;
22936
- });
22937
- });
22938
- },
22939
- sendMessage: function () {
22940
- return Promise.reject(new Error("sendMessage: no chat host (standalone mode)"));
22941
- },
22942
- updateModelContext: function () {
22943
- return Promise.reject(new Error("updateModelContext: no host (standalone mode)"));
22944
- },
22945
- openLink: function (url) {
22946
- window.open(url, "_blank");
22947
- return Promise.resolve({});
22948
- },
22949
- onTeardown: function () {}
22950
- });
22951
- }
22952
- };
22953
- })();
22954
- </script>
22955
- `;
22956
- function injectStandaloneAppBridge(html) {
22957
- const hasRunnerSelect = /window\.AgentprotoUI\s*=/.test(html);
22958
- const script = STANDALONE_REST_BRIDGE_SCRIPT + (hasRunnerSelect ? "" : RUNNER_SELECT_SCRIPT);
22959
- return injectAfterStructuralTag(html, script);
22960
- }
22961
- function createUiHtmlCache() {
22962
- const cache = /* @__PURE__ */ new Map();
22963
- return {
22964
- async get(path, version) {
22965
- const cached = cache.get(path);
22966
- if (cached && cached.version === version) return cached.html;
22967
- const raw = await readFile(path, "utf8");
22968
- const html = injectMcpAppBridge(raw);
22969
- cache.set(path, { version, html });
22970
- return html;
22971
- }
22972
- };
22973
- }
22974
- async function makeInstalledAppUiApps(appRegistry, cache, existingToolNames) {
22975
- const apps = [];
22976
- const seen = new Set(existingToolNames);
22977
- for (const app of appRegistry.listApps()) {
22978
- const ui = app.ui;
22979
- if (!ui) continue;
22980
- const toolId = appUiToolId(app.appId);
22981
- if (seen.has(toolId)) {
22982
- console.warn(
22983
- `[app-ui-apps] skipping UI panel for app "${app.appId}": tool id "${toolId}" collides with an existing tool or another installed app's panel.`
22984
- );
22985
- continue;
22986
- }
22987
- let html;
22988
- try {
22989
- html = await cache.get(ui.path, app.updatedAt);
22990
- } catch (err) {
22991
- console.warn(
22992
- `[app-ui-apps] skipping UI panel for app "${app.appId}": could not read "${ui.path}": ${err instanceof Error ? err.message : String(err)}`
22993
- );
22994
- continue;
22995
- }
22996
- seen.add(toolId);
22997
- apps.push({
22998
- id: toolId,
22999
- title: ui.title ?? app.name ?? app.appId,
23000
- ...ui.description ? { description: ui.description } : {},
23001
- inputSchema: z.object({}),
23002
- execute: async () => ({ appId: app.appId, tools: ui.tools ?? [] }),
23003
- html,
23004
- ...ui.csp ? {
23005
- csp: {
23006
- ...ui.csp.connectDomains ? { connectDomains: [...ui.csp.connectDomains] } : {},
23007
- ...ui.csp.resourceDomains ? { resourceDomains: [...ui.csp.resourceDomains] } : {},
23008
- ...ui.csp.frameDomains ? { frameDomains: [...ui.csp.frameDomains] } : {}
23009
- }
23010
- } : {}
23011
- });
23012
- }
23013
- return apps;
23014
- }
23015
23528
  function paginatedLegacyList3(opts) {
23016
23529
  return {
23017
23530
  name: "paginatedLegacyList",
@@ -23241,6 +23754,22 @@ async function sendOutbound(provider, input, deps2) {
23241
23754
  return { ok: false, error: "unsupported_provider" };
23242
23755
  }
23243
23756
  }
23757
+ function unwrapMcpToolResult(result) {
23758
+ if (!result || typeof result !== "object") return {};
23759
+ const envelope = result;
23760
+ if (envelope.structuredContent && typeof envelope.structuredContent === "object") {
23761
+ return envelope.structuredContent;
23762
+ }
23763
+ const text10 = envelope.content?.find((c) => c.type === "text")?.text;
23764
+ if (typeof text10 === "string") {
23765
+ try {
23766
+ const parsed = JSON.parse(text10);
23767
+ if (parsed && typeof parsed === "object") return parsed;
23768
+ } catch {
23769
+ }
23770
+ }
23771
+ return result;
23772
+ }
23244
23773
  async function sendAgentpush(input, deps2) {
23245
23774
  const alias = input.alias;
23246
23775
  if (!alias) {
@@ -23270,7 +23799,7 @@ async function sendAgentpush(input, deps2) {
23270
23799
  if (!upload.ok) {
23271
23800
  return { ok: false, error: `upload_media_failed: ${upload.error}` };
23272
23801
  }
23273
- const result = typeof upload.result === "object" && upload.result !== null ? upload.result : {};
23802
+ const result = unwrapMcpToolResult(upload.result);
23274
23803
  const providerMediaId = typeof result.media_id === "string" ? result.media_id : typeof result.id === "string" ? result.id : void 0;
23275
23804
  if (!providerMediaId) {
23276
23805
  return { ok: false, error: "upload_media_missing_media_id" };
@@ -23295,7 +23824,18 @@ async function sendAgentpush(input, deps2) {
23295
23824
  if (!out.ok) {
23296
23825
  return { ok: false, error: out.error };
23297
23826
  }
23298
- const providerMessageId = extractProviderMessageId(out.result);
23827
+ const payload = unwrapMcpToolResult(out.result);
23828
+ const status = typeof payload.status === "string" ? payload.status : void 0;
23829
+ if (status === "blocked") {
23830
+ const blockedReason = typeof payload.blocked_reason === "string" ? payload.blocked_reason : "blocked";
23831
+ const suggestion = typeof payload.suggestion === "string" ? payload.suggestion : void 0;
23832
+ return { ok: false, error: blockedReason, blockedReason, ...suggestion ? { suggestion } : {} };
23833
+ }
23834
+ if (status === "failed") {
23835
+ const error = typeof payload.error === "string" ? payload.error : "failed";
23836
+ return { ok: false, error };
23837
+ }
23838
+ const providerMessageId = extractProviderMessageId(payload);
23299
23839
  return { ok: true, providerMessageId };
23300
23840
  }
23301
23841
  async function sendTelegram(input, deps2) {
@@ -23320,12 +23860,7 @@ async function sendTelegram(input, deps2) {
23320
23860
  parse_mode: "MarkdownV2"
23321
23861
  })
23322
23862
  });
23323
- if (!res2.ok) {
23324
- const body2 = await res2.text().catch(() => "");
23325
- return { ok: false, error: `telegram_http_${res2.status}: ${body2}` };
23326
- }
23327
- const json2 = await res2.json().catch(() => ({}));
23328
- return { ok: true, providerMessageId: extractTelegramMessageId(json2) };
23863
+ return parseTelegramSendResponse(res2);
23329
23864
  }
23330
23865
  const files = [];
23331
23866
  for (const att of input.attachments) {
@@ -23359,12 +23894,7 @@ async function sendTelegram(input, deps2) {
23359
23894
  headers: { "Content-Type": contentType2 },
23360
23895
  body: toFetchBody(body2)
23361
23896
  });
23362
- if (!res2.ok) {
23363
- const bodyText = await res2.text().catch(() => "");
23364
- return { ok: false, error: `telegram_http_${res2.status}: ${bodyText}` };
23365
- }
23366
- const json2 = await res2.json().catch(() => ({}));
23367
- return { ok: true, providerMessageId: extractTelegramMessageId(json2) };
23897
+ return parseTelegramSendResponse(res2);
23368
23898
  }
23369
23899
  const mediaJson = input.attachments.map((att, index) => {
23370
23900
  const item = {
@@ -23397,11 +23927,18 @@ async function sendTelegram(input, deps2) {
23397
23927
  headers: { "Content-Type": contentType },
23398
23928
  body: toFetchBody(body)
23399
23929
  });
23930
+ return parseTelegramSendResponse(res);
23931
+ }
23932
+ async function parseTelegramSendResponse(res) {
23400
23933
  if (!res.ok) {
23401
- const bodyText = await res.text().catch(() => "");
23402
- return { ok: false, error: `telegram_http_${res.status}: ${bodyText}` };
23934
+ const body = await res.text().catch(() => "");
23935
+ return { ok: false, error: `telegram_http_${res.status}: ${body}` };
23403
23936
  }
23404
23937
  const json = await res.json().catch(() => ({}));
23938
+ if (json.ok !== true) {
23939
+ const description = typeof json.description === "string" ? json.description : "unknown_error";
23940
+ return { ok: false, error: `telegram_error: ${description}` };
23941
+ }
23405
23942
  return { ok: true, providerMessageId: extractTelegramMessageId(json) };
23406
23943
  }
23407
23944
  function extractProviderMessageId(result) {
@@ -26348,7 +26885,16 @@ function registerOrchestrationTools(rawServer, opts) {
26348
26885
  if (!out.ok) {
26349
26886
  return {
26350
26887
  content: [
26351
- { type: "text", text: JSON.stringify({ sent: false, bound: false, error: out.error }) }
26888
+ {
26889
+ type: "text",
26890
+ text: JSON.stringify({
26891
+ sent: false,
26892
+ bound: false,
26893
+ error: out.error,
26894
+ ...out.blockedReason ? { blocked_reason: out.blockedReason } : {},
26895
+ ...out.suggestion ? { suggestion: out.suggestion } : {}
26896
+ })
26897
+ }
26352
26898
  ],
26353
26899
  isError: true
26354
26900
  };
@@ -26364,7 +26910,18 @@ function registerOrchestrationTools(rawServer, opts) {
26364
26910
  provider
26365
26911
  });
26366
26912
  }
26367
- return { content: [{ type: "text", text: JSON.stringify({ sent: true, bound }) }] };
26913
+ return {
26914
+ content: [
26915
+ {
26916
+ type: "text",
26917
+ text: JSON.stringify({
26918
+ sent: true,
26919
+ bound,
26920
+ ...out.providerMessageId ? { message_id: out.providerMessageId } : {}
26921
+ })
26922
+ }
26923
+ ]
26924
+ };
26368
26925
  }
26369
26926
  );
26370
26927
  }
@@ -26734,6 +27291,10 @@ function registerPresetTools(server, opts = {}) {
26734
27291
  lister: async () => listPresets(process.env, adapterPresets)
26735
27292
  });
26736
27293
  }
27294
+ function sessionDescriptorForHttp(session) {
27295
+ const { ptyResumeEnv: _privateResumeEnv, ...publicDescriptor } = session;
27296
+ return publicDescriptor;
27297
+ }
26737
27298
  var DEFAULT_ALLOWED_ORIGINS = [
26738
27299
  "http://localhost:*",
26739
27300
  "http://127.0.0.1:*",
@@ -26798,7 +27359,7 @@ async function startHttpServer(opts) {
26798
27359
  }
26799
27360
  function authorizeMcp(req, res) {
26800
27361
  const origin = req.headers.origin;
26801
- if (typeof origin === "string" && origin.length > 0 && !originAllowed(origin)) {
27362
+ if (typeof origin === "string" && origin.length > 0 && !originAllowed(origin) && !embedTokenTrusted(req)) {
26802
27363
  const auth = readAuth();
26803
27364
  const header = req.headers.authorization;
26804
27365
  if (auth.mode === "bearer" && header === `Bearer ${auth.token}`) {
@@ -26828,8 +27389,12 @@ async function startHttpServer(opts) {
26828
27389
  }
26829
27390
  const origin = req.headers.origin;
26830
27391
  if (typeof origin === "string" && originAllowed(origin)) return "ok";
27392
+ if (embedTokenTrusted(req)) return "ok";
26831
27393
  return header ? "bad" : "missing";
26832
27394
  }
27395
+ function embedTokenTrusted(req) {
27396
+ return isValidAppEmbedToken(requestEmbedToken(req));
27397
+ }
26833
27398
  function originAllowed(origin) {
26834
27399
  const list = opts.strictOrigins ? opts.allowedOrigins ?? [] : [...DEFAULT_ALLOWED_ORIGINS, ...opts.allowedOrigins ?? []];
26835
27400
  for (const pattern of list) {
@@ -27115,7 +27680,7 @@ async function startHttpServer(opts) {
27115
27680
  }
27116
27681
  function guardBrowserOrigin(req, res) {
27117
27682
  const origin = req.headers.origin;
27118
- if (typeof origin === "string" && origin.length > 0 && !originAllowed(origin)) {
27683
+ if (typeof origin === "string" && origin.length > 0 && !originAllowed(origin) && !embedTokenTrusted(req)) {
27119
27684
  const auth = readAuth();
27120
27685
  const header = req.headers.authorization;
27121
27686
  if (auth.mode === "bearer" && header === `Bearer ${auth.token}`) {
@@ -27134,10 +27699,14 @@ async function startHttpServer(opts) {
27134
27699
  }
27135
27700
  function applyCors(req, res) {
27136
27701
  const origin = req.headers.origin;
27137
- const trusted = typeof origin === "string" && origin.length > 0 && originAllowed(origin);
27138
- if (trusted) {
27702
+ const tokenTrusted = embedTokenTrusted(req);
27703
+ const originTrusted = typeof origin === "string" && origin.length > 0 && originAllowed(origin);
27704
+ const trusted = originTrusted || tokenTrusted;
27705
+ if (originTrusted) {
27139
27706
  res.setHeader("Access-Control-Allow-Origin", origin);
27140
27707
  res.setHeader("Access-Control-Allow-Credentials", "true");
27708
+ } else if (tokenTrusted && origin && origin !== "null") {
27709
+ res.setHeader("Access-Control-Allow-Origin", origin);
27141
27710
  } else {
27142
27711
  res.setHeader("Access-Control-Allow-Origin", "*");
27143
27712
  }
@@ -28105,7 +28674,9 @@ async function startHttpServer(opts) {
28105
28674
  const uiMatch = path.match(/^\/apps\/(.+)\/ui$/);
28106
28675
  if (uiMatch && req.method === "GET") {
28107
28676
  const uiApp = opts.appRegistry.getApp(decodeURIComponent(uiMatch[1]));
28108
- if (!iframeEmbedOriginAllowed(req, uiApp ?? {}) && guardBrowserOrigin(req, res)) return;
28677
+ if (!iframeEmbedOriginAllowed(req, uiApp ?? {}) && !isValidAppEmbedToken(requestEmbedToken(req)) && guardBrowserOrigin(req, res)) {
28678
+ return;
28679
+ }
28109
28680
  if (!authorize(req, res)) return;
28110
28681
  await handleAppUiPage(
28111
28682
  req,
@@ -28851,7 +29422,7 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
28851
29422
  } else if (!includeCommands) {
28852
29423
  rows = rows.filter((s) => s.kind !== "command");
28853
29424
  }
28854
- json(200, { sessions: rows });
29425
+ json(200, { sessions: rows.map(sessionDescriptorForHttp) });
28855
29426
  return true;
28856
29427
  }
28857
29428
  if (path === "/sessions/summaries" && req.method === "GET") {
@@ -28941,7 +29512,7 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
28941
29512
  return true;
28942
29513
  }
28943
29514
  json(201, {
28944
- ...result.descriptor,
29515
+ ...sessionDescriptorForHttp(result.descriptor),
28945
29516
  ...result.warnings ? { warnings: result.warnings } : {},
28946
29517
  ...result.deduped ? { deduped: true } : {},
28947
29518
  ...result.dedupeSource ? { dedupeSource: result.dedupeSource } : {}
@@ -29157,7 +29728,7 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
29157
29728
  ...typeof b.name === "string" ? { name: b.name } : {},
29158
29729
  ...typeof b.label === "string" ? { label: b.label } : {}
29159
29730
  });
29160
- json(201, desc);
29731
+ json(201, sessionDescriptorForHttp(desc));
29161
29732
  } catch (err) {
29162
29733
  const msg = err instanceof Error ? err.message : String(err);
29163
29734
  const status = msg.includes("already in use") ? 409 : msg.includes("no PTY factory") ? 501 : 500;
@@ -29396,7 +29967,7 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
29396
29967
  ...listCatalogModels ? { listCatalogModels } : {}
29397
29968
  });
29398
29969
  json(200, {
29399
- ...restarted.desc,
29970
+ ...sessionDescriptorForHttp(restarted.desc),
29400
29971
  resumedFrom: restarted.resumedFrom,
29401
29972
  resumeVia: restarted.resumeVia,
29402
29973
  ...restarted.resumeFallback ? { resumeFallback: true } : {}
@@ -29471,7 +30042,7 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
29471
30042
  };
29472
30043
  try {
29473
30044
  const desc = registry.renameSession(resolved.id, patch);
29474
- json(200, desc);
30045
+ json(200, sessionDescriptorForHttp(desc));
29475
30046
  } catch (err) {
29476
30047
  const msg = err instanceof Error ? err.message : String(err);
29477
30048
  json(msg.includes("no session") ? 404 : 500, { error: "rename_failed", message: msg });
@@ -29522,7 +30093,7 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
29522
30093
  env: b.env && typeof b.env === "object" ? b.env : void 0,
29523
30094
  label: typeof b.label === "string" ? b.label : void 0
29524
30095
  });
29525
- json(201, desc);
30096
+ json(201, sessionDescriptorForHttp(desc));
29526
30097
  } catch (err) {
29527
30098
  json(500, {
29528
30099
  error: "spawn_failed",
@@ -29905,7 +30476,7 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
29905
30476
  json(404, { error: "session_not_found", id: rawIdOrName });
29906
30477
  return true;
29907
30478
  }
29908
- json(200, resolvedDesc);
30479
+ json(200, sessionDescriptorForHttp(resolvedDesc));
29909
30480
  return true;
29910
30481
  }
29911
30482
  if (!suffix && req.method === "DELETE") {
@@ -30817,7 +31388,7 @@ async function handleProviderInbound(req, res, slug, deps2) {
30817
31388
  res.end(JSON.stringify(result));
30818
31389
  }
30819
31390
  function requestHttpBaseUrl(req) {
30820
- return `http://${req.headers.host ?? "127.0.0.1"}`;
31391
+ return resolveRequestHttpBaseUrl(req.headers);
30821
31392
  }
30822
31393
  async function handleAppUiPage(req, res, appId, appRegistry, frameAncestors) {
30823
31394
  const app = appRegistry.getApp(appId);
@@ -30853,13 +31424,24 @@ async function handleAppUiPage(req, res, appId, appRegistry, frameAncestors) {
30853
31424
  if (frameAncestors.length === 0) {
30854
31425
  headers["x-frame-options"] = "SAMEORIGIN";
30855
31426
  }
31427
+ if (embedRequested && !isValidAppEmbedToken(requestEmbedToken(req))) {
31428
+ console.warn(
31429
+ `[app-ui] embed refused for "${appId}": ` + JSON.stringify({
31430
+ secFetchDest: req.headers["sec-fetch-dest"] ?? null,
31431
+ origin: req.headers.origin ?? null,
31432
+ referer: req.headers.referer ?? null,
31433
+ embedToken: requestEmbedToken(req) != null
31434
+ })
31435
+ );
31436
+ }
30856
31437
  }
30857
31438
  res.writeHead(200, headers);
30858
- res.end(injectStandaloneAppBridge(raw));
31439
+ res.end(injectStandaloneAppBridge(raw, requestHttpBaseUrl(req)));
30859
31440
  }
30860
31441
  function iframeEmbedAllowed(req, app) {
30861
31442
  const secFetchDest = req.headers["sec-fetch-dest"];
30862
31443
  if (secFetchDest !== void 0 && secFetchDest !== "iframe") return false;
31444
+ if (isValidAppEmbedToken(requestEmbedToken(req))) return true;
30863
31445
  const origin = typeof req.headers.origin === "string" && req.headers.origin.length > 0 ? req.headers.origin : null;
30864
31446
  const referer = typeof req.headers.referer === "string" && req.headers.referer.length > 0 ? req.headers.referer : null;
30865
31447
  if (!origin && !referer) return false;
@@ -30880,6 +31462,13 @@ function iframeEmbedAllowed(req, app) {
30880
31462
  }
30881
31463
  return false;
30882
31464
  }
31465
+ function requestEmbedToken(req) {
31466
+ try {
31467
+ return new URL(req.url ?? "/", "http://localhost").searchParams.get("et");
31468
+ } catch {
31469
+ return null;
31470
+ }
31471
+ }
30883
31472
  function iframeEmbedOriginAllowed(req, app) {
30884
31473
  const origin = typeof req.headers.origin === "string" && req.headers.origin.length > 0 ? req.headers.origin : null;
30885
31474
  const referer = typeof req.headers.referer === "string" && req.headers.referer.length > 0 ? req.headers.referer : null;
@@ -34838,6 +35427,7 @@ function createOrchestratorMcpServerFactory(deps2) {
34838
35427
  ...deps2.listHarnessCapabilities ? { listHarnessCapabilities: deps2.listHarnessCapabilities } : {},
34839
35428
  ...deps2.webhookNotifier ? { webhookNotifier: deps2.webhookNotifier } : {},
34840
35429
  ...deps2.resolveSandboxProvider ? { resolveSandboxProvider: deps2.resolveSandboxProvider } : {},
35430
+ ...deps2.defaultAgentPromptInterrupt != null ? { defaultAgentPromptInterrupt: deps2.defaultAgentPromptInterrupt } : {},
34841
35431
  daemonMcpUrl: deps2.daemonMcpUrl
34842
35432
  });
34843
35433
  registerOrchestrationTools(server, {
@@ -37961,7 +38551,7 @@ async function createGateway(opts) {
37961
38551
  const port = opts.port ?? 18790;
37962
38552
  const daemonMcpUrl = `http://127.0.0.1:${port}/mcp`;
37963
38553
  const resolveSandboxProviderResolved = opts.resolveSandboxProvider ?? makeSandboxResolver(makeSandboxCredsStore());
37964
- const ptyWsBaseUrl = process.env.AGENTPROTO_PUBLIC_WS_ORIGIN?.trim().replace(/\/+$/, "") || `ws://127.0.0.1:${port}`;
38554
+ const { httpOrigin: publicHttpOrigin, wsOrigin: ptyWsBaseUrl } = resolvePublicAppOrigins(port);
37965
38555
  const events = createRuntimeEvents();
37966
38556
  const conversations = fileConversationStore({ workspace });
37967
38557
  const workspaceFs = createWorkspaceFs({ workspace });
@@ -38151,6 +38741,13 @@ async function createGateway(opts) {
38151
38741
  dispatchTool
38152
38742
  });
38153
38743
  const appRegistry = createAppRegistry({ persist });
38744
+ const isSessionChatInstalled = () => {
38745
+ try {
38746
+ return appRegistry.getApp(SESSION_CHAT_APP_ID)?.ui != null;
38747
+ } catch {
38748
+ return false;
38749
+ }
38750
+ };
38154
38751
  const appUiHtmlCache = createUiHtmlCache();
38155
38752
  const workflowRunner = opts.resolveAgentAdapter ? createWorkflowRunner({
38156
38753
  registry: sessions,
@@ -38278,6 +38875,10 @@ async function createGateway(opts) {
38278
38875
  // can declare `sandbox` and resolve a provider, instead of throwing
38279
38876
  // `sandbox_provider_not_found`.
38280
38877
  resolveSandboxProvider: resolveSandboxProviderResolved,
38878
+ // config.json `defaults.agentPromptInterrupt` — same unset-default the
38879
+ // root /mcp surface uses, so a child driving/reporting through the scoped
38880
+ // orchestrator gateway honours it too.
38881
+ ...configDefaults?.agentPromptInterrupt != null ? { defaultAgentPromptInterrupt: configDefaults.agentPromptInterrupt } : {},
38281
38882
  ...opts.resolveAgentAdapter ? { resolveAgentAdapter: opts.resolveAgentAdapter } : {},
38282
38883
  ...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {},
38283
38884
  ...opts.listHarnessCapabilities ? { listHarnessCapabilities: opts.listHarnessCapabilities } : {}
@@ -38338,6 +38939,9 @@ async function createGateway(opts) {
38338
38939
  webhookNotifier,
38339
38940
  daemonMcpUrl,
38340
38941
  resolveSandboxProvider: resolveSandboxProviderResolved,
38942
+ // config.json `defaults.agentPromptInterrupt` — the unset-default for
38943
+ // `interrupt` on agent_prompt / message_parent. Omitted ⇒ false.
38944
+ ...configDefaults?.agentPromptInterrupt != null ? { defaultAgentPromptInterrupt: configDefaults.agentPromptInterrupt } : {},
38341
38945
  ...opts.provisionWorktree ? { provisionWorktree: opts.provisionWorktree } : {},
38342
38946
  ...opts.resolveAgentAdapter ? { resolveAgentAdapter: opts.resolveAgentAdapter } : {},
38343
38947
  ...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {},
@@ -38345,7 +38949,8 @@ async function createGateway(opts) {
38345
38949
  ...opts.installAgentAdapter ? { installAgentAdapter: opts.installAgentAdapter } : {},
38346
38950
  ...opts.listCatalogModels ? { listCatalogModels: opts.listCatalogModels } : {},
38347
38951
  ...opts.listWorktreeStatuses ? { listWorktreeStatuses: opts.listWorktreeStatuses } : {},
38348
- ...opts.runWorktreeGc ? { runWorktreeGc: opts.runWorktreeGc } : {}
38952
+ ...opts.runWorktreeGc ? { runWorktreeGc: opts.runWorktreeGc } : {},
38953
+ isSessionChatInstalled
38349
38954
  });
38350
38955
  registerBrainTools(server, {
38351
38956
  brains: workspaceBrains,
@@ -38402,18 +39007,12 @@ async function createGateway(opts) {
38402
39007
  listSessions: listSessionsFiltered,
38403
39008
  // httpBaseUrl = this daemon's own origin (SSE stream + bridge
38404
39009
  // fallback for the live-session widget).
38405
- httpBaseUrl: `http://127.0.0.1:${port}`,
39010
+ httpBaseUrl: publicHttpOrigin,
38406
39011
  // The session-chat widget is a thin launcher for the installed
38407
- // `@agentik/session-chat` studio app — resolve installed-ness from
38408
- // the AppRegistry at call time (not boot) so `app_install`/
38409
- // `app_uninstall` of that app is reflected without a daemon restart.
38410
- isSessionChatInstalled: () => {
38411
- try {
38412
- return appRegistry.getApp(SESSION_CHAT_APP_ID)?.ui != null;
38413
- } catch {
38414
- return false;
38415
- }
38416
- },
39012
+ // `@agentik/session-chat` studio app — same call-time-resolved check
39013
+ // `registerSessionTools` above gets, so the mount decision here and
39014
+ // `agent_start`'s launch-card binding never disagree.
39015
+ isSessionChatInstalled,
38417
39016
  // Work-board widget's read path — the root `/mcp` endpoint has no
38418
39017
  // scope, so this mount is always the operator caller (default
38419
39018
  // board `ws:<slug>`); `canAccessBoard` lets the operator read any