@actalk/inkos-studio 1.3.4 → 1.3.5-canary.34.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.
Files changed (63) hide show
  1. package/dist/api/server.d.ts.map +1 -1
  2. package/dist/api/server.js +110 -34
  3. package/dist/api/server.js.map +1 -1
  4. package/dist/assets/{_baseUniq-CUXaSiLD.js → _baseUniq-CpTphU9M.js} +1 -1
  5. package/dist/assets/{arc-FvWBPXrI.js → arc-BmdlE7tJ.js} +1 -1
  6. package/dist/assets/{architectureDiagram-Q4EWVU46-B8UpD9eK.js → architectureDiagram-Q4EWVU46-B08ry5zQ.js} +1 -1
  7. package/dist/assets/{blockDiagram-DXYQGD6D-CPxSckyQ.js → blockDiagram-DXYQGD6D-BNxUKAO_.js} +1 -1
  8. package/dist/assets/{c4Diagram-AHTNJAMY-BHFUy4R7.js → c4Diagram-AHTNJAMY-DoV0FOWJ.js} +1 -1
  9. package/dist/assets/channel-Q-ZfuLB-.js +1 -0
  10. package/dist/assets/{chunk-4BX2VUAB-wWYh9dQR.js → chunk-4BX2VUAB-CvSWEgQl.js} +1 -1
  11. package/dist/assets/{chunk-4TB4RGXK-B_UhnwUd.js → chunk-4TB4RGXK-BQAIQHv6.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-DJ2soGkg.js → chunk-55IACEB6-lFr-e-c6.js} +1 -1
  13. package/dist/assets/{chunk-EDXVE4YY-C4gQb1PT.js → chunk-EDXVE4YY-5yQ0WRsM.js} +1 -1
  14. package/dist/assets/{chunk-FMBD7UC4-3bX5pTEy.js → chunk-FMBD7UC4-C-hhtNlL.js} +1 -1
  15. package/dist/assets/{chunk-OYMX7WX6-ypA0Yl6Q.js → chunk-OYMX7WX6-CjZNxR6R.js} +1 -1
  16. package/dist/assets/{chunk-QZHKN3VN-DMNDYw8g.js → chunk-QZHKN3VN-eWWqsAhu.js} +1 -1
  17. package/dist/assets/{chunk-YZCP3GAM-CGOQPqQ5.js → chunk-YZCP3GAM-nJ78mfvK.js} +1 -1
  18. package/dist/assets/classDiagram-6PBFFD2Q-D2TnKGt8.js +1 -0
  19. package/dist/assets/classDiagram-v2-HSJHXN6E-D2TnKGt8.js +1 -0
  20. package/dist/assets/clone-pNCFH49W.js +1 -0
  21. package/dist/assets/{cose-bilkent-S5V4N54A-DBNsFf7M.js → cose-bilkent-S5V4N54A-CmEnoxru.js} +1 -1
  22. package/dist/assets/{dagre-KV5264BT-uxOmz6yQ.js → dagre-KV5264BT-CA8eB0J-.js} +1 -1
  23. package/dist/assets/{diagram-5BDNPKRD-B-7RdZ-8.js → diagram-5BDNPKRD-_vtscRZj.js} +1 -1
  24. package/dist/assets/{diagram-G4DWMVQ6-Bhteb-vj.js → diagram-G4DWMVQ6-DGxxw7hL.js} +1 -1
  25. package/dist/assets/{diagram-MMDJMWI5-maGfd5tL.js → diagram-MMDJMWI5-D9un5UaS.js} +1 -1
  26. package/dist/assets/{diagram-TYMM5635-BovUtJbC.js → diagram-TYMM5635-BG_AbiPQ.js} +1 -1
  27. package/dist/assets/{erDiagram-SMLLAGMA-6ffWXGPL.js → erDiagram-SMLLAGMA-PXwO0FVu.js} +1 -1
  28. package/dist/assets/{flowDiagram-DWJPFMVM-4fV85_dF.js → flowDiagram-DWJPFMVM-BAL1S6ll.js} +1 -1
  29. package/dist/assets/{ganttDiagram-T4ZO3ILL-bLy7-lid.js → ganttDiagram-T4ZO3ILL-CjfyXTlO.js} +1 -1
  30. package/dist/assets/{gitGraphDiagram-UUTBAWPF-hCqDpD-N.js → gitGraphDiagram-UUTBAWPF-GdUdCj3c.js} +1 -1
  31. package/dist/assets/{graph-Bz91hyDc.js → graph-DG9IRQPc.js} +1 -1
  32. package/dist/assets/{highlighted-body-OFNGDK62-BNyd72qC.js → highlighted-body-OFNGDK62-DyFe3hzx.js} +1 -1
  33. package/dist/assets/index-BbfGoXqD.js +1277 -0
  34. package/dist/assets/index-D12V4C17.css +1 -0
  35. package/dist/assets/{infoDiagram-42DDH7IO-D-Xu56Hx.js → infoDiagram-42DDH7IO-QI2F2hJ3.js} +1 -1
  36. package/dist/assets/{ishikawaDiagram-UXIWVN3A-DhACBOSM.js → ishikawaDiagram-UXIWVN3A-rixFkGkz.js} +1 -1
  37. package/dist/assets/{journeyDiagram-VCZTEJTY-0X971G0m.js → journeyDiagram-VCZTEJTY-Btze11PL.js} +1 -1
  38. package/dist/assets/{kanban-definition-6JOO6SKY-DvOLUhmo.js → kanban-definition-6JOO6SKY-D8ERKCyh.js} +1 -1
  39. package/dist/assets/{layout-DF8ziATB.js → layout-CsMnAU1N.js} +1 -1
  40. package/dist/assets/{linear-D_W5Fxl0.js → linear-vWd9anT-.js} +1 -1
  41. package/dist/assets/{min-Smhc5oQZ.js → min-D0IR8kco.js} +1 -1
  42. package/dist/assets/{mindmap-definition-QFDTVHPH-B1985XGN.js → mindmap-definition-QFDTVHPH-B9QfdTTi.js} +1 -1
  43. package/dist/assets/{pieDiagram-DEJITSTG-bAWD-keH.js → pieDiagram-DEJITSTG-B_04JnhY.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-34T5L4WZ-C-gvS6yv.js → quadrantDiagram-34T5L4WZ-lLuRcyTx.js} +1 -1
  45. package/dist/assets/{requirementDiagram-MS252O5E-BUQl_0Ji.js → requirementDiagram-MS252O5E-BQgrDamx.js} +1 -1
  46. package/dist/assets/{sankeyDiagram-XADWPNL6-C4SiZWPm.js → sankeyDiagram-XADWPNL6-DvhmUNk5.js} +1 -1
  47. package/dist/assets/{sequenceDiagram-FGHM5R23-Dq797rMU.js → sequenceDiagram-FGHM5R23-BWwpEzOy.js} +1 -1
  48. package/dist/assets/{stateDiagram-FHFEXIEX-C5Es6t2q.js → stateDiagram-FHFEXIEX-DYCX0F3e.js} +1 -1
  49. package/dist/assets/stateDiagram-v2-QKLJ7IA2-DDAseEQj.js +1 -0
  50. package/dist/assets/{timeline-definition-GMOUNBTQ-BlUkWWqo.js → timeline-definition-GMOUNBTQ-CN3nhCvg.js} +1 -1
  51. package/dist/assets/{vennDiagram-DHZGUBPP-D_rpvcv7.js → vennDiagram-DHZGUBPP-BsUxiq2N.js} +1 -1
  52. package/dist/assets/{wardley-RL74JXVD-8oOVQEtX.js → wardley-RL74JXVD-Kbj_jXoM.js} +1 -1
  53. package/dist/assets/{wardleyDiagram-NUSXRM2D-C9J1BpO8.js → wardleyDiagram-NUSXRM2D-Ct795uew.js} +1 -1
  54. package/dist/assets/{xychartDiagram-5P7HB3ND-DqS5X4cE.js → xychartDiagram-5P7HB3ND-B6Vf25Ms.js} +1 -1
  55. package/dist/index.html +2 -2
  56. package/package.json +2 -2
  57. package/dist/assets/channel-CTJbcQ5k.js +0 -1
  58. package/dist/assets/classDiagram-6PBFFD2Q-CM2qd_FL.js +0 -1
  59. package/dist/assets/classDiagram-v2-HSJHXN6E-CM2qd_FL.js +0 -1
  60. package/dist/assets/clone-Ft-RfRjl.js +0 -1
  61. package/dist/assets/index-CFZHr4vH.css +0 -1
  62. package/dist/assets/index-CnJ4cmEe.js +0 -1267
  63. package/dist/assets/stateDiagram-v2-QKLJ7IA2-Dc3H6t-6.js +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/api/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,EA+BL,KAAK,aAAa,EAInB,MAAM,oBAAoB,CAAC;AAsd5B,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,8EA8uD5E;AAID,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,IAAI,SAAO,EACX,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC,OAAO,CAAC,IAAI,CAAC,CA8Cf"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/api/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,EAmCL,KAAK,aAAa,EAGnB,MAAM,oBAAoB,CAAC;AAyd5B,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,8EA8zD5E;AAID,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,IAAI,SAAO,EACX,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC,OAAO,CAAC,IAAI,CAAC,CA8Cf"}
@@ -2,7 +2,7 @@ import { Hono } from "hono";
2
2
  import { cors } from "hono/cors";
3
3
  import { streamSSE } from "hono/streaming";
4
4
  import { serve } from "@hono/node-server";
5
- import { StateManager, PipelineRunner, createLLMClient, createLogger, createInteractionToolsFromDeps, computeAnalytics, loadProjectConfig, loadProjectSession, processProjectInteractionRequest, resolveSessionActiveBook, findOrCreateBookSession, listBookSessions, loadBookSession, persistBookSession, appendBookSessionMessage, runAgentSession, buildAgentSystemPrompt, resolveServicePreset, resolveServiceProviderFamily, resolveServiceModelsBaseUrl, resolveServiceModel, loadSecrets, saveSecrets, getServiceApiKey, listModelsForService, chatCompletion, buildExportArtifact, GLOBAL_ENV_PATH, } from "@actalk/inkos-core";
5
+ import { StateManager, PipelineRunner, createLLMClient, createLogger, createInteractionToolsFromDeps, computeAnalytics, loadProjectConfig, loadProjectSession, processProjectInteractionRequest, resolveSessionActiveBook, listBookSessions, loadBookSession, persistBookSession, appendBookSessionMessage, createAndPersistBookSession, renameBookSession, deleteBookSession, migrateBookSession, SessionAlreadyMigratedError, runAgentSession, buildAgentSystemPrompt, resolveServicePreset, resolveServiceProviderFamily, resolveServiceModelsBaseUrl, resolveServiceModel, loadSecrets, saveSecrets, getServiceApiKey, listModelsForService, chatCompletion, buildExportArtifact, GLOBAL_ENV_PATH, } from "@actalk/inkos-core";
6
6
  import { access, readFile, readdir, writeFile } from "node:fs/promises";
7
7
  import { join } from "node:path";
8
8
  import { isSafeBookId } from "./safety.js";
@@ -66,6 +66,8 @@ function extractToolError(result) {
66
66
  }
67
67
  const subscribers = new Set();
68
68
  const bookCreateStatus = new Map();
69
+ // 内存缓存:service -> 模型列表 + 更新时间戳;避免每次 sidebar 挂载时都打真实 LLM /models
70
+ const modelListCache = new Map();
69
71
  function broadcast(event, data) {
70
72
  for (const handler of subscribers) {
71
73
  handler(event, data);
@@ -426,7 +428,19 @@ export function createStudioServer(initialConfig, root) {
426
428
  }
427
429
  async function buildPipelineConfig(overrides) {
428
430
  const currentConfig = overrides?.currentConfig ?? await loadCurrentProjectConfig();
429
- const logger = createLogger({ tag: "studio", sinks: [sseSink, consoleSink] });
431
+ const scopedSseSink = overrides?.sessionIdForSSE
432
+ ? {
433
+ write(entry) {
434
+ broadcast("log", {
435
+ sessionId: overrides.sessionIdForSSE,
436
+ level: entry.level,
437
+ tag: entry.tag,
438
+ message: entry.message,
439
+ });
440
+ },
441
+ }
442
+ : sseSink;
443
+ const logger = createLogger({ tag: "studio", sinks: [scopedSseSink, consoleSink] });
430
444
  return {
431
445
  client: overrides?.client ?? createLLMClient(currentConfig.llm),
432
446
  model: overrides?.model ?? currentConfig.llm.model,
@@ -437,6 +451,7 @@ export function createStudioServer(initialConfig, root) {
437
451
  logger,
438
452
  onStreamProgress: (progress) => {
439
453
  broadcast("llm:progress", {
454
+ ...(overrides?.sessionIdForSSE ? { sessionId: overrides.sessionIdForSSE } : {}),
440
455
  status: progress.status,
441
456
  elapsedMs: progress.elapsedMs,
442
457
  totalChars: progress.totalChars,
@@ -811,19 +826,28 @@ export function createStudioServer(initialConfig, root) {
811
826
  });
812
827
  app.get("/api/v1/services/:service/models", async (c) => {
813
828
  const service = c.req.param("service");
829
+ const refresh = c.req.query("refresh") === "1";
814
830
  const apiKey = c.req.query("apiKey") || await getServiceApiKey(root, service);
815
831
  // No key = no models
816
832
  if (!apiKey)
817
833
  return c.json({ models: [] });
818
- // Fast path: services with knownModels return immediately
819
834
  const preset = resolveServicePreset(isCustomServiceId(service) ? "custom" : service);
835
+ const resolvedBaseUrl = await resolveConfiguredServiceBaseUrl(root, service);
836
+ // Cache by service + resolved baseUrl + apiKey fingerprint; valid for 10 min unless ?refresh=1
837
+ const cacheKey = `${service}::${resolvedBaseUrl ?? ""}::${apiKey.slice(-8)}`;
838
+ if (!refresh) {
839
+ const cached = modelListCache.get(cacheKey);
840
+ if (cached && Date.now() - cached.at < 10 * 60 * 1000) {
841
+ return c.json({ models: cached.models });
842
+ }
843
+ }
844
+ // Fast path: services with knownModels return immediately
820
845
  if (preset?.knownModels && preset.knownModels.length > 0) {
821
- return c.json({
822
- models: preset.knownModels.map((id) => ({ id, name: id })),
823
- });
846
+ const models = preset.knownModels.map((id) => ({ id, name: id }));
847
+ modelListCache.set(cacheKey, { models, at: Date.now() });
848
+ return c.json({ models });
824
849
  }
825
850
  // Simple /models API call + fallback to pi-ai built-in list (no slow probe)
826
- const resolvedBaseUrl = await resolveConfiguredServiceBaseUrl(root, service);
827
851
  if (!resolvedBaseUrl)
828
852
  return c.json({ models: [] });
829
853
  const modelsBase = preset?.modelsBaseUrl ?? resolvedBaseUrl;
@@ -844,6 +868,7 @@ export function createStudioServer(initialConfig, root) {
844
868
  const builtIn = await listModelsForService(service, apiKey);
845
869
  models = builtIn.map((m) => ({ id: m.id, name: m.name }));
846
870
  }
871
+ modelListCache.set(cacheKey, { models, at: Date.now() });
847
872
  return c.json({ models });
848
873
  });
849
874
  // --- Project info ---
@@ -1001,13 +1026,7 @@ export function createStudioServer(initialConfig, root) {
1001
1026
  app.get("/api/v1/sessions", async (c) => {
1002
1027
  const bookId = c.req.query("bookId");
1003
1028
  const sessions = await listBookSessions(root, bookId === undefined ? null : bookId === "null" ? null : bookId);
1004
- return c.json({ sessions: sessions.map((s) => ({
1005
- sessionId: s.sessionId,
1006
- bookId: s.bookId,
1007
- messageCount: s.messages.length,
1008
- createdAt: s.createdAt,
1009
- updatedAt: s.updatedAt,
1010
- })) });
1029
+ return c.json({ sessions });
1011
1030
  });
1012
1031
  app.get("/api/v1/sessions/:sessionId", async (c) => {
1013
1032
  const session = await loadBookSession(root, c.req.param("sessionId"));
@@ -1018,30 +1037,49 @@ export function createStudioServer(initialConfig, root) {
1018
1037
  app.post("/api/v1/sessions", async (c) => {
1019
1038
  const body = await c.req.json().catch(() => ({}));
1020
1039
  const bookId = body.bookId ?? null;
1021
- const session = await findOrCreateBookSession(root, bookId);
1040
+ const sessionId = body.sessionId;
1041
+ // sessionId 只允许 timestamp-random 格式;防止注入任意文件名
1042
+ const safeSessionId = sessionId && /^[0-9]+-[a-z0-9]+$/.test(sessionId) ? sessionId : undefined;
1043
+ const session = await createAndPersistBookSession(root, bookId, safeSessionId);
1044
+ return c.json({ session });
1045
+ });
1046
+ app.put("/api/v1/sessions/:sessionId", async (c) => {
1047
+ const sessionId = c.req.param("sessionId");
1048
+ const body = await c.req.json().catch(() => ({}));
1049
+ const title = body.title?.trim();
1050
+ if (!title) {
1051
+ throw new ApiError(400, "INVALID_SESSION_TITLE", "Session title is required");
1052
+ }
1053
+ const session = await renameBookSession(root, sessionId, title);
1054
+ if (!session) {
1055
+ return c.json({ error: "Session not found" }, 404);
1056
+ }
1022
1057
  return c.json({ session });
1023
1058
  });
1059
+ app.delete("/api/v1/sessions/:sessionId", async (c) => {
1060
+ await deleteBookSession(root, c.req.param("sessionId"));
1061
+ return c.json({ ok: true });
1062
+ });
1024
1063
  app.post("/api/v1/agent", async (c) => {
1025
1064
  const { instruction, activeBookId, sessionId: reqSessionId, model: reqModel, service: reqService } = await c.req.json();
1026
1065
  const sessionId = reqSessionId;
1027
1066
  if (!instruction?.trim()) {
1028
1067
  return c.json({ error: "No instruction provided" }, 400);
1029
1068
  }
1030
- broadcast("agent:start", { instruction, activeBookId });
1069
+ if (!sessionId?.trim()) {
1070
+ throw new ApiError(400, "SESSION_ID_REQUIRED", "sessionId is required");
1071
+ }
1072
+ broadcast("agent:start", { instruction, activeBookId, sessionId });
1031
1073
  try {
1032
1074
  // Load config + create LLM client (pipeline created after model resolution)
1033
1075
  const config = await loadCurrentProjectConfig({ requireApiKey: false });
1034
1076
  const client = createLLMClient(config.llm);
1035
- // Resolve or create BookSession for history
1036
- let bookSession;
1037
- if (sessionId) {
1038
- bookSession =
1039
- (await loadBookSession(root, sessionId)) ??
1040
- (await findOrCreateBookSession(root, activeBookId ?? null));
1041
- }
1042
- else {
1043
- bookSession = await findOrCreateBookSession(root, activeBookId ?? null);
1077
+ const loadedBookSession = await loadBookSession(root, sessionId);
1078
+ if (!loadedBookSession) {
1079
+ throw new ApiError(404, "SESSION_NOT_FOUND", `Session not found: ${sessionId}`);
1044
1080
  }
1081
+ let bookSession = loadedBookSession;
1082
+ const streamSessionId = loadedBookSession.sessionId;
1045
1083
  // Build initial message context from persisted session
1046
1084
  const initialMessages = bookSession.messages
1047
1085
  .filter((m) => m.role === "user" || m.role === "assistant")
@@ -1130,6 +1168,7 @@ export function createStudioServer(initialConfig, root) {
1130
1168
  client: pipelineClient,
1131
1169
  model: reqModel ?? config.llm.model,
1132
1170
  currentConfig: config,
1171
+ sessionIdForSSE: bookSession.sessionId,
1133
1172
  }));
1134
1173
  // Run pi-agent session
1135
1174
  const collectedToolExecs = [];
@@ -1145,16 +1184,16 @@ export function createStudioServer(initialConfig, root) {
1145
1184
  if (event.type === "message_update") {
1146
1185
  const ame = event.assistantMessageEvent;
1147
1186
  if (ame.type === "text_delta") {
1148
- broadcast("draft:delta", { text: ame.delta });
1187
+ broadcast("draft:delta", { sessionId: streamSessionId, text: ame.delta });
1149
1188
  }
1150
1189
  else if (ame.type === "thinking_delta") {
1151
- broadcast("thinking:delta", { text: ame.delta });
1190
+ broadcast("thinking:delta", { sessionId: streamSessionId, text: ame.delta });
1152
1191
  }
1153
1192
  else if (ame.type === "thinking_start") {
1154
- broadcast("thinking:start", {});
1193
+ broadcast("thinking:start", { sessionId: streamSessionId });
1155
1194
  }
1156
1195
  else if (ame.type === "thinking_end") {
1157
- broadcast("thinking:end", {});
1196
+ broadcast("thinking:end", { sessionId: streamSessionId });
1158
1197
  }
1159
1198
  }
1160
1199
  if (event.type === "tool_execution_start") {
@@ -1174,6 +1213,7 @@ export function createStudioServer(initialConfig, root) {
1174
1213
  startedAt: Date.now(),
1175
1214
  });
1176
1215
  broadcast("tool:start", {
1216
+ sessionId: streamSessionId,
1177
1217
  id: event.toolCallId,
1178
1218
  tool: event.toolName,
1179
1219
  args,
@@ -1181,7 +1221,11 @@ export function createStudioServer(initialConfig, root) {
1181
1221
  });
1182
1222
  }
1183
1223
  if (event.type === "tool_execution_update") {
1184
- broadcast("tool:update", { tool: event.toolName, partialResult: event.partialResult });
1224
+ broadcast("tool:update", {
1225
+ sessionId: streamSessionId,
1226
+ tool: event.toolName,
1227
+ partialResult: event.partialResult,
1228
+ });
1185
1229
  }
1186
1230
  if (event.type === "tool_execution_end") {
1187
1231
  const exec = collectedToolExecs.find(t => t.id === event.toolCallId);
@@ -1195,6 +1239,7 @@ export function createStudioServer(initialConfig, root) {
1195
1239
  exec.result = summarizeResult(event.result);
1196
1240
  }
1197
1241
  broadcast("tool:end", {
1242
+ sessionId: streamSessionId,
1198
1243
  id: event.toolCallId,
1199
1244
  tool: event.toolName,
1200
1245
  result: event.result,
@@ -1209,6 +1254,16 @@ export function createStudioServer(initialConfig, root) {
1209
1254
  content: instruction,
1210
1255
  timestamp: Date.now(),
1211
1256
  });
1257
+ // 第一条用户消息就是 session 的标题:如果 title 还是 null,用消息内容(单行、≤20字)写入。
1258
+ // 后续消息不覆盖;用户手动改名通过 renameBookSession 覆盖。
1259
+ if (bookSession.title === null) {
1260
+ const oneLine = instruction.trim().replace(/\s+/g, " ");
1261
+ const title = oneLine.length > 20 ? `${oneLine.slice(0, 20)}…` : oneLine;
1262
+ if (title) {
1263
+ bookSession = { ...bookSession, title };
1264
+ broadcast("session:title", { sessionId: bookSession.sessionId, title });
1265
+ }
1266
+ }
1212
1267
  if (result.responseText) {
1213
1268
  const lastAssistant = result.messages?.filter((m) => m.role === "assistant").pop();
1214
1269
  const thinking = lastAssistant?.thinking;
@@ -1277,24 +1332,45 @@ export function createStudioServer(initialConfig, root) {
1277
1332
  }, 502);
1278
1333
  }
1279
1334
  await persistBookSession(root, bookSession);
1280
- broadcast("agent:complete", { instruction, activeBookId });
1335
+ broadcast("agent:complete", { instruction, activeBookId, sessionId: bookSession.sessionId });
1281
1336
  // If a sub_agent created a new book during this session, broadcast book:created
1282
1337
  // so the sidebar refreshes.
1283
1338
  if (!activeBookId && collectedToolExecs.some((t) => t.agent === "architect" && t.status === "completed")) {
1284
1339
  const books = await state.listBooks();
1285
1340
  const latestBook = books.at(-1);
1286
1341
  if (latestBook) {
1287
- broadcast("book:created", { bookId: latestBook });
1342
+ try {
1343
+ const migratedSession = await migrateBookSession(root, bookSession.sessionId, latestBook);
1344
+ if (migratedSession) {
1345
+ bookSession = migratedSession;
1346
+ }
1347
+ }
1348
+ catch (e) {
1349
+ if (!(e instanceof SessionAlreadyMigratedError)) {
1350
+ throw e;
1351
+ }
1352
+ }
1353
+ broadcast("book:created", { bookId: latestBook, sessionId: bookSession.sessionId });
1288
1354
  }
1289
1355
  }
1290
1356
  return c.json({
1291
1357
  response: result.responseText,
1292
- session: { sessionId: bookSession.sessionId },
1358
+ session: {
1359
+ sessionId: bookSession.sessionId,
1360
+ ...(bookSession.bookId ? { activeBookId: bookSession.bookId } : {}),
1361
+ },
1293
1362
  });
1294
1363
  }
1295
1364
  catch (e) {
1365
+ if (e instanceof ApiError) {
1366
+ throw e;
1367
+ }
1368
+ if (e instanceof SessionAlreadyMigratedError) {
1369
+ const migratedMessage = e instanceof Error ? e.message : String(e);
1370
+ throw new ApiError(409, "SESSION_ALREADY_MIGRATED", migratedMessage);
1371
+ }
1296
1372
  const msg = e instanceof Error ? e.message : String(e);
1297
- broadcast("agent:error", { instruction, activeBookId, error: msg });
1373
+ broadcast("agent:error", { instruction, activeBookId, sessionId, error: msg });
1298
1374
  // Agent busy — return 429 with user-friendly message
1299
1375
  if (/already processing|prompt.*queue/i.test(msg)) {
1300
1376
  return c.json({