negotium 0.2.4 → 0.2.5

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.
Files changed (62) hide show
  1. package/dist/agent-helpers.js +241 -127
  2. package/dist/agent-helpers.js.map +19 -19
  3. package/dist/background-bash.js +1 -3
  4. package/dist/background-bash.js.map +4 -4
  5. package/dist/browser-runtime.js +1 -3
  6. package/dist/browser-runtime.js.map +4 -4
  7. package/dist/{chunk-1s9ryz8g.js → chunk-qmbcbhyy.js} +20 -12
  8. package/dist/chunk-qmbcbhyy.js.map +35 -0
  9. package/dist/hosted-agent.js +13 -10
  10. package/dist/hosted-agent.js.map +10 -10
  11. package/dist/main.js +598 -438
  12. package/dist/main.js.map +30 -29
  13. package/dist/mcp-factories.js +238 -123
  14. package/dist/mcp-factories.js.map +18 -18
  15. package/dist/prompts.js +1 -3
  16. package/dist/prompts.js.map +4 -4
  17. package/dist/query-runtime.js +1 -3
  18. package/dist/query-runtime.js.map +5 -5
  19. package/dist/registry.js +3 -3
  20. package/dist/registry.js.map +2 -2
  21. package/dist/rollout.js +1 -1
  22. package/dist/runtime/src/agents/api-topic-agent-switch.ts +6 -12
  23. package/dist/runtime/src/agents/claude-provider.ts +2 -2
  24. package/dist/runtime/src/agents/codex-provider.ts +1 -0
  25. package/dist/runtime/src/agents/index.ts +19 -14
  26. package/dist/runtime/src/agents/maestro-provider.ts +1 -1
  27. package/dist/runtime/src/agents/rollout/codex.ts +25 -18
  28. package/dist/runtime/src/agents/self-config-core.ts +2 -4
  29. package/dist/runtime/src/agents/topic-agent-switch.ts +4 -10
  30. package/dist/runtime/src/application/submit-runtime-gateway-turn.ts +77 -19
  31. package/dist/runtime/src/application/switch-topic-model.ts +7 -10
  32. package/dist/runtime/src/index.ts +4 -0
  33. package/dist/runtime/src/mcp/wiki-server.ts +19 -12
  34. package/dist/runtime/src/platform/config.ts +0 -2
  35. package/dist/runtime/src/platform/log-rotation.ts +45 -0
  36. package/dist/runtime/src/platform/mcp-config.ts +9 -3
  37. package/dist/runtime/src/query/active-rooms.ts +7 -1
  38. package/dist/runtime/src/runtime/errors.ts +1 -0
  39. package/dist/runtime/src/runtime/turn-event-stream.ts +46 -1
  40. package/dist/runtime/src/runtime/turn-runner.ts +176 -100
  41. package/dist/runtime/src/runtime/user-turn-envelope.ts +19 -0
  42. package/dist/runtime/src/storage/runtime-gateway-submissions.ts +12 -3
  43. package/dist/runtime/src/storage/runtime-turn-requests.ts +68 -7
  44. package/dist/runtime/src/types.ts +2 -0
  45. package/dist/runtime/src/version.ts +1 -1
  46. package/dist/runtime-helpers.js +1 -3
  47. package/dist/runtime-helpers.js.map +4 -4
  48. package/dist/storage.js.map +1 -1
  49. package/dist/types/packages/core/src/agents/api-topic-agent-switch.d.ts +1 -1
  50. package/dist/types/packages/core/src/agents/self-config-core.d.ts +1 -1
  51. package/dist/types/packages/core/src/platform/config.d.ts +0 -1
  52. package/dist/types/packages/core/src/platform/mcp-config.d.ts +3 -0
  53. package/dist/types/packages/core/src/query/active-rooms.d.ts +7 -1
  54. package/dist/types/packages/core/src/runtime/turn-runner.d.ts +42 -1
  55. package/dist/types/packages/core/src/runtime/user-turn-envelope.d.ts +7 -0
  56. package/dist/types/packages/core/src/storage/runtime-turn-requests.d.ts +17 -0
  57. package/dist/types/packages/core/src/types.d.ts +2 -0
  58. package/dist/types/packages/core/src/version.d.ts +1 -1
  59. package/dist/vault.js +1 -3
  60. package/dist/vault.js.map +4 -4
  61. package/package.json +1 -1
  62. package/dist/chunk-1s9ryz8g.js.map +0 -35
@@ -331,7 +331,7 @@ function resolveDefaultModel(agent, registryDefaultModel) {
331
331
  function codexAuthFilePath() {
332
332
  return process.env.NEGOTIUM_CODEX_AUTH_FILE || join(process.env.CODEX_HOME || join(homedir(), ".codex"), "auth.json");
333
333
  }
334
- var HOME, PROJECT_ROOT, STATE_DIR_ENV, STATE_DIR, WORKSPACE_DIR, TOPIC_WORKSPACE_DIR, SHARED_WIKI_DIR, CRON_WORKSPACE_DIR, BROWSER_DIR, BROWSER_PROFILES_DIR, BINARIES_DIR, SECRETS_DIR, CONTEXTS_DIR, DM_WORKSPACE_DIR, SESSION_WORKSPACE_DIR, CLAUDE_EXECUTABLE_ENV, CLAUDE_EXECUTABLE, DEFAULT_OUTPUT_LANGUAGE = "English", BROWSER_RS_VERSION = "v0.1.16", BROWSER_RS_MIN_SECURE_VERSION = "0.1.15", BROWSER_RS_BIN, PLAYWRIGHT_MCP_BIN, TSX_BIN, TSX_LOADER, TSCONFIG_PATH, SESSION_COMM_SERVER, TASK_SERVER, BROWSER_MCP_SSE_PROXY_SERVER, CANONICAL_MCP_PROXY_SERVER, WIKI_SERVER, TOKEN_STATS_SERVER, COMPACTION_LOG_SERVER, SYSTEM_HEALTH_SERVER, AGENT_HEALTH_SERVER, BACKGROUND_BASH_SERVER, VAULT_SERVER, BG_BASH_BASE_PORT, BG_BASH_MAX_PORT, RUNTIME_MCP_SECRET, NODE_CONTROL_TOKEN, VAULT_MASTER_KEY, NEGOTIUM_PORT, hostname, DATA_DIR, LOG_DIR, UPLOADS_DIR, VAULT_DIR, SESSIONS_DB, DEBUG_FILE, USERS_LOG_DIR, RUN_DIR, PROGRESS_DIR, DM_CMD_DIR, DM_RESP_DIR, SESSION_INBOX_DIR, SESSION_ASKS_DIR, PLAYWRIGHT_BASE_PORT, PLAYWRIGHT_MAX_PORT, PLAYWRIGHT_PORTS_DIR, ACTIVE_QUERY_STALE_MS, AGENTS_PROMPTS_DIR, RESOURCES_DIR, FILE_TAG_REGEX, MODEL_SONNET = "claude-sonnet-5", MODEL_OPUS = "claude-opus-5", MODEL_FABLE = "claude-fable-5", FALLBACK_AGENT, SESSION_AGENT, GATEWAY_AGENT, FALLBACK_MODEL, SESSION_MODEL, GATEWAY_MODEL, FFMPEG_BIN, FFPROBE_BIN, PYTHON_BIN, FASTER_WHISPER_WRAPPER, WHISPER_MODEL, TESSERACT_BIN, PDFTOTEXT_BIN, _envMaxTellDepth, MAX_TELL_DEPTH;
334
+ var HOME, PROJECT_ROOT, STATE_DIR_ENV, STATE_DIR, WORKSPACE_DIR, TOPIC_WORKSPACE_DIR, SHARED_WIKI_DIR, CRON_WORKSPACE_DIR, BROWSER_DIR, BROWSER_PROFILES_DIR, BINARIES_DIR, SECRETS_DIR, DM_WORKSPACE_DIR, SESSION_WORKSPACE_DIR, CLAUDE_EXECUTABLE_ENV, CLAUDE_EXECUTABLE, DEFAULT_OUTPUT_LANGUAGE = "English", BROWSER_RS_VERSION = "v0.1.16", BROWSER_RS_MIN_SECURE_VERSION = "0.1.15", BROWSER_RS_BIN, PLAYWRIGHT_MCP_BIN, TSX_BIN, TSX_LOADER, TSCONFIG_PATH, SESSION_COMM_SERVER, TASK_SERVER, BROWSER_MCP_SSE_PROXY_SERVER, CANONICAL_MCP_PROXY_SERVER, WIKI_SERVER, TOKEN_STATS_SERVER, COMPACTION_LOG_SERVER, SYSTEM_HEALTH_SERVER, AGENT_HEALTH_SERVER, BACKGROUND_BASH_SERVER, VAULT_SERVER, BG_BASH_BASE_PORT, BG_BASH_MAX_PORT, RUNTIME_MCP_SECRET, NODE_CONTROL_TOKEN, VAULT_MASTER_KEY, NEGOTIUM_PORT, hostname, DATA_DIR, LOG_DIR, UPLOADS_DIR, VAULT_DIR, SESSIONS_DB, DEBUG_FILE, USERS_LOG_DIR, RUN_DIR, PROGRESS_DIR, DM_CMD_DIR, DM_RESP_DIR, SESSION_INBOX_DIR, SESSION_ASKS_DIR, PLAYWRIGHT_BASE_PORT, PLAYWRIGHT_MAX_PORT, PLAYWRIGHT_PORTS_DIR, ACTIVE_QUERY_STALE_MS, AGENTS_PROMPTS_DIR, RESOURCES_DIR, FILE_TAG_REGEX, MODEL_SONNET = "claude-sonnet-5", MODEL_OPUS = "claude-opus-5", MODEL_FABLE = "claude-fable-5", FALLBACK_AGENT, SESSION_AGENT, GATEWAY_AGENT, FALLBACK_MODEL, SESSION_MODEL, GATEWAY_MODEL, FFMPEG_BIN, FFPROBE_BIN, PYTHON_BIN, FASTER_WHISPER_WRAPPER, WHISPER_MODEL, TESSERACT_BIN, PDFTOTEXT_BIN, _envMaxTellDepth, MAX_TELL_DEPTH;
335
335
  var init_config = __esm(() => {
336
336
  init_config_helpers();
337
337
  init_logger();
@@ -348,7 +348,6 @@ var init_config = __esm(() => {
348
348
  BROWSER_PROFILES_DIR = resolve2(BROWSER_DIR, "profiles");
349
349
  BINARIES_DIR = resolve2(STATE_DIR, "binaries");
350
350
  SECRETS_DIR = resolve2(STATE_DIR, "secrets");
351
- CONTEXTS_DIR = resolve2(STATE_DIR, "data", "contexts");
352
351
  DM_WORKSPACE_DIR = resolve2(STATE_DIR, "data", "dm");
353
352
  SESSION_WORKSPACE_DIR = resolve2(STATE_DIR, "data", "sessions");
354
353
  CLAUDE_EXECUTABLE_ENV = envText("NEGOTIUM_CLAUDE_EXECUTABLE");
@@ -408,7 +407,6 @@ var init_config = __esm(() => {
408
407
  mkdirSync(TOPIC_WORKSPACE_DIR, { recursive: true });
409
408
  mkdirSync(SHARED_WIKI_DIR, { recursive: true });
410
409
  mkdirSync(CRON_WORKSPACE_DIR, { recursive: true });
411
- mkdirSync(CONTEXTS_DIR, { recursive: true });
412
410
  mkdirSync(DM_WORKSPACE_DIR, { recursive: true });
413
411
  mkdirSync(SESSION_WORKSPACE_DIR, { recursive: true });
414
412
  mkdirSync(BROWSER_DIR, { recursive: true });
@@ -951,6 +949,7 @@ function getManagerMcpServers(opts) {
951
949
  function getForumMcpServers(opts) {
952
950
  const {
953
951
  userId,
952
+ vaultUserId,
954
953
  session,
955
954
  topicId,
956
955
  subagentParentTopicId,
@@ -981,6 +980,7 @@ function getForumMcpServers(opts) {
981
980
  };
982
981
  const base = buildScope("forum", {
983
982
  userId,
983
+ vaultUserId,
984
984
  session,
985
985
  topicId,
986
986
  subagentParentTopicId,
@@ -1074,6 +1074,7 @@ function getMcpServersForQuery(opts) {
1074
1074
  }
1075
1075
  return getForumMcpServers({
1076
1076
  userId: opts.userId || "local",
1077
+ vaultUserId: opts.vaultUserId,
1077
1078
  session: opts.session || "default",
1078
1079
  topicId: opts.topicId,
1079
1080
  subagentParentTopicId: opts.subagentParentTopicId,
@@ -1275,11 +1276,11 @@ var init_mcp_config = __esm(() => {
1275
1276
  vault: {
1276
1277
  ...commonRuntimeMcpPolicy("vault"),
1277
1278
  build(ctx) {
1278
- const { userId, agent } = ctx;
1279
- const args = [`--user-id=${userId}`];
1279
+ const { userId, vaultUserId, agent } = ctx;
1280
+ const args = [`--user-id=${vaultUserId ?? userId}`];
1280
1281
  if (agent !== "codex")
1281
1282
  args.push("--list-only=true");
1282
- return buildBuiltinMcpServer("vault", ctx, () => buildStdioMcpServer(agent, VAULT_SERVER, args));
1283
+ return buildBuiltinMcpServer("vault", { ...ctx, userId: vaultUserId ?? userId }, () => buildStdioMcpServer(agent, VAULT_SERVER, args));
1283
1284
  }
1284
1285
  }
1285
1286
  };
@@ -1617,6 +1618,14 @@ function renderUserPromptBatch(prompts) {
1617
1618
  ].join(`
1618
1619
  `);
1619
1620
  }
1621
+ function renderUserTurnPrompt(message) {
1622
+ const rawLabel = message.actorLabel?.trim() || message.actorUserId?.trim();
1623
+ const label = rawLabel?.replace(/[\r\n\t]+/g, " ").replace(/\[/g, "(").replace(/\]/g, ")").replace(/\s{2,}/g, " ");
1624
+ return label ? `[@${label}]: ${message.prompt}` : message.prompt;
1625
+ }
1626
+ function renderUserTurnBatch(messages) {
1627
+ return renderUserPromptBatch(messages.map(renderUserTurnPrompt));
1628
+ }
1620
1629
 
1621
1630
  // ../../packages/core/src/agents/rollout/shared.ts
1622
1631
  import { mkdirSync as mkdirSync3 } from "fs";
@@ -2138,7 +2147,7 @@ var init_claude_registry = __esm(() => {
2138
2147
  });
2139
2148
 
2140
2149
  // ../../packages/core/src/version.ts
2141
- var NEGOTIUM_VERSION = "0.2.4";
2150
+ var NEGOTIUM_VERSION = "0.2.5";
2142
2151
 
2143
2152
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
2144
2153
  import { spawn as spawn2 } from "child_process";
@@ -2434,6 +2443,18 @@ function patchEnvContext(envContext, cwd, currentDate, timezone) {
2434
2443
  }
2435
2444
  }
2436
2445
  }
2446
+ function codexRolloutPath(threadId, fallback) {
2447
+ const threadCreatedAt = decodeUuidV7Timestamp(threadId);
2448
+ const createdAt = threadCreatedAt === null ? fallback : new Date(threadCreatedAt);
2449
+ const yyyy = String(createdAt.getFullYear());
2450
+ const mm = String(createdAt.getMonth() + 1).padStart(2, "0");
2451
+ const dd = String(createdAt.getDate()).padStart(2, "0");
2452
+ const hh = String(createdAt.getHours()).padStart(2, "0");
2453
+ const min = String(createdAt.getMinutes()).padStart(2, "0");
2454
+ const ss = String(createdAt.getSeconds()).padStart(2, "0");
2455
+ const dir = join6(codexSessionsDir(), yyyy, mm, dd);
2456
+ return join6(dir, `rollout-${yyyy}-${mm}-${dd}T${hh}-${min}-${ss}-${threadId}.jsonl`);
2457
+ }
2437
2458
  function canonicalFilePath(path) {
2438
2459
  const absolute = resolve4(path);
2439
2460
  try {
@@ -2746,12 +2767,7 @@ function writeCodexRollout(opts) {
2746
2767
  type: "event_msg",
2747
2768
  payload: { type: "task_complete" }
2748
2769
  });
2749
- const yyyy = now.getUTCFullYear();
2750
- const mm = String(now.getUTCMonth() + 1).padStart(2, "0");
2751
- const dd = String(now.getUTCDate()).padStart(2, "0");
2752
- const tsStr = tsIso.replace(/[:.]/g, "-").slice(0, 19);
2753
- const dir = join6(codexSessionsDir(), String(yyyy), mm, dd);
2754
- const path = join6(dir, `rollout-${tsStr}-${threadId}.jsonl`);
2770
+ const path = codexRolloutPath(threadId, now);
2755
2771
  if (opts.threadId) {
2756
2772
  sweepPriorRolloutsForThread(opts.threadId);
2757
2773
  }
@@ -3930,7 +3946,7 @@ async function* claudeProvider(opts) {
3930
3946
  }
3931
3947
  };
3932
3948
  }
3933
- const userId = opts.userId ?? "";
3949
+ const userId = opts.vaultUserId ?? opts.userId ?? "";
3934
3950
  const withVault = substituteClaudeToolInput(userId, tool_name, tool_input);
3935
3951
  if (JSON.stringify(withVault) === JSON.stringify(tool_input)) {
3936
3952
  return { continue: true };
@@ -3950,7 +3966,7 @@ async function* claudeProvider(opts) {
3950
3966
  hooks: [
3951
3967
  async (input) => {
3952
3968
  const { tool_response } = input;
3953
- const userId = opts.userId ?? "";
3969
+ const userId = opts.vaultUserId ?? opts.userId ?? "";
3954
3970
  const redacted = deepMapStrings(tool_response, (value) => redactHostedSecrets(userId, value));
3955
3971
  if (JSON.stringify(redacted) === JSON.stringify(tool_response)) {
3956
3972
  return { continue: true };
@@ -5364,6 +5380,8 @@ async function* codexProvider(opts) {
5364
5380
  throw err;
5365
5381
  if (!currentSessionId || !isMissingCodexRolloutError(err))
5366
5382
  throw err;
5383
+ if (opts.userId && opts.session)
5384
+ throw err;
5367
5385
  logger.warn({ staleSessionId: currentSessionId, err: errMsg(err) }, "codexProvider: stale/missing rollout, restarting fresh thread");
5368
5386
  thread = codex.startThread(threadOptions);
5369
5387
  prompt = promptForThread(opts, true);
@@ -5663,7 +5681,7 @@ function maestroProvider(opts) {
5663
5681
  if (opts.agent !== undefined && opts.agent !== "maestro") {
5664
5682
  throw new Error(`maestroProvider: unexpected agent "${opts.agent}", expected "maestro"`);
5665
5683
  }
5666
- const userId = opts.userId ?? "";
5684
+ const userId = opts.vaultUserId ?? opts.userId ?? "";
5667
5685
  setMcpResolver(hostedMcpServers);
5668
5686
  const callerDisallowedTools = opts.disallowedTools ?? [];
5669
5687
  const callerToolHooks = opts.toolHooks ?? [];
@@ -5782,16 +5800,16 @@ async function resolveSessionFileMissing(agent, sessionId, cwd) {
5782
5800
  }
5783
5801
  async function maybeRebuildSession(opts) {
5784
5802
  if (!opts.sessionId)
5785
- return true;
5803
+ return "ready";
5786
5804
  try {
5787
5805
  const missing = await resolveSessionFileMissing(opts.agent, opts.sessionId, opts.cwd);
5788
5806
  if (!missing)
5789
- return true;
5807
+ return "ready";
5790
5808
  logger.info({ agent: opts.agent, sessionId: opts.sessionId, topic: opts.session }, "session file missing \u2014 rebuilding from unified log");
5791
5809
  const entries = readConversation(opts.userId, opts.session);
5792
5810
  if (entries.length === 0) {
5793
5811
  logger.info({ agent: opts.agent, sessionId: opts.sessionId }, "no unified log entries to rebuild \u2014 starting fresh session");
5794
- return false;
5812
+ return "fresh";
5795
5813
  }
5796
5814
  const operations = getRegistryOperations(opts.agent);
5797
5815
  const result = operations.writeRollout({
@@ -5802,19 +5820,25 @@ async function maybeRebuildSession(opts) {
5802
5820
  ...opts.effort ? { effort: opts.effort } : {}
5803
5821
  });
5804
5822
  logger.info({ agent: opts.agent, sessionId: result.sessionId, rolloutPath: result.rolloutPath }, "session rebuilt from unified log");
5805
- return true;
5823
+ return "ready";
5806
5824
  } catch (err) {
5807
- logger.warn({ err, agent: opts.agent, sessionId: opts.sessionId }, "session rebuild failed \u2014 starting fresh session");
5808
- return false;
5825
+ logger.warn({ err, agent: opts.agent, sessionId: opts.sessionId }, "session rebuild failed \u2014 preserving resume key");
5826
+ return "failed";
5809
5827
  }
5810
5828
  }
5811
5829
  async function* runAgent(opts) {
5812
5830
  const recording = shouldRecordTurn(opts);
5813
5831
  const dispatchOpts = { ...opts };
5814
5832
  if (hasSessionRepairContext(dispatchOpts)) {
5815
- const ok = await maybeRebuildSession(dispatchOpts);
5816
- if (!ok) {
5833
+ const repair = await maybeRebuildSession(dispatchOpts);
5834
+ if (repair === "fresh") {
5817
5835
  dispatchOpts.sessionId = undefined;
5836
+ } else if (repair === "failed") {
5837
+ yield {
5838
+ type: "error",
5839
+ content: "Provider session could not be rebuilt from conversation history; the existing session was preserved for retry."
5840
+ };
5841
+ return;
5818
5842
  }
5819
5843
  }
5820
5844
  const taskScope = resolveTaskEventScope(dispatchOpts);
@@ -11783,7 +11807,7 @@ function rowToRequest(row) {
11783
11807
  if (row.user_messages_json) {
11784
11808
  try {
11785
11809
  const parsed = JSON.parse(row.user_messages_json);
11786
- if (Array.isArray(parsed) && parsed.length > 0 && parsed.every((item) => item && typeof item === "object" && typeof item.prompt === "string" && (item.attachments === undefined || Array.isArray(item.attachments) && item.attachments?.every((attachment) => typeof attachment === "string")))) {
11810
+ if (Array.isArray(parsed) && parsed.length > 0 && parsed.every((item) => item && typeof item === "object" && typeof item.prompt === "string" && (item.attachments === undefined || Array.isArray(item.attachments) && item.attachments?.every((attachment) => typeof attachment === "string")) && (item.actorUserId === undefined || typeof item.actorUserId === "string") && (item.actorLabel === undefined || typeof item.actorLabel === "string"))) {
11787
11811
  userMessages = parsed;
11788
11812
  }
11789
11813
  } catch {
@@ -11835,19 +11859,24 @@ function mergeRuntimeUserTurnRequest(input) {
11835
11859
  return db.transaction(() => {
11836
11860
  const rows = db.query("SELECT * FROM runtime_user_turn_requests WHERE topic_id = ? ORDER BY created_at ASC, rowid ASC").all(input.topicId);
11837
11861
  const previous = rows.map(rowToRequest);
11862
+ const omittedRequestIds = new Set([
11863
+ ...input.omitRequestIds ?? [],
11864
+ ...previous.filter((request) => Boolean(request.execution?.providerSessionId)).map((request) => request.requestId)
11865
+ ]);
11866
+ const carried = previous.filter((request) => !omittedRequestIds.has(request.requestId));
11838
11867
  const alreadyIncludedRequestIds = new Set(input.alreadyIncludedRequestIds ?? []);
11839
- const alreadyIncludedMessages = previous.filter((request) => alreadyIncludedRequestIds.has(request.requestId)).flatMap((request) => request.userMessages);
11868
+ const alreadyIncludedMessages = carried.filter((request) => alreadyIncludedRequestIds.has(request.requestId)).flatMap((request) => request.userMessages);
11840
11869
  const includedPrefixMatches = alreadyIncludedMessages.every((message, index) => {
11841
11870
  const candidate = input.userMessages[index];
11842
- return candidate?.prompt === message.prompt && JSON.stringify(candidate.attachments ?? []) === JSON.stringify(message.attachments ?? []);
11871
+ return candidate?.prompt === message.prompt && candidate.actorUserId === message.actorUserId && candidate.actorLabel === message.actorLabel && JSON.stringify(candidate.attachments ?? []) === JSON.stringify(message.attachments ?? []);
11843
11872
  });
11844
11873
  const alreadyIncludedMessageCount = includedPrefixMatches ? alreadyIncludedMessages.length : 0;
11845
11874
  const userMessages = [
11846
- ...previous.flatMap((request) => request.userMessages),
11875
+ ...carried.flatMap((request) => request.userMessages),
11847
11876
  ...input.userMessages.slice(alreadyIncludedMessageCount)
11848
11877
  ];
11849
11878
  const incomingLoggedCount = Math.min(Math.max(0, input.execution.loggedUserMessageCount ?? 0), input.userMessages.length);
11850
- const loggedUserMessageCount = previous.reduce((count, request) => count + loggedMessageCount(request), 0) + Math.max(0, incomingLoggedCount - alreadyIncludedMessageCount);
11879
+ const loggedUserMessageCount = carried.reduce((count, request) => count + loggedMessageCount(request), 0) + Math.max(0, incomingLoggedCount - alreadyIncludedMessageCount);
11851
11880
  const execution = {
11852
11881
  ...input.execution,
11853
11882
  loggedUserMessageCount,
@@ -11859,7 +11888,7 @@ function mergeRuntimeUserTurnRequest(input) {
11859
11888
  ],
11860
11889
  conversationPrompts: userMessages.slice(loggedUserMessageCount).map((message) => message.prompt)
11861
11890
  };
11862
- const sessionBase = previous.find((request) => request.execution?.sessionIdSpecified)?.execution;
11891
+ const sessionBase = carried.find((request) => request.execution?.sessionIdSpecified)?.execution;
11863
11892
  if (sessionBase?.sessionIdSpecified) {
11864
11893
  execution.sessionId = sessionBase.sessionId;
11865
11894
  execution.sessionIdSpecified = true;
@@ -11870,13 +11899,29 @@ function mergeRuntimeUserTurnRequest(input) {
11870
11899
  (request_id, topic_id, user_id, prompt, user_messages_json, attachments_json,
11871
11900
  allow_auto_continue, execution_json, topic_epoch, created_at,
11872
11901
  status, claimed_by, claimed_at, running_query_id)
11873
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, NULL)`).run(input.requestId, input.topicId, input.userId, renderUserPromptBatch(userMessages.map((message) => message.prompt)), JSON.stringify(userMessages), attachments?.length ? JSON.stringify(attachments) : null, input.allowAutoContinue ? 1 : 0, JSON.stringify(execution), input.topicEpoch, now);
11902
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, NULL)`).run(input.requestId, input.topicId, input.userId, renderUserTurnBatch(userMessages), JSON.stringify(userMessages), attachments?.length ? JSON.stringify(attachments) : null, input.allowAutoContinue ? 1 : 0, JSON.stringify(execution), input.topicEpoch, now);
11874
11903
  return {
11875
11904
  requestId: input.requestId,
11876
11905
  supersededRequestIds: previous.map((request) => request.requestId)
11877
11906
  };
11878
11907
  }).immediate();
11879
11908
  }
11909
+ function markRuntimeUserTurnProviderSessionObserved(topicId, requestId, sessionId) {
11910
+ return db.transaction(() => {
11911
+ const row = db.query("SELECT * FROM runtime_user_turn_requests WHERE topic_id = ? AND request_id = ?").get(topicId, requestId);
11912
+ if (!row)
11913
+ return false;
11914
+ const request = rowToRequest(row);
11915
+ const execution = {
11916
+ ...request.execution,
11917
+ providerSessionId: sessionId
11918
+ };
11919
+ const result = db.query(`UPDATE runtime_user_turn_requests
11920
+ SET execution_json = ?
11921
+ WHERE topic_id = ? AND request_id = ?`).run(JSON.stringify(execution), topicId, requestId);
11922
+ return result.changes === 1;
11923
+ }).immediate();
11924
+ }
11880
11925
  function markRuntimeUserTurnMessagesLogged(topicId, requestId, ownerId, loggedUserMessages) {
11881
11926
  if (loggedUserMessages.length === 0)
11882
11927
  return false;
@@ -11884,7 +11929,7 @@ function markRuntimeUserTurnMessagesLogged(topicId, requestId, ownerId, loggedUs
11884
11929
  const requests = db.query("SELECT * FROM runtime_user_turn_requests WHERE topic_id = ? ORDER BY created_at ASC, rowid ASC").all(topicId).map(rowToRequest);
11885
11930
  const hasLoggedPrefix = (request2) => loggedUserMessages.length <= request2.userMessages.length && loggedUserMessages.every((message, index) => {
11886
11931
  const candidate = request2.userMessages[index];
11887
- return candidate?.prompt === message.prompt && JSON.stringify(candidate.attachments ?? []) === JSON.stringify(message.attachments ?? []);
11932
+ return candidate?.prompt === message.prompt && candidate.actorUserId === message.actorUserId && candidate.actorLabel === message.actorLabel && JSON.stringify(candidate.attachments ?? []) === JSON.stringify(message.attachments ?? []);
11888
11933
  });
11889
11934
  const request = requests.find((candidate) => candidate.requestId === requestId && candidate.claimedBy === ownerId && hasLoggedPrefix(candidate)) ?? requests.find((candidate) => candidate.execution?.supersededRequestIds?.includes(requestId) && hasLoggedPrefix(candidate));
11890
11935
  if (!request)
@@ -14141,7 +14186,7 @@ function classifyAgentError(err2, agent) {
14141
14186
  return `${name} error: ${snippet}`;
14142
14187
  }
14143
14188
  function isSessionExpiredError(message) {
14144
- return message.includes(SESSION_EXPIRED_MSG) || /session was recorded with model .+ but is resuming with/i.test(message) || /session.*not found|session.*expired|unknown conversation/i.test(message);
14189
+ return message.includes(SESSION_EXPIRED_MSG) || /session was recorded with model .+ but is resuming with/i.test(message) || /failed to resolve rollout path .+file does not exist/i.test(message) || /session.*not found|session.*expired|unknown conversation/i.test(message);
14145
14190
  }
14146
14191
  var SESSION_EXPIRED_MSG = "No conversation found with session ID";
14147
14192
  var init_errors = __esm(() => {
@@ -14729,6 +14774,14 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
14729
14774
  for await (const event of events) {
14730
14775
  if (abortController.signal.aborted)
14731
14776
  break;
14777
+ if (!control.providerTurnContentObserved && (event.type === "text_delta" || event.type === "text" || event.type === "reasoning" || event.type === "tool_use" || event.type === "tool_result" || event.type === "file" || event.type === "result")) {
14778
+ control.providerTurnContentObserved = true;
14779
+ if (control.sessionId) {
14780
+ for (const requestId of control.durableRequestIds ?? []) {
14781
+ markRuntimeUserTurnProviderSessionObserved(topicId, requestId, control.sessionId);
14782
+ }
14783
+ }
14784
+ }
14732
14785
  switch (event.type) {
14733
14786
  case "text_delta": {
14734
14787
  pendingSawDelta = true;
@@ -15035,6 +15088,13 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
15035
15088
  }
15036
15089
  break;
15037
15090
  case "session":
15091
+ control.sessionId = event.sessionId;
15092
+ control.providerSessionObserved = true;
15093
+ if (control.providerTurnContentObserved) {
15094
+ for (const requestId of control.durableRequestIds ?? []) {
15095
+ markRuntimeUserTurnProviderSessionObserved(topicId, requestId, event.sessionId);
15096
+ }
15097
+ }
15038
15098
  if (!silent && onSessionId && getRoomQuery(topicId)?.queryId === queryId) {
15039
15099
  try {
15040
15100
  if (control.injectParams)
@@ -15109,7 +15169,9 @@ async function runTurnEventStream(topicId, topicTitle, queryId, events, control,
15109
15169
  if (forkHandle && outcome.kind !== "session-expired" && (!forkRequestId || !interSessionQueue.hasRequest(topicId, forkRequestId))) {
15110
15170
  cleanupAgentFork(forkHandle);
15111
15171
  }
15112
- if (roomId === topicId && outcome.kind !== "session-expired" && !getRoomQuery(topicId)) {
15172
+ const queuedUserTurn = roomId === topicId ? getRuntimeUserTurnRequest(topicId) : null;
15173
+ const hasReplacementUserTurn = queuedUserTurn !== null && queuedUserTurn.requestId !== queryId;
15174
+ if (roomId === topicId && outcome.kind !== "session-expired" && !getRoomQuery(topicId) && !hasReplacementUserTurn) {
15113
15175
  const next = takeDeferredInject(topicId);
15114
15176
  if (next)
15115
15177
  redispatchInject(next);
@@ -15141,6 +15203,7 @@ var init_turn_event_stream = __esm(async () => {
15141
15203
  init_visuals(),
15142
15204
  init_api_messages(),
15143
15205
  init_api_topics(),
15206
+ init_runtime_turn_requests(),
15144
15207
  init_token_stats()
15145
15208
  ]);
15146
15209
  });
@@ -15221,6 +15284,7 @@ __export(exports_turn_runner, {
15221
15284
  wasLocallyRequeuedAfterUserPreemption: () => wasLocallyRequeuedAfterUserPreemption,
15222
15285
  userConversationPromptsToRecord: () => userConversationPromptsToRecord,
15223
15286
  upsertTaskPanelMessage: () => upsertTaskPanelMessage,
15287
+ tryReconstructTopicRollout: () => tryReconstructTopicRollout,
15224
15288
  triggerTopicAiTurn: () => triggerTopicAiTurn,
15225
15289
  topicAllowsVisualFileId: () => topicAllowsVisualFileId,
15226
15290
  taskPanelMessageId: () => taskPanelMessageId,
@@ -15236,9 +15300,11 @@ __export(exports_turn_runner, {
15236
15300
  resolveVisualMediaInput: () => resolveVisualMediaInput,
15237
15301
  resolveTopicTurnSession: () => resolveTopicTurnSession,
15238
15302
  resolveTopicTurnExecution: () => resolveTopicTurnExecution,
15303
+ resolveSessionRetry: () => resolveSessionRetry,
15239
15304
  resolveModelForAgent: () => resolveModelForAgent,
15240
15305
  resolveInitialTurnSessionId: () => resolveInitialTurnSessionId,
15241
15306
  resolveCompactionExecution: () => resolveCompactionExecution,
15307
+ renderUserTurnBatch: () => renderUserTurnBatch,
15242
15308
  renderUserPromptBatch: () => renderUserPromptBatch,
15243
15309
  renderTaskPanel: () => renderTaskPanel,
15244
15310
  promptWithAttachments: () => promptWithAttachments,
@@ -15393,6 +15459,7 @@ function redispatchInject(inject) {
15393
15459
  aiMention: topic.aiMention
15394
15460
  },
15395
15461
  userId: inject.userId,
15462
+ vaultUserId: inject.vaultUserId,
15396
15463
  prompt: inject.prompt,
15397
15464
  allowAutoContinue: false,
15398
15465
  origin: inject.origin,
@@ -15518,12 +15585,10 @@ async function deliverAskError(queryId, sourceLabel, error) {
15518
15585
  }
15519
15586
  function tryReconstructTopicRollout(opts) {
15520
15587
  const { topicId, topicTitle, userId, agent, sessionId, cwd, model, effort } = opts;
15521
- if (!sessionId && !opts.allowFreshSession)
15522
- return null;
15523
15588
  try {
15524
15589
  const entries = readConversation(userId, topicTitle);
15525
15590
  if (entries.length === 0)
15526
- return null;
15591
+ return { kind: "no-history" };
15527
15592
  const result = getRegistryOperations(agent).writeRollout({
15528
15593
  cwd,
15529
15594
  entries,
@@ -15538,15 +15603,18 @@ function tryReconstructTopicRollout(opts) {
15538
15603
  rolloutPath: result.rolloutPath,
15539
15604
  entries: entries.length
15540
15605
  }, sessionId ? "ai: session expired \u2014 rollout reconstructed from unified log" : "ai: provider session bridged from shared conversation log");
15541
- return result.sessionId;
15606
+ return { kind: "rebuilt", sessionId: result.sessionId };
15542
15607
  } catch (err2) {
15543
15608
  logger.warn({ err: err2, topicId, agent, sessionId }, "ai: session reconstruct failed");
15544
- return null;
15609
+ return { kind: "failed", error: stringifyError(err2) };
15545
15610
  }
15546
15611
  }
15547
- function resolveSessionRetryId(opts) {
15612
+ function resolveSessionRetry(opts) {
15548
15613
  const reconstructed = tryReconstructTopicRollout(opts);
15549
- if (reconstructed)
15614
+ if (reconstructed.kind === "rebuilt") {
15615
+ return { kind: "retry", sessionId: reconstructed.sessionId };
15616
+ }
15617
+ if (reconstructed.kind === "failed")
15550
15618
  return reconstructed;
15551
15619
  if (opts.onSessionReset)
15552
15620
  opts.onSessionReset();
@@ -15554,7 +15622,7 @@ function resolveSessionRetryId(opts) {
15554
15622
  clearTopicSessionId(opts.topicId, "session-expired");
15555
15623
  }
15556
15624
  logger.info({ topicId: opts.topicId, agent: opts.agent, hadSessionId: Boolean(opts.sessionId) }, "ai: session expired \u2014 retrying with fresh session");
15557
- return null;
15625
+ return { kind: "retry", sessionId: null };
15558
15626
  }
15559
15627
  function mergeSupersedingUserTurn(running, incoming) {
15560
15628
  const userMessages = [
@@ -15562,7 +15630,7 @@ function mergeSupersedingUserTurn(running, incoming) {
15562
15630
  ...incoming.userMessages ?? [legacyUserTurnEnvelope(incoming.prompt, incoming.attachments)]
15563
15631
  ];
15564
15632
  return {
15565
- prompt: renderUserPromptBatch(userMessages.map((message) => message.prompt)),
15633
+ prompt: renderUserTurnBatch(userMessages),
15566
15634
  userMessages,
15567
15635
  attachments: flattenUserTurnAttachments(userMessages),
15568
15636
  sessionId: running.sessionId
@@ -15589,11 +15657,12 @@ function serializableUserTurnExecution(params) {
15589
15657
  bridgeSessionFromHistory: params.bridgeSessionFromHistory,
15590
15658
  peerBridge: params.peerBridge,
15591
15659
  from: params.from,
15660
+ vaultUserId: params.vaultUserId,
15592
15661
  conversationPrompts: params._conversationPrompts ?? params._userMessages?.map((message) => message.prompt) ?? [params.prompt],
15593
15662
  loggedUserMessageCount: params._loggedUserMessageCount
15594
15663
  };
15595
15664
  }
15596
- function waitToStartRemoteUserTurn(params, queryId) {
15665
+ function waitToStartRemoteUserTurn(params, queryId, options) {
15597
15666
  const execution = serializableUserTurnExecution(params);
15598
15667
  const incomingUserMessages = params._userMessages ?? [
15599
15668
  legacyUserTurnEnvelope(params.prompt, params.attachments)
@@ -15606,7 +15675,8 @@ function waitToStartRemoteUserTurn(params, queryId) {
15606
15675
  requestId: queryId,
15607
15676
  execution,
15608
15677
  topicEpoch: execution.runtimeEpoch ?? getRuntimeTopicEpoch(params.topic.id),
15609
- alreadyIncludedRequestIds: params._durableRequestIds
15678
+ alreadyIncludedRequestIds: params._durableRequestIds,
15679
+ omitRequestIds: options?.omitRequestIds
15610
15680
  });
15611
15681
  for (const supersededRequestId of merged.supersededRequestIds) {
15612
15682
  if (supersededRequestId !== merged.requestId) {
@@ -15657,6 +15727,7 @@ async function drainOneDurableUserTurn() {
15657
15727
  const queryId = startAiTurn({
15658
15728
  topic,
15659
15729
  userId: request.userId,
15730
+ vaultUserId: execution?.vaultUserId,
15660
15731
  prompt: request.prompt,
15661
15732
  _userMessages: request.userMessages,
15662
15733
  _conversationPrompts: execution?.conversationPrompts ?? [request.prompt],
@@ -15727,12 +15798,13 @@ function startAiTurn(params) {
15727
15798
  }
15728
15799
  const topic = storedTopic;
15729
15800
  const { userId, allowAutoContinue, onDispatched } = params;
15801
+ const vaultUserId = params.vaultUserId ?? userId;
15730
15802
  const origin = params.origin ?? "user";
15731
15803
  const conversationPrompts = params._conversationPrompts ?? [params.prompt];
15732
15804
  let loggedUserMessageCount = params._loggedUserMessageCount;
15733
15805
  let durableRequestIds = params._durableRequestIds ?? [];
15734
15806
  let userMessages = isUserOrigin(origin) ? params._userMessages ?? [legacyUserTurnEnvelope(params.prompt, params.attachments)] : undefined;
15735
- let prompt = userMessages ? renderUserPromptBatch(userMessages.map((message) => message.prompt)) : params.prompt;
15807
+ let prompt = userMessages ? renderUserTurnBatch(userMessages) : params.prompt;
15736
15808
  let attachments2 = userMessages ? flattenUserTurnAttachments(userMessages) : params.attachments;
15737
15809
  const execution = resolveTopicTurnExecution(topic, params);
15738
15810
  const sessionResolution = resolveTopicTurnSession(topic, params.sessionId, {
@@ -15797,6 +15869,7 @@ function startAiTurn(params) {
15797
15869
  topicId,
15798
15870
  runtimeEpoch,
15799
15871
  userId,
15872
+ vaultUserId,
15800
15873
  prompt,
15801
15874
  origin,
15802
15875
  sourceNode,
@@ -15881,16 +15954,22 @@ function startAiTurn(params) {
15881
15954
  }
15882
15955
  if (decision.action === "abort-replace") {
15883
15956
  const running = decision.running;
15957
+ let omitRequestIds;
15884
15958
  if (isUserOrigin(running.origin)) {
15885
- const merged = mergeSupersedingUserTurn(running, { prompt, userMessages, attachments: attachments2 });
15886
- userMessages = merged.userMessages;
15887
- prompt = merged.prompt;
15888
- attachments2 = merged.attachments;
15889
- sessionId = merged.sessionId;
15890
- loggedUserMessageCount = running.userMessages?.length ?? 1;
15891
- durableRequestIds = [
15892
- ...new Set([...running.durableRequestIds ?? [], ...durableRequestIds])
15893
- ];
15959
+ if (running.providerTurnContentObserved && running.sessionId) {
15960
+ sessionId = running.sessionId;
15961
+ omitRequestIds = running.durableRequestIds;
15962
+ } else {
15963
+ const merged = mergeSupersedingUserTurn(running, { prompt, userMessages, attachments: attachments2 });
15964
+ userMessages = merged.userMessages;
15965
+ prompt = merged.prompt;
15966
+ attachments2 = merged.attachments;
15967
+ sessionId = merged.sessionId;
15968
+ loggedUserMessageCount = running.userMessages?.length ?? 1;
15969
+ durableRequestIds = [
15970
+ ...new Set([...running.durableRequestIds ?? [], ...durableRequestIds])
15971
+ ];
15972
+ }
15894
15973
  }
15895
15974
  if (running.injectParams) {
15896
15975
  const runningInject = running.injectParams;
@@ -15911,6 +15990,20 @@ function startAiTurn(params) {
15911
15990
  running.abortReason = "internal" /* Internal */;
15912
15991
  running.abortController.abort();
15913
15992
  logger.info({ topicId, supersededQueryId: running.queryId, supersededOrigin: running.origin }, "ai: new user message superseded running turn");
15993
+ const queuedQueryId = waitToStartRemoteUserTurn({
15994
+ ...params,
15995
+ topic,
15996
+ prompt,
15997
+ attachments: attachments2,
15998
+ sessionId,
15999
+ _userMessages: userMessages,
16000
+ _conversationPrompts: conversationPrompts,
16001
+ _loggedUserMessageCount: loggedUserMessageCount,
16002
+ _durableRequestIds: durableRequestIds,
16003
+ _runtimeEpoch: runtimeEpoch
16004
+ }, queryId, { omitRequestIds });
16005
+ announceQueuedUserTurn({ ...params, topic }, queuedQueryId);
16006
+ return queuedQueryId;
15914
16007
  }
15915
16008
  const abortController = new AbortController;
15916
16009
  const control = {
@@ -15929,6 +16022,7 @@ function startAiTurn(params) {
15929
16022
  topicId,
15930
16023
  runtimeEpoch,
15931
16024
  userId,
16025
+ vaultUserId,
15932
16026
  prompt,
15933
16027
  origin,
15934
16028
  requestId,
@@ -16002,8 +16096,9 @@ function startAiTurn(params) {
16002
16096
  const resolvedEffort = execution.effort;
16003
16097
  const workspaceCwd = cwd ?? workspaceCwdFor(topicId);
16004
16098
  mkdirSync19(workspaceCwd, { recursive: true });
16099
+ let historyBridgeError;
16005
16100
  if (bridgeSessionFromHistory && !sessionId) {
16006
- const bridgedSessionId = tryReconstructTopicRollout({
16101
+ const bridge = tryReconstructTopicRollout({
16007
16102
  topicId,
16008
16103
  topicTitle: sessionName,
16009
16104
  userId,
@@ -16011,25 +16106,32 @@ function startAiTurn(params) {
16011
16106
  sessionId: null,
16012
16107
  cwd: workspaceCwd,
16013
16108
  model: resolvedModel,
16014
- ...resolvedEffort ? { effort: resolvedEffort } : {},
16015
- allowFreshSession: true
16109
+ ...resolvedEffort ? { effort: resolvedEffort } : {}
16016
16110
  });
16017
- if (bridgedSessionId) {
16018
- sessionId = bridgedSessionId;
16019
- control.sessionId = bridgedSessionId;
16111
+ if (bridge.kind === "rebuilt") {
16112
+ sessionId = bridge.sessionId;
16113
+ control.sessionId = bridge.sessionId;
16020
16114
  if (control.injectParams)
16021
- control.injectParams.sessionId = bridgedSessionId;
16115
+ control.injectParams.sessionId = bridge.sessionId;
16022
16116
  try {
16023
- onSessionId?.(bridgedSessionId);
16117
+ onSessionId?.(bridge.sessionId);
16024
16118
  } catch (err2) {
16025
16119
  logger.warn({ err: err2, topicId, queryId, agent: agentKind }, "ai: isolated session owner rejected bridged provider session id");
16026
16120
  }
16121
+ } else if (bridge.kind === "failed") {
16122
+ historyBridgeError = "Conversation history could not be encoded into a provider session; no fresh session was started.";
16027
16123
  }
16028
16124
  }
16029
16125
  const messageAttachmentGroups = userMessages?.map((message, index) => materializePromptAttachments(topicId, `${queryId}-${index}`, message.attachments));
16030
16126
  const promptAttachments = messageAttachmentGroups?.flat() ?? materializePromptAttachments(topicId, queryId, attachments2);
16031
- const promptWithFiles = userMessages ? renderUserPromptBatch(userMessages.map((message, index) => promptWithAttachments(message.prompt, messageAttachmentGroups?.[index] ?? []))) : promptWithAttachments(prompt, promptAttachments);
16032
- const renderedUserPrompts = userMessages ? userMessages.map((message, index) => promptWithAttachments(message.prompt, messageAttachmentGroups?.[index] ?? [])) : undefined;
16127
+ const promptWithFiles = userMessages ? renderUserTurnBatch(userMessages.map((message, index) => ({
16128
+ ...message,
16129
+ prompt: promptWithAttachments(message.prompt, messageAttachmentGroups?.[index] ?? [])
16130
+ }))) : promptWithAttachments(prompt, promptAttachments);
16131
+ const renderedUserPrompts = userMessages ? userMessages.map((message, index) => renderUserTurnPrompt({
16132
+ ...message,
16133
+ prompt: promptWithAttachments(message.prompt, messageAttachmentGroups?.[index] ?? [])
16134
+ })) : undefined;
16033
16135
  const agentPrompt = topic.aiMode === "mention" && isUserOrigin(origin) && !silent ? buildMentionOnlyChannelPrompt({
16034
16136
  topicId,
16035
16137
  userId,
@@ -16145,6 +16247,10 @@ function startAiTurn(params) {
16145
16247
  const turnMcpEnabled = browserProfileOwner ? enabledMcp : enabledMcp.filter((name) => name !== "playwright");
16146
16248
  const wantsBgBash = !isManager && enabledMcp.includes("background-bash");
16147
16249
  async function* runWithPlaywright() {
16250
+ if (historyBridgeError) {
16251
+ yield { type: "error", content: historyBridgeError };
16252
+ return;
16253
+ }
16148
16254
  if (prepareSession && !sessionId) {
16149
16255
  if (abortController.signal.aborted)
16150
16256
  return;
@@ -16217,6 +16323,7 @@ ${playwrightNote}`;
16217
16323
  systemPrompt: effectiveSystemPrompt,
16218
16324
  sessionId,
16219
16325
  userId,
16326
+ vaultUserId,
16220
16327
  session: sessionName,
16221
16328
  sessionType: sessionType ?? (isManager ? "manager" : "forum"),
16222
16329
  abortController,
@@ -16250,11 +16357,10 @@ ${playwrightNote}`;
16250
16357
  WsHub.get().broadcastTyping(topicId, "ai");
16251
16358
  WsHub.get().broadcastAiActive(topicId, queryId);
16252
16359
  }
16253
- streamAgentEvents(topicId, topic.title, queryId, events, control, agentKind, resolvedModel, resolvedEffort, userId, !sessionRetried, onSessionId, { silent, peerBridge, sourceNode }).then(async (outcome) => {
16360
+ streamAgentEvents(topicId, topic.title, queryId, events, control, agentKind, resolvedModel, resolvedEffort, userId, !sessionRetried, onSessionId, { silent, peerBridge, sourceNode }).then(async (streamOutcome) => {
16361
+ let outcome = streamOutcome;
16254
16362
  if (outcome.kind === "session-expired") {
16255
- if (!silent)
16256
- WsHub.get().broadcastAborted(topicId, queryId, "stopped");
16257
- const retrySessionId = resolveSessionRetryId({
16363
+ const retry = resolveSessionRetry({
16258
16364
  topicId,
16259
16365
  topicTitle: sessionName,
16260
16366
  userId,
@@ -16267,55 +16373,66 @@ ${playwrightNote}`;
16267
16373
  externalSessionOwner: Boolean(onSessionId),
16268
16374
  onSessionReset
16269
16375
  });
16270
- logger.info({
16271
- topicId,
16272
- prevQueryId: queryId,
16273
- agent: agentKind,
16274
- retrySessionId: retrySessionId ? retrySessionId.slice(0, 8) : null
16275
- }, "ai: retrying query after session expiry");
16276
- if (!retrySessionId && prepareSession && forkHandle) {
16277
- cleanupAgentFork(forkHandle);
16278
- forkHandle = undefined;
16376
+ if (retry.kind === "failed") {
16377
+ outcome = {
16378
+ kind: "provider-error",
16379
+ error: "Provider session expired and conversation history could not be rebuilt; the existing session was preserved for retry."
16380
+ };
16381
+ } else {
16382
+ if (!silent)
16383
+ WsHub.get().broadcastAborted(topicId, queryId, "stopped");
16384
+ const retrySessionId = retry.sessionId;
16385
+ logger.info({
16386
+ topicId,
16387
+ prevQueryId: queryId,
16388
+ agent: agentKind,
16389
+ retrySessionId: retrySessionId ? retrySessionId.slice(0, 8) : null
16390
+ }, "ai: retrying query after session expiry");
16391
+ if (!retrySessionId && prepareSession && forkHandle) {
16392
+ cleanupAgentFork(forkHandle);
16393
+ forkHandle = undefined;
16394
+ }
16395
+ startAiTurn({
16396
+ topic,
16397
+ userId,
16398
+ vaultUserId,
16399
+ prompt,
16400
+ _userMessages: userMessages,
16401
+ _conversationPrompts: conversationPrompts,
16402
+ _loggedUserMessageCount: loggedUserMessageCount,
16403
+ _durableRequestIds: durableRequestIds,
16404
+ attachments: attachments2,
16405
+ allowAutoContinue,
16406
+ origin,
16407
+ onDispatched,
16408
+ requestId,
16409
+ depth,
16410
+ silent,
16411
+ contextId,
16412
+ agentOverride,
16413
+ modelOverride,
16414
+ effortOverride,
16415
+ sessionId: retrySessionId,
16416
+ sessionScope,
16417
+ turnConcurrency,
16418
+ forkHandle,
16419
+ prepareSession,
16420
+ cwd,
16421
+ sessionName,
16422
+ sessionType,
16423
+ visualTools,
16424
+ fileDeliveryTools,
16425
+ onSessionId,
16426
+ onSessionReset,
16427
+ bridgeSessionFromHistory,
16428
+ onSettled,
16429
+ peerBridge,
16430
+ askReplySources,
16431
+ _runtimeEpoch: runtimeEpoch,
16432
+ _sessionRetried: true
16433
+ });
16434
+ return;
16279
16435
  }
16280
- startAiTurn({
16281
- topic,
16282
- userId,
16283
- prompt,
16284
- _userMessages: userMessages,
16285
- _conversationPrompts: conversationPrompts,
16286
- _loggedUserMessageCount: loggedUserMessageCount,
16287
- _durableRequestIds: durableRequestIds,
16288
- attachments: attachments2,
16289
- allowAutoContinue,
16290
- origin,
16291
- onDispatched,
16292
- requestId,
16293
- depth,
16294
- silent,
16295
- contextId,
16296
- agentOverride,
16297
- modelOverride,
16298
- effortOverride,
16299
- sessionId: retrySessionId,
16300
- sessionScope,
16301
- turnConcurrency,
16302
- forkHandle,
16303
- prepareSession,
16304
- cwd,
16305
- sessionName,
16306
- sessionType,
16307
- visualTools,
16308
- fileDeliveryTools,
16309
- onSessionId,
16310
- onSessionReset,
16311
- bridgeSessionFromHistory,
16312
- onSettled,
16313
- peerBridge,
16314
- askReplySources,
16315
- _runtimeEpoch: runtimeEpoch,
16316
- _sessionRetried: true
16317
- });
16318
- return;
16319
16436
  }
16320
16437
  if (outcome.kind === "provider-error") {
16321
16438
  if (!silent) {
@@ -17416,14 +17533,11 @@ function switchApiTopicAgent(opts) {
17416
17533
  topicTitle: opts.topicTitle,
17417
17534
  from: opts.fromAgent,
17418
17535
  to: opts.agent
17419
- }, "api-topic switch: rollout encoding failed \u2014 falling back to fresh session");
17420
- try {
17421
- commitApiTopicSwitch(opts, null);
17422
- } catch (commitErr) {
17423
- logger.warn({ err: commitErr, topicId: opts.topicId, agent: opts.agent }, "api-topic switch fallback commit failed");
17424
- return { ok: false, error: `failed to set agent for "${opts.topicTitle}"` };
17425
- }
17426
- return { ok: true, outcome: { kind: "fresh", agent: opts.agent, reason: "bridge-failed" } };
17536
+ }, "api-topic switch: rollout encoding failed \u2014 preserving current agent session");
17537
+ return {
17538
+ ok: false,
17539
+ error: `failed to preserve conversation while switching agent for "${opts.topicTitle}"`
17540
+ };
17427
17541
  }
17428
17542
  try {
17429
17543
  appendConversationEventStrict(opts.userId, opts.topicTitle, opts.agent, {
@@ -17648,7 +17762,7 @@ function createSelfConfigCore(host, productOverrides = {}) {
17648
17762
  return err(switched.error);
17649
17763
  host.runtime.configChanged(topic.id, "agent");
17650
17764
  ctx.onConfigChanged?.("agent");
17651
- const sessionNote = switched.outcome.kind === "bridged" ? ` Conversation history was bridged into a ${agent} session (${switched.outcome.bridgedSessionId}).` : switched.outcome.reason === "bridge-failed" ? " Conversation history bridge failed; the next turn starts fresh." : " No prior conversation history was found; the next turn starts fresh.";
17765
+ const sessionNote = switched.outcome.kind === "bridged" ? ` Conversation history was bridged into a ${agent} session (${switched.outcome.bridgedSessionId}).` : " No prior conversation history was found; the next turn starts fresh.";
17652
17766
  return ok(`Agent for this topic set to '${agent}'. Model/effort reset to '${agent}' defaults.${sessionNote}`);
17653
17767
  }
17654
17768
  function getAgent(ctx) {
@@ -18179,4 +18293,4 @@ export {
18179
18293
  DEFAULT_SELF_CONFIG_PRODUCT
18180
18294
  };
18181
18295
 
18182
- //# debugId=215B24B8D101FF5A64756E2164756E21
18296
+ //# debugId=0A6AFB912A18777064756E2164756E21