@tea-agent/loop-agent 0.43.0-next.3 → 0.43.0-next.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +81 -9
  2. package/dist/application/task-lifecycle/advance.js +17 -4
  3. package/dist/application/task-lifecycle/observe.js +43 -29
  4. package/dist/application/task-lifecycle/plan-transitions.js +5 -4
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/executors/dag-pi-executor.js +244 -33
  7. package/dist/executors/pi-executor.js +2 -0
  8. package/dist/executors/pi-sdk-executor.js +75 -39
  9. package/dist/task/source-prepare/source-fidelity-pi.js +2 -2
  10. package/dist/task/source-references.js +48 -23
  11. package/dist/worker/console/chat/assistant-content.js +23 -2
  12. package/dist/worker/console/chat/explore-tools.js +13 -0
  13. package/dist/worker/console/chat/pi-runtime.js +131 -95
  14. package/dist/worker/console/chat/routes.js +175 -54
  15. package/dist/worker/console/chat/scm-routes.js +193 -0
  16. package/dist/worker/console/chat/scm-service.js +253 -0
  17. package/dist/worker/console/chat/scm-tools.js +111 -0
  18. package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
  19. package/dist/worker/console/chat/session-catalog.js +1 -0
  20. package/dist/worker/console/chat/session-mode-view.js +48 -0
  21. package/dist/worker/console/chat/session-mode.js +218 -0
  22. package/dist/worker/console/chat/session-store.js +22 -6
  23. package/dist/worker/console/chat/shortcuts.js +6 -0
  24. package/dist/worker/console/chat/terminal-routes.js +216 -0
  25. package/dist/worker/console/chat/terminal-sessions.js +284 -0
  26. package/dist/worker/console/chat/terminal-tools.js +199 -0
  27. package/dist/worker/console/chat/tool-preview.js +11 -0
  28. package/dist/worker/console/chat/tools.js +2 -0
  29. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-hUmM_GRf.js → abnfDiagram-N423BO3Z-BuyrlONK.js} +1 -1
  30. package/dist/worker/console/static/assets/{arc-Bt-xuieW.js → arc-BvCl_wpi.js} +1 -1
  31. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DTqkL4DJ.js → architectureDiagram-T3A2C74G-BkBpBaRq.js} +1 -1
  32. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Cy0MNnzV.js → blockDiagram-VBNYF7ZC-yve4Mcb_.js} +1 -1
  33. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DDdlT0Pd.js → c4Diagram-5PPSVZJV-9yLGboBx.js} +1 -1
  34. package/dist/worker/console/static/assets/channel-BRahIhjw.js +1 -0
  35. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-3JawwNZ6.js → chunk-2GRJ4B5K-BKP69b8r.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DoTp7Cri.js → chunk-2Q5K7J3B-NWFRnXju.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CcirhZaS.js → chunk-5RXB4S5H-BiweqLEc.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-VrG21gnP.js → chunk-5VM5RSS4-CP9GHmRY.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DDOgIS70.js → chunk-6Q2QTUOP-BiGDkBtj.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CBjmgvJ_.js → chunk-GF5L2VYU-DkJ4ZjQS.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CCQSht8M.js → chunk-JWPE2WC7-B5jjn7zq.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BpGMkUUV.js → chunk-KBJHAD2P-C6KoK9O0.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DNDT41QL.js → chunk-RYQCIY6F-BvNIfyn-.js} +1 -1
  44. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-euXnDFbn.js → chunk-XXDRQBXY-BZ33iX4W.js} +1 -1
  45. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-GOC80mhq.js +1 -0
  46. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-GOC80mhq.js +1 -0
  47. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BJ6NAeql.js → cose-bilkent-JH36ORCC-CP8eIuZd.js} +1 -1
  48. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-F5JKIvIq.js → cynefin-VYW2F7L2-DZECjpOe.js} +1 -1
  49. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cwmkl_Gi.js → cynefinDiagram-MW4NZA55-CvVJoFqf.js} +1 -1
  50. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BmCZ8815.js → dagre-VZM6K2ZE-D_JTqdAn.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zWA3hUnX.js → diagram-7IWD3JNH-Db7AQ0z0.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BzA-888F.js → diagram-B4RE2ZJO-AZPVo5CZ.js} +1 -1
  53. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-tPNheoch.js → diagram-LBJQPF4R-BgXqI3E6.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Cdi7Fyqp.js → diagram-Q27KOJAE-BYjFn3Ba.js} +1 -1
  55. package/dist/worker/console/static/assets/{diagram-UB23O5K3-DFK9VBf1.js → diagram-UB23O5K3-B4rJgGGO.js} +1 -1
  56. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-xCUr1kpg.js → ebnfDiagram-BXEA7PRR-C6XWZhpw.js} +1 -1
  57. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BANQHY_A.js → erDiagram-JOGREHBK-1YdQRIQW.js} +1 -1
  58. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-VHJYEMkP.js → flowDiagram-UKHOOZJN-l3a0AwN0.js} +1 -1
  59. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiyZQ0C6.js → ganttDiagram-PKOTCBZU-CQL96F2Z.js} +1 -1
  60. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CZtIeBBa.js → gitGraphDiagram-DS77QQ5N-BxCvafRb.js} +1 -1
  61. package/dist/worker/console/static/assets/index-4wCGsXuU.js +465 -0
  62. package/dist/worker/console/static/assets/index-CiF9WBoR.css +1 -0
  63. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BuSspC0X.js → infoDiagram-6WML65LV-BUipIeDO.js} +1 -1
  64. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DRJZyPFy.js → ishikawaDiagram-WSZJBQD7-CRfomVMX.js} +1 -1
  65. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DIfXIVuE.js → journeyDiagram-NVQOT4AX-_UFvyviX.js} +1 -1
  66. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CT10_7Lt.js → kanban-definition-27J2QSJJ-CKIRoqKv.js} +1 -1
  67. package/dist/worker/console/static/assets/{linear-ufemj74H.js → linear-CjttJDCM.js} +1 -1
  68. package/dist/worker/console/static/assets/{mermaid.core-DEQe8-_m.js → mermaid.core-DPvHX155.js} +5 -5
  69. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BRLUrbrC.js → mindmap-definition-FAOFIHXS-Qmj-qwBe.js} +1 -1
  70. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BgibKtiF.js → pegDiagram-VL7TDLO6-Auw6DRgq.js} +1 -1
  71. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D3eyfdV9.js → pieDiagram-7S7Q4E2Y-CqMrfD4c.js} +1 -1
  72. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DXeTk5Px.js → quadrantDiagram-CIZ2JOQS-BAddeggH.js} +1 -1
  73. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BgXdFEe7.js → railroadDiagram-AXF67PYL-DG1JuOu2.js} +1 -1
  74. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CseC2A7R.js → requirementDiagram-LRYGKXZP-DLYGMKlC.js} +1 -1
  75. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-D80PFzfJ.js → sankeyDiagram-W5VNT64P-g68SbLM9.js} +1 -1
  76. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BWzHW7LS.js → sequenceDiagram-SI44F4Z6-CmszLKv8.js} +1 -1
  77. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-xWQYTLAn.js → sizeCapture-X5ZJPWSS-D2tEKUCj.js} +1 -1
  78. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CLxtpZeL.js → stateDiagram-OKZ733FA-Syc5E_Jf.js} +1 -1
  79. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CwWwuPI-.js +1 -0
  80. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DwugtmWV.js → swimlanes-SLNWSIFB-Ibn0IRCa.js} +2 -2
  81. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-A7fqemr6.js +8 -0
  82. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DHEOwHIe.js → timeline-definition-Z64GVDOM-2ZW9W3iS.js} +1 -1
  83. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-pmbZKlBK.js → vennDiagram-T6HMQDX7-v50HsKUh.js} +1 -1
  84. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CivHoM5q.js → wardleyDiagram-T6FBY63Y-DqCXEoDH.js} +1 -1
  85. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BfHIE7o6.js → xychartDiagram-ELKLHX3M-vIP8Vo9d.js} +1 -1
  86. package/dist/worker/console/static/index.html +2 -2
  87. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
  88. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
  89. package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
  90. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
  91. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
  92. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +20 -4
  93. package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
  94. package/dist/worker/console/static-src/operator-chat/useChatThread.js +39 -4
  95. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
  96. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
  97. package/dist/worker/console/workspace-context.js +15 -1
  98. package/dist/workflows/dag/frontend-capacity.js +9 -0
  99. package/dist/workflows/dag/frontend-design-policy.js +2 -1
  100. package/dist/workflows/dag/frontend-shape-facts.js +12 -2
  101. package/package.json +6 -3
  102. package/skills/frontend-plan/SKILL.md +7 -0
  103. package/dist/worker/console/static/assets/channel-BpzRnZmU.js +0 -1
  104. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlMVqCc.js +0 -1
  105. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlMVqCc.js +0 -1
  106. package/dist/worker/console/static/assets/index-BlN6ytLr.css +0 -1
  107. package/dist/worker/console/static/assets/index-CA-jfWlE.js +0 -449
  108. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CiVoq4G-.js +0 -1
  109. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BkdgdpfR.js +0 -8
@@ -16,6 +16,7 @@ import { randomUUID } from "node:crypto";
16
16
  import { access } from "node:fs/promises";
17
17
  import { parseAssistantContent } from "./assistant-content.js";
18
18
  import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, } from "./tools.js";
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";
19
20
  import { buildModelCallableToolSchemas } from "./tool-adapter.js";
20
21
  import { cleanChatTitle, acceptedOperationIdFromResult, isBlankChatSession } from "./session-store.js";
21
22
  import { hashChatTurnPayload, projectChatToolEventPayload, projectCompactSnapshot, } from "./chat-event-store.js";
@@ -44,11 +45,79 @@ import { buildGoalCompactionInstructions, buildGoalContinuationContext, } from "
44
45
  import { classifyGoalRoundAdmission, } from "./goal-round-driver.js";
45
46
  import { isTerminalOperationState } from "../../../infrastructure/console/operation-store.js";
46
47
  import { SddDataAlignmentError, } from "./sdd-data-alignment.js";
48
+ import { TerminalSessionRegistry } from "./terminal-sessions.js";
49
+ import { handleTerminalRequest } from "./terminal-routes.js";
50
+ import { handleScmRequest } from "./scm-routes.js";
47
51
  function projectBrowserChatSession(record) {
48
52
  const { telemetrySessionId, ...projected } = record;
49
53
  void telemetrySessionId;
50
54
  return projected;
51
55
  }
56
+ function reopenInitFromRecord(record) {
57
+ return {
58
+ sessionId: record.sessionId,
59
+ sessionFile: record.sessionFile,
60
+ ...(record.modelProvider && record.modelId
61
+ ? { model: { provider: record.modelProvider, modelId: record.modelId } }
62
+ : {}),
63
+ sessionMode: record.sessionMode,
64
+ };
65
+ }
66
+ async function readChatSessionMode(deps, sessionId) {
67
+ let record;
68
+ try {
69
+ record = await deps.store.get(sessionId);
70
+ }
71
+ catch (error) {
72
+ if (error instanceof UnknownChatSessionModeError) {
73
+ return {
74
+ ok: false,
75
+ status: 409,
76
+ code: error.code,
77
+ message: error.message,
78
+ };
79
+ }
80
+ throw error;
81
+ }
82
+ if (!record) {
83
+ return {
84
+ ok: false,
85
+ status: 404,
86
+ code: "NOT_FOUND",
87
+ message: `chat session not found: ${sessionId}`,
88
+ };
89
+ }
90
+ // Normalize: the store contract always fills sessionMode, but handler-level
91
+ // fakes / partial records may omit it — treat missing as workflow (the
92
+ // stock-record read rule), while a present-but-unknown value still fails
93
+ // closed in the store layer.
94
+ return {
95
+ ok: true,
96
+ record,
97
+ sessionMode: readStoredSessionMode(record.sessionMode),
98
+ };
99
+ }
100
+ async function requireWorkflowSession(res, deps, sessionId) {
101
+ const resolved = await readChatSessionMode(deps, sessionId);
102
+ if (!resolved.ok) {
103
+ sendJson(res, resolved.status, {
104
+ ok: false,
105
+ error: { code: resolved.code, message: resolved.message },
106
+ });
107
+ return undefined;
108
+ }
109
+ if (!isWorkflowSessionMode(resolved.sessionMode)) {
110
+ sendJson(res, 409, {
111
+ ok: false,
112
+ error: {
113
+ code: SESSION_MODE_ACTION_DENIED_CODE,
114
+ message: SESSION_MODE_ACTION_DENIED_MESSAGE,
115
+ },
116
+ });
117
+ return undefined;
118
+ }
119
+ return resolved.record;
120
+ }
52
121
  /**
53
122
  * Reattach the durable event-ring order to session-store rows. The session
54
123
  * store remains the content authority; the ring only supplies chronology.
@@ -565,6 +634,22 @@ workspaceId) {
565
634
  });
566
635
  return;
567
636
  }
637
+ let sessionMode;
638
+ try {
639
+ sessionMode = parseCreateSessionMode(body.sessionMode);
640
+ }
641
+ catch (error) {
642
+ sendJson(res, 400, {
643
+ ok: false,
644
+ error: {
645
+ code: error instanceof InvalidChatSessionModeError
646
+ ? error.code
647
+ : "INVALID_SESSION_MODE",
648
+ message: error instanceof Error ? error.message : String(error),
649
+ },
650
+ });
651
+ return;
652
+ }
568
653
  const provider = typeof body.modelProvider === "string" ? body.modelProvider : undefined;
569
654
  const modelId = typeof body.modelId === "string" ? body.modelId : undefined;
570
655
  const snapshotInput = body.snapshot;
@@ -586,6 +671,16 @@ workspaceId) {
586
671
  let createdSessionId;
587
672
  let persisted = false;
588
673
  try {
674
+ if (snapshotInput !== undefined && !shouldAcceptOperatorSnapshot(sessionMode)) {
675
+ sendJson(res, 409, {
676
+ ok: false,
677
+ error: {
678
+ code: SESSION_MODE_ACTION_DENIED_CODE,
679
+ message: "Operator snapshots cannot be imported into Pi sessions",
680
+ },
681
+ });
682
+ return;
683
+ }
589
684
  const snapshot = snapshotInput === undefined ? undefined : projectCompactSnapshot(snapshotInput);
590
685
  // A Console record promises that its Pi JSONL can be reopened after a
591
686
  // Console restart. Do not make that promise until Pi has finished
@@ -593,6 +688,7 @@ workspaceId) {
593
688
  // future JSONL path, leaving an unrecoverable orphan when init failed or
594
689
  // the process restarted in that window.
595
690
  const handle = await deps.runtime.createSession({
691
+ sessionMode,
596
692
  ...(provider && modelId ? { model: { provider, modelId } } : {}),
597
693
  ...(snapshot
598
694
  ? {
@@ -619,6 +715,7 @@ workspaceId) {
619
715
  sessionFile: handle.sessionFile,
620
716
  modelProvider: selection.model?.provider ?? handle.model?.provider,
621
717
  modelId: selection.model?.modelId ?? handle.model?.modelId,
718
+ sessionMode,
622
719
  // AC-R7-002: explicit parameter from the dispatcher — the runtime
623
720
  // store.create wrapper is gone; body workspaceId (if any) only acts as
624
721
  // a fallback for direct (non-dispatcher) handler tests.
@@ -689,6 +786,7 @@ export async function handleForkChatSession(req, res, deps, sourceSessionId) {
689
786
  try {
690
787
  const handle = await deps.runtime.forkSession({
691
788
  sourceSessionFile: source.sessionFile ?? "",
789
+ sessionMode: source.sessionMode,
692
790
  ...(source.modelProvider && source.modelId
693
791
  ? { model: { provider: source.modelProvider, modelId: source.modelId } }
694
792
  : {}),
@@ -755,10 +853,7 @@ export async function handleBranchChatSession(req, res, deps, sessionId) {
755
853
  }
756
854
  try {
757
855
  if (!deps.runtime.hasSession(sessionId)) {
758
- const reopened = await deps.runtime.reopenSession({
759
- sessionId,
760
- sessionFile: record.sessionFile,
761
- });
856
+ const reopened = await deps.runtime.reopenSession(reopenInitFromRecord(record));
762
857
  if (!reopened.ok) {
763
858
  const mapped = await mapFailedReopen(deps, sessionId, reopened);
764
859
  sendJson(res, mapped.status, {
@@ -1231,13 +1326,8 @@ export async function handleChatGoal(req, res, deps, sessionId) {
1231
1326
  });
1232
1327
  return;
1233
1328
  }
1234
- if (!(await deps.store.get(sessionId))) {
1235
- sendJson(res, 404, {
1236
- ok: false,
1237
- error: { code: "NOT_FOUND", message: `chat session not found: ${sessionId}` },
1238
- });
1329
+ if (!(await requireWorkflowSession(res, deps, sessionId)))
1239
1330
  return;
1240
- }
1241
1331
  if (req.method === "GET") {
1242
1332
  sendJson(res, 200, { ok: true, goal: registry.get(sessionId) });
1243
1333
  return;
@@ -1451,6 +1541,7 @@ export async function listChatSessionSummaries(deps, ownerWorkspaceId) {
1451
1541
  ...(session.thinkingLevel !== undefined
1452
1542
  ? { thinkingLevel: session.thinkingLevel }
1453
1543
  : {}),
1544
+ sessionMode: session.sessionMode,
1454
1545
  activeTurnId: active?.turnId,
1455
1546
  turnPhase: active?.abortRequestedAt
1456
1547
  ? "stopping"
@@ -1474,6 +1565,16 @@ export async function handlePatchChatSession(req, res, deps, sessionId) {
1474
1565
  }
1475
1566
  try {
1476
1567
  const body = await readJsonBody(req);
1568
+ if ("sessionMode" in body) {
1569
+ sendJson(res, 409, {
1570
+ ok: false,
1571
+ error: {
1572
+ code: SESSION_MODE_IMMUTABLE_CODE,
1573
+ message: "sessionMode cannot be changed after create",
1574
+ },
1575
+ });
1576
+ return;
1577
+ }
1477
1578
  let record;
1478
1579
  if (typeof body.title === "string")
1479
1580
  record = await deps.store.rename(sessionId, body.title);
@@ -1532,13 +1633,7 @@ export async function handleReopenChatSession(req, res, deps, sessionId) {
1532
1633
  });
1533
1634
  return;
1534
1635
  }
1535
- const result = await deps.runtime.reopenSession({
1536
- sessionId,
1537
- sessionFile: record.sessionFile,
1538
- ...(record.modelProvider && record.modelId
1539
- ? { model: { provider: record.modelProvider, modelId: record.modelId } }
1540
- : {}),
1541
- });
1636
+ const result = await deps.runtime.reopenSession(reopenInitFromRecord(record));
1542
1637
  if (!result.ok) {
1543
1638
  const mapped = await mapFailedReopen(deps, sessionId, result);
1544
1639
  sendJson(res, mapped.status, {
@@ -1618,13 +1713,7 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
1618
1713
  });
1619
1714
  return;
1620
1715
  }
1621
- const reopen = await deps.runtime.reopenSession({
1622
- sessionId,
1623
- sessionFile: record.sessionFile,
1624
- ...(record.modelProvider && record.modelId
1625
- ? { model: { provider: record.modelProvider, modelId: record.modelId } }
1626
- : {}),
1627
- });
1716
+ const reopen = await deps.runtime.reopenSession(reopenInitFromRecord(record));
1628
1717
  if (!reopen.ok) {
1629
1718
  const mapped = await mapFailedReopen(deps, sessionId, reopen);
1630
1719
  sendJson(res, mapped.status, {
@@ -1888,13 +1977,7 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1888
1977
  });
1889
1978
  return;
1890
1979
  }
1891
- const reopen = await deps.runtime.reopenSession({
1892
- sessionId,
1893
- sessionFile: record.sessionFile,
1894
- ...(record.modelProvider && record.modelId
1895
- ? { model: { provider: record.modelProvider, modelId: record.modelId } }
1896
- : {}),
1897
- });
1980
+ const reopen = await deps.runtime.reopenSession(reopenInitFromRecord(record));
1898
1981
  if (!reopen.ok) {
1899
1982
  const mapped = await mapFailedReopen(deps, sessionId, reopen);
1900
1983
  sendJson(res, mapped.status, {
@@ -2172,6 +2255,10 @@ export async function driveGoalRound(deps, sessionId) {
2172
2255
  registry.disarm(sessionId);
2173
2256
  return "idle";
2174
2257
  }
2258
+ if (!isWorkflowSessionMode(readStoredSessionMode(record.sessionMode))) {
2259
+ registry.disarm(sessionId);
2260
+ return "idle";
2261
+ }
2175
2262
  const runtimeFacts = deps.runtime.getSessionBusyFacts(sessionId);
2176
2263
  const operationStore = deps.runtime.actionContext?.operations;
2177
2264
  const linkedOperationIds = linkedOperationIdsForGoal(deps.events.snapshot(sessionId));
@@ -2820,6 +2907,7 @@ export async function handleChatCommands(res, deps, sessionId) {
2820
2907
  const warning = typeof listed.warning === "string" && listed.warning.trim()
2821
2908
  ? listed.warning.trim().slice(0, 160)
2822
2909
  : undefined;
2910
+ const resolvedMode = await readChatSessionMode(deps, sessionId);
2823
2911
  res.statusCode = 200;
2824
2912
  res.setHeader("content-type", "application/json; charset=utf-8");
2825
2913
  res.end(JSON.stringify({
@@ -2827,18 +2915,25 @@ export async function handleChatCommands(res, deps, sessionId) {
2827
2915
  commands,
2828
2916
  capabilities: {
2829
2917
  sddBingo: Boolean(await deps.sddDataAlignment?.isEligible()),
2918
+ ...(resolvedMode.ok ? { sessionMode: resolvedMode.sessionMode } : {}),
2830
2919
  },
2831
2920
  ...(warning ? { warning } : {}),
2832
2921
  }));
2833
2922
  }
2834
2923
  catch (error) {
2835
2924
  // Whole-list / readiness failures stay fail-soft with empty commands + warning.
2925
+ // Keep the two capability shapes symmetric: the fallback mode mirrors the
2926
+ // client-side prop fallback in useComposer.
2927
+ const fallbackMode = await readChatSessionMode(deps, sessionId).catch(() => undefined);
2836
2928
  res.statusCode = 200;
2837
2929
  res.setHeader("content-type", "application/json; charset=utf-8");
2838
2930
  res.end(JSON.stringify({
2839
2931
  ok: true,
2840
2932
  commands: [],
2841
- capabilities: { sddBingo: false },
2933
+ capabilities: {
2934
+ sddBingo: false,
2935
+ ...(fallbackMode?.ok ? { sessionMode: fallbackMode.sessionMode } : {}),
2936
+ },
2842
2937
  warning: error instanceof Error ? error.message : "commands unavailable",
2843
2938
  }));
2844
2939
  }
@@ -3083,13 +3178,7 @@ export async function ensureActiveSession(deps, sessionId) {
3083
3178
  message: `chat session not found: ${sessionId}`,
3084
3179
  };
3085
3180
  }
3086
- const reopened = await deps.runtime.reopenSession({
3087
- sessionId,
3088
- sessionFile: record.sessionFile,
3089
- ...(record.modelProvider && record.modelId
3090
- ? { model: { provider: record.modelProvider, modelId: record.modelId } }
3091
- : {}),
3092
- });
3181
+ const reopened = await deps.runtime.reopenSession(reopenInitFromRecord(record));
3093
3182
  if (!reopened.ok) {
3094
3183
  return { ok: false, ...(await mapFailedReopen(deps, sessionId, reopened)) };
3095
3184
  }
@@ -3418,7 +3507,7 @@ async function handleStartInterview(req, res, deps, sessionId) {
3418
3507
  });
3419
3508
  return;
3420
3509
  }
3421
- if (!(await requireChatSession(res, deps, sessionId)))
3510
+ if (!(await requireWorkflowSession(res, deps, sessionId)))
3422
3511
  return;
3423
3512
  try {
3424
3513
  const body = await readJsonBody(req);
@@ -3460,7 +3549,7 @@ async function handleAnswerInterview(req, res, deps, sessionId, interviewSession
3460
3549
  });
3461
3550
  return;
3462
3551
  }
3463
- if (!(await requireChatSession(res, deps, sessionId)))
3552
+ if (!(await requireWorkflowSession(res, deps, sessionId)))
3464
3553
  return;
3465
3554
  try {
3466
3555
  const body = await readJsonBody(req);
@@ -3496,7 +3585,7 @@ async function handleAnswerInterview(req, res, deps, sessionId, interviewSession
3496
3585
  }
3497
3586
  }
3498
3587
  async function handleGetInterviewFact(res, deps, sessionId, interviewSessionId, fact) {
3499
- if (!(await requireChatSession(res, deps, sessionId)))
3588
+ if (!(await requireWorkflowSession(res, deps, sessionId)))
3500
3589
  return;
3501
3590
  const state = await new ChatInterviewAdapter(deps.appData, deps.events).get(interviewSessionId);
3502
3591
  if (!state || state.interview.operatorSessionId !== sessionId) {
@@ -3594,7 +3683,7 @@ async function handleContractApplyHumanGate(req, res, deps, sessionId, step) {
3594
3683
  });
3595
3684
  return;
3596
3685
  }
3597
- if (!(await requireChatSession(res, deps, sessionId)))
3686
+ if (!(await requireWorkflowSession(res, deps, sessionId)))
3598
3687
  return;
3599
3688
  const ctx = deps.runtime.actionContext;
3600
3689
  if (!ctx) {
@@ -3776,7 +3865,7 @@ async function handleDagHumanGate(req, res, deps, sessionId, step) {
3776
3865
  });
3777
3866
  return;
3778
3867
  }
3779
- if (!(await requireChatSession(res, deps, sessionId)))
3868
+ if (!(await requireWorkflowSession(res, deps, sessionId)))
3780
3869
  return;
3781
3870
  const ctx = deps.runtime.actionContext;
3782
3871
  if (!ctx) {
@@ -3885,7 +3974,7 @@ async function handleMutationHumanGate(req, res, deps, sessionId) {
3885
3974
  });
3886
3975
  return;
3887
3976
  }
3888
- if (!(await requireChatSession(res, deps, sessionId)))
3977
+ if (!(await requireWorkflowSession(res, deps, sessionId)))
3889
3978
  return;
3890
3979
  const ctx = deps.runtime.actionContext;
3891
3980
  if (!ctx) {
@@ -3974,7 +4063,7 @@ async function handleConfirmTaskKind(req, res, deps, sessionId, interviewSession
3974
4063
  });
3975
4064
  return;
3976
4065
  }
3977
- if (!(await requireChatSession(res, deps, sessionId)))
4066
+ if (!(await requireWorkflowSession(res, deps, sessionId)))
3978
4067
  return;
3979
4068
  try {
3980
4069
  const body = await readJsonBody(req);
@@ -4030,13 +4119,7 @@ export async function handleChatCompact(req, res, deps, sessionId) {
4030
4119
  return;
4031
4120
  }
4032
4121
  if (!deps.runtime.hasSession(sessionId)) {
4033
- const reopened = await deps.runtime.reopenSession({
4034
- sessionId,
4035
- sessionFile: record.sessionFile,
4036
- ...(record.modelProvider && record.modelId
4037
- ? { model: { provider: record.modelProvider, modelId: record.modelId } }
4038
- : {}),
4039
- });
4122
+ const reopened = await deps.runtime.reopenSession(reopenInitFromRecord(record));
4040
4123
  if (!reopened.ok) {
4041
4124
  const mapped = await mapFailedReopen(deps, sessionId, reopened);
4042
4125
  sendJson(res, mapped.status, {
@@ -4060,7 +4143,9 @@ export async function handleChatCompact(req, res, deps, sessionId) {
4060
4143
  const rawInstructions = typeof body.instructions === "string" ? body.instructions : undefined;
4061
4144
  const currentTodos = projectCurrentTodos(deps.events.snapshot(sessionId));
4062
4145
  const todoInstructions = buildTodoCompactionInstructions(rawInstructions, currentTodos);
4063
- const customInstructions = buildGoalCompactionInstructions(todoInstructions, deps.goalRegistry?.get(sessionId) ?? null);
4146
+ const customInstructions = isWorkflowSessionMode(record.sessionMode)
4147
+ ? buildGoalCompactionInstructions(todoInstructions, deps.goalRegistry?.get(sessionId) ?? null)
4148
+ : todoInstructions;
4064
4149
  const turnId = deps.events.latestTurnId(sessionId) ?? `${sessionId}:compact`;
4065
4150
  const compactionId = `${sessionId}:compaction:${randomUUID()}`;
4066
4151
  deps.events.append(sessionId, turnId, {
@@ -4146,6 +4231,42 @@ export async function handleChatMessages(res, deps, sessionId, url) {
4146
4231
  export async function handleChatRequest(req, res, deps, pathname, workspaceScope) {
4147
4232
  if (!deps)
4148
4233
  return false;
4234
+ try {
4235
+ return await dispatchChatRequest(req, res, deps, pathname, workspaceScope);
4236
+ }
4237
+ catch (error) {
4238
+ // Store-level fail-closed for corrupt sessionMode: bounded 409 instead of
4239
+ // a generic 500, for any handler that reads the record without its own
4240
+ // UnknownChatSessionModeError mapping (readChatSessionMode covers the rest).
4241
+ if (error instanceof UnknownChatSessionModeError) {
4242
+ sendJson(res, 409, {
4243
+ ok: false,
4244
+ error: { code: error.code, message: error.message },
4245
+ });
4246
+ return true;
4247
+ }
4248
+ throw error;
4249
+ }
4250
+ }
4251
+ async function dispatchChatRequest(req, res, deps, pathname, workspaceScope) {
4252
+ // Terminal management API (model + Console UI shared PTY registry).
4253
+ if (await handleTerminalRequest(req, res, {
4254
+ terminals: deps.terminals ?? new TerminalSessionRegistry(),
4255
+ gateMutation: (r) => gateMutation(r, deps),
4256
+ // Terminals open in the workspace repo root, not the Console process cwd.
4257
+ defaultCwd: deps.appData.repoRoot,
4258
+ }, pathname)) {
4259
+ return true;
4260
+ }
4261
+ // Source control API (model + Console UI shared workspace ScmService).
4262
+ if (deps.scm) {
4263
+ if (await handleScmRequest(req, res, {
4264
+ scm: deps.scm,
4265
+ gateMutation: (r) => gateMutation(r, deps),
4266
+ }, pathname)) {
4267
+ return true;
4268
+ }
4269
+ }
4149
4270
  if (!pathname.startsWith("/api/operator/v1/chat/") &&
4150
4271
  !pathname.startsWith("/api/operator/v1/pi/"))
4151
4272
  return false;
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Operator Chat — source control HTTP API (Console UI side).
3
+ *
4
+ * Endpoints (all under /api/operator/v1/chat):
5
+ * GET /scm/status branch + changes + counts
6
+ * GET /scm/diff?path=&staged= unified diff (one path or --stat)
7
+ * POST /scm/stage { paths: string[] } | { all: true }
8
+ * POST /scm/unstage { paths: string[] }
9
+ * POST /scm/discard { paths: string[] } [dangerous]
10
+ * POST /scm/commit { message }
11
+ * GET /scm/log?limit= recent commits
12
+ * GET /scm/branches branch list + current
13
+ * POST /scm/branches { name, create? } switch or create
14
+ * POST /scm/push push to upstream
15
+ * POST /scm/pull pull from upstream
16
+ *
17
+ * Reads are open (same-origin console); every mutation passes gateMutation.
18
+ * The ScmService is workspace-scoped (repoRoot fixed at construction).
19
+ */
20
+ function sendJson(res, status, body) {
21
+ res.writeHead(status, {
22
+ "content-type": "application/json; charset=utf-8",
23
+ "cache-control": "no-store",
24
+ });
25
+ res.end(JSON.stringify(body));
26
+ }
27
+ function mapScmError(res, error) {
28
+ const status = error.code === "NO_PATHS" || error.code === "EMPTY_MESSAGE" || error.code === "INVALID_NAME"
29
+ ? 400
30
+ : error.code === "GIT_TIMEOUT"
31
+ ? 504
32
+ : 500;
33
+ sendJson(res, status, {
34
+ ok: false,
35
+ error: { code: error.code, message: error.message, ...(error.stderr ? { stderr: error.stderr } : {}) },
36
+ });
37
+ }
38
+ async function readJsonBody(req, maxBytes = 256 * 1024) {
39
+ const chunks = [];
40
+ let total = 0;
41
+ for await (const chunk of req) {
42
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
43
+ total += buf.length;
44
+ if (total > maxBytes)
45
+ throw new Error("request body too large");
46
+ chunks.push(buf);
47
+ }
48
+ if (chunks.length === 0)
49
+ return {};
50
+ const raw = Buffer.concat(chunks).toString("utf8");
51
+ if (!raw.trim())
52
+ return {};
53
+ return JSON.parse(raw);
54
+ }
55
+ function parsePaths(body) {
56
+ if (!Array.isArray(body.paths))
57
+ return [];
58
+ return body.paths
59
+ .filter((p) => typeof p === "string" && p.length > 0 && !p.includes("\0"))
60
+ .slice(0, 500);
61
+ }
62
+ /** Route dispatcher; returns true when the request was handled. */
63
+ export async function handleScmRequest(req, res, deps, pathname) {
64
+ if (!pathname.startsWith("/api/operator/v1/chat/scm"))
65
+ return false;
66
+ const method = (req.method ?? "GET").toUpperCase();
67
+ const rest = pathname.slice("/api/operator/v1/chat/scm".length);
68
+ const parts = rest.split("/").filter(Boolean);
69
+ const sub = parts[0] ?? "";
70
+ const url = new URL(req.url ?? "/", "http://localhost");
71
+ const gateFailed = (gate) => {
72
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
73
+ return true;
74
+ };
75
+ try {
76
+ if (method === "GET" && sub === "status" && parts.length === 1) {
77
+ const r = await deps.scm.status();
78
+ if (!r.ok)
79
+ return mapScmError(res, r), true;
80
+ sendJson(res, 200, { ok: true, data: r });
81
+ return true;
82
+ }
83
+ if (method === "GET" && sub === "diff" && parts.length === 1) {
84
+ const path = url.searchParams.get("path") ?? undefined;
85
+ const staged = url.searchParams.get("staged") === "1";
86
+ const r = await deps.scm.diff({ path, staged });
87
+ if (!r.ok)
88
+ return mapScmError(res, r), true;
89
+ sendJson(res, 200, { ok: true, data: r });
90
+ return true;
91
+ }
92
+ if (method === "GET" && sub === "log" && parts.length === 1) {
93
+ const limit = Number(url.searchParams.get("limit") ?? 30);
94
+ const r = await deps.scm.log(Number.isFinite(limit) ? Math.min(Math.max(limit, 1), 200) : 30);
95
+ if (!r.ok)
96
+ return mapScmError(res, r), true;
97
+ sendJson(res, 200, { ok: true, data: r });
98
+ return true;
99
+ }
100
+ if (method === "GET" && sub === "branches" && parts.length === 1) {
101
+ const r = await deps.scm.branchList();
102
+ if (!r.ok)
103
+ return mapScmError(res, r), true;
104
+ sendJson(res, 200, { ok: true, data: r });
105
+ return true;
106
+ }
107
+ // Everything below mutates → gate first.
108
+ const gate = deps.gateMutation(req);
109
+ if (!gate.ok)
110
+ return gateFailed(gate);
111
+ if (method === "POST" && sub === "stage" && parts.length === 1) {
112
+ const body = await readJsonBody(req);
113
+ const r = body.all === true
114
+ ? await deps.scm.stage([], true)
115
+ : await deps.scm.stage(parsePaths(body));
116
+ if (!r.ok)
117
+ return mapScmError(res, r), true;
118
+ sendJson(res, 200, { ok: true });
119
+ return true;
120
+ }
121
+ if (method === "POST" && sub === "unstage" && parts.length === 1) {
122
+ const body = await readJsonBody(req);
123
+ const r = await deps.scm.unstage(parsePaths(body));
124
+ if (!r.ok)
125
+ return mapScmError(res, r), true;
126
+ sendJson(res, 200, { ok: true });
127
+ return true;
128
+ }
129
+ if (method === "POST" && sub === "discard" && parts.length === 1) {
130
+ const body = await readJsonBody(req);
131
+ const r = await deps.scm.discard(parsePaths(body));
132
+ if (!r.ok)
133
+ return mapScmError(res, r), true;
134
+ sendJson(res, 200, { ok: true });
135
+ return true;
136
+ }
137
+ if (method === "POST" && sub === "commit" && parts.length === 1) {
138
+ const body = await readJsonBody(req);
139
+ const message = typeof body.message === "string" ? body.message : "";
140
+ const r = await deps.scm.commit(message);
141
+ if (!r.ok)
142
+ return mapScmError(res, r), true;
143
+ sendJson(res, 200, { ok: true, data: r });
144
+ return true;
145
+ }
146
+ if (method === "POST" && sub === "branches" && parts.length === 1) {
147
+ const body = await readJsonBody(req);
148
+ const name = typeof body.name === "string" ? body.name : "";
149
+ if (!name) {
150
+ sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "name is required" } });
151
+ return true;
152
+ }
153
+ const r = body.create === true
154
+ ? await deps.scm.branchCreate(name)
155
+ : await deps.scm.branchSwitch(name);
156
+ if (!r.ok)
157
+ return mapScmError(res, r), true;
158
+ sendJson(res, 200, { ok: true });
159
+ return true;
160
+ }
161
+ if (method === "POST" && sub === "push" && parts.length === 1) {
162
+ const r = await deps.scm.push();
163
+ if (!r.ok)
164
+ return mapScmError(res, r), true;
165
+ sendJson(res, 200, { ok: true, data: r });
166
+ return true;
167
+ }
168
+ if (method === "POST" && sub === "pull" && parts.length === 1) {
169
+ const r = await deps.scm.pull();
170
+ if (!r.ok)
171
+ return mapScmError(res, r), true;
172
+ sendJson(res, 200, { ok: true, data: r });
173
+ return true;
174
+ }
175
+ sendJson(res, 404, { ok: false, error: { code: "NOT_FOUND", message: `unknown scm route: ${method} ${pathname}` } });
176
+ return true;
177
+ }
178
+ catch (error) {
179
+ if (!res.headersSent) {
180
+ sendJson(res, 500, {
181
+ ok: false,
182
+ error: {
183
+ code: "SCM_INTERNAL",
184
+ message: error instanceof Error ? error.message : String(error),
185
+ },
186
+ });
187
+ }
188
+ else {
189
+ res.end();
190
+ }
191
+ return true;
192
+ }
193
+ }