@tea-agent/loop-agent 0.44.0-next.5 → 0.44.0-next.7

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 (111) hide show
  1. package/CHANGELOG.md +299 -445
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/pi-sdk-executor.js +7 -7
  4. package/dist/infrastructure/console/operation-store.js +46 -14
  5. package/dist/shared/git-workspace-status.js +2 -0
  6. package/dist/shared/wizard-local-telemetry.js +2 -1
  7. package/dist/worker/console/chat/assistant-content.js +5 -34
  8. package/dist/worker/console/chat/chat-event-store.js +27 -10
  9. package/dist/worker/console/chat/db-connections.js +72 -8
  10. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +52 -24
  11. package/dist/worker/console/chat/pi-runtime.js +251 -18
  12. package/dist/worker/console/chat/repo-browser.js +49 -0
  13. package/dist/worker/console/chat/routes.js +215 -66
  14. package/dist/worker/console/chat/session-store.js +147 -19
  15. package/dist/worker/console/chat/turn-process.js +69 -23
  16. package/dist/worker/console/chat/user-turn-outline.js +19 -0
  17. package/dist/worker/console/console-update-runtime.js +2 -2
  18. package/dist/worker/console/operator-user-error.js +28 -1
  19. package/dist/worker/console/pi-readiness.js +4 -4
  20. package/dist/worker/console/recovery-cta.js +14 -14
  21. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BHuhGM1e.js → abnfDiagram-N423BO3Z-DTuku_-7.js} +1 -1
  22. package/dist/worker/console/static/assets/{arc-CKh5E_sr.js → arc-CQbCsSqb.js} +1 -1
  23. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DizhiBEv.js → architectureDiagram-T3A2C74G-C-xUntTR.js} +1 -1
  24. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-3Neea-Z-.js → blockDiagram-VBNYF7ZC-DhNlwGAl.js} +1 -1
  25. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCP_Qrgi.js → c4Diagram-5PPSVZJV-B7j3nx8o.js} +1 -1
  26. package/dist/worker/console/static/assets/channel-5_os7t_x.js +1 -0
  27. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C_wydftK.js → chunk-2GRJ4B5K-BCrcxEh2.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-jRPXZbzx.js → chunk-2Q5K7J3B-CsaePDb3.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BLQf2OYO.js → chunk-5RXB4S5H--SRQc1rN.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-0qRq64rI.js → chunk-5VM5RSS4-C5BFY2Po.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-ClMg8xmV.js → chunk-6Q2QTUOP-Caso6uN7.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Coop2FlL.js → chunk-GF5L2VYU-AY2ZSAx2.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CDdO-jPJ.js → chunk-JWPE2WC7-DiMqZQ_8.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D8Gkppjl.js → chunk-KBJHAD2P-gpvvriOp.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DA9HIySf.js → chunk-RYQCIY6F-CUdyoJhI.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DZNDRX1k.js → chunk-XXDRQBXY-Bp1xW3T4.js} +1 -1
  37. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-loz0n18W.js +1 -0
  38. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-loz0n18W.js +1 -0
  39. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CZ1zGyjq.js → cose-bilkent-JH36ORCC-D-JIcOIT.js} +1 -1
  40. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B8u1_FtM.js → cynefin-VYW2F7L2-C-NKFflx.js} +1 -1
  41. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-CPBB6lfo.js → cynefinDiagram-MW4NZA55-CSOcvOAA.js} +1 -1
  42. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CG3Eov3E.js → dagre-VZM6K2ZE-BQSBltMy.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ycI-dOjh.js → diagram-7IWD3JNH-B0LVC45G.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bx_GyQ7Y.js → diagram-B4RE2ZJO-Cypg3nqy.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-m0G6mP4d.js → diagram-LBJQPF4R-BBDU88XJ.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DTDJkqcJ.js → diagram-Q27KOJAE-BZusLqSb.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BCfFrG-p.js → diagram-UB23O5K3-Cfolotcr.js} +1 -1
  48. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BIkTNPpp.js → ebnfDiagram-BXEA7PRR-CjfyUy0m.js} +1 -1
  49. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DTcMWrd2.js → erDiagram-JOGREHBK-DJdz7txE.js} +1 -1
  50. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BqzDaNQ7.js → flowDiagram-UKHOOZJN-CQqgPO6y.js} +1 -1
  51. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BSl_vGFe.js → ganttDiagram-PKOTCBZU-BOpGfS2m.js} +1 -1
  52. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DCA5HXM6.js → gitGraphDiagram-DS77QQ5N-C6UCu0qx.js} +1 -1
  53. package/dist/worker/console/static/assets/index-CUT440A8.css +1 -0
  54. package/dist/worker/console/static/assets/index-DWKejNVJ.js +486 -0
  55. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DvqT18ge.js → infoDiagram-6WML65LV-BWeaVEUB.js} +1 -1
  56. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B6zGKrQ3.js → ishikawaDiagram-WSZJBQD7-C8gmlBbk.js} +1 -1
  57. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BpLQPI9C.js → journeyDiagram-NVQOT4AX-DF3bm8Bp.js} +1 -1
  58. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CzIz_-Lt.js → kanban-definition-27J2QSJJ-D2fG2Vxi.js} +1 -1
  59. package/dist/worker/console/static/assets/{linear-CMQ8Jpp-.js → linear-BGKgMc2T.js} +1 -1
  60. package/dist/worker/console/static/assets/{mermaid.core-BdAMrTf7.js → mermaid.core-DF6mMPJ9.js} +5 -5
  61. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-hSwF6sOq.js → mindmap-definition-FAOFIHXS-AMhePheC.js} +1 -1
  62. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DZdv509B.js → pegDiagram-VL7TDLO6-DN1Cl0W0.js} +1 -1
  63. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CmD4-b0K.js → pieDiagram-7S7Q4E2Y-Cx9ngUeN.js} +1 -1
  64. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DtKxNxlS.js → quadrantDiagram-CIZ2JOQS-BcYPTQJb.js} +1 -1
  65. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-D48iIoiS.js → railroadDiagram-AXF67PYL-BqHUZWnH.js} +1 -1
  66. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C3N0K-2p.js → requirementDiagram-LRYGKXZP-CmqWZ0-B.js} +1 -1
  67. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CACWR-Xs.js → sankeyDiagram-W5VNT64P--4YetH1y.js} +1 -1
  68. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-g_QgugvZ.js → sequenceDiagram-SI44F4Z6-DGD4VMfD.js} +1 -1
  69. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DqLoRPGK.js → sizeCapture-X5ZJPWSS-QVw9q7rh.js} +1 -1
  70. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-ES59VgMG.js → stateDiagram-OKZ733FA-CuemV9pe.js} +1 -1
  71. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BdlGdIJw.js +1 -0
  72. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-fjyLVNWo.js → swimlanes-SLNWSIFB-C7oKzZKs.js} +2 -2
  73. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DjjbHbde.js +8 -0
  74. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cd5-biKv.js → timeline-definition-Z64GVDOM-C2Of60nT.js} +1 -1
  75. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CZTu6mPf.js → vennDiagram-T6HMQDX7-DHtpQHyL.js} +1 -1
  76. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DR01jjbe.js → wardleyDiagram-T6FBY63Y-COi9OhSy.js} +1 -1
  77. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-iZcuGVGa.js → xychartDiagram-ELKLHX3M-DPHQfRrt.js} +1 -1
  78. package/dist/worker/console/static/index.html +2 -2
  79. package/dist/worker/console/static-src/app/console-types.js +9 -1
  80. package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
  81. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +58 -3
  82. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
  83. package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
  84. package/dist/worker/console/static-src/operator-chat/process-label.js +19 -6
  85. package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
  86. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +2 -0
  87. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
  88. package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
  89. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
  90. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +202 -41
  91. package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -23
  92. package/dist/worker/console/static-src/operator-chat/useChatThread.js +55 -18
  93. package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
  94. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
  95. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
  96. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
  97. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  98. package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
  99. package/dist/worker/console/workspace-initialization.js +3 -3
  100. package/dist/worker/observability/read-model.js +23 -2
  101. package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
  102. package/dist/worker/scheduler/scheduled-goal-evidence.js +6 -1
  103. package/docs/templates/backend-test-dag.json +1 -0
  104. package/package.json +4 -4
  105. package/dist/worker/console/static/assets/channel-DxgzA1GF.js +0 -1
  106. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BZcm7fs6.js +0 -1
  107. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BZcm7fs6.js +0 -1
  108. package/dist/worker/console/static/assets/index-BAEDjTbD.css +0 -1
  109. package/dist/worker/console/static/assets/index-wKEJUaP5.js +0 -482
  110. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyCIq6jx.js +0 -1
  111. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CsKtO34q.js +0 -8
@@ -19,14 +19,14 @@ import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_
19
19
  import { InvalidChatSessionModeError, SESSION_MODE_ACTION_DENIED_CODE, SESSION_MODE_ACTION_DENIED_MESSAGE, SESSION_MODE_IMMUTABLE_CODE, UnknownChatSessionModeError, isWorkflowSessionMode, parseCreateSessionMode, readStoredSessionMode, shouldAcceptOperatorSnapshot, } from "./session-mode.js";
20
20
  import { buildModelCallableToolSchemas } from "./tool-adapter.js";
21
21
  import { cleanChatTitle, acceptedOperationIdFromResult, isBlankChatSession } from "./session-store.js";
22
+ import { projectUserTurnOutline } from "./user-turn-outline.js";
22
23
  import { hashChatTurnPayload, projectChatToolEventPayload, projectUnknownChatEventPayload, projectCompactSnapshot, } from "./chat-event-store.js";
23
24
  import { projectDeferredTurnSummary, projectDeferredTurnSummaries, } from "./deferred-turn.js";
24
25
  import { THINKING_LEVELS, RuntimeSnapshotUnavailableError, } from "./pi-runtime.js";
25
26
  import { ResourcePreferencesError, setSkillAutoInvocationPreference, } from "./resource-preferences-store.js";
26
27
  import { ComposerDraftStore } from "./composer-draft-store.js";
27
- import { walkRepoFiles } from "./repo-walk.js";
28
- import { isSensitivePath, scrubSecrets } from "./explore-tools.js";
29
- import { listRepoDirectory, matchRepoHtmlPreviewRoute, readRepoHtmlAsset, readRepoPreview, RepoBrowserError, } from "./repo-browser.js";
28
+ import { scrubSecrets } from "./explore-tools.js";
29
+ import { listChatFilePalette, listRepoDirectory, matchRepoHtmlPreviewRoute, readRepoHtmlAsset, readRepoPreview, RepoBrowserError, } from "./repo-browser.js";
30
30
  import { projectRuntimeContext } from "./runtime-context.js";
31
31
  import { patchModelsConfig, readModelsConfig, readPackageInventory, } from "./pi-console-config.js";
32
32
  import { ChatInterviewAdapter, projectChatInterviewState, } from "./interview-adapter.js";
@@ -66,6 +66,14 @@ function reopenInitFromRecord(record) {
66
66
  sessionMode: record.sessionMode,
67
67
  };
68
68
  }
69
+ function workspaceProjectionFromRecord(record) {
70
+ return {
71
+ sessionMode: record.sessionMode,
72
+ ...(record.modelProvider ? { modelProvider: record.modelProvider } : {}),
73
+ ...(record.modelId ? { modelId: record.modelId } : {}),
74
+ ...(record.thinkingLevel ? { thinkingLevel: record.thinkingLevel } : {}),
75
+ };
76
+ }
69
77
  async function readChatSessionMode(deps, sessionId) {
70
78
  let record;
71
79
  try {
@@ -224,6 +232,43 @@ function turnSummary(turn) {
224
232
  abortRequestedAt: turn.abortRequestedAt ?? null,
225
233
  };
226
234
  }
235
+ function turnStateSummary(turn) {
236
+ return {
237
+ turnId: turn.turnId,
238
+ state: turn.state === "queued" ? "running" : turn.state,
239
+ };
240
+ }
241
+ function turnStatesForIds(events, sessionId, turnIds) {
242
+ const projected = new Map();
243
+ for (const turnId of turnIds) {
244
+ if (!turnId || projected.has(turnId))
245
+ continue;
246
+ const turn = events.getTurn(sessionId, turnId);
247
+ if (turn)
248
+ projected.set(turnId, turnStateSummary(turn));
249
+ }
250
+ return [...projected.values()];
251
+ }
252
+ const MAX_STATE_QUERY_TURN_IDS = 8;
253
+ const MAX_STATE_QUERY_TURN_ID_CHARS = 240;
254
+ function requestedStateTurnIds(req) {
255
+ const result = [];
256
+ const seen = new Set();
257
+ const url = new URL(req.url ?? "/", "http://localhost");
258
+ for (const raw of url.searchParams.getAll("turnId")) {
259
+ const turnId = raw.trim();
260
+ if (!turnId ||
261
+ turnId.length > MAX_STATE_QUERY_TURN_ID_CHARS ||
262
+ seen.has(turnId)) {
263
+ continue;
264
+ }
265
+ seen.add(turnId);
266
+ result.push(turnId);
267
+ if (result.length >= MAX_STATE_QUERY_TURN_IDS)
268
+ break;
269
+ }
270
+ return result;
271
+ }
227
272
  function isTerminalTurnState(state) {
228
273
  return state === "settled" || state === "aborted" || state === "failed";
229
274
  }
@@ -414,6 +459,20 @@ export function turnEventToStorePartial(event) {
414
459
  : {}),
415
460
  }),
416
461
  };
462
+ case "tool_update":
463
+ return {
464
+ kind: "tool_update",
465
+ data: projectChatToolEventPayload({
466
+ kind: "tool_update",
467
+ toolCallId: String(event.toolCallId ?? ""),
468
+ toolName: String(event.toolName ?? ""),
469
+ value: event.partialResult,
470
+ ...(typeof event.step === "number" ? { step: event.step } : {}),
471
+ ...(typeof event.parentToolCallId === "string"
472
+ ? { parentToolCallId: event.parentToolCallId }
473
+ : {}),
474
+ }),
475
+ };
417
476
  case "tool_result": {
418
477
  // Plan A (2026-08-16): surface the accepted-operation link on the live
419
478
  // SSE frame so the frontend semantic event can resolve operation:<id>.
@@ -917,25 +976,47 @@ export async function handleForkChatSession(req, res, deps, sourceSessionId) {
917
976
  });
918
977
  return;
919
978
  }
979
+ let forkedHandle;
980
+ let forkPersisted = false;
920
981
  try {
982
+ const body = await readJsonBody(req);
983
+ const throughMessageId = typeof body.throughMessageId === "string" && body.throughMessageId.trim()
984
+ ? body.throughMessageId.trim()
985
+ : undefined;
986
+ const throughUserMessageIndex = throughMessageId
987
+ ? source.messages
988
+ .filter((message) => message.role === "user")
989
+ .findIndex((message) => message.id === throughMessageId)
990
+ : undefined;
991
+ if (throughMessageId && throughUserMessageIndex === -1) {
992
+ throw new Error("FORK_MESSAGE_NOT_FOUND");
993
+ }
921
994
  const handle = await deps.runtime.forkSession({
922
995
  sourceSessionFile: source.sessionFile ?? "",
923
996
  sessionMode: source.sessionMode,
924
997
  ...(source.modelProvider && source.modelId
925
998
  ? { model: { provider: source.modelProvider, modelId: source.modelId } }
926
999
  : {}),
1000
+ ...(throughUserMessageIndex !== undefined
1001
+ ? { throughUserMessageIndex }
1002
+ : {}),
927
1003
  });
1004
+ forkedHandle = handle;
928
1005
  await deps.store.forkFrom({
929
1006
  sourceSessionId,
930
1007
  targetSessionId: handle.sessionId,
931
1008
  targetSessionFile: handle.sessionFile,
1009
+ ...(throughMessageId ? { throughMessageId } : {}),
932
1010
  });
1011
+ forkPersisted = true;
933
1012
  await deps.attachSession?.(handle.sessionId).catch(() => undefined);
934
- const copied = deps.events.forkContextRefs({
935
- sourceSessionId,
936
- targetSessionId: handle.sessionId,
937
- targetTurnId: `${handle.sessionId}:fork`,
938
- });
1013
+ const copied = throughMessageId
1014
+ ? { copiedCount: 0 }
1015
+ : deps.events.forkContextRefs({
1016
+ sourceSessionId,
1017
+ targetSessionId: handle.sessionId,
1018
+ targetTurnId: `${handle.sessionId}:fork`,
1019
+ });
939
1020
  await deps.operationLinker.recoverSession(handle.sessionId);
940
1021
  sendJson(res, 201, {
941
1022
  ok: true,
@@ -949,6 +1030,12 @@ export async function handleForkChatSession(req, res, deps, sourceSessionId) {
949
1030
  });
950
1031
  }
951
1032
  catch (error) {
1033
+ if (forkedHandle && !forkPersisted) {
1034
+ deps.runtime.dispose(forkedHandle.sessionId);
1035
+ await deps.store
1036
+ .discardUnownedSession(forkedHandle.sessionId, forkedHandle.sessionFile)
1037
+ .catch(() => undefined);
1038
+ }
952
1039
  sendJson(res, 409, {
953
1040
  ok: false,
954
1041
  error: {
@@ -1098,12 +1185,18 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
1098
1185
  const sddSessionIdentity = deps.sddDataAlignment && (await deps.sddDataAlignment.isEligible())
1099
1186
  ? { telemetrySessionId: record.telemetrySessionId ?? null }
1100
1187
  : undefined;
1188
+ const projectedMessages = projectStoredMessagesWithEventOrder(messagePage.messages, snapshot);
1189
+ const turnStates = turnStatesForIds(deps.events, sessionId, [
1190
+ ...projectedMessages.map((message) => message.turnId),
1191
+ ...snapshot.map((event) => event.turnId),
1192
+ ]);
1101
1193
  sendJson(res, 200, {
1102
1194
  ok: true,
1103
1195
  eventEpoch: deps.events.epoch(sessionId),
1104
1196
  session: {
1105
1197
  ...projectBrowserChatSession(record),
1106
- messages: projectStoredMessagesWithEventOrder(messagePage.messages, snapshot),
1198
+ messages: projectedMessages,
1199
+ turnStates,
1107
1200
  composerDraft,
1108
1201
  activeTurnId: activeTurn?.turnId,
1109
1202
  turnPhase: activeTurn
@@ -1123,6 +1216,7 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
1123
1216
  sessionStats: telemetry.stats,
1124
1217
  usage: telemetry.usage,
1125
1218
  turnTelemetry: deriveChatTurnTelemetry(snapshot),
1219
+ userTurnOutline: projectUserTurnOutline(record.messages ?? []),
1126
1220
  ...(sddSessionIdentity
1127
1221
  ? { sddDataAlignment: sddSessionIdentity }
1128
1222
  : {}),
@@ -1401,7 +1495,14 @@ function emitUserMessageEvent(deps, sessionId, turnId, userMessage, clientReques
1401
1495
  data: {
1402
1496
  ...(clientRequestId ? { clientRequestId } : {}),
1403
1497
  ...(steering ? { steering: true } : {}),
1404
- message: { id: userMessage.id, text: userMessage.text, at: userMessage.at },
1498
+ message: {
1499
+ id: userMessage.id,
1500
+ text: userMessage.text,
1501
+ at: userMessage.at,
1502
+ ...(userMessage.attachments?.length
1503
+ ? { attachments: userMessage.attachments }
1504
+ : {}),
1505
+ },
1405
1506
  },
1406
1507
  });
1407
1508
  }
@@ -1409,7 +1510,7 @@ function emitUserMessageEvent(deps, sessionId, turnId, userMessage, clientReques
1409
1510
  * get_state equivalent): exposes the active turn + the latest turn's durable
1410
1511
  * outcome WITHOUT the full message payload, so the UI can poll it on an
1411
1512
  * interval / visibility / online without paying for session detail. */
1412
- export async function handleGetChatSessionState(_req, res, deps, sessionId) {
1513
+ export async function handleGetChatSessionState(req, res, deps, sessionId) {
1413
1514
  const record = await deps.store.get(sessionId);
1414
1515
  if (!record) {
1415
1516
  sendJson(res, 404, {
@@ -1440,9 +1541,15 @@ export async function handleGetChatSessionState(_req, res, deps, sessionId) {
1440
1541
  : runtimeBusy
1441
1542
  ? "running"
1442
1543
  : "idle";
1544
+ const turnStates = turnStatesForIds(deps.events, sessionId, [
1545
+ active?.turnId,
1546
+ latest?.turnId,
1547
+ ...requestedStateTurnIds(req),
1548
+ ]);
1443
1549
  sendJson(res, 200, {
1444
1550
  ok: true,
1445
1551
  state: {
1552
+ turnStates,
1446
1553
  // Legacy flat fields retained for older clients.
1447
1554
  activeTurnId: active?.turnId ?? null,
1448
1555
  activeTurnState: active?.state ?? null,
@@ -3021,28 +3128,8 @@ async function handlePiConfig(req, res, deps, resource) {
3021
3128
  async function handleChatFiles(res, deps, sessionId, query) {
3022
3129
  if (!(await requireChatSession(res, deps, sessionId)))
3023
3130
  return;
3024
- const q = query.trim().toLowerCase();
3025
- const walked = await walkRepoFiles(deps.appData.repoRoot, {
3026
- skipSensitive: true,
3027
- maxFiles: 5000,
3028
- includeDirs: true,
3029
- });
3030
- const entries = walked
3031
- .filter((entry) => !isSensitivePath(entry.repoRelative) &&
3032
- (!q || entry.repoRelative.toLowerCase().includes(q)))
3033
- .sort((a, b) => {
3034
- const aDir = a.repoRelative.endsWith("/");
3035
- const bDir = b.repoRelative.endsWith("/");
3036
- if (aDir !== bDir)
3037
- return aDir ? -1 : 1;
3038
- return a.repoRelative.localeCompare(b.repoRelative);
3039
- })
3040
- .slice(0, 50)
3041
- .map((entry) => ({
3042
- path: entry.repoRelative,
3043
- isDir: entry.repoRelative.endsWith("/"),
3044
- }));
3045
- sendJson(res, 200, { ok: true, files: entries });
3131
+ const files = await listChatFilePalette(deps.appData.repoRoot, query);
3132
+ sendJson(res, 200, { ok: true, files });
3046
3133
  }
3047
3134
  export async function handleChatCommands(res, deps, sessionId) {
3048
3135
  try {
@@ -3146,20 +3233,25 @@ async function handleSddBingo(req, res, deps, sessionId) {
3146
3233
  }
3147
3234
  }
3148
3235
  export async function handleRuntimeContext(res, deps, sessionId) {
3149
- const ensured = await ensureActiveSession(deps, sessionId);
3150
- if (!ensured.ok) {
3151
- sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
3236
+ const record = await deps.store.get(sessionId);
3237
+ if (!record) {
3238
+ sendJson(res, 404, {
3239
+ ok: false,
3240
+ error: {
3241
+ code: "PI_SESSION_NOT_FOUND",
3242
+ message: `chat session not found: ${sessionId}`,
3243
+ },
3244
+ });
3152
3245
  return;
3153
3246
  }
3154
- // AC-03: system prompt comes from the REAL Pi session (agent.state.systemPrompt
3155
- // fallback chain); resources come from that session's ResourceLoader. The raw
3156
- // prompt stays server-side; the browser only ever receives the scrubbed +
3157
- // length-limited projection. RF-02: a critical projection failure is 503
3158
- // PI_RUNTIME_UNAVAILABLE — never a 500 leak or a fake-empty context.
3247
+ // Live Agent is preferred. Cold inspect uses workspace ResourceLoader /
3248
+ // ModelRuntime without SessionManager.open(jsonl); prompt still reopens.
3249
+ // RF-02: a critical projection failure is 503 PI_RUNTIME_UNAVAILABLE.
3159
3250
  let snapshot;
3160
3251
  try {
3161
3252
  snapshot = await deps.runtime.getRuntimeSnapshot(sessionId, {
3162
3253
  includeRaw: true,
3254
+ workspaceProjection: workspaceProjectionFromRecord(record),
3163
3255
  });
3164
3256
  }
3165
3257
  catch (error) {
@@ -3234,6 +3326,7 @@ export async function handleContextInsights(res, deps, sessionId) {
3234
3326
  try {
3235
3327
  const runtimeSnapshot = await deps.runtime.getRuntimeSnapshot(sessionId, {
3236
3328
  includeRaw: true,
3329
+ workspaceProjection: workspaceProjectionFromRecord(record),
3237
3330
  });
3238
3331
  if (runtimeSnapshot) {
3239
3332
  systemPromptChars = runtimeSnapshot.systemPrompt.characters;
@@ -3279,16 +3372,23 @@ export async function handleContextInsights(res, deps, sessionId) {
3279
3372
  // Exported for the direct handler-boundary tests (trace binding for
3280
3373
  // vt-route-model-errors); router dispatch behavior is unchanged.
3281
3374
  export async function handleModels(req, res, deps, sessionId) {
3282
- // AC-01/AC-06: models (and model/thinking mutation) requests reuse the same
3283
- // activation path. A missing/reopen-failed runtime is a STRUCTURED error —
3284
- // never a fabricated empty model list.
3285
- const ensured = await ensureActiveSession(deps, sessionId);
3286
- if (!ensured.ok) {
3287
- sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
3288
- return;
3289
- }
3290
3375
  try {
3291
3376
  if ((req.method ?? "GET").toUpperCase() === "GET") {
3377
+ // Catalog is workspace-scoped (models.json / ModelRuntime). Do not wait
3378
+ // for Pi conversation reopen and do not start it here — prompt / reload
3379
+ // / model mutation still reopen, and concurrent reopen would stall this
3380
+ // cheap list on the single Node thread.
3381
+ const record = await deps.store.get(sessionId);
3382
+ if (!record) {
3383
+ sendJson(res, 404, {
3384
+ ok: false,
3385
+ error: {
3386
+ code: "PI_SESSION_NOT_FOUND",
3387
+ message: `chat session not found: ${sessionId}`,
3388
+ },
3389
+ });
3390
+ return;
3391
+ }
3292
3392
  let models;
3293
3393
  try {
3294
3394
  models = await deps.runtime.listModels(sessionId);
@@ -3317,6 +3417,11 @@ export async function handleModels(req, res, deps, sessionId) {
3317
3417
  });
3318
3418
  return;
3319
3419
  }
3420
+ const ensured = await ensureActiveSession(deps, sessionId);
3421
+ if (!ensured.ok) {
3422
+ sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
3423
+ return;
3424
+ }
3320
3425
  const gate = gateMutation(req, deps);
3321
3426
  if (!gate.ok) {
3322
3427
  sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
@@ -3372,18 +3477,24 @@ async function requireChatSession(res, deps, sessionId) {
3372
3477
  });
3373
3478
  return false;
3374
3479
  }
3375
- /**
3376
- * Unified session activation (design §2.1 / AC-01): models, runtime snapshot,
3377
- * reload and model/thinking mutation requests all flow through this path.
3378
- *
3379
- * - runtime already holds the session → ok;
3380
- * - only a persisted record exists → reopen from record.sessionFile;
3381
- * - no record / missing session file → 404 PI_SESSION_NOT_FOUND;
3382
- * - SDK reopen failure → 503 PI_RUNTIME_UNAVAILABLE.
3383
- *
3384
- * Never fabricates a fresh session or an empty model list.
3385
- */
3480
+ const ensureActiveSessionInFlight = new WeakMap();
3386
3481
  export async function ensureActiveSession(deps, sessionId) {
3482
+ let bySession = ensureActiveSessionInFlight.get(deps);
3483
+ if (!bySession) {
3484
+ bySession = new Map();
3485
+ ensureActiveSessionInFlight.set(deps, bySession);
3486
+ }
3487
+ const existing = bySession.get(sessionId);
3488
+ if (existing)
3489
+ return existing;
3490
+ const pending = runEnsureActiveSession(deps, sessionId).finally(() => {
3491
+ if (bySession.get(sessionId) === pending)
3492
+ bySession.delete(sessionId);
3493
+ });
3494
+ bySession.set(sessionId, pending);
3495
+ return pending;
3496
+ }
3497
+ async function runEnsureActiveSession(deps, sessionId) {
3387
3498
  // Wait for create-path materialization before treating the session as missing.
3388
3499
  if (typeof deps.runtime.ensureSessionReady === "function") {
3389
3500
  try {
@@ -3629,19 +3740,28 @@ export async function handleReloadSession(req, res, deps, sessionId) {
3629
3740
  });
3630
3741
  }
3631
3742
  /**
3632
- * GET /sessions/:id/runtime-snapshot (AC-01): the unified live snapshot.
3743
+ * GET /sessions/:id/runtime-snapshot (AC-01): live Agent when present,
3744
+ * otherwise workspace ResourceLoader projection (no JSONL reopen).
3633
3745
  */
3634
3746
  export async function handleRuntimeSnapshot(_res, res, deps, sessionId) {
3635
- const ensured = await ensureActiveSession(deps, sessionId);
3636
- if (!ensured.ok) {
3637
- sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
3747
+ const record = await deps.store.get(sessionId);
3748
+ if (!record) {
3749
+ sendJson(res, 404, {
3750
+ ok: false,
3751
+ error: {
3752
+ code: "PI_SESSION_NOT_FOUND",
3753
+ message: `chat session not found: ${sessionId}`,
3754
+ },
3755
+ });
3638
3756
  return;
3639
3757
  }
3640
3758
  // RF-02: a critical snapshot projection failure is 503 PI_RUNTIME_UNAVAILABLE
3641
3759
  // — never a 200 fake-empty snapshot.
3642
3760
  let snapshot;
3643
3761
  try {
3644
- snapshot = await deps.runtime.getRuntimeSnapshot(sessionId);
3762
+ snapshot = await deps.runtime.getRuntimeSnapshot(sessionId, {
3763
+ workspaceProjection: workspaceProjectionFromRecord(record),
3764
+ });
3645
3765
  }
3646
3766
  catch (error) {
3647
3767
  if (sendRuntimeUnavailable(res, error))
@@ -4464,7 +4584,14 @@ export async function handleChatMessages(res, deps, sessionId, url) {
4464
4584
  limit: Number.isFinite(limit) ? limit : 30,
4465
4585
  beforeId: url.searchParams.get("beforeId") ?? undefined,
4466
4586
  });
4467
- sendJson(res, 200, { ok: true, ...page });
4587
+ const snapshot = deps.events.snapshot(sessionId);
4588
+ const messages = projectStoredMessagesWithEventOrder(page.messages, snapshot);
4589
+ sendJson(res, 200, {
4590
+ ok: true,
4591
+ ...page,
4592
+ messages,
4593
+ turnStates: turnStatesForIds(deps.events, sessionId, messages.map((message) => message.turnId)),
4594
+ });
4468
4595
  }
4469
4596
  catch (error) {
4470
4597
  sendJson(res, 404, {
@@ -4476,6 +4603,23 @@ export async function handleChatMessages(res, deps, sessionId, url) {
4476
4603
  });
4477
4604
  }
4478
4605
  }
4606
+ export async function handleChatAttachment(res, deps, sessionId, attachmentId) {
4607
+ const attachment = await deps.store.readAttachment(sessionId, attachmentId);
4608
+ if (!attachment) {
4609
+ sendJson(res, 404, {
4610
+ ok: false,
4611
+ error: { code: "NOT_FOUND", message: "chat attachment not found" },
4612
+ });
4613
+ return;
4614
+ }
4615
+ res.writeHead(200, {
4616
+ "content-type": attachment.metadata.mimeType,
4617
+ "content-length": String(attachment.bytes.length),
4618
+ "cache-control": "private, max-age=31536000, immutable",
4619
+ "x-content-type-options": "nosniff",
4620
+ });
4621
+ res.end(attachment.bytes);
4622
+ }
4479
4623
  /**
4480
4624
  * Dispatch a chat HTTP request. Returns true if handled.
4481
4625
  * Mounted under /api/operator/v1/chat/*.
@@ -4913,6 +5057,11 @@ async function dispatchChatRequest(req, res, deps, pathname, workspaceScope) {
4913
5057
  await handleChatMessages(res, deps, decodeURIComponent(sessionMessages[1]), url);
4914
5058
  return true;
4915
5059
  }
5060
+ const sessionAttachment = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/attachments\/([^/]+)$/);
5061
+ if (method === "GET" && sessionAttachment) {
5062
+ await handleChatAttachment(res, deps, decodeURIComponent(sessionAttachment[1]), decodeURIComponent(sessionAttachment[2]));
5063
+ return true;
5064
+ }
4916
5065
  const sessionPrompt = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/prompt$/);
4917
5066
  if (method === "POST" && sessionPrompt) {
4918
5067
  await handleChatPrompt(req, res, deps, decodeURIComponent(sessionPrompt[1]));