@tea-agent/loop-agent 0.40.0-next.11 → 0.40.0-next.13

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 (128) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +1 -1
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/cli/command-definitions.js +9 -5
  5. package/dist/cli/program.js +38 -3
  6. package/dist/commands/client-recovery.js +8 -36
  7. package/dist/commands/task-advance.js +23 -1
  8. package/dist/commands/task-source-prepare.js +5 -0
  9. package/dist/commands/task-status.js +1 -0
  10. package/dist/executors/pi-sdk-executor.js +33 -5
  11. package/dist/shared/operator/capabilities.js +9 -1
  12. package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
  13. package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
  14. package/dist/shared/pi-context-pressure/env.js +58 -0
  15. package/dist/shared/pi-context-pressure/extension.js +100 -0
  16. package/dist/shared/pi-context-pressure/index.js +7 -0
  17. package/dist/shared/pi-context-pressure/overflow.js +252 -0
  18. package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
  19. package/dist/shared/pi-context-pressure/telemetry.js +51 -0
  20. package/dist/worker/console/chat/assistant-content.js +7 -0
  21. package/dist/worker/console/chat/chat-event-store.js +82 -11
  22. package/dist/worker/console/chat/goal-round-driver.js +72 -0
  23. package/dist/worker/console/chat/goals.js +300 -0
  24. package/dist/worker/console/chat/pi-runtime.js +285 -8
  25. package/dist/worker/console/chat/provider-error.js +2 -1
  26. package/dist/worker/console/chat/repo-browser.js +94 -12
  27. package/dist/worker/console/chat/routes.js +485 -29
  28. package/dist/worker/console/chat/session-catalog.js +48 -0
  29. package/dist/worker/console/chat/session-store.js +27 -18
  30. package/dist/worker/console/chat/shortcuts.js +7 -0
  31. package/dist/worker/console/chat/sift-bridge.js +1 -0
  32. package/dist/worker/console/chat/turn-process.js +32 -3
  33. package/dist/worker/console/operator-actions.js +16 -2
  34. package/dist/worker/console/operator-selection.js +6 -2
  35. package/dist/worker/console/pi-plugins.catalog.json +73658 -0
  36. package/dist/worker/console/pi-plugins.js +100 -0
  37. package/dist/worker/console/routes.js +221 -6
  38. package/dist/worker/console/server.js +172 -42
  39. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bs-CDAXM.js → abnfDiagram-N423BO3Z-Wj-fsQlk.js} +1 -1
  40. package/dist/worker/console/static/assets/{arc-CAkA3We3.js → arc-CkXT-vzQ.js} +1 -1
  41. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DEmI_zqr.js → architectureDiagram-T3A2C74G-xlfnojWU.js} +1 -1
  42. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-uDHANebE.js → blockDiagram-VBNYF7ZC-BilahvuB.js} +1 -1
  43. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bv97b4_B.js → c4Diagram-5PPSVZJV-DqtAVhjz.js} +1 -1
  44. package/dist/worker/console/static/assets/channel-SnQ_u-Td.js +1 -0
  45. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-shmQKsFz.js → chunk-2GRJ4B5K-pAqQXZ5y.js} +1 -1
  46. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BAraNvcN.js → chunk-2Q5K7J3B-BEWxeq2r.js} +1 -1
  47. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BirhxUop.js → chunk-5RXB4S5H-CvlbGDTp.js} +1 -1
  48. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C9LgUJpR.js → chunk-5VM5RSS4-D0uGd9-f.js} +1 -1
  49. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DrU-s-t7.js → chunk-6Q2QTUOP-DSdWIteg.js} +1 -1
  50. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Tx-H_FA2.js → chunk-GF5L2VYU-Dweg7f0v.js} +1 -1
  51. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BjLtNFrR.js → chunk-JWPE2WC7-BpZKI_k7.js} +1 -1
  52. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-UNLVwSsy.js → chunk-KBJHAD2P-IPLknC4m.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CBEUX8XD.js → chunk-RYQCIY6F-E93k9lVI.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-WTL7IZOq.js → chunk-XXDRQBXY-BrESwkPq.js} +1 -1
  55. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-SGM51NVY.js +1 -0
  56. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-SGM51NVY.js +1 -0
  57. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CkHG6W3s.js → cose-bilkent-JH36ORCC-DmFHvJkB.js} +1 -1
  58. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BRzFqTaL.js → cynefin-VYW2F7L2-CDigXiSL.js} +1 -1
  59. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2_CFp_i7.js → cynefinDiagram-MW4NZA55-B0zhrbaA.js} +1 -1
  60. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJtsmdkE.js → dagre-VZM6K2ZE-DrKybVxm.js} +1 -1
  61. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DNEFFo6w.js → diagram-7IWD3JNH-CeUrnbFk.js} +1 -1
  62. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B_IWV7eo.js → diagram-B4RE2ZJO-BJyunUMi.js} +1 -1
  63. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DvvRx2dz.js → diagram-LBJQPF4R-a5TBP73u.js} +1 -1
  64. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CsibsWLg.js → diagram-Q27KOJAE-ChRgBURJ.js} +1 -1
  65. package/dist/worker/console/static/assets/{diagram-UB23O5K3-les_djBQ.js → diagram-UB23O5K3-CqCnPRSk.js} +1 -1
  66. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BEm2n7qJ.js → ebnfDiagram-BXEA7PRR-BI-UD9_S.js} +1 -1
  67. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BI7vHHVm.js → erDiagram-JOGREHBK-CDLvvkYZ.js} +1 -1
  68. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CUFCERrB.js → flowDiagram-UKHOOZJN-Heoo4afj.js} +1 -1
  69. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-wxyLiGWt.js → ganttDiagram-PKOTCBZU-kFpOitHd.js} +1 -1
  70. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bt897K8Z.js → gitGraphDiagram-DS77QQ5N-FvyZ9WAY.js} +1 -1
  71. package/dist/worker/console/static/assets/index-C80SO8y0.js +437 -0
  72. package/dist/worker/console/static/assets/index-CmJCHmJP.css +1 -0
  73. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DYNx_IPv.js → infoDiagram-6WML65LV-HBzlwtgI.js} +1 -1
  74. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CzpLNEu6.js → ishikawaDiagram-WSZJBQD7-C9Iim5sm.js} +1 -1
  75. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-97f9owNU.js → journeyDiagram-NVQOT4AX-CJwJTg0S.js} +1 -1
  76. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BxM9hyBn.js → kanban-definition-27J2QSJJ-CZNJ7S4q.js} +1 -1
  77. package/dist/worker/console/static/assets/{linear-ByuxcHvp.js → linear-RZ0AnGi8.js} +1 -1
  78. package/dist/worker/console/static/assets/{mermaid.core-DwPGvpCv.js → mermaid.core-EYSdm-nd.js} +5 -5
  79. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DCoqG9RT.js → mindmap-definition-FAOFIHXS-D0OLECFN.js} +1 -1
  80. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BLSBmBMx.js → pegDiagram-VL7TDLO6-CPTvGbh7.js} +1 -1
  81. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B0t_0oJW.js → pieDiagram-7S7Q4E2Y-Ddc1aDea.js} +1 -1
  82. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js → quadrantDiagram-CIZ2JOQS-CWCTCdAt.js} +1 -1
  83. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-B3jRdATC.js → railroadDiagram-AXF67PYL-CfsaBL8E.js} +1 -1
  84. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-cTtXEN.js → requirementDiagram-LRYGKXZP-BH2rqGf6.js} +1 -1
  85. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B25igJRE.js → sankeyDiagram-W5VNT64P-BZgJ1Til.js} +1 -1
  86. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-jW_lM1_D.js → sequenceDiagram-SI44F4Z6-DmPr-o-y.js} +1 -1
  87. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CrzTgsHE.js → sizeCapture-X5ZJPWSS-CF6HzDh_.js} +1 -1
  88. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B01MvlqE.js → stateDiagram-OKZ733FA-DhS8N_B9.js} +1 -1
  89. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-N56zxwsk.js +1 -0
  90. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-YcF12FiU.js → swimlanes-SLNWSIFB-CYNzoiaD.js} +2 -2
  91. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-o47kGgVt.js +8 -0
  92. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ClnRcg2s.js → timeline-definition-Z64GVDOM-QBICF5qH.js} +1 -1
  93. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B6gr_z-q.js → vennDiagram-T6HMQDX7-fquBvWcf.js} +1 -1
  94. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CvSgguGH.js → wardleyDiagram-T6FBY63Y-BYaJLlSd.js} +1 -1
  95. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-23OtSZCb.js → xychartDiagram-ELKLHX3M--ePe_JCJ.js} +1 -1
  96. package/dist/worker/console/static/index.html +2 -2
  97. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +95 -25
  98. package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
  99. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
  100. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +24 -19
  101. package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
  102. package/dist/worker/console/static-src/operator-chat/useChatThread.js +8 -1
  103. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
  104. package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
  105. package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
  106. package/dist/worker/console/workspace-context.js +28 -3
  107. package/dist/worker/console/workspace-registry.js +333 -60
  108. package/dist/worker/observe/static/inspect-bootstrap.d.ts +8 -0
  109. package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
  110. package/dist/worker/observe/static/inspect-workspace.js +54 -10
  111. package/dist/worker/observe/static/operator-chrome.css +23 -4
  112. package/dist/worker/observe/static/operator-chrome.js +6 -2
  113. package/dist/worker/observe/static/styles.css +87 -25
  114. package/dist/worker/observe/static/views/dag-inspector.js +42 -1
  115. package/dist/worker/observe/static/views/dags.js +7 -5
  116. package/dist/worker/observe/static/views/session-timeline.js +135 -0
  117. package/dist/workflows/dag/init-hybrid.js +3 -3
  118. package/docs/architecture/runtime-boundaries.md +2 -1
  119. package/docs/templates/backend-test-dag.json +3 -3
  120. package/package.json +3 -2
  121. package/skills/loop-agent/references/command-reference.md +14 -0
  122. package/dist/worker/console/static/assets/channel-amxUpk7o.js +0 -1
  123. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +0 -1
  124. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +0 -1
  125. package/dist/worker/console/static/assets/index-BHhUOTri.css +0 -1
  126. package/dist/worker/console/static/assets/index-DqJbO3-p.js +0 -407
  127. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +0 -1
  128. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +0 -8
@@ -24,7 +24,7 @@ import { ResourcePreferencesError, setSkillAutoInvocationPreference, } from "./r
24
24
  import { ComposerDraftStore } from "./composer-draft-store.js";
25
25
  import { walkRepoFiles } from "./repo-walk.js";
26
26
  import { isSensitivePath, scrubSecrets } from "./explore-tools.js";
27
- import { listRepoDirectory, readRepoPreview, RepoBrowserError, } from "./repo-browser.js";
27
+ import { listRepoDirectory, matchRepoHtmlPreviewRoute, readRepoHtmlAsset, readRepoPreview, RepoBrowserError, } from "./repo-browser.js";
28
28
  import { projectRuntimeContext } from "./runtime-context.js";
29
29
  import { patchModelsConfig, readModelsConfig, readPackageInventory, } from "./pi-console-config.js";
30
30
  import { ChatInterviewAdapter, projectChatInterviewState, } from "./interview-adapter.js";
@@ -39,6 +39,71 @@ import { contractApplyPayloadHash, issueHumanGateToken, verifyHumanGateToken, }
39
39
  import { sendJson } from "../routes.js";
40
40
  import { openSseResponse, writeSseEvent } from "../operation-sse.js";
41
41
  import { buildTodoCompactionInstructions, projectCurrentTodos } from "./todos.js";
42
+ import { buildGoalCompactionInstructions, buildGoalContinuationContext, } from "./goals.js";
43
+ import { classifyGoalRoundAdmission, } from "./goal-round-driver.js";
44
+ import { isTerminalOperationState } from "../operation-store.js";
45
+ /**
46
+ * Reattach the durable event-ring order to session-store rows. The session
47
+ * store remains the content authority; the ring only supplies chronology.
48
+ * This makes refresh hydration use the same turnId/seq ordering as live SSE.
49
+ */
50
+ export function projectStoredMessagesWithEventOrder(messages, events) {
51
+ let assistantCursor = 0;
52
+ const nextAssistantEvent = (message) => {
53
+ const expectedKind = message.kind === "provider-error" ? "error" : "message_end";
54
+ const messageAt = Date.parse(message.at);
55
+ for (let index = assistantCursor; index < events.length; index += 1) {
56
+ const event = events[index];
57
+ if (event.kind !== expectedKind)
58
+ continue;
59
+ const data = event.data;
60
+ const eventText = typeof data.text === "string"
61
+ ? data.text
62
+ : typeof data.message === "string"
63
+ ? data.message
64
+ : undefined;
65
+ const closeInTime = Number.isFinite(messageAt) && Number.isFinite(Date.parse(event.at))
66
+ ? Math.abs(Date.parse(event.at) - messageAt) <= 30_000
67
+ : true;
68
+ if (eventText === message.text && closeInTime) {
69
+ assistantCursor = index + 1;
70
+ return event;
71
+ }
72
+ }
73
+ // The bounded ring may no longer contain this old message. Leave it on the
74
+ // legacy timestamp fallback; never steal a newer event just to fill a seq.
75
+ return undefined;
76
+ };
77
+ return messages.map((message) => {
78
+ let start;
79
+ let finish;
80
+ if (message.role === "user") {
81
+ start = events.find((event) => {
82
+ if (event.kind !== "user_message")
83
+ return false;
84
+ const payload = event.data;
85
+ return payload.message?.id === message.id;
86
+ });
87
+ }
88
+ else if (message.role === "assistant") {
89
+ start = nextAssistantEvent(message);
90
+ }
91
+ else if (message.role === "tool" && message.toolCallId) {
92
+ start = events.find((event) => event.kind === "tool_call" &&
93
+ event.data.toolCallId ===
94
+ message.toolCallId);
95
+ finish = events.find((event) => event.kind === "tool_result" &&
96
+ event.data.toolCallId ===
97
+ message.toolCallId);
98
+ start ??= finish;
99
+ }
100
+ return {
101
+ ...message,
102
+ ...(start ? { eventSeq: start.seq, turnId: start.turnId } : {}),
103
+ ...(finish ? { finishedSeq: finish.seq } : {}),
104
+ };
105
+ });
106
+ }
42
107
  function turnSummary(turn) {
43
108
  return {
44
109
  turnId: turn.turnId,
@@ -514,6 +579,7 @@ workspaceId) {
514
579
  // a fallback for direct (non-dispatcher) handler tests.
515
580
  workspaceId: workspaceId ?? (typeof body.workspaceId === "string" && body.workspaceId ? body.workspaceId : undefined),
516
581
  });
582
+ await deps.attachSession?.(handle.sessionId).catch(() => undefined);
517
583
  if (snapshot)
518
584
  deps.events.append(handle.sessionId, `${handle.sessionId}:snapshot`, {
519
585
  kind: "compact",
@@ -595,6 +661,7 @@ export async function handleForkChatSession(req, res, deps, sourceSessionId) {
595
661
  targetSessionId: handle.sessionId,
596
662
  targetSessionFile: handle.sessionFile,
597
663
  });
664
+ await deps.attachSession?.(handle.sessionId).catch(() => undefined);
598
665
  const copied = deps.events.forkContextRefs({
599
666
  sourceSessionId,
600
667
  targetSessionId: handle.sessionId,
@@ -746,6 +813,7 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
746
813
  eventEpoch: deps.events.epoch(sessionId),
747
814
  session: {
748
815
  ...record,
816
+ messages: projectStoredMessagesWithEventOrder(record.messages, snapshot),
749
817
  composerDraft,
750
818
  activeTurnId: activeTurn?.turnId,
751
819
  turnPhase: activeTurn
@@ -755,10 +823,14 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
755
823
  : "idle",
756
824
  pendingQuestion: pendingQuestion ?? null,
757
825
  todos: projectCurrentTodos(snapshot) ?? [],
826
+ goal: deps.goalRegistry?.get(sessionId) ?? null,
758
827
  },
759
828
  hasBash: true,
760
829
  sessionStats: telemetry.stats,
761
830
  usage: telemetry.usage,
831
+ contextPressure: typeof deps.runtime.getContextPressureTelemetry === "function"
832
+ ? deps.runtime.getContextPressureTelemetry(sessionId)
833
+ : undefined,
762
834
  compactions: snapshot
763
835
  .filter((event) => event.kind === "compact" ||
764
836
  event.kind === "compaction_start" ||
@@ -767,6 +839,8 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
767
839
  eventId: event.eventId,
768
840
  kind: event.kind,
769
841
  at: event.at,
842
+ seq: event.seq,
843
+ turnId: event.turnId,
770
844
  data: event.data,
771
845
  })),
772
846
  lastEventId: lastEvent?.eventId ?? null,
@@ -1070,9 +1144,162 @@ export async function handleGetChatSessionState(_req, res, deps, sessionId) {
1070
1144
  deferredTurns: projectDeferredTurnSummaries(record.deferredTurns ?? []),
1071
1145
  pendingQuestion: pendingQuestion ?? null,
1072
1146
  todos: projectCurrentTodos(deps.events.snapshot(sessionId)) ?? [],
1147
+ goal: deps.goalRegistry?.get(sessionId) ?? null,
1073
1148
  },
1074
1149
  });
1075
1150
  }
1151
+ function goalHttpStatus(code) {
1152
+ if (code === "GOAL_NOT_FOUND")
1153
+ return 404;
1154
+ if (code === "GOAL_INVALID_INPUT")
1155
+ return 400;
1156
+ return 409;
1157
+ }
1158
+ export async function handleChatGoal(req, res, deps, sessionId) {
1159
+ const registry = deps.goalRegistry;
1160
+ if (!registry) {
1161
+ sendJson(res, 503, {
1162
+ ok: false,
1163
+ error: { code: "GOAL_UNAVAILABLE", message: "Goal runtime is unavailable" },
1164
+ });
1165
+ return;
1166
+ }
1167
+ if (!(await deps.store.get(sessionId))) {
1168
+ sendJson(res, 404, {
1169
+ ok: false,
1170
+ error: { code: "NOT_FOUND", message: `chat session not found: ${sessionId}` },
1171
+ });
1172
+ return;
1173
+ }
1174
+ if (req.method === "GET") {
1175
+ sendJson(res, 200, { ok: true, goal: registry.get(sessionId) });
1176
+ return;
1177
+ }
1178
+ const gate = gateMutation(req, deps);
1179
+ if (!gate.ok) {
1180
+ sendJson(res, gate.status, {
1181
+ ok: false,
1182
+ error: { code: gate.code, message: gate.message },
1183
+ });
1184
+ return;
1185
+ }
1186
+ let body = {};
1187
+ try {
1188
+ body = await readJsonBody(req);
1189
+ }
1190
+ catch (error) {
1191
+ sendJson(res, 400, {
1192
+ ok: false,
1193
+ error: {
1194
+ code: "GOAL_INVALID_INPUT",
1195
+ message: error instanceof Error ? error.message : String(error),
1196
+ },
1197
+ });
1198
+ return;
1199
+ }
1200
+ if (req.method === "POST") {
1201
+ const result = registry.create({
1202
+ sessionId,
1203
+ objective: body.objective,
1204
+ maxRounds: typeof body.maxRounds === "number" ? body.maxRounds : undefined,
1205
+ arm: true,
1206
+ });
1207
+ if (!result.ok) {
1208
+ sendJson(res, goalHttpStatus(result.code), {
1209
+ ok: false,
1210
+ error: { code: result.code, message: result.message },
1211
+ goal: result.goal,
1212
+ });
1213
+ return;
1214
+ }
1215
+ deps.goalRoundDriver?.wake(sessionId);
1216
+ sendJson(res, 201, { ok: true, goal: result.goal });
1217
+ return;
1218
+ }
1219
+ const current = registry.get(sessionId);
1220
+ const goalId = typeof body.goalId === "string" ? body.goalId : "";
1221
+ const revision = typeof body.revision === "number" ? body.revision : Number.NaN;
1222
+ if (!current || !goalId || !Number.isSafeInteger(revision)) {
1223
+ sendJson(res, current ? 400 : 404, {
1224
+ ok: false,
1225
+ error: {
1226
+ code: current ? "GOAL_INVALID_INPUT" : "GOAL_NOT_FOUND",
1227
+ message: current
1228
+ ? "goalId and integer revision are required"
1229
+ : "Goal not found",
1230
+ },
1231
+ });
1232
+ return;
1233
+ }
1234
+ if (req.method === "DELETE") {
1235
+ const result = registry.clear({
1236
+ sessionId,
1237
+ goalId,
1238
+ expectedRevision: revision,
1239
+ });
1240
+ if (!result.ok) {
1241
+ sendJson(res, goalHttpStatus(result.code), {
1242
+ ok: false,
1243
+ error: { code: result.code, message: result.message },
1244
+ goal: result.goal,
1245
+ });
1246
+ return;
1247
+ }
1248
+ deps.goalRoundDriver?.cancel(sessionId);
1249
+ sendJson(res, 200, { ok: true, goal: null });
1250
+ return;
1251
+ }
1252
+ if (req.method === "PATCH") {
1253
+ const action = typeof body.action === "string" ? body.action : "edit";
1254
+ let phase;
1255
+ let arm;
1256
+ if (action === "pause") {
1257
+ phase = "paused";
1258
+ arm = false;
1259
+ }
1260
+ else if (action === "resume") {
1261
+ phase = "active";
1262
+ arm = true;
1263
+ }
1264
+ else if (action === "edit") {
1265
+ phase = current.phase;
1266
+ arm = current.armed;
1267
+ }
1268
+ else {
1269
+ sendJson(res, 400, {
1270
+ ok: false,
1271
+ error: { code: "GOAL_INVALID_INPUT", message: "unsupported Goal action" },
1272
+ });
1273
+ return;
1274
+ }
1275
+ const result = registry.update({
1276
+ sessionId,
1277
+ goalId,
1278
+ expectedRevision: revision,
1279
+ phase,
1280
+ arm,
1281
+ ...(body.objective !== undefined ? { objective: body.objective } : {}),
1282
+ });
1283
+ if (!result.ok) {
1284
+ sendJson(res, goalHttpStatus(result.code), {
1285
+ ok: false,
1286
+ error: { code: result.code, message: result.message },
1287
+ goal: result.goal,
1288
+ });
1289
+ return;
1290
+ }
1291
+ if (action === "resume")
1292
+ deps.goalRoundDriver?.wake(sessionId);
1293
+ else if (action === "pause")
1294
+ deps.goalRoundDriver?.cancel(sessionId);
1295
+ sendJson(res, 200, { ok: true, goal: result.goal });
1296
+ return;
1297
+ }
1298
+ sendJson(res, 405, {
1299
+ ok: false,
1300
+ error: { code: "METHOD_NOT_ALLOWED", message: "method not allowed" },
1301
+ });
1302
+ }
1076
1303
  export async function handleAnswerUserQuestion(req, res, deps, sessionId, requestId) {
1077
1304
  const gate = gateMutation(req, deps);
1078
1305
  if (!gate.ok) {
@@ -1121,27 +1348,53 @@ export async function handleCancelUserQuestion(req, res, deps, sessionId, reques
1121
1348
  }
1122
1349
  }
1123
1350
  export async function handleListChatSessions(_req, res, deps) {
1124
- const sessions = await deps.store.list({ includeArchived: true });
1351
+ const sessions = await listChatSessionSummaries(deps);
1125
1352
  sendJson(res, 200, {
1126
1353
  ok: true,
1127
1354
  schemaVersion: 2,
1128
- sessions: sessions.map((session) => {
1129
- const active = deps.events.getActiveTurn(session.sessionId);
1130
- return {
1131
- ...session,
1132
- activeTurnId: active?.turnId,
1133
- turnPhase: active?.abortRequestedAt
1134
- ? "stopping"
1135
- : active
1136
- ? "running"
1137
- : "idle",
1138
- deferredTurn: projectDeferredTurnSummary(session.deferredTurns?.[0]),
1139
- deferredTurns: projectDeferredTurnSummaries(session.deferredTurns ?? []),
1140
- };
1141
- }),
1355
+ sessions,
1142
1356
  hasBash: true,
1143
1357
  });
1144
1358
  }
1359
+ /**
1360
+ * Build sidebar-only summaries from one Workspace store without opening a
1361
+ * runtime. Message bodies, Pi session paths and tool audit rows belong to the
1362
+ * detail endpoint; returning them here makes the global multi-Workspace index
1363
+ * grow with the entire chat history.
1364
+ */
1365
+ export async function listChatSessionSummaries(deps, ownerWorkspaceId) {
1366
+ const sessions = await deps.store.list({ includeArchived: true });
1367
+ return sessions.map((session) => {
1368
+ const active = deps.events?.getActiveTurn(session.sessionId);
1369
+ const workspaceId = ownerWorkspaceId ?? session.workspaceId;
1370
+ return {
1371
+ sessionId: session.sessionId,
1372
+ ...(workspaceId ? { workspaceId } : {}),
1373
+ state: session.state,
1374
+ createdAt: session.createdAt,
1375
+ updatedAt: session.updatedAt,
1376
+ ...(session.title !== undefined ? { title: session.title } : {}),
1377
+ ...(session.displayTitle !== undefined
1378
+ ? { displayTitle: session.displayTitle }
1379
+ : {}),
1380
+ ...(session.modelProvider !== undefined
1381
+ ? { modelProvider: session.modelProvider }
1382
+ : {}),
1383
+ ...(session.modelId !== undefined ? { modelId: session.modelId } : {}),
1384
+ ...(session.thinkingLevel !== undefined
1385
+ ? { thinkingLevel: session.thinkingLevel }
1386
+ : {}),
1387
+ activeTurnId: active?.turnId,
1388
+ turnPhase: active?.abortRequestedAt
1389
+ ? "stopping"
1390
+ : active
1391
+ ? "running"
1392
+ : "idle",
1393
+ deferredTurn: projectDeferredTurnSummary(session.deferredTurns?.[0]),
1394
+ deferredTurns: projectDeferredTurnSummaries(session.deferredTurns ?? []),
1395
+ };
1396
+ });
1397
+ }
1145
1398
  export async function handlePatchChatSession(req, res, deps, sessionId) {
1146
1399
  const gate = gateMutation(req, deps);
1147
1400
  if (!gate.ok) {
@@ -1644,6 +1897,22 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1644
1897
  });
1645
1898
  return;
1646
1899
  }
1900
+ try {
1901
+ trustedPayload?.onTurnCreated?.(turn.turnId);
1902
+ }
1903
+ catch (error) {
1904
+ const message = error instanceof Error ? error.message : String(error);
1905
+ deps.events.setTurnState(sessionId, turn.turnId, "failed", {
1906
+ code: "GOAL_ADMISSION_STALE",
1907
+ message,
1908
+ });
1909
+ sendJson(res, 409, {
1910
+ ok: false,
1911
+ accepted: false,
1912
+ error: { code: "GOAL_ADMISSION_STALE", message },
1913
+ });
1914
+ return;
1915
+ }
1647
1916
  const controller = new AbortController();
1648
1917
  const execution = deps.turnExecutions?.register({
1649
1918
  sessionId,
@@ -1693,6 +1962,7 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1693
1962
  if (placeholder)
1694
1963
  emitSessionTitle(deps, sessionId, turn.turnId, placeholder);
1695
1964
  },
1965
+ persistUserMessage: trustedPayload?.persistUserMessage,
1696
1966
  onOperationAccepted: (accepted) => deps.operationLinker.link({
1697
1967
  sessionId,
1698
1968
  turnId: turn.turnId,
@@ -1755,16 +2025,139 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1755
2025
  }
1756
2026
  }
1757
2027
  finally {
2028
+ if (terminalState !== "settled") {
2029
+ pauseGoalForSafety(deps, sessionId, turn.turnId);
2030
+ deps.goalRoundDriver?.cancel(sessionId);
2031
+ }
1758
2032
  execution?.resolveCompletion({
1759
2033
  ok: terminalState === "settled",
1760
2034
  state: terminalState,
1761
2035
  error: terminalError,
1762
2036
  });
1763
2037
  deps.turnExecutions?.clear(sessionId, turn.turnId);
1764
- void dispatchDeferredTurn(deps, sessionId);
2038
+ void dispatchDeferredTurn(deps, sessionId).finally(() => {
2039
+ deps.goalRoundDriver?.wake(sessionId);
2040
+ });
1765
2041
  }
1766
2042
  })();
1767
2043
  }
2044
+ function buildGoalRoundPrompt(goal) {
2045
+ const remaining = Math.max(0, goal.maxRounds - goal.round);
2046
+ return [
2047
+ buildGoalContinuationContext(goal),
2048
+ `This is autonomous Goal round ${goal.round + 1} of at most ${goal.maxRounds} (${remaining} rounds remain before this one).`,
2049
+ "Continue from the current session state. Prefer the user's queued or newly supplied instructions when present. Use todo_write for multi-step work.",
2050
+ "Before ending this round, call get_goal. If the objective is fully achieved, call update_goal(status=complete). If no safe progress remains, call update_goal(status=blocked) with the concrete blocker. Otherwise leave it active for the next bounded round.",
2051
+ ].filter(Boolean).join("\n\n");
2052
+ }
2053
+ function pauseGoalForSafety(deps, sessionId, turnId) {
2054
+ const current = deps.goalRegistry?.get(sessionId);
2055
+ if (!current || current.phase !== "active") {
2056
+ deps.goalRegistry?.disarm(sessionId);
2057
+ return;
2058
+ }
2059
+ deps.goalRegistry?.update({
2060
+ sessionId,
2061
+ goalId: current.goalId,
2062
+ expectedRevision: current.revision,
2063
+ phase: "paused",
2064
+ arm: false,
2065
+ turnId,
2066
+ });
2067
+ }
2068
+ export function linkedOperationIdsForGoal(events) {
2069
+ const linked = new Set();
2070
+ for (const event of events) {
2071
+ const operationId = event.data && typeof event.data === "object"
2072
+ ? event.data.operationId
2073
+ : undefined;
2074
+ if (typeof operationId !== "string" || !operationId)
2075
+ continue;
2076
+ if (event.kind === "operation-ref" || event.kind === "operation-status")
2077
+ linked.add(operationId);
2078
+ if (event.kind === "operation-unlinked")
2079
+ linked.delete(operationId);
2080
+ }
2081
+ return [...linked];
2082
+ }
2083
+ /** One admission attempt used by the process-local GoalRoundDriver. */
2084
+ export async function driveGoalRound(deps, sessionId) {
2085
+ const registry = deps.goalRegistry;
2086
+ if (!registry || !registry.isArmed(sessionId))
2087
+ return "idle";
2088
+ const goal = registry.get(sessionId);
2089
+ if (!goal || goal.phase !== "active" || !goal.armed)
2090
+ return "idle";
2091
+ if (goal.round >= goal.maxRounds) {
2092
+ registry.update({
2093
+ sessionId,
2094
+ goalId: goal.goalId,
2095
+ expectedRevision: goal.revision,
2096
+ phase: "paused",
2097
+ arm: false,
2098
+ });
2099
+ return "idle";
2100
+ }
2101
+ const record = await deps.store.get(sessionId);
2102
+ if (!record) {
2103
+ registry.disarm(sessionId);
2104
+ return "idle";
2105
+ }
2106
+ const runtimeFacts = deps.runtime.getSessionBusyFacts(sessionId);
2107
+ const operationStore = deps.runtime.actionContext?.operations;
2108
+ const linkedOperationIds = linkedOperationIdsForGoal(deps.events.snapshot(sessionId));
2109
+ const operations = operationStore
2110
+ ? await Promise.all(linkedOperationIds.map((operationId) => operationStore.get(operationId)))
2111
+ : [];
2112
+ const admission = classifyGoalRoundAdmission({
2113
+ phase: goal.phase,
2114
+ armed: goal.armed,
2115
+ round: goal.round,
2116
+ maxRounds: goal.maxRounds,
2117
+ hasQueuedUserTurn: (record.deferredTurns ?? []).some((item) => item.state === "queued" || item.state === "dispatching"),
2118
+ hasActiveTurn: Boolean(deps.events.getActiveTurn(sessionId)),
2119
+ runtimeBusy: Object.values(runtimeFacts).some((value) => value === true),
2120
+ operationBusy: operations.some((operation) => Boolean(operation) && !isTerminalOperationState(operation.state)),
2121
+ });
2122
+ if (admission.action === "wait")
2123
+ return "wait";
2124
+ if (admission.action === "idle")
2125
+ return "idle";
2126
+ const expectedRound = goal.round + 1;
2127
+ const clientRequestId = `goal:${goal.goalId}:${expectedRound}`;
2128
+ const started = await startTrustedChatTurn(deps, sessionId, {
2129
+ clientRequestId,
2130
+ text: buildGoalRoundPrompt(goal),
2131
+ images: [],
2132
+ persistUserMessage: false,
2133
+ onTurnCreated: (turnId) => {
2134
+ const advanced = registry.beginRound({
2135
+ sessionId,
2136
+ goalId: goal.goalId,
2137
+ expectedRevision: goal.revision,
2138
+ turnId,
2139
+ });
2140
+ if (!advanced.ok) {
2141
+ registry.disarm(sessionId);
2142
+ throw new Error(advanced.message);
2143
+ }
2144
+ },
2145
+ });
2146
+ const accepted = started.body.accepted === true;
2147
+ const turnId = started.body.turn && typeof started.body.turn === "object"
2148
+ ? started.body.turn.turnId
2149
+ : undefined;
2150
+ if (!accepted || !turnId) {
2151
+ const code = started.body.error && typeof started.body.error === "object"
2152
+ ? String(started.body.error.code ?? "")
2153
+ : "";
2154
+ if (code === "TURN_ACTIVE")
2155
+ return "wait";
2156
+ registry.disarm(sessionId);
2157
+ return "idle";
2158
+ }
2159
+ return "started";
2160
+ }
1768
2161
  async function emitDeferredTurnUpdate(deps, sessionId, turnId, _deferredTurn, _revision) {
1769
2162
  const record = await deps.store.get(sessionId);
1770
2163
  if (!record)
@@ -1985,6 +2378,8 @@ export async function handleAbortChatTurn(req, res, deps, sessionId, turnId) {
1985
2378
  });
1986
2379
  return;
1987
2380
  }
2381
+ pauseGoalForSafety(deps, sessionId, turnId);
2382
+ deps.goalRoundDriver?.cancel(sessionId);
1988
2383
  if (isTerminalTurnState(turn.state)) {
1989
2384
  sendJson(res, 200, {
1990
2385
  ok: true,
@@ -2155,6 +2550,8 @@ export async function handleDeleteChatSession(req, res, deps, sessionId) {
2155
2550
  }
2156
2551
  await deps.questionRegistry?.cancelForSession(sessionId, "SESSION_DELETED").catch(() => undefined);
2157
2552
  deps.todoRegistry?.clear(sessionId);
2553
+ deps.goalRegistry?.clearRuntime(sessionId);
2554
+ deps.goalRoundDriver?.cancel(sessionId);
2158
2555
  deps.runtime.dispose(sessionId);
2159
2556
  deps.operationLinker.clearSession(sessionId);
2160
2557
  deps.events.clear(sessionId);
@@ -2170,6 +2567,7 @@ export async function handleDeleteChatSession(req, res, deps, sessionId) {
2170
2567
  });
2171
2568
  return;
2172
2569
  }
2570
+ await deps.detachSession?.(sessionId).catch(() => undefined);
2173
2571
  sendJson(res, 200, { ok: true, sessionId, disposed: true, removed: true });
2174
2572
  }
2175
2573
  export async function handleGetTaskContext(_res, res, deps, sessionId) {
@@ -2229,6 +2627,46 @@ async function handleRepoBrowser(res, deps, kind, rawPath) {
2229
2627
  });
2230
2628
  }
2231
2629
  }
2630
+ async function handleRepoHtmlPreview(res, deps, rawPath) {
2631
+ try {
2632
+ const asset = await readRepoHtmlAsset(deps.appData.repoRoot, rawPath);
2633
+ const origin = deps.getConsoleOrigin();
2634
+ res.writeHead(200, {
2635
+ "Content-Type": asset.contentType,
2636
+ "Content-Length": asset.body.byteLength,
2637
+ "Cache-Control": "no-store",
2638
+ "X-Content-Type-Options": "nosniff",
2639
+ "Referrer-Policy": "no-referrer",
2640
+ // The iframe also has an empty sandbox attribute. This response-level
2641
+ // sandbox keeps direct/popup loads isolated, while explicit local-origin
2642
+ // style/image/font/media sources let relative project assets render.
2643
+ "Content-Security-Policy": [
2644
+ "sandbox",
2645
+ "default-src 'none'",
2646
+ "script-src 'none'",
2647
+ "object-src 'none'",
2648
+ "frame-src 'none'",
2649
+ "form-action 'none'",
2650
+ "base-uri 'none'",
2651
+ "connect-src 'none'",
2652
+ `style-src 'unsafe-inline' ${origin}`,
2653
+ `img-src data: blob: ${origin}`,
2654
+ `font-src data: ${origin}`,
2655
+ `media-src blob: ${origin}`,
2656
+ ].join("; "),
2657
+ });
2658
+ res.end(asset.body);
2659
+ }
2660
+ catch (error) {
2661
+ const failure = error instanceof RepoBrowserError
2662
+ ? error
2663
+ : new RepoBrowserError(500, "READ_FAILED", error instanceof Error ? error.message : String(error));
2664
+ sendJson(res, failure.status, {
2665
+ ok: false,
2666
+ error: { code: failure.code, message: failure.message },
2667
+ });
2668
+ }
2669
+ }
2232
2670
  async function handlePiConfig(req, res, deps, resource) {
2233
2671
  try {
2234
2672
  if (resource === "models") {
@@ -2974,7 +3412,7 @@ async function ctxConfirmationCard(deps, sessionId, turnId, confirmationId) {
2974
3412
  taskId,
2975
3413
  confirmationId,
2976
3414
  expiresAt: confirmation.expiresAt,
2977
- inspectHref: `/inspect/?taskId=${encodeURIComponent(taskId)}#/`,
3415
+ inspectHref: `/#/inspect?taskId=${encodeURIComponent(taskId)}`,
2978
3416
  humanGateToken: confirmation.humanGateToken,
2979
3417
  displayOnlyChallenges: [
2980
3418
  "source",
@@ -3000,7 +3438,7 @@ async function ctxMutationGateCard(deps, sessionId, receiptId) {
3000
3438
  receiptId: receipt.receiptId,
3001
3439
  expiresAt: receipt.expiresAt,
3002
3440
  humanGateToken: receipt.humanGateToken,
3003
- inspectHref: `/inspect/#/`,
3441
+ inspectHref: `/#/inspect`,
3004
3442
  }));
3005
3443
  }
3006
3444
  function actionResultBody(result) {
@@ -3089,7 +3527,7 @@ async function handleContractApplyHumanGate(req, res, deps, sessionId, step) {
3089
3527
  humanGateToken,
3090
3528
  contractRevision: binding.revision,
3091
3529
  contractDiffSummary: `revision ${binding.revision}; draft ${binding.draftSha256.slice(0, 12)}; observed ${binding.expectedObservedHash.slice(0, 12)}`,
3092
- inspectHref: `/inspect/?taskId=${encodeURIComponent(taskId)}#/`,
3530
+ inspectHref: `/#/inspect?taskId=${encodeURIComponent(taskId)}`,
3093
3531
  });
3094
3532
  appendHumanGateCard(deps, sessionId, card);
3095
3533
  sendJson(res, 201, { ok: true, card });
@@ -3143,8 +3581,8 @@ async function handleContractApplyHumanGate(req, res, deps, sessionId, step) {
3143
3581
  contractRevision: receipt.revision,
3144
3582
  operationId,
3145
3583
  inspectHref: operationId
3146
- ? `/inspect/?operationId=${encodeURIComponent(operationId)}#/`
3147
- : `/inspect/?taskId=${encodeURIComponent(receipt.taskId)}#/`,
3584
+ ? `/#/inspect?operationId=${encodeURIComponent(operationId)}`
3585
+ : `/#/inspect?taskId=${encodeURIComponent(receipt.taskId)}`,
3148
3586
  });
3149
3587
  appendHumanGateCard(deps, sessionId, card);
3150
3588
  sendJson(res, 202, {
@@ -3190,8 +3628,8 @@ async function handleContractApplyHumanGate(req, res, deps, sessionId, step) {
3190
3628
  contractRevision: receipt.revision,
3191
3629
  operationId,
3192
3630
  inspectHref: operationId
3193
- ? `/inspect/?operationId=${encodeURIComponent(operationId)}#/`
3194
- : `/inspect/?taskId=${encodeURIComponent(receipt.taskId)}#/`,
3631
+ ? `/#/inspect?operationId=${encodeURIComponent(operationId)}`
3632
+ : `/#/inspect?taskId=${encodeURIComponent(receipt.taskId)}`,
3195
3633
  });
3196
3634
  appendHumanGateCard(deps, sessionId, card);
3197
3635
  if (operationId)
@@ -3292,8 +3730,8 @@ async function handleDagHumanGate(req, res, deps, sessionId, step) {
3292
3730
  confirmationId,
3293
3731
  expiresAt: String(confirmation?.expiresAt ?? new Date().toISOString()),
3294
3732
  inspectHref: operationId
3295
- ? `/inspect/?operationId=${encodeURIComponent(operationId)}#/`
3296
- : `/inspect/?taskId=${encodeURIComponent(taskId)}#/`,
3733
+ ? `/#/inspect?operationId=${encodeURIComponent(operationId)}`
3734
+ : `/#/inspect?taskId=${encodeURIComponent(taskId)}`,
3297
3735
  ...(confirmation?.humanGateToken
3298
3736
  ? { humanGateToken: confirmation.humanGateToken }
3299
3737
  : {}),
@@ -3387,8 +3825,8 @@ async function handleMutationHumanGate(req, res, deps, sessionId) {
3387
3825
  expiresAt: receipt.expiresAt,
3388
3826
  operationId,
3389
3827
  inspectHref: operationId
3390
- ? `/inspect/?operationId=${encodeURIComponent(operationId)}#/`
3391
- : `/inspect/#/`,
3828
+ ? `/#/inspect?operationId=${encodeURIComponent(operationId)}`
3829
+ : `/#/inspect`,
3392
3830
  });
3393
3831
  appendHumanGateCard(deps, sessionId, card);
3394
3832
  if (operationId) {
@@ -3512,7 +3950,8 @@ export async function handleChatCompact(req, res, deps, sessionId) {
3512
3950
  }
3513
3951
  const rawInstructions = typeof body.instructions === "string" ? body.instructions : undefined;
3514
3952
  const currentTodos = projectCurrentTodos(deps.events.snapshot(sessionId));
3515
- const customInstructions = buildTodoCompactionInstructions(rawInstructions, currentTodos);
3953
+ const todoInstructions = buildTodoCompactionInstructions(rawInstructions, currentTodos);
3954
+ const customInstructions = buildGoalCompactionInstructions(todoInstructions, deps.goalRegistry?.get(sessionId) ?? null);
3516
3955
  const turnId = deps.events.latestTurnId(sessionId) ?? `${sessionId}:compact`;
3517
3956
  const compactionId = `${sessionId}:compaction:${randomUUID()}`;
3518
3957
  deps.events.append(sessionId, turnId, {
@@ -3649,6 +4088,17 @@ export async function handleChatRequest(req, res, deps, pathname, workspaceScope
3649
4088
  await handleChatCapabilities(req, res, deps);
3650
4089
  return true;
3651
4090
  }
4091
+ const htmlPreviewRoute = matchRepoHtmlPreviewRoute(pathname);
4092
+ if (method === "GET" && htmlPreviewRoute) {
4093
+ if (!htmlPreviewRoute.rawPath)
4094
+ sendJson(res, 400, {
4095
+ ok: false,
4096
+ error: { code: "INVALID_PATH", message: "path is required" },
4097
+ });
4098
+ else
4099
+ await handleRepoHtmlPreview(res, deps, htmlPreviewRoute.rawPath);
4100
+ return true;
4101
+ }
3652
4102
  if (method === "GET" && pathname === "/api/operator/v1/chat/repo/tree") {
3653
4103
  await handleRepoBrowser(res, deps, "tree", url.searchParams.get("path") ?? "");
3654
4104
  return true;
@@ -3780,6 +4230,12 @@ export async function handleChatRequest(req, res, deps, pathname, workspaceScope
3780
4230
  await handleCreateChatTurn(req, res, deps, decodeURIComponent(sessionTurn[1]));
3781
4231
  return true;
3782
4232
  }
4233
+ const sessionGoal = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/goal$/);
4234
+ if (sessionGoal &&
4235
+ (method === "GET" || method === "POST" || method === "PATCH" || method === "DELETE")) {
4236
+ await handleChatGoal(req, res, deps, decodeURIComponent(sessionGoal[1]));
4237
+ return true;
4238
+ }
3783
4239
  const deferredTurn = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/deferred-turn$/);
3784
4240
  if (method === "PUT" && deferredTurn) {
3785
4241
  await handlePutDeferredTurn(req, res, deps, decodeURIComponent(deferredTurn[1]));