@tea-agent/loop-agent 0.42.0-next.9 → 0.42.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.
- package/CHANGELOG.md +111 -45
- package/dist/application/dag/run-dag.js +8 -2
- package/dist/application/evaluation/budget.js +19 -1
- package/dist/application/task-lifecycle/advance.js +26 -8
- package/dist/application/task-lifecycle/observe.js +43 -29
- package/dist/application/task-lifecycle/plan-transitions.js +5 -4
- package/dist/build-stamp.json +3 -3
- package/dist/cli/program.js +1 -1
- package/dist/commands/dag-rerun-task.js +2 -0
- package/dist/commands/task-source-prepare.js +3 -1
- package/dist/executors/dag-pi-executor.js +2584 -600
- package/dist/executors/pi-executor.js +22 -1
- package/dist/executors/pi-extension-resolver.js +14 -2
- package/dist/executors/pi-sdk-executor.js +140 -39
- package/dist/executors/shell-executor.js +135 -55
- package/dist/shared/dag-failure-category.js +6 -0
- package/dist/shared/frontend-execution-policy.js +23 -0
- package/dist/task/config-types.js +4 -0
- package/dist/task/contract/apply.js +36 -2
- package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
- package/dist/task/source-prepare/ledger-review.js +6 -9
- package/dist/task/source-prepare/parse-intent.js +7 -0
- package/dist/task/source-prepare/semantic-intake.js +16 -26
- package/dist/task/source-prepare/source-fidelity-pi.js +28 -9
- package/dist/task/source-references.js +48 -23
- package/dist/worker/console/chat/assistant-content.js +23 -2
- package/dist/worker/console/chat/browser-policy.js +143 -0
- package/dist/worker/console/chat/browser-routes.js +148 -0
- package/dist/worker/console/chat/chat-event-store.js +4 -2
- package/dist/worker/console/chat/explore-tools.js +13 -0
- package/dist/worker/console/chat/pi-runtime.js +173 -94
- package/dist/worker/console/chat/resource-loader.js +4 -1
- package/dist/worker/console/chat/routes.js +208 -66
- package/dist/worker/console/chat/scm-routes.js +217 -0
- package/dist/worker/console/chat/scm-service.js +283 -0
- package/dist/worker/console/chat/scm-tools.js +111 -0
- package/dist/worker/console/chat/sdd-data-alignment.js +78 -11
- package/dist/worker/console/chat/session-catalog.js +32 -0
- package/dist/worker/console/chat/session-mode-view.js +48 -0
- package/dist/worker/console/chat/session-mode.js +218 -0
- package/dist/worker/console/chat/session-store.js +27 -7
- package/dist/worker/console/chat/shortcuts.js +6 -0
- package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
- package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
- package/dist/worker/console/chat/subagents/index.js +5 -0
- package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
- package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
- package/dist/worker/console/chat/subagents/types.js +13 -0
- package/dist/worker/console/chat/terminal-routes.js +216 -0
- package/dist/worker/console/chat/terminal-sessions.js +284 -0
- package/dist/worker/console/chat/terminal-tools.js +199 -0
- package/dist/worker/console/chat/tool-preview.js +21 -0
- package/dist/worker/console/chat/tools.js +13 -1
- package/dist/worker/console/chat/turn-process.js +1 -0
- package/dist/worker/console/interview/tools.js +1 -0
- package/dist/worker/console/server.js +2 -28
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-C9eI7nEo.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-VJWsxWhB.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-BGcyiMSo.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-BScnFwyi.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Bl9_BsLi.js} +1 -1
- package/dist/worker/console/static/assets/channel-B6sQYuOE.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-F1uKxiUt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-rSRMynVu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-DAsJm1WD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-DQIdVI0a.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-4A_8rJ-Q.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-BHnT-vjh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-Rq7QKBkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BXh-AI2u.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-BblcLy8m.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-zHZmVnB6.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Z6s5QoeI.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Z6s5QoeI.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-Cmwz0Wi4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-Dq76MHQU.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-zPahV1fM.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-DQJ3xPc_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BwX7u6fv.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CodnDCco.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-gAjtWkxG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-CGCqz0cS.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-RHwYEywd.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-B6seb_p_.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-CmwCKYe8.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-BfNsaZ0V.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-zmYgi_Z3.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-BDEzzSKF.js} +1 -1
- package/dist/worker/console/static/assets/index-DgenUAfc.js +468 -0
- package/dist/worker/console/static/assets/index-aF4u-Y__.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-DeSz8l42.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-B1LV29aH.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-CCw4W2oU.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BozaMoRJ.js} +1 -1
- package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-G1JR40SX.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-CCgPF0oA.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-CEqnCHnD.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-DpmQPKuI.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-BP501AxR.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-D1dBhl_A.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-DgZDS5pY.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-BzB3pyIq.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-B9ZprFFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CC2-_XrA.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-UVtAUqgB.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-CWcS5JYG.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BooX8u1Q.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-C4p-dn06.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D64_eqqE.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-Dnz52OHX.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-CzTE0Nfu.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-NAR9cjQQ.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-DVMk8413.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/chat-view-types.js +1 -1
- package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +142 -7
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +44 -0
- package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
- package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +33 -18
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +48 -16
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +170 -7
- package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
- package/dist/worker/console/workspace-context.js +15 -1
- package/dist/worker/observe/node-transparency.js +81 -72
- package/dist/worker/observe/routes.js +20 -1
- package/dist/worker/observe/static/constants.js +22 -22
- package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
- package/dist/worker/observe/static/dag-history-labels.js +1 -0
- package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
- package/dist/worker/observe/static/dag-inspector-humanize.js +342 -0
- package/dist/worker/observe/static/dag-node-purpose.js +10 -10
- package/dist/worker/observe/static/dom.js +20 -1
- package/dist/worker/observe/static/format-pool.d.ts +2 -0
- package/dist/worker/observe/static/format-pool.js +6 -0
- package/dist/worker/observe/static/format.js +7 -0
- package/dist/worker/observe/static/index.html +4 -4
- package/dist/worker/observe/static/inspect-workspace.js +34 -7
- package/dist/worker/observe/static/inspector-submission.js +32 -0
- package/dist/worker/observe/static/kpi.js +1 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
- package/dist/worker/observe/static/relations.js +7 -5
- package/dist/worker/observe/static/router.js +13 -0
- package/dist/worker/observe/static/run-processing.js +2 -0
- package/dist/worker/observe/static/shell-chrome.js +36 -3
- package/dist/worker/observe/static/state.js +35 -2
- package/dist/worker/observe/static/styles.css +431 -39
- package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
- package/dist/worker/observe/static/task-failure-labels.js +67 -0
- package/dist/worker/observe/static/task-history.js +12 -0
- package/dist/worker/observe/static/views/batch.js +6 -13
- package/dist/worker/observe/static/views/dag-graph.js +51 -4
- package/dist/worker/observe/static/views/dag-inspector.js +1069 -426
- package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
- package/dist/worker/observe/static/views/dag.d.ts +6 -0
- package/dist/worker/observe/static/views/dag.js +64 -22
- package/dist/worker/observe/static/views/dags.js +2 -0
- package/dist/worker/observe/static/views/dashboard.js +21 -12
- package/dist/worker/observe/static/views/failures.js +21 -11
- package/dist/worker/observe/static/views/feature.js +11 -29
- package/dist/worker/observe/static/views/pool.js +37 -28
- package/dist/worker/observe/static/views/run.js +48 -5
- package/dist/worker/observe/static/views/session-timeline.js +194 -243
- package/dist/worker/observe/static/views/task.js +81 -62
- package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
- package/dist/workflows/dag/budget-enforcement.js +53 -3
- package/dist/workflows/dag/dag-retry-schema.js +3 -0
- package/dist/workflows/dag/frontend-capacity.js +9 -0
- package/dist/workflows/dag/frontend-contract-facts.js +130 -0
- package/dist/workflows/dag/frontend-design-policy.js +103 -17
- package/dist/workflows/dag/frontend-durable-tools.js +193 -0
- package/dist/workflows/dag/frontend-execution-groups.js +24 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +423 -41
- package/dist/workflows/dag/frontend-input-projection.js +76 -0
- package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
- package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
- package/dist/workflows/dag/frontend-plan-render.js +21 -3
- package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
- package/dist/workflows/dag/frontend-recovery-controller.js +32 -8
- package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +4 -1
- package/dist/workflows/dag/frontend-recovery-run.js +146 -16
- package/dist/workflows/dag/frontend-review-scopes.js +139 -0
- package/dist/workflows/dag/frontend-risk.js +2 -0
- package/dist/workflows/dag/frontend-session-budget.js +249 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +37 -3
- package/dist/workflows/dag/frontend-shape-facts.js +12 -2
- package/dist/workflows/dag/frontend-shape.js +46 -7
- package/dist/workflows/dag/frontend-test-execution-evidence.js +49 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +14 -0
- package/dist/workflows/dag/frontend-verification-trace.js +38 -4
- package/dist/workflows/dag/frontend-writer-admission.js +2 -2
- package/dist/workflows/dag/init-hybrid.js +70 -35
- package/dist/workflows/dag/node-execution.js +225 -226
- package/dist/workflows/dag/prompt.js +4 -0
- package/dist/workflows/dag/recovery-lease.js +106 -16
- package/dist/workflows/dag/rerun-feedback.js +1 -0
- package/dist/workflows/dag/rerun-plan.js +96 -4
- package/dist/workflows/dag/rerun-run.js +28 -6
- package/dist/workflows/dag/rerun-task.js +221 -18
- package/dist/workflows/dag/retry-policy.js +27 -10
- package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
- package/dist/workflows/dag/runner.js +246 -19
- package/dist/workflows/dag/structured-output-repair.js +4 -1
- package/dist/workflows/dag/types.js +10 -3
- package/dist/workflows/dag/validate.js +10 -8
- package/dist/workflows/dag/workspace-checkpoint.js +66 -0
- package/docs/operations/README.md +2 -0
- package/docs/templates/agent-dag.schema.json +2 -2
- package/docs/templates/backend-test-dag.json +6 -4
- package/docs/templates/frontend-design-contract.md +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +68 -4
- package/docs/templates/frontend-implementation-dag.json +5 -5
- package/harness.json +1 -1
- package/package.json +8 -6
- package/skills/frontend-contract/SKILL.md +2 -1
- package/skills/frontend-contract/references/contract-protocol.md +19 -3
- package/skills/frontend-design-review/SKILL.md +12 -11
- package/skills/frontend-plan/SKILL.md +22 -2
- package/skills/frontend-plan/references/decision-contract.md +114 -5
- package/skills/frontend-plan/references/design-decisions.md +32 -0
- package/skills/frontend-review/SKILL.md +10 -11
- package/skills/frontend-scout/references/scout-evidence.md +4 -0
- package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
- package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +0 -8
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator Chat — terminal management HTTP API (Console UI side).
|
|
3
|
+
*
|
|
4
|
+
* Endpoints (all under /api/operator/v1/chat):
|
|
5
|
+
* GET /terminals list terminal sessions (all owners)
|
|
6
|
+
* POST /terminals spawn { shell, name?, cwd?, ownerSessionId? }
|
|
7
|
+
* GET /terminals/:id session info + buffered output
|
|
8
|
+
* POST /terminals/:id/input { data } write to the PTY
|
|
9
|
+
* POST /terminals/:id/resize { cols, rows }
|
|
10
|
+
* POST /terminals/:id/interrupt send Ctrl+C
|
|
11
|
+
* POST /terminals/:id/kill stop and await full exit
|
|
12
|
+
* GET /terminals/:id/stream SSE: buffered replay + live chunks
|
|
13
|
+
*
|
|
14
|
+
* Mutating routes require the Console boot token (same gate as other chat
|
|
15
|
+
* mutations). The stream is read-only and uses the same origin checks as the
|
|
16
|
+
* chat event stream.
|
|
17
|
+
*/
|
|
18
|
+
import { TerminalSessionError, availableShellKinds, } from "./terminal-sessions.js";
|
|
19
|
+
function sendJson(res, status, body) {
|
|
20
|
+
const payload = JSON.stringify(body);
|
|
21
|
+
res.writeHead(status, {
|
|
22
|
+
"content-type": "application/json; charset=utf-8",
|
|
23
|
+
"cache-control": "no-store",
|
|
24
|
+
});
|
|
25
|
+
res.end(payload);
|
|
26
|
+
}
|
|
27
|
+
function mapTerminalError(res, error) {
|
|
28
|
+
if (error instanceof TerminalSessionError) {
|
|
29
|
+
const status = error.code === "TERMINAL_NOT_FOUND"
|
|
30
|
+
? 404
|
|
31
|
+
: error.code === "TERMINAL_EXITED" || error.code === "TERMINAL_SEND_ACTIVE"
|
|
32
|
+
? 409
|
|
33
|
+
: 500;
|
|
34
|
+
sendJson(res, status, {
|
|
35
|
+
ok: false,
|
|
36
|
+
error: { code: error.code, message: error.message },
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
sendJson(res, 500, {
|
|
41
|
+
ok: false,
|
|
42
|
+
error: {
|
|
43
|
+
code: "TERMINAL_INTERNAL",
|
|
44
|
+
message: error instanceof Error ? error.message : String(error),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function readJsonBody(req, maxBytes = 512 * 1024) {
|
|
49
|
+
const chunks = [];
|
|
50
|
+
let total = 0;
|
|
51
|
+
for await (const chunk of req) {
|
|
52
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
53
|
+
total += buf.length;
|
|
54
|
+
if (total > maxBytes)
|
|
55
|
+
throw new Error("request body too large");
|
|
56
|
+
chunks.push(buf);
|
|
57
|
+
}
|
|
58
|
+
if (chunks.length === 0)
|
|
59
|
+
return {};
|
|
60
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
61
|
+
if (!raw.trim())
|
|
62
|
+
return {};
|
|
63
|
+
return JSON.parse(raw);
|
|
64
|
+
}
|
|
65
|
+
/** Route dispatcher; returns true when the request was handled. */
|
|
66
|
+
export async function handleTerminalRequest(req, res, deps, pathname) {
|
|
67
|
+
if (!pathname.startsWith("/api/operator/v1/chat/terminals"))
|
|
68
|
+
return false;
|
|
69
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
70
|
+
const rest = pathname.slice("/api/operator/v1/chat/terminals".length);
|
|
71
|
+
const parts = rest.split("/").filter(Boolean);
|
|
72
|
+
try {
|
|
73
|
+
// GET /terminals — list (+ available shell kinds for this platform)
|
|
74
|
+
if (method === "GET" && parts.length === 0) {
|
|
75
|
+
sendJson(res, 200, {
|
|
76
|
+
ok: true,
|
|
77
|
+
data: {
|
|
78
|
+
terminals: deps.terminals.list(),
|
|
79
|
+
availableShells: availableShellKinds().kinds,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
// POST /terminals — spawn
|
|
85
|
+
if (method === "POST" && parts.length === 0) {
|
|
86
|
+
const gate = deps.gateMutation(req);
|
|
87
|
+
if (!gate.ok) {
|
|
88
|
+
sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
const body = await readJsonBody(req);
|
|
92
|
+
const kinds = availableShellKinds().kinds;
|
|
93
|
+
const shell = kinds.includes(body.shell)
|
|
94
|
+
? body.shell
|
|
95
|
+
: undefined;
|
|
96
|
+
if (!shell) {
|
|
97
|
+
sendJson(res, 400, {
|
|
98
|
+
ok: false,
|
|
99
|
+
error: {
|
|
100
|
+
code: "INVALID_INPUT",
|
|
101
|
+
message: `shell must be one of: ${kinds.join(", ")} (available on this platform)`,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
const info = deps.terminals.spawn({
|
|
107
|
+
ownerSessionId: typeof body.ownerSessionId === "string" && body.ownerSessionId
|
|
108
|
+
? body.ownerSessionId
|
|
109
|
+
: "console-ui",
|
|
110
|
+
name: typeof body.name === "string" && body.name ? body.name : `${shell}-ui`,
|
|
111
|
+
shell: shell,
|
|
112
|
+
cwd: typeof body.cwd === "string" && body.cwd
|
|
113
|
+
? body.cwd
|
|
114
|
+
: (deps.defaultCwd ?? undefined),
|
|
115
|
+
cols: typeof body.cols === "number" ? body.cols : undefined,
|
|
116
|
+
rows: typeof body.rows === "number" ? body.rows : undefined,
|
|
117
|
+
});
|
|
118
|
+
sendJson(res, 201, { ok: true, data: info });
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
const sessionId = parts[0];
|
|
122
|
+
if (!sessionId)
|
|
123
|
+
return false;
|
|
124
|
+
const sub = parts[1] ?? "";
|
|
125
|
+
// GET /terminals/:id — info + buffered output
|
|
126
|
+
if (method === "GET" && parts.length === 1) {
|
|
127
|
+
const info = deps.terminals.get(sessionId);
|
|
128
|
+
const output = deps.terminals.readAll(sessionId);
|
|
129
|
+
sendJson(res, 200, { ok: true, data: { ...info, output } });
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
// GET /terminals/:id/stream — SSE
|
|
133
|
+
if (method === "GET" && sub === "stream") {
|
|
134
|
+
// Validate existence first (404 before opening the stream).
|
|
135
|
+
const info = deps.terminals.get(sessionId);
|
|
136
|
+
void info;
|
|
137
|
+
res.writeHead(200, {
|
|
138
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
139
|
+
"cache-control": "no-cache, no-store",
|
|
140
|
+
connection: "keep-alive",
|
|
141
|
+
"x-accel-buffering": "no",
|
|
142
|
+
});
|
|
143
|
+
const writeFrame = (event, data) => {
|
|
144
|
+
if (res.writableEnded)
|
|
145
|
+
return;
|
|
146
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
147
|
+
};
|
|
148
|
+
const buffered = deps.terminals.readAll(sessionId);
|
|
149
|
+
writeFrame("snapshot", {
|
|
150
|
+
truncated: buffered.truncated,
|
|
151
|
+
totalOutputChars: buffered.totalOutputChars,
|
|
152
|
+
text: buffered.text,
|
|
153
|
+
info: deps.terminals.get(sessionId),
|
|
154
|
+
});
|
|
155
|
+
const unsubscribe = deps.terminals.subscribe(sessionId, (chunk) => {
|
|
156
|
+
writeFrame("data", { chunk });
|
|
157
|
+
});
|
|
158
|
+
const heartbeat = setInterval(() => {
|
|
159
|
+
if (!res.writableEnded)
|
|
160
|
+
res.write(": heartbeat\n\n");
|
|
161
|
+
}, 15_000);
|
|
162
|
+
res.once("close", () => {
|
|
163
|
+
clearInterval(heartbeat);
|
|
164
|
+
unsubscribe();
|
|
165
|
+
});
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
// Everything else mutates → gate first.
|
|
169
|
+
const gate = deps.gateMutation(req);
|
|
170
|
+
if (!gate.ok) {
|
|
171
|
+
sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
if (method === "POST" && sub === "input" && parts.length === 2) {
|
|
175
|
+
const body = await readJsonBody(req);
|
|
176
|
+
if (typeof body.data !== "string") {
|
|
177
|
+
sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "data (string) is required" } });
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
deps.terminals.send(sessionId, body.data);
|
|
181
|
+
sendJson(res, 200, { ok: true });
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
if (method === "POST" && sub === "resize" && parts.length === 2) {
|
|
185
|
+
const body = await readJsonBody(req);
|
|
186
|
+
const cols = Number(body.cols);
|
|
187
|
+
const rows = Number(body.rows);
|
|
188
|
+
if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols < 2 || rows < 2 || cols > 500 || rows > 200) {
|
|
189
|
+
sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "cols and rows (integers 2..500/200) are required" } });
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
deps.terminals.resize(sessionId, cols, rows);
|
|
193
|
+
sendJson(res, 200, { ok: true });
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
if (method === "POST" && sub === "interrupt" && parts.length === 2) {
|
|
197
|
+
deps.terminals.interrupt(sessionId);
|
|
198
|
+
sendJson(res, 200, { ok: true });
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
if (method === "POST" && sub === "kill" && parts.length === 2) {
|
|
202
|
+
await deps.terminals.kill(sessionId);
|
|
203
|
+
sendJson(res, 200, { ok: true });
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
sendJson(res, 404, { ok: false, error: { code: "NOT_FOUND", message: `unknown terminal route: ${method} ${pathname}` } });
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (!res.headersSent)
|
|
211
|
+
mapTerminalError(res, error);
|
|
212
|
+
else
|
|
213
|
+
res.end();
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator Chat — persistent PTY terminal sessions (DSH-style terminal seam).
|
|
3
|
+
*
|
|
4
|
+
* Process-local registry of owner-scoped persistent terminal sessions:
|
|
5
|
+
* - spawn: create a named PTY (shell) owned by a chat session
|
|
6
|
+
* - send: write bytes into the PTY (model tool or console UI input)
|
|
7
|
+
* - read: ring-buffered output history + live subscriber fan-out
|
|
8
|
+
* - signal/kill: interrupt or fully stop the process tree, awaited
|
|
9
|
+
* - dispose: stop everything owned by one chat session
|
|
10
|
+
*
|
|
11
|
+
* Design mirrors the DSH terminal seam contract (process-local, no sharing
|
|
12
|
+
* across agents, single active send, kill waits for full stop) but stays
|
|
13
|
+
* deliberately small: no backend abstraction (node-pty is the only backend),
|
|
14
|
+
* no cross-process persistence (sessions die with the Console process).
|
|
15
|
+
*/
|
|
16
|
+
import { spawn as ptySpawn } from "node-pty";
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
18
|
+
import { randomBytes } from "node:crypto";
|
|
19
|
+
/** Bounded ring buffer of raw output chunks (bytes preserved as utf8 text). */
|
|
20
|
+
const MAX_BUFFER_CHARS = 200_000;
|
|
21
|
+
export class TerminalSessionError extends Error {
|
|
22
|
+
code;
|
|
23
|
+
constructor(code, message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.name = "TerminalSessionError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
30
|
+
function firstExisting(paths) {
|
|
31
|
+
return paths.find((p) => existsSync(p));
|
|
32
|
+
}
|
|
33
|
+
/** Windows PowerShell 5+ or pwsh 7; prefer pwsh 7 when installed. */
|
|
34
|
+
function resolvePowershell() {
|
|
35
|
+
if (IS_WINDOWS) {
|
|
36
|
+
const pwsh7 = firstExisting([
|
|
37
|
+
"C:\\Program Files\\PowerShell\\7\\pwsh.exe",
|
|
38
|
+
"C:\\Program Files (x86)\\PowerShell\\7\\pwsh.exe",
|
|
39
|
+
]);
|
|
40
|
+
if (pwsh7) {
|
|
41
|
+
return {
|
|
42
|
+
file: pwsh7,
|
|
43
|
+
args: [
|
|
44
|
+
"-NoLogo",
|
|
45
|
+
"-NoProfile",
|
|
46
|
+
"-ExecutionPolicy",
|
|
47
|
+
"Bypass",
|
|
48
|
+
"-NoExit",
|
|
49
|
+
"-Command",
|
|
50
|
+
"$PSDefaultParameterValues['Out-File:Encoding']='utf8'; [Console]::OutputEncoding=[Text.Encoding]::UTF8",
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const ps5 = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
|
|
55
|
+
if (existsSync(ps5)) {
|
|
56
|
+
return {
|
|
57
|
+
file: ps5,
|
|
58
|
+
args: [
|
|
59
|
+
"-NoProfile",
|
|
60
|
+
"-ExecutionPolicy",
|
|
61
|
+
"Bypass",
|
|
62
|
+
"-NoExit",
|
|
63
|
+
"-Command",
|
|
64
|
+
"$PSDefaultParameterValues['Out-File:Encoding']='utf8'; [Console]::OutputEncoding=[Text.Encoding]::UTF8",
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
// macOS / Linux: only pwsh 7 if explicitly installed.
|
|
71
|
+
const pwsh = firstExisting([
|
|
72
|
+
"/usr/local/bin/pwsh",
|
|
73
|
+
"/opt/homebrew/bin/pwsh",
|
|
74
|
+
"/usr/bin/pwsh",
|
|
75
|
+
]);
|
|
76
|
+
return pwsh
|
|
77
|
+
? { file: pwsh, args: ["-NoLogo", "-NoProfile"] }
|
|
78
|
+
: undefined;
|
|
79
|
+
}
|
|
80
|
+
/** bash: Git Bash on Windows, /bin/bash (or Homebrew) on unix-likes. */
|
|
81
|
+
function resolveBash() {
|
|
82
|
+
if (IS_WINDOWS) {
|
|
83
|
+
const file = firstExisting([
|
|
84
|
+
"C:\\Program Files\\Git\\bin\\bash.exe",
|
|
85
|
+
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
86
|
+
"C:\\Program Files\\Git\\usr\\bin\\bash.exe",
|
|
87
|
+
]);
|
|
88
|
+
return file ? { file, args: ["-i", "-l"] } : undefined;
|
|
89
|
+
}
|
|
90
|
+
const file = firstExisting(["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash", "/opt/homebrew/bin/bash"]);
|
|
91
|
+
return file ? { file, args: ["-i"] } : undefined;
|
|
92
|
+
}
|
|
93
|
+
/** zsh: macOS default shell; on Linux only when installed. */
|
|
94
|
+
function resolveZsh() {
|
|
95
|
+
if (IS_WINDOWS)
|
|
96
|
+
return undefined;
|
|
97
|
+
const file = firstExisting(["/bin/zsh", "/usr/bin/zsh", "/usr/local/bin/zsh", "/opt/homebrew/bin/zsh"]);
|
|
98
|
+
return file ? { file, args: ["-i"] } : undefined;
|
|
99
|
+
}
|
|
100
|
+
let cachedCatalog;
|
|
101
|
+
/** Platform shell catalog, computed once per process. Drives both the UI
|
|
102
|
+
* button set and the model tool's shell parameter enum. */
|
|
103
|
+
export function availableShellKinds() {
|
|
104
|
+
if (cachedCatalog)
|
|
105
|
+
return cachedCatalog;
|
|
106
|
+
const resolved = new Map();
|
|
107
|
+
const pwsh = resolvePowershell();
|
|
108
|
+
if (pwsh)
|
|
109
|
+
resolved.set("powershell", pwsh);
|
|
110
|
+
const bash = resolveBash();
|
|
111
|
+
if (bash)
|
|
112
|
+
resolved.set("bash", bash);
|
|
113
|
+
const zsh = resolveZsh();
|
|
114
|
+
if (zsh)
|
|
115
|
+
resolved.set("zsh", zsh);
|
|
116
|
+
cachedCatalog = { kinds: [...resolved.keys()], resolved };
|
|
117
|
+
return cachedCatalog;
|
|
118
|
+
}
|
|
119
|
+
export function resolveShellCommand(shell) {
|
|
120
|
+
const resolved = availableShellKinds().resolved.get(shell);
|
|
121
|
+
if (!resolved) {
|
|
122
|
+
throw new TerminalSessionError("TERMINAL_SPAWN_FAILED", `shell "${shell}" is not available on this platform (${process.platform})`);
|
|
123
|
+
}
|
|
124
|
+
return resolved;
|
|
125
|
+
}
|
|
126
|
+
export class TerminalSessionRegistry {
|
|
127
|
+
entries = new Map();
|
|
128
|
+
nextSeq = 0;
|
|
129
|
+
spawn(options) {
|
|
130
|
+
let file;
|
|
131
|
+
let args;
|
|
132
|
+
try {
|
|
133
|
+
({ file, args } = resolveShellCommand(options.shell));
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
throw new TerminalSessionError("TERMINAL_SPAWN_FAILED", `failed to spawn ${options.shell}: ${error instanceof Error ? error.message : String(error)}`);
|
|
137
|
+
}
|
|
138
|
+
const sessionId = `term-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
|
|
139
|
+
let pty;
|
|
140
|
+
try {
|
|
141
|
+
pty = ptySpawn(file, args, {
|
|
142
|
+
name: "xterm-256color",
|
|
143
|
+
cwd: options.cwd,
|
|
144
|
+
cols: options.cols ?? 120,
|
|
145
|
+
rows: options.rows ?? 30,
|
|
146
|
+
env: process.env ?? {},
|
|
147
|
+
encoding: "utf8",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
throw new TerminalSessionError("TERMINAL_SPAWN_FAILED", `failed to spawn ${options.shell}: ${error instanceof Error ? error.message : String(error)}`);
|
|
152
|
+
}
|
|
153
|
+
const entry = {
|
|
154
|
+
info: {
|
|
155
|
+
sessionId,
|
|
156
|
+
ownerSessionId: options.ownerSessionId,
|
|
157
|
+
name: options.name,
|
|
158
|
+
shell: options.shell,
|
|
159
|
+
createdAt: new Date().toISOString(),
|
|
160
|
+
totalOutputChars: 0,
|
|
161
|
+
exited: false,
|
|
162
|
+
},
|
|
163
|
+
pty,
|
|
164
|
+
buffer: [],
|
|
165
|
+
bufferChars: 0,
|
|
166
|
+
subscribers: new Set(),
|
|
167
|
+
disposing: undefined,
|
|
168
|
+
};
|
|
169
|
+
this.entries.set(sessionId, entry);
|
|
170
|
+
pty.onData((chunk) => {
|
|
171
|
+
entry.info.totalOutputChars += chunk.length;
|
|
172
|
+
entry.buffer.push(chunk);
|
|
173
|
+
entry.bufferChars += chunk.length;
|
|
174
|
+
while (entry.bufferChars > MAX_BUFFER_CHARS && entry.buffer.length > 1) {
|
|
175
|
+
const dropped = entry.buffer.shift();
|
|
176
|
+
entry.bufferChars -= dropped?.length ?? 0;
|
|
177
|
+
}
|
|
178
|
+
for (const notify of entry.subscribers) {
|
|
179
|
+
try {
|
|
180
|
+
notify(chunk);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// subscriber callbacks must never break the pty stream
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
pty.onExit(({ exitCode }) => {
|
|
188
|
+
entry.info.exited = true;
|
|
189
|
+
entry.info.exitCode = exitCode;
|
|
190
|
+
for (const notify of entry.subscribers) {
|
|
191
|
+
try {
|
|
192
|
+
notify(`\r\n\u001b[90m[terminal exited: code ${exitCode}]\u001b[0m\r\n`);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// ignore
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
return { ...entry.info };
|
|
200
|
+
}
|
|
201
|
+
list(ownerSessionId) {
|
|
202
|
+
const all = [...this.entries.values()].map((e) => ({ ...e.info }));
|
|
203
|
+
return ownerSessionId
|
|
204
|
+
? all.filter((i) => i.ownerSessionId === ownerSessionId)
|
|
205
|
+
: all;
|
|
206
|
+
}
|
|
207
|
+
get(sessionId) {
|
|
208
|
+
const entry = this.entries.get(sessionId);
|
|
209
|
+
if (!entry)
|
|
210
|
+
throw new TerminalSessionError("TERMINAL_NOT_FOUND", sessionId);
|
|
211
|
+
return { ...entry.info };
|
|
212
|
+
}
|
|
213
|
+
mustGet(sessionId) {
|
|
214
|
+
const entry = this.entries.get(sessionId);
|
|
215
|
+
if (!entry)
|
|
216
|
+
throw new TerminalSessionError("TERMINAL_NOT_FOUND", sessionId);
|
|
217
|
+
return entry;
|
|
218
|
+
}
|
|
219
|
+
/** Full buffered output (post-trim prefix may be incomplete). */
|
|
220
|
+
readAll(sessionId) {
|
|
221
|
+
const entry = this.mustGet(sessionId);
|
|
222
|
+
return {
|
|
223
|
+
text: entry.buffer.join(""),
|
|
224
|
+
truncated: entry.info.totalOutputChars > entry.bufferChars,
|
|
225
|
+
totalOutputChars: entry.info.totalOutputChars,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/** Subscribe to live output chunks; returns unsubscribe. */
|
|
229
|
+
subscribe(sessionId, onChunk) {
|
|
230
|
+
const entry = this.mustGet(sessionId);
|
|
231
|
+
entry.subscribers.add(onChunk);
|
|
232
|
+
return () => entry.subscribers.delete(onChunk);
|
|
233
|
+
}
|
|
234
|
+
send(sessionId, data) {
|
|
235
|
+
const entry = this.mustGet(sessionId);
|
|
236
|
+
if (entry.info.exited)
|
|
237
|
+
throw new TerminalSessionError("TERMINAL_EXITED", "terminal already exited");
|
|
238
|
+
entry.pty.write(data);
|
|
239
|
+
}
|
|
240
|
+
/** Resize the PTY (console UI attach). */
|
|
241
|
+
resize(sessionId, cols, rows) {
|
|
242
|
+
const entry = this.mustGet(sessionId);
|
|
243
|
+
if (entry.info.exited)
|
|
244
|
+
return;
|
|
245
|
+
entry.pty.resize(cols, rows);
|
|
246
|
+
}
|
|
247
|
+
/** Send an interrupt character (Ctrl+C) to the foreground process. */
|
|
248
|
+
interrupt(sessionId) {
|
|
249
|
+
const entry = this.mustGet(sessionId);
|
|
250
|
+
if (entry.info.exited)
|
|
251
|
+
return;
|
|
252
|
+
entry.pty.write("\x03");
|
|
253
|
+
}
|
|
254
|
+
/** Kill and wait for full stop, then remove the registry entry so closed
|
|
255
|
+
* terminals disappear from the list (explicit close = forget). Idempotent. */
|
|
256
|
+
async kill(sessionId) {
|
|
257
|
+
const entry = this.entries.get(sessionId);
|
|
258
|
+
if (!entry)
|
|
259
|
+
throw new TerminalSessionError("TERMINAL_NOT_FOUND", sessionId);
|
|
260
|
+
if (!entry.info.exited) {
|
|
261
|
+
entry.disposing ??= new Promise((resolve) => {
|
|
262
|
+
const timer = setTimeout(() => resolve(), 5_000);
|
|
263
|
+
entry.pty.onExit(() => {
|
|
264
|
+
clearTimeout(timer);
|
|
265
|
+
resolve();
|
|
266
|
+
});
|
|
267
|
+
try {
|
|
268
|
+
entry.pty.kill();
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
clearTimeout(timer);
|
|
272
|
+
resolve();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
await entry.disposing;
|
|
276
|
+
}
|
|
277
|
+
this.entries.delete(sessionId);
|
|
278
|
+
}
|
|
279
|
+
/** Kill every session owned by one chat session; awaited. */
|
|
280
|
+
async disposeOwner(ownerSessionId) {
|
|
281
|
+
const owned = this.list(ownerSessionId).map((i) => i.sessionId);
|
|
282
|
+
await Promise.all(owned.map((id) => this.kill(id).catch(() => undefined)));
|
|
283
|
+
}
|
|
284
|
+
}
|