@tea-agent/loop-agent 0.40.0-next.9 → 0.41.0-next.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 (148) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +29 -20
  3. package/README.md +2 -2
  4. package/dist/application/dag/generate-task-dag.js +6 -2
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/cli/command-definitions.js +9 -5
  7. package/dist/cli/program.js +38 -3
  8. package/dist/commands/client-recovery.js +8 -36
  9. package/dist/commands/init-model-catalog.js +22 -0
  10. package/dist/commands/task-advance.js +23 -1
  11. package/dist/commands/task-source-prepare.js +5 -0
  12. package/dist/commands/task-status.js +1 -0
  13. package/dist/executors/pi-sdk-executor.js +33 -5
  14. package/dist/executors/shell-executor.js +94 -20
  15. package/dist/governance/checks.js +1 -0
  16. package/dist/shared/operator/capabilities.js +9 -1
  17. package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
  18. package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
  19. package/dist/shared/pi-context-pressure/env.js +58 -0
  20. package/dist/shared/pi-context-pressure/extension.js +100 -0
  21. package/dist/shared/pi-context-pressure/index.js +7 -0
  22. package/dist/shared/pi-context-pressure/overflow.js +252 -0
  23. package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
  24. package/dist/shared/pi-context-pressure/telemetry.js +51 -0
  25. package/dist/worker/console/chat/assistant-content.js +7 -0
  26. package/dist/worker/console/chat/chat-event-store.js +82 -11
  27. package/dist/worker/console/chat/goal-round-driver.js +72 -0
  28. package/dist/worker/console/chat/goals.js +300 -0
  29. package/dist/worker/console/chat/pi-runtime.js +286 -8
  30. package/dist/worker/console/chat/provider-error.js +2 -1
  31. package/dist/worker/console/chat/repo-browser.js +94 -12
  32. package/dist/worker/console/chat/routes.js +495 -30
  33. package/dist/worker/console/chat/semantic-activity.js +12 -11
  34. package/dist/worker/console/chat/session-catalog.js +48 -0
  35. package/dist/worker/console/chat/session-stats.js +96 -0
  36. package/dist/worker/console/chat/session-store.js +27 -18
  37. package/dist/worker/console/chat/shortcuts.js +7 -0
  38. package/dist/worker/console/chat/sift-bridge.js +1 -0
  39. package/dist/worker/console/chat/turn-process.js +60 -7
  40. package/dist/worker/console/dag-execution-receipt.js +20 -2
  41. package/dist/worker/console/operator-actions.js +20 -5
  42. package/dist/worker/console/operator-selection.js +6 -2
  43. package/dist/worker/console/pi-plugins.catalog.json +73658 -0
  44. package/dist/worker/console/pi-plugins.js +179 -0
  45. package/dist/worker/console/routes.js +246 -6
  46. package/dist/worker/console/server.js +173 -42
  47. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bs-CDAXM.js → abnfDiagram-N423BO3Z-CV1qyRbn.js} +1 -1
  48. package/dist/worker/console/static/assets/{arc-CAkA3We3.js → arc-B_ljC4ma.js} +1 -1
  49. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DEmI_zqr.js → architectureDiagram-T3A2C74G-DTBqsauL.js} +1 -1
  50. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-uDHANebE.js → blockDiagram-VBNYF7ZC-CMZj6nYE.js} +1 -1
  51. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bv97b4_B.js → c4Diagram-5PPSVZJV-C4ey_S7v.js} +1 -1
  52. package/dist/worker/console/static/assets/channel-BvYm_83n.js +1 -0
  53. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-shmQKsFz.js → chunk-2GRJ4B5K-CGcAlY6Z.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BAraNvcN.js → chunk-2Q5K7J3B-XRSM64J1.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BirhxUop.js → chunk-5RXB4S5H-COSIFYbz.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C9LgUJpR.js → chunk-5VM5RSS4-BwuaXiOK.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DrU-s-t7.js → chunk-6Q2QTUOP-DNbFuVlv.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Tx-H_FA2.js → chunk-GF5L2VYU-C4DZKffE.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BjLtNFrR.js → chunk-JWPE2WC7-M3A6iczx.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-UNLVwSsy.js → chunk-KBJHAD2P-C5zDzQEM.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CBEUX8XD.js → chunk-RYQCIY6F-uAcUDub8.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-WTL7IZOq.js → chunk-XXDRQBXY-EPpz2uSP.js} +1 -1
  63. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BNdy3f9F.js +1 -0
  64. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BNdy3f9F.js +1 -0
  65. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CkHG6W3s.js → cose-bilkent-JH36ORCC-BAkfFGTf.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BRzFqTaL.js → cynefin-VYW2F7L2-Cjb4d22A.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2_CFp_i7.js → cynefinDiagram-MW4NZA55-Cf7QSgcu.js} +1 -1
  68. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJtsmdkE.js → dagre-VZM6K2ZE-UlyVBCtl.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DNEFFo6w.js → diagram-7IWD3JNH-DWdRAZ8p.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B_IWV7eo.js → diagram-B4RE2ZJO-Y2ag2Pm0.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DvvRx2dz.js → diagram-LBJQPF4R-DBHcuPXI.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CsibsWLg.js → diagram-Q27KOJAE-BK7eno2C.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-UB23O5K3-les_djBQ.js → diagram-UB23O5K3-Dbyku82r.js} +1 -1
  74. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BEm2n7qJ.js → ebnfDiagram-BXEA7PRR--rS0Q41A.js} +1 -1
  75. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BI7vHHVm.js → erDiagram-JOGREHBK-Cms8pJeK.js} +1 -1
  76. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CUFCERrB.js → flowDiagram-UKHOOZJN-DouONwBy.js} +1 -1
  77. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-wxyLiGWt.js → ganttDiagram-PKOTCBZU-DuKB5yDe.js} +1 -1
  78. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bt897K8Z.js → gitGraphDiagram-DS77QQ5N-DE6zp0SI.js} +1 -1
  79. package/dist/worker/console/static/assets/index-C39zQYik.js +451 -0
  80. package/dist/worker/console/static/assets/index-Crixt9tN.css +1 -0
  81. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DYNx_IPv.js → infoDiagram-6WML65LV-BLETv9SH.js} +1 -1
  82. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CzpLNEu6.js → ishikawaDiagram-WSZJBQD7-DyU-2lJb.js} +1 -1
  83. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-97f9owNU.js → journeyDiagram-NVQOT4AX-BotFSygt.js} +1 -1
  84. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BxM9hyBn.js → kanban-definition-27J2QSJJ-CqcAMgVk.js} +1 -1
  85. package/dist/worker/console/static/assets/{linear-ByuxcHvp.js → linear-Bf4U8h5G.js} +1 -1
  86. package/dist/worker/console/static/assets/{mermaid.core-DwPGvpCv.js → mermaid.core-D0JEVefG.js} +5 -5
  87. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DCoqG9RT.js → mindmap-definition-FAOFIHXS-DEFgNgOo.js} +1 -1
  88. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BLSBmBMx.js → pegDiagram-VL7TDLO6-CirKhxo1.js} +1 -1
  89. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B0t_0oJW.js → pieDiagram-7S7Q4E2Y-BlaQYInF.js} +1 -1
  90. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js → quadrantDiagram-CIZ2JOQS-BbyzqH4U.js} +1 -1
  91. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-B3jRdATC.js → railroadDiagram-AXF67PYL-qA-XykV6.js} +1 -1
  92. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-cTtXEN.js → requirementDiagram-LRYGKXZP-Cc85Zcxc.js} +1 -1
  93. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B25igJRE.js → sankeyDiagram-W5VNT64P-CS7BVnoH.js} +1 -1
  94. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-jW_lM1_D.js → sequenceDiagram-SI44F4Z6-DMQ-kKmZ.js} +1 -1
  95. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CrzTgsHE.js → sizeCapture-X5ZJPWSS-DXKs1dcE.js} +1 -1
  96. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B01MvlqE.js → stateDiagram-OKZ733FA-xG1ti87X.js} +1 -1
  97. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-tYINJZ8x.js +1 -0
  98. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-YcF12FiU.js → swimlanes-SLNWSIFB-BQwmKGHd.js} +2 -2
  99. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-OoBOXPiG.js +8 -0
  100. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ClnRcg2s.js → timeline-definition-Z64GVDOM-BwL9oHtP.js} +1 -1
  101. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B6gr_z-q.js → vennDiagram-T6HMQDX7-BWk-i_Y1.js} +1 -1
  102. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CvSgguGH.js → wardleyDiagram-T6FBY63Y-DAvETpEZ.js} +1 -1
  103. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-23OtSZCb.js → xychartDiagram-ELKLHX3M-Cd4pASnF.js} +1 -1
  104. package/dist/worker/console/static/index.html +2 -2
  105. package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
  106. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +95 -25
  107. package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
  108. package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
  109. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
  110. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +26 -19
  111. package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
  112. package/dist/worker/console/static-src/operator-chat/useChatThread.js +14 -1
  113. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
  114. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
  115. package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
  116. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
  117. package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
  118. package/dist/worker/console/static-src/shell/workspace-route.js +3 -1
  119. package/dist/worker/console/workspace-context.js +28 -3
  120. package/dist/worker/console/workspace-registry.js +333 -60
  121. package/dist/worker/observability/read-model.js +4 -0
  122. package/dist/worker/observe/static/inspect-bootstrap.d.ts +8 -0
  123. package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
  124. package/dist/worker/observe/static/inspect-workspace.js +54 -10
  125. package/dist/worker/observe/static/operator-chrome.css +23 -4
  126. package/dist/worker/observe/static/operator-chrome.js +7 -2
  127. package/dist/worker/observe/static/styles.css +87 -25
  128. package/dist/worker/observe/static/views/dag-inspector.js +42 -1
  129. package/dist/worker/observe/static/views/dags.js +7 -5
  130. package/dist/worker/observe/static/views/session-timeline.js +135 -0
  131. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +162 -8
  132. package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
  133. package/dist/workflows/dag/backend-test-result-contract.js +5 -1
  134. package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
  135. package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
  136. package/dist/workflows/dag/init-hybrid.js +5 -4
  137. package/docs/architecture/runtime-boundaries.md +2 -1
  138. package/docs/templates/backend-test-dag.json +6 -5
  139. package/docs/templates/init-managed-agents.md +1 -1
  140. package/package.json +3 -2
  141. package/skills/loop-agent/references/command-reference.md +15 -1
  142. package/dist/worker/console/static/assets/channel-amxUpk7o.js +0 -1
  143. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +0 -1
  144. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +0 -1
  145. package/dist/worker/console/static/assets/index-BHhUOTri.css +0 -1
  146. package/dist/worker/console/static/assets/index-DqJbO3-p.js +0 -407
  147. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +0 -1
  148. 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";
@@ -34,11 +34,84 @@ import { dispatchOperatorAction } from "../operator-actions.js";
34
34
  import { makeHumanGateCard, } from "./human-gate-card.js";
35
35
  import { ContractApplyReceiptStore } from "./contract-apply-receipt-store.js";
36
36
  import { projectTaskContext } from "./context-panel.js";
37
- import { deriveChatSessionTelemetry } from "./session-stats.js";
37
+ import { deriveChatSessionTelemetry, deriveChatTurnTelemetry } from "./session-stats.js";
38
38
  import { contractApplyPayloadHash, issueHumanGateToken, verifyHumanGateToken, } from "../human-gate-token.js";
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
103
+ ? {
104
+ eventSeq: start.seq,
105
+ turnId: start.turnId,
106
+ startedAt: start.at,
107
+ }
108
+ : {}),
109
+ ...(finish
110
+ ? { finishedSeq: finish.seq, finishedAt: finish.at }
111
+ : {}),
112
+ };
113
+ });
114
+ }
42
115
  function turnSummary(turn) {
43
116
  return {
44
117
  turnId: turn.turnId,
@@ -514,6 +587,7 @@ workspaceId) {
514
587
  // a fallback for direct (non-dispatcher) handler tests.
515
588
  workspaceId: workspaceId ?? (typeof body.workspaceId === "string" && body.workspaceId ? body.workspaceId : undefined),
516
589
  });
590
+ await deps.attachSession?.(handle.sessionId).catch(() => undefined);
517
591
  if (snapshot)
518
592
  deps.events.append(handle.sessionId, `${handle.sessionId}:snapshot`, {
519
593
  kind: "compact",
@@ -595,6 +669,7 @@ export async function handleForkChatSession(req, res, deps, sourceSessionId) {
595
669
  targetSessionId: handle.sessionId,
596
670
  targetSessionFile: handle.sessionFile,
597
671
  });
672
+ await deps.attachSession?.(handle.sessionId).catch(() => undefined);
598
673
  const copied = deps.events.forkContextRefs({
599
674
  sourceSessionId,
600
675
  targetSessionId: handle.sessionId,
@@ -746,6 +821,7 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
746
821
  eventEpoch: deps.events.epoch(sessionId),
747
822
  session: {
748
823
  ...record,
824
+ messages: projectStoredMessagesWithEventOrder(record.messages, snapshot),
749
825
  composerDraft,
750
826
  activeTurnId: activeTurn?.turnId,
751
827
  turnPhase: activeTurn
@@ -755,10 +831,15 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
755
831
  : "idle",
756
832
  pendingQuestion: pendingQuestion ?? null,
757
833
  todos: projectCurrentTodos(snapshot) ?? [],
834
+ goal: deps.goalRegistry?.get(sessionId) ?? null,
758
835
  },
759
836
  hasBash: true,
760
837
  sessionStats: telemetry.stats,
761
838
  usage: telemetry.usage,
839
+ turnTelemetry: deriveChatTurnTelemetry(snapshot),
840
+ contextPressure: typeof deps.runtime.getContextPressureTelemetry === "function"
841
+ ? deps.runtime.getContextPressureTelemetry(sessionId)
842
+ : undefined,
762
843
  compactions: snapshot
763
844
  .filter((event) => event.kind === "compact" ||
764
845
  event.kind === "compaction_start" ||
@@ -767,6 +848,8 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
767
848
  eventId: event.eventId,
768
849
  kind: event.kind,
769
850
  at: event.at,
851
+ seq: event.seq,
852
+ turnId: event.turnId,
770
853
  data: event.data,
771
854
  })),
772
855
  lastEventId: lastEvent?.eventId ?? null,
@@ -1070,9 +1153,162 @@ export async function handleGetChatSessionState(_req, res, deps, sessionId) {
1070
1153
  deferredTurns: projectDeferredTurnSummaries(record.deferredTurns ?? []),
1071
1154
  pendingQuestion: pendingQuestion ?? null,
1072
1155
  todos: projectCurrentTodos(deps.events.snapshot(sessionId)) ?? [],
1156
+ goal: deps.goalRegistry?.get(sessionId) ?? null,
1073
1157
  },
1074
1158
  });
1075
1159
  }
1160
+ function goalHttpStatus(code) {
1161
+ if (code === "GOAL_NOT_FOUND")
1162
+ return 404;
1163
+ if (code === "GOAL_INVALID_INPUT")
1164
+ return 400;
1165
+ return 409;
1166
+ }
1167
+ export async function handleChatGoal(req, res, deps, sessionId) {
1168
+ const registry = deps.goalRegistry;
1169
+ if (!registry) {
1170
+ sendJson(res, 503, {
1171
+ ok: false,
1172
+ error: { code: "GOAL_UNAVAILABLE", message: "Goal runtime is unavailable" },
1173
+ });
1174
+ return;
1175
+ }
1176
+ if (!(await deps.store.get(sessionId))) {
1177
+ sendJson(res, 404, {
1178
+ ok: false,
1179
+ error: { code: "NOT_FOUND", message: `chat session not found: ${sessionId}` },
1180
+ });
1181
+ return;
1182
+ }
1183
+ if (req.method === "GET") {
1184
+ sendJson(res, 200, { ok: true, goal: registry.get(sessionId) });
1185
+ return;
1186
+ }
1187
+ const gate = gateMutation(req, deps);
1188
+ if (!gate.ok) {
1189
+ sendJson(res, gate.status, {
1190
+ ok: false,
1191
+ error: { code: gate.code, message: gate.message },
1192
+ });
1193
+ return;
1194
+ }
1195
+ let body = {};
1196
+ try {
1197
+ body = await readJsonBody(req);
1198
+ }
1199
+ catch (error) {
1200
+ sendJson(res, 400, {
1201
+ ok: false,
1202
+ error: {
1203
+ code: "GOAL_INVALID_INPUT",
1204
+ message: error instanceof Error ? error.message : String(error),
1205
+ },
1206
+ });
1207
+ return;
1208
+ }
1209
+ if (req.method === "POST") {
1210
+ const result = registry.create({
1211
+ sessionId,
1212
+ objective: body.objective,
1213
+ maxRounds: typeof body.maxRounds === "number" ? body.maxRounds : undefined,
1214
+ arm: true,
1215
+ });
1216
+ if (!result.ok) {
1217
+ sendJson(res, goalHttpStatus(result.code), {
1218
+ ok: false,
1219
+ error: { code: result.code, message: result.message },
1220
+ goal: result.goal,
1221
+ });
1222
+ return;
1223
+ }
1224
+ deps.goalRoundDriver?.wake(sessionId);
1225
+ sendJson(res, 201, { ok: true, goal: result.goal });
1226
+ return;
1227
+ }
1228
+ const current = registry.get(sessionId);
1229
+ const goalId = typeof body.goalId === "string" ? body.goalId : "";
1230
+ const revision = typeof body.revision === "number" ? body.revision : Number.NaN;
1231
+ if (!current || !goalId || !Number.isSafeInteger(revision)) {
1232
+ sendJson(res, current ? 400 : 404, {
1233
+ ok: false,
1234
+ error: {
1235
+ code: current ? "GOAL_INVALID_INPUT" : "GOAL_NOT_FOUND",
1236
+ message: current
1237
+ ? "goalId and integer revision are required"
1238
+ : "Goal not found",
1239
+ },
1240
+ });
1241
+ return;
1242
+ }
1243
+ if (req.method === "DELETE") {
1244
+ const result = registry.clear({
1245
+ sessionId,
1246
+ goalId,
1247
+ expectedRevision: revision,
1248
+ });
1249
+ if (!result.ok) {
1250
+ sendJson(res, goalHttpStatus(result.code), {
1251
+ ok: false,
1252
+ error: { code: result.code, message: result.message },
1253
+ goal: result.goal,
1254
+ });
1255
+ return;
1256
+ }
1257
+ deps.goalRoundDriver?.cancel(sessionId);
1258
+ sendJson(res, 200, { ok: true, goal: null });
1259
+ return;
1260
+ }
1261
+ if (req.method === "PATCH") {
1262
+ const action = typeof body.action === "string" ? body.action : "edit";
1263
+ let phase;
1264
+ let arm;
1265
+ if (action === "pause") {
1266
+ phase = "paused";
1267
+ arm = false;
1268
+ }
1269
+ else if (action === "resume") {
1270
+ phase = "active";
1271
+ arm = true;
1272
+ }
1273
+ else if (action === "edit") {
1274
+ phase = current.phase;
1275
+ arm = current.armed;
1276
+ }
1277
+ else {
1278
+ sendJson(res, 400, {
1279
+ ok: false,
1280
+ error: { code: "GOAL_INVALID_INPUT", message: "unsupported Goal action" },
1281
+ });
1282
+ return;
1283
+ }
1284
+ const result = registry.update({
1285
+ sessionId,
1286
+ goalId,
1287
+ expectedRevision: revision,
1288
+ phase,
1289
+ arm,
1290
+ ...(body.objective !== undefined ? { objective: body.objective } : {}),
1291
+ });
1292
+ if (!result.ok) {
1293
+ sendJson(res, goalHttpStatus(result.code), {
1294
+ ok: false,
1295
+ error: { code: result.code, message: result.message },
1296
+ goal: result.goal,
1297
+ });
1298
+ return;
1299
+ }
1300
+ if (action === "resume")
1301
+ deps.goalRoundDriver?.wake(sessionId);
1302
+ else if (action === "pause")
1303
+ deps.goalRoundDriver?.cancel(sessionId);
1304
+ sendJson(res, 200, { ok: true, goal: result.goal });
1305
+ return;
1306
+ }
1307
+ sendJson(res, 405, {
1308
+ ok: false,
1309
+ error: { code: "METHOD_NOT_ALLOWED", message: "method not allowed" },
1310
+ });
1311
+ }
1076
1312
  export async function handleAnswerUserQuestion(req, res, deps, sessionId, requestId) {
1077
1313
  const gate = gateMutation(req, deps);
1078
1314
  if (!gate.ok) {
@@ -1121,27 +1357,53 @@ export async function handleCancelUserQuestion(req, res, deps, sessionId, reques
1121
1357
  }
1122
1358
  }
1123
1359
  export async function handleListChatSessions(_req, res, deps) {
1124
- const sessions = await deps.store.list({ includeArchived: true });
1360
+ const sessions = await listChatSessionSummaries(deps);
1125
1361
  sendJson(res, 200, {
1126
1362
  ok: true,
1127
1363
  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
- }),
1364
+ sessions,
1142
1365
  hasBash: true,
1143
1366
  });
1144
1367
  }
1368
+ /**
1369
+ * Build sidebar-only summaries from one Workspace store without opening a
1370
+ * runtime. Message bodies, Pi session paths and tool audit rows belong to the
1371
+ * detail endpoint; returning them here makes the global multi-Workspace index
1372
+ * grow with the entire chat history.
1373
+ */
1374
+ export async function listChatSessionSummaries(deps, ownerWorkspaceId) {
1375
+ const sessions = await deps.store.list({ includeArchived: true });
1376
+ return sessions.map((session) => {
1377
+ const active = deps.events?.getActiveTurn(session.sessionId);
1378
+ const workspaceId = ownerWorkspaceId ?? session.workspaceId;
1379
+ return {
1380
+ sessionId: session.sessionId,
1381
+ ...(workspaceId ? { workspaceId } : {}),
1382
+ state: session.state,
1383
+ createdAt: session.createdAt,
1384
+ updatedAt: session.updatedAt,
1385
+ ...(session.title !== undefined ? { title: session.title } : {}),
1386
+ ...(session.displayTitle !== undefined
1387
+ ? { displayTitle: session.displayTitle }
1388
+ : {}),
1389
+ ...(session.modelProvider !== undefined
1390
+ ? { modelProvider: session.modelProvider }
1391
+ : {}),
1392
+ ...(session.modelId !== undefined ? { modelId: session.modelId } : {}),
1393
+ ...(session.thinkingLevel !== undefined
1394
+ ? { thinkingLevel: session.thinkingLevel }
1395
+ : {}),
1396
+ activeTurnId: active?.turnId,
1397
+ turnPhase: active?.abortRequestedAt
1398
+ ? "stopping"
1399
+ : active
1400
+ ? "running"
1401
+ : "idle",
1402
+ deferredTurn: projectDeferredTurnSummary(session.deferredTurns?.[0]),
1403
+ deferredTurns: projectDeferredTurnSummaries(session.deferredTurns ?? []),
1404
+ };
1405
+ });
1406
+ }
1145
1407
  export async function handlePatchChatSession(req, res, deps, sessionId) {
1146
1408
  const gate = gateMutation(req, deps);
1147
1409
  if (!gate.ok) {
@@ -1644,6 +1906,22 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1644
1906
  });
1645
1907
  return;
1646
1908
  }
1909
+ try {
1910
+ trustedPayload?.onTurnCreated?.(turn.turnId);
1911
+ }
1912
+ catch (error) {
1913
+ const message = error instanceof Error ? error.message : String(error);
1914
+ deps.events.setTurnState(sessionId, turn.turnId, "failed", {
1915
+ code: "GOAL_ADMISSION_STALE",
1916
+ message,
1917
+ });
1918
+ sendJson(res, 409, {
1919
+ ok: false,
1920
+ accepted: false,
1921
+ error: { code: "GOAL_ADMISSION_STALE", message },
1922
+ });
1923
+ return;
1924
+ }
1647
1925
  const controller = new AbortController();
1648
1926
  const execution = deps.turnExecutions?.register({
1649
1927
  sessionId,
@@ -1693,6 +1971,7 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1693
1971
  if (placeholder)
1694
1972
  emitSessionTitle(deps, sessionId, turn.turnId, placeholder);
1695
1973
  },
1974
+ persistUserMessage: trustedPayload?.persistUserMessage,
1696
1975
  onOperationAccepted: (accepted) => deps.operationLinker.link({
1697
1976
  sessionId,
1698
1977
  turnId: turn.turnId,
@@ -1755,16 +2034,139 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1755
2034
  }
1756
2035
  }
1757
2036
  finally {
2037
+ if (terminalState !== "settled") {
2038
+ pauseGoalForSafety(deps, sessionId, turn.turnId);
2039
+ deps.goalRoundDriver?.cancel(sessionId);
2040
+ }
1758
2041
  execution?.resolveCompletion({
1759
2042
  ok: terminalState === "settled",
1760
2043
  state: terminalState,
1761
2044
  error: terminalError,
1762
2045
  });
1763
2046
  deps.turnExecutions?.clear(sessionId, turn.turnId);
1764
- void dispatchDeferredTurn(deps, sessionId);
2047
+ void dispatchDeferredTurn(deps, sessionId).finally(() => {
2048
+ deps.goalRoundDriver?.wake(sessionId);
2049
+ });
1765
2050
  }
1766
2051
  })();
1767
2052
  }
2053
+ function buildGoalRoundPrompt(goal) {
2054
+ const remaining = Math.max(0, goal.maxRounds - goal.round);
2055
+ return [
2056
+ buildGoalContinuationContext(goal),
2057
+ `This is autonomous Goal round ${goal.round + 1} of at most ${goal.maxRounds} (${remaining} rounds remain before this one).`,
2058
+ "Continue from the current session state. Prefer the user's queued or newly supplied instructions when present. Use todo_write for multi-step work.",
2059
+ "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.",
2060
+ ].filter(Boolean).join("\n\n");
2061
+ }
2062
+ function pauseGoalForSafety(deps, sessionId, turnId) {
2063
+ const current = deps.goalRegistry?.get(sessionId);
2064
+ if (!current || current.phase !== "active") {
2065
+ deps.goalRegistry?.disarm(sessionId);
2066
+ return;
2067
+ }
2068
+ deps.goalRegistry?.update({
2069
+ sessionId,
2070
+ goalId: current.goalId,
2071
+ expectedRevision: current.revision,
2072
+ phase: "paused",
2073
+ arm: false,
2074
+ turnId,
2075
+ });
2076
+ }
2077
+ export function linkedOperationIdsForGoal(events) {
2078
+ const linked = new Set();
2079
+ for (const event of events) {
2080
+ const operationId = event.data && typeof event.data === "object"
2081
+ ? event.data.operationId
2082
+ : undefined;
2083
+ if (typeof operationId !== "string" || !operationId)
2084
+ continue;
2085
+ if (event.kind === "operation-ref" || event.kind === "operation-status")
2086
+ linked.add(operationId);
2087
+ if (event.kind === "operation-unlinked")
2088
+ linked.delete(operationId);
2089
+ }
2090
+ return [...linked];
2091
+ }
2092
+ /** One admission attempt used by the process-local GoalRoundDriver. */
2093
+ export async function driveGoalRound(deps, sessionId) {
2094
+ const registry = deps.goalRegistry;
2095
+ if (!registry || !registry.isArmed(sessionId))
2096
+ return "idle";
2097
+ const goal = registry.get(sessionId);
2098
+ if (!goal || goal.phase !== "active" || !goal.armed)
2099
+ return "idle";
2100
+ if (goal.round >= goal.maxRounds) {
2101
+ registry.update({
2102
+ sessionId,
2103
+ goalId: goal.goalId,
2104
+ expectedRevision: goal.revision,
2105
+ phase: "paused",
2106
+ arm: false,
2107
+ });
2108
+ return "idle";
2109
+ }
2110
+ const record = await deps.store.get(sessionId);
2111
+ if (!record) {
2112
+ registry.disarm(sessionId);
2113
+ return "idle";
2114
+ }
2115
+ const runtimeFacts = deps.runtime.getSessionBusyFacts(sessionId);
2116
+ const operationStore = deps.runtime.actionContext?.operations;
2117
+ const linkedOperationIds = linkedOperationIdsForGoal(deps.events.snapshot(sessionId));
2118
+ const operations = operationStore
2119
+ ? await Promise.all(linkedOperationIds.map((operationId) => operationStore.get(operationId)))
2120
+ : [];
2121
+ const admission = classifyGoalRoundAdmission({
2122
+ phase: goal.phase,
2123
+ armed: goal.armed,
2124
+ round: goal.round,
2125
+ maxRounds: goal.maxRounds,
2126
+ hasQueuedUserTurn: (record.deferredTurns ?? []).some((item) => item.state === "queued" || item.state === "dispatching"),
2127
+ hasActiveTurn: Boolean(deps.events.getActiveTurn(sessionId)),
2128
+ runtimeBusy: Object.values(runtimeFacts).some((value) => value === true),
2129
+ operationBusy: operations.some((operation) => Boolean(operation) && !isTerminalOperationState(operation.state)),
2130
+ });
2131
+ if (admission.action === "wait")
2132
+ return "wait";
2133
+ if (admission.action === "idle")
2134
+ return "idle";
2135
+ const expectedRound = goal.round + 1;
2136
+ const clientRequestId = `goal:${goal.goalId}:${expectedRound}`;
2137
+ const started = await startTrustedChatTurn(deps, sessionId, {
2138
+ clientRequestId,
2139
+ text: buildGoalRoundPrompt(goal),
2140
+ images: [],
2141
+ persistUserMessage: false,
2142
+ onTurnCreated: (turnId) => {
2143
+ const advanced = registry.beginRound({
2144
+ sessionId,
2145
+ goalId: goal.goalId,
2146
+ expectedRevision: goal.revision,
2147
+ turnId,
2148
+ });
2149
+ if (!advanced.ok) {
2150
+ registry.disarm(sessionId);
2151
+ throw new Error(advanced.message);
2152
+ }
2153
+ },
2154
+ });
2155
+ const accepted = started.body.accepted === true;
2156
+ const turnId = started.body.turn && typeof started.body.turn === "object"
2157
+ ? started.body.turn.turnId
2158
+ : undefined;
2159
+ if (!accepted || !turnId) {
2160
+ const code = started.body.error && typeof started.body.error === "object"
2161
+ ? String(started.body.error.code ?? "")
2162
+ : "";
2163
+ if (code === "TURN_ACTIVE")
2164
+ return "wait";
2165
+ registry.disarm(sessionId);
2166
+ return "idle";
2167
+ }
2168
+ return "started";
2169
+ }
1768
2170
  async function emitDeferredTurnUpdate(deps, sessionId, turnId, _deferredTurn, _revision) {
1769
2171
  const record = await deps.store.get(sessionId);
1770
2172
  if (!record)
@@ -1985,6 +2387,8 @@ export async function handleAbortChatTurn(req, res, deps, sessionId, turnId) {
1985
2387
  });
1986
2388
  return;
1987
2389
  }
2390
+ pauseGoalForSafety(deps, sessionId, turnId);
2391
+ deps.goalRoundDriver?.cancel(sessionId);
1988
2392
  if (isTerminalTurnState(turn.state)) {
1989
2393
  sendJson(res, 200, {
1990
2394
  ok: true,
@@ -2155,6 +2559,8 @@ export async function handleDeleteChatSession(req, res, deps, sessionId) {
2155
2559
  }
2156
2560
  await deps.questionRegistry?.cancelForSession(sessionId, "SESSION_DELETED").catch(() => undefined);
2157
2561
  deps.todoRegistry?.clear(sessionId);
2562
+ deps.goalRegistry?.clearRuntime(sessionId);
2563
+ deps.goalRoundDriver?.cancel(sessionId);
2158
2564
  deps.runtime.dispose(sessionId);
2159
2565
  deps.operationLinker.clearSession(sessionId);
2160
2566
  deps.events.clear(sessionId);
@@ -2170,6 +2576,7 @@ export async function handleDeleteChatSession(req, res, deps, sessionId) {
2170
2576
  });
2171
2577
  return;
2172
2578
  }
2579
+ await deps.detachSession?.(sessionId).catch(() => undefined);
2173
2580
  sendJson(res, 200, { ok: true, sessionId, disposed: true, removed: true });
2174
2581
  }
2175
2582
  export async function handleGetTaskContext(_res, res, deps, sessionId) {
@@ -2229,6 +2636,46 @@ async function handleRepoBrowser(res, deps, kind, rawPath) {
2229
2636
  });
2230
2637
  }
2231
2638
  }
2639
+ async function handleRepoHtmlPreview(res, deps, rawPath) {
2640
+ try {
2641
+ const asset = await readRepoHtmlAsset(deps.appData.repoRoot, rawPath);
2642
+ const origin = deps.getConsoleOrigin();
2643
+ res.writeHead(200, {
2644
+ "Content-Type": asset.contentType,
2645
+ "Content-Length": asset.body.byteLength,
2646
+ "Cache-Control": "no-store",
2647
+ "X-Content-Type-Options": "nosniff",
2648
+ "Referrer-Policy": "no-referrer",
2649
+ // The iframe also has an empty sandbox attribute. This response-level
2650
+ // sandbox keeps direct/popup loads isolated, while explicit local-origin
2651
+ // style/image/font/media sources let relative project assets render.
2652
+ "Content-Security-Policy": [
2653
+ "sandbox",
2654
+ "default-src 'none'",
2655
+ "script-src 'none'",
2656
+ "object-src 'none'",
2657
+ "frame-src 'none'",
2658
+ "form-action 'none'",
2659
+ "base-uri 'none'",
2660
+ "connect-src 'none'",
2661
+ `style-src 'unsafe-inline' ${origin}`,
2662
+ `img-src data: blob: ${origin}`,
2663
+ `font-src data: ${origin}`,
2664
+ `media-src blob: ${origin}`,
2665
+ ].join("; "),
2666
+ });
2667
+ res.end(asset.body);
2668
+ }
2669
+ catch (error) {
2670
+ const failure = error instanceof RepoBrowserError
2671
+ ? error
2672
+ : new RepoBrowserError(500, "READ_FAILED", error instanceof Error ? error.message : String(error));
2673
+ sendJson(res, failure.status, {
2674
+ ok: false,
2675
+ error: { code: failure.code, message: failure.message },
2676
+ });
2677
+ }
2678
+ }
2232
2679
  async function handlePiConfig(req, res, deps, resource) {
2233
2680
  try {
2234
2681
  if (resource === "models") {
@@ -2974,7 +3421,7 @@ async function ctxConfirmationCard(deps, sessionId, turnId, confirmationId) {
2974
3421
  taskId,
2975
3422
  confirmationId,
2976
3423
  expiresAt: confirmation.expiresAt,
2977
- inspectHref: `/inspect/?taskId=${encodeURIComponent(taskId)}#/`,
3424
+ inspectHref: `/#/inspect?taskId=${encodeURIComponent(taskId)}`,
2978
3425
  humanGateToken: confirmation.humanGateToken,
2979
3426
  displayOnlyChallenges: [
2980
3427
  "source",
@@ -3000,7 +3447,7 @@ async function ctxMutationGateCard(deps, sessionId, receiptId) {
3000
3447
  receiptId: receipt.receiptId,
3001
3448
  expiresAt: receipt.expiresAt,
3002
3449
  humanGateToken: receipt.humanGateToken,
3003
- inspectHref: `/inspect/#/`,
3450
+ inspectHref: `/#/inspect`,
3004
3451
  }));
3005
3452
  }
3006
3453
  function actionResultBody(result) {
@@ -3089,7 +3536,7 @@ async function handleContractApplyHumanGate(req, res, deps, sessionId, step) {
3089
3536
  humanGateToken,
3090
3537
  contractRevision: binding.revision,
3091
3538
  contractDiffSummary: `revision ${binding.revision}; draft ${binding.draftSha256.slice(0, 12)}; observed ${binding.expectedObservedHash.slice(0, 12)}`,
3092
- inspectHref: `/inspect/?taskId=${encodeURIComponent(taskId)}#/`,
3539
+ inspectHref: `/#/inspect?taskId=${encodeURIComponent(taskId)}`,
3093
3540
  });
3094
3541
  appendHumanGateCard(deps, sessionId, card);
3095
3542
  sendJson(res, 201, { ok: true, card });
@@ -3143,8 +3590,8 @@ async function handleContractApplyHumanGate(req, res, deps, sessionId, step) {
3143
3590
  contractRevision: receipt.revision,
3144
3591
  operationId,
3145
3592
  inspectHref: operationId
3146
- ? `/inspect/?operationId=${encodeURIComponent(operationId)}#/`
3147
- : `/inspect/?taskId=${encodeURIComponent(receipt.taskId)}#/`,
3593
+ ? `/#/inspect?operationId=${encodeURIComponent(operationId)}`
3594
+ : `/#/inspect?taskId=${encodeURIComponent(receipt.taskId)}`,
3148
3595
  });
3149
3596
  appendHumanGateCard(deps, sessionId, card);
3150
3597
  sendJson(res, 202, {
@@ -3190,8 +3637,8 @@ async function handleContractApplyHumanGate(req, res, deps, sessionId, step) {
3190
3637
  contractRevision: receipt.revision,
3191
3638
  operationId,
3192
3639
  inspectHref: operationId
3193
- ? `/inspect/?operationId=${encodeURIComponent(operationId)}#/`
3194
- : `/inspect/?taskId=${encodeURIComponent(receipt.taskId)}#/`,
3640
+ ? `/#/inspect?operationId=${encodeURIComponent(operationId)}`
3641
+ : `/#/inspect?taskId=${encodeURIComponent(receipt.taskId)}`,
3195
3642
  });
3196
3643
  appendHumanGateCard(deps, sessionId, card);
3197
3644
  if (operationId)
@@ -3292,8 +3739,8 @@ async function handleDagHumanGate(req, res, deps, sessionId, step) {
3292
3739
  confirmationId,
3293
3740
  expiresAt: String(confirmation?.expiresAt ?? new Date().toISOString()),
3294
3741
  inspectHref: operationId
3295
- ? `/inspect/?operationId=${encodeURIComponent(operationId)}#/`
3296
- : `/inspect/?taskId=${encodeURIComponent(taskId)}#/`,
3742
+ ? `/#/inspect?operationId=${encodeURIComponent(operationId)}`
3743
+ : `/#/inspect?taskId=${encodeURIComponent(taskId)}`,
3297
3744
  ...(confirmation?.humanGateToken
3298
3745
  ? { humanGateToken: confirmation.humanGateToken }
3299
3746
  : {}),
@@ -3387,8 +3834,8 @@ async function handleMutationHumanGate(req, res, deps, sessionId) {
3387
3834
  expiresAt: receipt.expiresAt,
3388
3835
  operationId,
3389
3836
  inspectHref: operationId
3390
- ? `/inspect/?operationId=${encodeURIComponent(operationId)}#/`
3391
- : `/inspect/#/`,
3837
+ ? `/#/inspect?operationId=${encodeURIComponent(operationId)}`
3838
+ : `/#/inspect`,
3392
3839
  });
3393
3840
  appendHumanGateCard(deps, sessionId, card);
3394
3841
  if (operationId) {
@@ -3512,7 +3959,8 @@ export async function handleChatCompact(req, res, deps, sessionId) {
3512
3959
  }
3513
3960
  const rawInstructions = typeof body.instructions === "string" ? body.instructions : undefined;
3514
3961
  const currentTodos = projectCurrentTodos(deps.events.snapshot(sessionId));
3515
- const customInstructions = buildTodoCompactionInstructions(rawInstructions, currentTodos);
3962
+ const todoInstructions = buildTodoCompactionInstructions(rawInstructions, currentTodos);
3963
+ const customInstructions = buildGoalCompactionInstructions(todoInstructions, deps.goalRegistry?.get(sessionId) ?? null);
3516
3964
  const turnId = deps.events.latestTurnId(sessionId) ?? `${sessionId}:compact`;
3517
3965
  const compactionId = `${sessionId}:compaction:${randomUUID()}`;
3518
3966
  deps.events.append(sessionId, turnId, {
@@ -3649,6 +4097,17 @@ export async function handleChatRequest(req, res, deps, pathname, workspaceScope
3649
4097
  await handleChatCapabilities(req, res, deps);
3650
4098
  return true;
3651
4099
  }
4100
+ const htmlPreviewRoute = matchRepoHtmlPreviewRoute(pathname);
4101
+ if (method === "GET" && htmlPreviewRoute) {
4102
+ if (!htmlPreviewRoute.rawPath)
4103
+ sendJson(res, 400, {
4104
+ ok: false,
4105
+ error: { code: "INVALID_PATH", message: "path is required" },
4106
+ });
4107
+ else
4108
+ await handleRepoHtmlPreview(res, deps, htmlPreviewRoute.rawPath);
4109
+ return true;
4110
+ }
3652
4111
  if (method === "GET" && pathname === "/api/operator/v1/chat/repo/tree") {
3653
4112
  await handleRepoBrowser(res, deps, "tree", url.searchParams.get("path") ?? "");
3654
4113
  return true;
@@ -3780,6 +4239,12 @@ export async function handleChatRequest(req, res, deps, pathname, workspaceScope
3780
4239
  await handleCreateChatTurn(req, res, deps, decodeURIComponent(sessionTurn[1]));
3781
4240
  return true;
3782
4241
  }
4242
+ const sessionGoal = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/goal$/);
4243
+ if (sessionGoal &&
4244
+ (method === "GET" || method === "POST" || method === "PATCH" || method === "DELETE")) {
4245
+ await handleChatGoal(req, res, deps, decodeURIComponent(sessionGoal[1]));
4246
+ return true;
4247
+ }
3783
4248
  const deferredTurn = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/deferred-turn$/);
3784
4249
  if (method === "PUT" && deferredTurn) {
3785
4250
  await handlePutDeferredTurn(req, res, deps, decodeURIComponent(deferredTurn[1]));