@tea-agent/loop-agent 0.43.0-next.18 → 0.43.0-next.19
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/AGENTS.md +1 -1
- package/CHANGELOG.md +60 -2308
- package/README.md +7 -0
- package/dist/application/task-lifecycle/advance.js +38 -7
- package/dist/build-stamp.json +3 -3
- package/dist/commands/init.js +13 -9
- package/dist/commands/task-advance.js +32 -0
- package/dist/executors/dag-pi-executor.js +1615 -188
- package/dist/executors/pi-executor.js +85 -3
- package/dist/executors/pi-sdk-executor.js +18 -0
- package/dist/executors/shell-executor.js +92 -7
- package/dist/shared/backend-dogfood-preflight.js +47 -0
- package/dist/shared/dag-failure-category.js +3 -0
- package/dist/shared/operator/capabilities.js +180 -0
- package/dist/task/config-types.js +29 -0
- package/dist/task/contract/project.js +9 -0
- package/dist/task/contract/schema.js +17 -0
- package/dist/task/source-prepare/build-draft.js +60 -0
- package/dist/worker/console/chat/pi-mode-loop-isolation.js +155 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +24 -16
- package/dist/worker/console/chat/pi-runtime/custom-tools/scheduled-goal-tools.js +22 -0
- package/dist/worker/console/chat/pi-runtime/progressive-tools.js +195 -0
- package/dist/worker/console/chat/pi-runtime.js +86 -27
- package/dist/worker/console/chat/routes.js +19 -0
- package/dist/worker/console/chat/scheduled-goal-booking.js +59 -0
- package/dist/worker/console/chat/scheduled-goal-delivery.js +27 -0
- package/dist/worker/console/chat/scheduled-goal-request.js +190 -0
- package/dist/worker/console/chat/session-mode.js +7 -3
- package/dist/worker/console/chat/session-store.js +42 -9
- package/dist/worker/console/chat/tool-preview.js +115 -0
- package/dist/worker/console/chat/turn-order.js +13 -0
- package/dist/worker/console/chat/turn-process.js +32 -30
- package/dist/worker/console/operator-actions.js +50 -0
- package/dist/worker/console/prd-intake-bridge.js +54 -1
- package/dist/worker/console/scheduled-goal-host.js +98 -0
- package/dist/worker/console/scheduled-goal-operation-adapter.js +127 -0
- package/dist/worker/console/server.js +15 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B_UDknhR.js → abnfDiagram-N423BO3Z-8-j6y-sd.js} +1 -1
- package/dist/worker/console/static/assets/{arc-D6hU0drN.js → arc-eoQiMvuk.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DmVikdRK.js → architectureDiagram-T3A2C74G-C4A3uMcI.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DydABCRg.js → blockDiagram-VBNYF7ZC-D4zD2F-Q.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DrXKC2Jl.js → c4Diagram-5PPSVZJV-j1RkJziL.js} +1 -1
- package/dist/worker/console/static/assets/channel-ChE7y-cx.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D2-qFxr7.js → chunk-2GRJ4B5K-YBHmhik1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ChfeF41P.js → chunk-2Q5K7J3B-COfWyo9P.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Drwrz55x.js → chunk-5RXB4S5H-I99OUkHY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CAQGErL7.js → chunk-5VM5RSS4-XKxoJNJ4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DHO38w5U.js → chunk-6Q2QTUOP-DLT_cYx1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-EYIN0EOI.js → chunk-GF5L2VYU-CxcKZ9mV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CrFLKqkT.js → chunk-JWPE2WC7-V1EqXdjY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DMchffoo.js → chunk-KBJHAD2P-DebTtdFp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DaSvtKQa.js → chunk-RYQCIY6F-B-ivNRDf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DcW8W9Qj.js → chunk-XXDRQBXY-DC_Ds11b.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BUirrMEi.js → cose-bilkent-JH36ORCC-sWEqKwIb.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXzO8iXR.js → cynefin-VYW2F7L2-BXa_dcu4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DAtgaxIn.js → cynefinDiagram-MW4NZA55-C-Mle84F.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C1re9Noh.js → dagre-VZM6K2ZE-CXPDBITe.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-N8Kj08J9.js → diagram-7IWD3JNH-CC-WJQfa.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-D6BpSLs3.js → diagram-B4RE2ZJO-CDAV5Vs4.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C2Vlgnl4.js → diagram-LBJQPF4R-CmFiAcNz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CMbqtVLS.js → diagram-Q27KOJAE-DPBZHuyn.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-A770Eb-0.js → diagram-UB23O5K3-jUlm_Ds3.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-LV2w_2pT.js → ebnfDiagram-BXEA7PRR-DWhQ3mfY.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-bwqf56ah.js → erDiagram-JOGREHBK-Tr2gMqet.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BnVtHhZh.js → flowDiagram-UKHOOZJN-DJjVQHPA.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BGVToEqc.js → ganttDiagram-PKOTCBZU-D74dQ4u0.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DL2l7Vne.js → gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js} +1 -1
- package/dist/worker/console/static/assets/index-BWkIfcrK.css +1 -0
- package/dist/worker/console/static/assets/{index-B_V4wvXs.js → index-Cdkvw_H6.js} +97 -97
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BWIpubOx.js → infoDiagram-6WML65LV-ILCbxJyb.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DjYBD8vv.js → ishikawaDiagram-WSZJBQD7-DeuWBQ89.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-C0_mBaHb.js → journeyDiagram-NVQOT4AX-CF5ih8Fk.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Cy71zbX6.js → kanban-definition-27J2QSJJ-C7yOSuRO.js} +1 -1
- package/dist/worker/console/static/assets/{linear-Dt3_w3Vn.js → linear-BDZ9riWi.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DnNOlfEP.js → mermaid.core-7pKqYtpZ.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DAbTspwq.js → mindmap-definition-FAOFIHXS-LeJDybSU.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-NNiM141p.js → pegDiagram-VL7TDLO6-BJvT3pMD.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CN80Z1Do.js → pieDiagram-7S7Q4E2Y-_rGqpMan.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bxyfh0JU.js → quadrantDiagram-CIZ2JOQS-DPcSv5aA.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BlWnC79M.js → railroadDiagram-AXF67PYL-Drxx4hkJ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BrEu4P_e.js → requirementDiagram-LRYGKXZP-BCTUdU4z.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bt70igBr.js → sankeyDiagram-W5VNT64P-B6wzbZmB.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C8DQR-r9.js → sequenceDiagram-SI44F4Z6-BGt8d3QQ.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DEMinJAT.js → sizeCapture-X5ZJPWSS-7nlhJKo7.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-DDdDBzQc.js → stateDiagram-OKZ733FA-Cv2stqMA.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-G2wcBs3T.js → swimlanes-SLNWSIFB-qDAo4Yc1.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Dzn4g2gJ.js → timeline-definition-Z64GVDOM-CKbDNKTr.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-5v2rP9oO.js → vennDiagram-T6HMQDX7-DI-9EHic.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-B4EOx7ew.js → wardleyDiagram-T6FBY63Y-BrzzRDpX.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CQ1v8eYP.js → xychartDiagram-ELKLHX3M-BclH5hGh.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +41 -11
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +3 -17
- package/dist/worker/console/static-src/operator-chat/timeline-merge.js +29 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +2 -5
- package/dist/worker/console/workspace-context.js +11 -0
- package/dist/worker/observe/static/operator-chrome.js +3 -1
- package/dist/worker/scheduler/scheduled-goal-dispatch.js +117 -0
- package/dist/worker/scheduler/scheduled-goal-evidence.js +167 -0
- package/dist/worker/scheduler/scheduled-goal-recovery.js +62 -0
- package/dist/worker/scheduler/scheduled-goal-store.js +699 -0
- package/dist/worker/scheduler/scheduled-goal-supervisor.js +132 -0
- package/dist/worker/scheduler/scheduled-goal-time.js +102 -0
- package/dist/worker/scheduler/scheduled-goal-types.js +95 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +6 -24
- package/dist/workflows/dag/backend-test-plan-protocol.js +82 -5
- package/dist/workflows/dag/dag-retry-schema.js +11 -0
- package/dist/workflows/dag/frontend-closeout.js +4 -2
- package/dist/workflows/dag/frontend-committed-facts.js +461 -0
- package/dist/workflows/dag/frontend-durable-tools.js +15 -3
- package/dist/workflows/dag/frontend-implementation-contract.js +365 -8
- package/dist/workflows/dag/frontend-plan-canary.js +53 -0
- package/dist/workflows/dag/frontend-plan-decision-contract.js +803 -0
- package/dist/workflows/dag/frontend-plan-render.js +0 -2
- package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +8 -61
- package/dist/workflows/dag/frontend-recovery-run.js +57 -0
- package/dist/workflows/dag/frontend-review-context.js +43 -70
- package/dist/workflows/dag/frontend-risk.js +92 -10
- package/dist/workflows/dag/frontend-session-budget.js +117 -3
- package/dist/workflows/dag/frontend-shape.js +11 -55
- package/dist/workflows/dag/frontend-test-execution-evidence.js +35 -10
- package/dist/workflows/dag/frontend-typed-event-store.js +32 -25
- package/dist/workflows/dag/frontend-verification-trace.js +11 -2
- package/dist/workflows/dag/frontend-writer-admission.js +3 -47
- package/dist/workflows/dag/frontend-writer-status.js +0 -23
- package/dist/workflows/dag/init-hybrid.js +182 -185
- package/dist/workflows/dag/lifecycle.js +7 -2
- package/dist/workflows/dag/node-execution.js +48 -13
- package/dist/workflows/dag/rerun-plan.js +10 -0
- package/dist/workflows/dag/rerun-task.js +77 -1
- package/dist/workflows/dag/retry-policy.js +18 -0
- package/dist/workflows/dag/scheduler.js +2 -4
- package/dist/workflows/dag/types.js +23 -0
- package/docs/README.md +1 -0
- package/docs/init-surface.manifest.json +1 -0
- package/docs/operations/README.md +2 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +2 -2
- package/docs/templates/backend-test-dag.json +14 -10
- package/docs/templates/frontend-implementation-contract.schema.json +0 -7
- package/docs/templates/frontend-implementation-dag.json +8 -9
- package/package.json +6 -3
- package/skills/frontend-bounded-implement/SKILL.md +3 -4
- package/skills/frontend-design-review/SKILL.md +7 -12
- package/skills/frontend-design-review/references/review-checklist.md +7 -7
- package/skills/frontend-plan/SKILL.md +8 -18
- package/skills/frontend-plan/references/decision-contract.md +19 -26
- package/skills/frontend-review/SKILL.md +21 -25
- package/skills/frontend-review/references/review-findings.md +48 -24
- package/dist/worker/console/static/assets/channel-Drecd94a.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-8Udu0t8-.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-8Udu0t8-.js +0 -1
- package/dist/worker/console/static/assets/index-DcudonhZ.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CyCiS0Lt.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-U0lhi6N0.js +0 -8
- package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -975
|
@@ -477,6 +477,24 @@ export class ChatSessionStore {
|
|
|
477
477
|
};
|
|
478
478
|
});
|
|
479
479
|
}
|
|
480
|
+
/** Idempotent persistent delivery, serialized with normal Chat updates. */
|
|
481
|
+
async appendScheduledGoalResult(sessionId, resultKey, text) {
|
|
482
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(sessionId) || !/^[a-f0-9]{64}$/.test(resultKey))
|
|
483
|
+
throw new Error("invalid scheduled goal delivery identity");
|
|
484
|
+
return this.serialize(sessionId, async () => {
|
|
485
|
+
const record = await this.get(sessionId);
|
|
486
|
+
if (!record)
|
|
487
|
+
return false;
|
|
488
|
+
const id = `scheduled-goal-${resultKey}`;
|
|
489
|
+
if (record.messages.some((message) => message.id === id))
|
|
490
|
+
return true;
|
|
491
|
+
const at = new Date().toISOString();
|
|
492
|
+
record.messages.push({ id, role: "assistant", text, at });
|
|
493
|
+
record.updatedAt = at;
|
|
494
|
+
await this.persist(record);
|
|
495
|
+
return true;
|
|
496
|
+
});
|
|
497
|
+
}
|
|
480
498
|
async appendMessage(sessionId, message) {
|
|
481
499
|
return this.serialize(sessionId, async () => {
|
|
482
500
|
const record = await this.get(sessionId);
|
|
@@ -637,21 +655,36 @@ export async function runChatTurn(options) {
|
|
|
637
655
|
// never observes agent_settled before its accepted operation ref.
|
|
638
656
|
settledEvent = event;
|
|
639
657
|
}
|
|
658
|
+
else if (event.type === "message_end") {
|
|
659
|
+
// Persist the assistant bubble FIRST so the SSE frame and the restore
|
|
660
|
+
// projection share the same durable message id (2026-09-13). The text-
|
|
661
|
+
// match + 30s window in projectStoredMessagesWithEventOrder was the only
|
|
662
|
+
// bridge left between the two, and it mis-aligned on duplicate text.
|
|
663
|
+
let messageId;
|
|
664
|
+
try {
|
|
665
|
+
const stored = await store.appendMessage(sessionId, {
|
|
666
|
+
role: "assistant",
|
|
667
|
+
text: event.text,
|
|
668
|
+
...(event.content && event.content.length > 0
|
|
669
|
+
? { content: event.content }
|
|
670
|
+
: {}),
|
|
671
|
+
});
|
|
672
|
+
messageId = stored.id;
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
// Persistence failure must not swallow the SSE frame — the client
|
|
676
|
+
// still needs the terminal text even if the durable row is absent.
|
|
677
|
+
}
|
|
678
|
+
options.onEvent?.(messageId ? { ...event, messageId } : event);
|
|
679
|
+
}
|
|
640
680
|
else {
|
|
641
681
|
options.onEvent?.(event);
|
|
642
682
|
}
|
|
643
683
|
switch (event.type) {
|
|
644
684
|
case "message_update":
|
|
645
685
|
case "message_end":
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
role: "assistant",
|
|
649
|
-
text: event.text,
|
|
650
|
-
...(event.content && event.content.length > 0
|
|
651
|
-
? { content: event.content }
|
|
652
|
-
: {}),
|
|
653
|
-
}));
|
|
654
|
-
}
|
|
686
|
+
// message_update carries no durable row; message_end was already
|
|
687
|
+
// persisted + framed above. Nothing further to persist here.
|
|
655
688
|
break;
|
|
656
689
|
case "tool_call":
|
|
657
690
|
// Forwarded to the caller (SSE) for UI feedback. The actual tool
|
|
@@ -148,6 +148,115 @@ export function getToolPreview(args, options) {
|
|
|
148
148
|
return "";
|
|
149
149
|
return s.length > maxLen ? `${s.slice(0, maxLen)}…` : s;
|
|
150
150
|
}
|
|
151
|
+
/** DSH-style human-readable ask_user_question row (rowTitle「提问」). */
|
|
152
|
+
export function isAskUserQuestionTool(toolName) {
|
|
153
|
+
return (toolName ?? "").trim().toLowerCase() === "ask_user_question";
|
|
154
|
+
}
|
|
155
|
+
function normalizeOptionLabel(value) {
|
|
156
|
+
const record = asRecord(value);
|
|
157
|
+
if (!record)
|
|
158
|
+
return undefined;
|
|
159
|
+
const label = typeof record.label === "string" ? record.label.trim() : "";
|
|
160
|
+
return label || undefined;
|
|
161
|
+
}
|
|
162
|
+
function normalizeAskQuestion(value) {
|
|
163
|
+
const record = asRecord(value);
|
|
164
|
+
if (!record)
|
|
165
|
+
return undefined;
|
|
166
|
+
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
167
|
+
const question = typeof record.question === "string" ? record.question.trim() : "";
|
|
168
|
+
if (!id || !question)
|
|
169
|
+
return undefined;
|
|
170
|
+
const options = Array.isArray(record.options)
|
|
171
|
+
? record.options
|
|
172
|
+
.map((item) => {
|
|
173
|
+
const label = normalizeOptionLabel(item);
|
|
174
|
+
if (!label)
|
|
175
|
+
return undefined;
|
|
176
|
+
const description = typeof item === "object" && item !== null &&
|
|
177
|
+
"description" in item && typeof item.description === "string"
|
|
178
|
+
? item.description
|
|
179
|
+
: undefined;
|
|
180
|
+
return { label, ...(description && description.trim() ? { description: description.trim() } : {}) };
|
|
181
|
+
})
|
|
182
|
+
.filter((item) => Boolean(item))
|
|
183
|
+
: undefined;
|
|
184
|
+
return {
|
|
185
|
+
id,
|
|
186
|
+
question,
|
|
187
|
+
...(typeof record.header === "string" && record.header.trim()
|
|
188
|
+
? { header: record.header.trim() }
|
|
189
|
+
: {}),
|
|
190
|
+
...(typeof record.detail === "string" && record.detail.trim()
|
|
191
|
+
? { detail: record.detail.trim() }
|
|
192
|
+
: {}),
|
|
193
|
+
...(options && options.length > 0 ? { options } : {}),
|
|
194
|
+
...(record.multi_select === true || record.multiSelect === true
|
|
195
|
+
? { multiSelect: true }
|
|
196
|
+
: {}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/** Project ask_user_question args (questions array) into readable views. */
|
|
200
|
+
export function parseAskUserQuestions(args) {
|
|
201
|
+
const input = parseArgs(args);
|
|
202
|
+
if (!input)
|
|
203
|
+
return [];
|
|
204
|
+
const raw = Array.isArray(input)
|
|
205
|
+
? input
|
|
206
|
+
: Array.isArray(input.questions)
|
|
207
|
+
? input.questions
|
|
208
|
+
: [];
|
|
209
|
+
return raw
|
|
210
|
+
.map(normalizeAskQuestion)
|
|
211
|
+
.filter((item) => Boolean(item));
|
|
212
|
+
}
|
|
213
|
+
/** Project ask_user_question ToolResult into answer views keyed by question id. */
|
|
214
|
+
export function parseAskUserAnswers(result) {
|
|
215
|
+
if (result === undefined || result === null)
|
|
216
|
+
return [];
|
|
217
|
+
const text = extractToolResultText(result);
|
|
218
|
+
if (!text)
|
|
219
|
+
return [];
|
|
220
|
+
let parsed;
|
|
221
|
+
try {
|
|
222
|
+
parsed = JSON.parse(text);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
const record = asRecord(parsed);
|
|
228
|
+
const raw = record && Array.isArray(record.answers) ? record.answers : undefined;
|
|
229
|
+
if (!raw)
|
|
230
|
+
return [];
|
|
231
|
+
const out = [];
|
|
232
|
+
for (const entry of raw) {
|
|
233
|
+
const rec = entry && typeof entry === "object" && !Array.isArray(entry)
|
|
234
|
+
? entry
|
|
235
|
+
: undefined;
|
|
236
|
+
if (!rec || typeof rec.id !== "string")
|
|
237
|
+
continue;
|
|
238
|
+
const selected = Array.isArray(rec.selected)
|
|
239
|
+
? rec.selected.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean)
|
|
240
|
+
: [];
|
|
241
|
+
const custom = typeof rec.custom === "string" ? rec.custom.trim() : "";
|
|
242
|
+
out.push({ id: rec.id, selected, ...(custom ? { custom } : {}) });
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
/** "{answered}/{total} 已回答" summary; total from args questions, answered from result answers. */
|
|
247
|
+
export function getAskUserQuestionSummary(args, result) {
|
|
248
|
+
const questions = parseAskUserQuestions(args);
|
|
249
|
+
if (questions.length === 0)
|
|
250
|
+
return null;
|
|
251
|
+
const answers = parseAskUserAnswers(result);
|
|
252
|
+
let answered = 0;
|
|
253
|
+
for (const question of questions) {
|
|
254
|
+
const answer = answers.find((item) => item.id === question.id);
|
|
255
|
+
if (answer && (answer.selected.length > 0 || answer.custom))
|
|
256
|
+
answered += 1;
|
|
257
|
+
}
|
|
258
|
+
return { answered, total: questions.length };
|
|
259
|
+
}
|
|
151
260
|
/** Stable DSH-style tool row identity; unknown tools keep their real name. */
|
|
152
261
|
export function getToolPresentation(toolName) {
|
|
153
262
|
const raw = toolName?.trim() || "Tool";
|
|
@@ -191,6 +300,9 @@ export function getToolPresentation(toolName) {
|
|
|
191
300
|
if (name === "agent") {
|
|
192
301
|
return { kind: "search", label: "Explore" };
|
|
193
302
|
}
|
|
303
|
+
if (isAskUserQuestionTool(toolName)) {
|
|
304
|
+
return { kind: "tool", label: "提问" };
|
|
305
|
+
}
|
|
194
306
|
return { kind: "tool", label: raw };
|
|
195
307
|
}
|
|
196
308
|
/** A tool call that failed on its own. A call interrupted by Stop carries
|
|
@@ -213,6 +325,9 @@ export function getToolSectionLabels(toolName) {
|
|
|
213
325
|
if (name === "mcp" || name.startsWith("mcp_")) {
|
|
214
326
|
return { argsLabel: "参数", resultLabel: "结果" };
|
|
215
327
|
}
|
|
328
|
+
if (isAskUserQuestionTool(toolName)) {
|
|
329
|
+
return { argsLabel: "提问", resultLabel: "您的回答" };
|
|
330
|
+
}
|
|
216
331
|
return { argsLabel: "入参", resultLabel: "结果" };
|
|
217
332
|
}
|
|
218
333
|
/**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function compareChatOrder(a, b) {
|
|
2
|
+
if (a.eventSeq !== undefined &&
|
|
3
|
+
b.eventSeq !== undefined &&
|
|
4
|
+
a.eventSeq !== b.eventSeq) {
|
|
5
|
+
return a.eventSeq - b.eventSeq;
|
|
6
|
+
}
|
|
7
|
+
if (a.createdAt !== b.createdAt)
|
|
8
|
+
return a.createdAt - b.createdAt;
|
|
9
|
+
// Same timestamp with mismatched sequence presence: keep the original
|
|
10
|
+
// insertion order (stable sort). Deciding here would re-order live SSE
|
|
11
|
+
// frames that happen to share a millisecond with a tool row.
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Turn grouping for Operator Chat inline process UX (pi-web ProcessDetails mind).
|
|
3
3
|
*/
|
|
4
|
+
import { compareChatOrder } from "./turn-order.js";
|
|
4
5
|
import { getDisplayableAssistantBlocks, joinAssistantText, parseAssistantContent, splitFinalAssistantBlocks, } from "./assistant-content.js";
|
|
5
6
|
import { dagProgressActivityForTool, semanticActivityForTool, } from "./semantic-activity.js";
|
|
6
7
|
function emptyGroup(key) {
|
|
@@ -264,13 +265,14 @@ function fillProcessDetails(group) {
|
|
|
264
265
|
items,
|
|
265
266
|
});
|
|
266
267
|
}
|
|
267
|
-
|
|
268
|
+
// Unified order (DSH rule): server seq is the single authoritative key,
|
|
269
|
+
// createdAt falls back, insertion order breaks remaining ties. No
|
|
270
|
+
// "fullySequenced" switch — a partially-sequenced group must not
|
|
271
|
+
// silently re-sort its peers by wall clock when most nodes carry seq.
|
|
268
272
|
nodes.sort((a, b) => {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (a.createdAt !== b.createdAt)
|
|
273
|
-
return a.createdAt - b.createdAt;
|
|
273
|
+
const byOrder = compareChatOrder(a, b);
|
|
274
|
+
if (byOrder !== 0)
|
|
275
|
+
return byOrder;
|
|
274
276
|
return a.insertionOrder - b.insertionOrder;
|
|
275
277
|
});
|
|
276
278
|
const processItems = nodes.flatMap((node) => node.items);
|
|
@@ -301,20 +303,7 @@ function fillProcessDetails(group) {
|
|
|
301
303
|
export function buildTurnProcessGroups(input) {
|
|
302
304
|
const visible = input.messages
|
|
303
305
|
.filter((m) => m.role === "user" || m.role === "assistant")
|
|
304
|
-
.sort(
|
|
305
|
-
if (a.eventSeq !== undefined &&
|
|
306
|
-
b.eventSeq !== undefined &&
|
|
307
|
-
a.eventSeq !== b.eventSeq) {
|
|
308
|
-
return a.eventSeq - b.eventSeq;
|
|
309
|
-
}
|
|
310
|
-
if (a.createdAt !== b.createdAt)
|
|
311
|
-
return a.createdAt - b.createdAt;
|
|
312
|
-
if (a.eventSeq !== undefined && b.eventSeq === undefined)
|
|
313
|
-
return 1;
|
|
314
|
-
if (a.eventSeq === undefined && b.eventSeq !== undefined)
|
|
315
|
-
return -1;
|
|
316
|
-
return 0;
|
|
317
|
-
});
|
|
306
|
+
.sort(compareChatOrder);
|
|
318
307
|
const groups = [];
|
|
319
308
|
let current = null;
|
|
320
309
|
const startGroup = (key) => {
|
|
@@ -326,6 +315,8 @@ export function buildTurnProcessGroups(input) {
|
|
|
326
315
|
if (message.role === "user") {
|
|
327
316
|
current = startGroup(`user-${message.id}`);
|
|
328
317
|
current.user = message;
|
|
318
|
+
if (message.turnId)
|
|
319
|
+
current.turnId = message.turnId;
|
|
329
320
|
continue;
|
|
330
321
|
}
|
|
331
322
|
if (message.kind === "compaction") {
|
|
@@ -333,12 +324,16 @@ export function buildTurnProcessGroups(input) {
|
|
|
333
324
|
// prevents it from replacing the previous turn's assistant answer.
|
|
334
325
|
current = startGroup(`compaction-${message.id}`);
|
|
335
326
|
current.assistants.push(message);
|
|
327
|
+
if (message.turnId)
|
|
328
|
+
current.turnId = message.turnId;
|
|
336
329
|
current = null;
|
|
337
330
|
continue;
|
|
338
331
|
}
|
|
339
332
|
if (!current)
|
|
340
333
|
current = startGroup(`assistant-${message.id}`);
|
|
341
334
|
current.assistants.push(message);
|
|
335
|
+
if (current.turnId === undefined && message.turnId)
|
|
336
|
+
current.turnId = message.turnId;
|
|
342
337
|
}
|
|
343
338
|
const assigned = new Set();
|
|
344
339
|
const assistantToGroup = new Map();
|
|
@@ -360,10 +355,10 @@ export function buildTurnProcessGroups(input) {
|
|
|
360
355
|
if (assigned.has(tool.id))
|
|
361
356
|
continue;
|
|
362
357
|
if (tool.turnId) {
|
|
363
|
-
const byTurn = groups.find((group) => group.
|
|
358
|
+
const byTurn = groups.find((group) => group.turnId === tool.turnId ||
|
|
359
|
+
group.user?.turnId === tool.turnId ||
|
|
364
360
|
group.assistants.some((assistant) => assistant.turnId === tool.turnId) ||
|
|
365
361
|
group.tools.some((t) => t.turnId === tool.turnId) ||
|
|
366
|
-
group.key === `turn-${tool.turnId}` ||
|
|
367
362
|
(group.user && `restored-${group.user.id}` === tool.turnId));
|
|
368
363
|
if (byTurn) {
|
|
369
364
|
byTurn.tools.push(tool);
|
|
@@ -379,13 +374,25 @@ export function buildTurnProcessGroups(input) {
|
|
|
379
374
|
}
|
|
380
375
|
}
|
|
381
376
|
}
|
|
377
|
+
// Time-window fallback for legacy rows with neither assistantMessageId nor
|
|
378
|
+
// turnId. The window ends at the NEXT group's first anchor (user OR first
|
|
379
|
+
// assistant) — using POSITIVE_INFINITY would swallow every later group's
|
|
380
|
+
// tools when the very next group has no user message (e.g. compaction or a
|
|
381
|
+
// stand-alone assistant row), dumping one turn's bash cards into another.
|
|
382
382
|
for (const tool of input.toolCalls) {
|
|
383
383
|
if (assigned.has(tool.id))
|
|
384
384
|
continue;
|
|
385
385
|
const windowGroup = groups.find((group, index) => {
|
|
386
386
|
const start = group.user?.createdAt ?? group.assistants[0]?.createdAt ?? 0;
|
|
387
|
-
|
|
388
|
-
|
|
387
|
+
let end = Number.POSITIVE_INFINITY;
|
|
388
|
+
for (let i = index + 1; i < groups.length; i += 1) {
|
|
389
|
+
const next = groups[i];
|
|
390
|
+
const nextAnchor = next?.user?.createdAt ?? next?.assistants[0]?.createdAt;
|
|
391
|
+
if (nextAnchor !== undefined) {
|
|
392
|
+
end = nextAnchor;
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
389
396
|
return tool.createdAt >= start && tool.createdAt < end;
|
|
390
397
|
});
|
|
391
398
|
// Time window only. Never dump unmatched tools onto the latest group:
|
|
@@ -396,12 +403,7 @@ export function buildTurnProcessGroups(input) {
|
|
|
396
403
|
}
|
|
397
404
|
}
|
|
398
405
|
for (const group of groups) {
|
|
399
|
-
group.tools.sort(
|
|
400
|
-
if (a.eventSeq !== undefined && b.eventSeq !== undefined) {
|
|
401
|
-
return a.eventSeq - b.eventSeq;
|
|
402
|
-
}
|
|
403
|
-
return a.createdAt - b.createdAt;
|
|
404
|
-
});
|
|
406
|
+
group.tools.sort(compareChatOrder);
|
|
405
407
|
group.live =
|
|
406
408
|
input.streaming &&
|
|
407
409
|
input.activeAssistantId !== null &&
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { prepareScheduledTask } from "./chat/scheduled-goal-request.js";
|
|
2
|
+
import { ScheduledGoalStore } from "../scheduler/scheduled-goal-store.js";
|
|
3
|
+
import { prepareScheduledGoalBooking, confirmScheduledGoalBooking, getScheduledGoalBooking } from "./chat/scheduled-goal-booking.js";
|
|
1
4
|
import { access, readFile } from "node:fs/promises";
|
|
2
5
|
import { buildOperatorCapabilitiesDocument, OFFICIAL_TAIL_ACTION_IDS, } from "../../shared/operator/capabilities.js";
|
|
3
6
|
import { DAG_RERUN_PROMPT_OVERRIDE_ENV, isDagRerunPromptOverrideEnabled, MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS, } from "../../shared/dag-prompt-override.js";
|
|
@@ -886,6 +889,52 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
886
889
|
}
|
|
887
890
|
return acceptConsoleUpdateAndInit(ctx, req.clientRequestId, channel);
|
|
888
891
|
}
|
|
892
|
+
if (["prepareScheduledTask", "prepareScheduledGoal", "confirmScheduledGoal", "scheduledGoalGet", "scheduledGoalList", "cancelScheduledGoal"].includes(action)) {
|
|
893
|
+
try {
|
|
894
|
+
const goals = new ScheduledGoalStore(ctx.repoRoot);
|
|
895
|
+
const { ChatSessionStore } = await import("./chat/session-store.js");
|
|
896
|
+
const deps = { goals, client: ctx.client, sessions: new ChatSessionStore(ctx.appData) };
|
|
897
|
+
let result;
|
|
898
|
+
if (action === "prepareScheduledTask") {
|
|
899
|
+
if (!ctx.chatSessionRef || Object.keys(p).some((key) => key !== "request"))
|
|
900
|
+
return invalid(action, "requires a host-bound Chat session and request only");
|
|
901
|
+
result = await prepareScheduledTask(p.request, { repoRoot: ctx.repoRoot, preparationRoot: nodePath.join(ctx.appData.staged, "scheduled-tasks"),
|
|
902
|
+
sessionId: ctx.chatSessionRef, client: ctx.client, sessions: deps.sessions });
|
|
903
|
+
}
|
|
904
|
+
else if (action === "prepareScheduledGoal") {
|
|
905
|
+
if (Object.keys(p).some((key) => key !== "booking"))
|
|
906
|
+
return invalid(action, "only booking may be provided");
|
|
907
|
+
result = await prepareScheduledGoalBooking(p.booking, deps);
|
|
908
|
+
}
|
|
909
|
+
else if (action === "confirmScheduledGoal") {
|
|
910
|
+
result = await confirmScheduledGoalBooking(p, deps, { source: req.source, requestId: req.clientRequestId });
|
|
911
|
+
}
|
|
912
|
+
else if (action === "cancelScheduledGoal") {
|
|
913
|
+
if (req.source !== "human-ui" || !req.clientRequestId?.trim())
|
|
914
|
+
return invalid(action, "human cancellation required");
|
|
915
|
+
if (Object.keys(p).some((key) => !["goalId", "stateVersion", "reason"].includes(key)) ||
|
|
916
|
+
typeof p.goalId !== "string" || !Number.isSafeInteger(p.stateVersion) || typeof p.reason !== "string")
|
|
917
|
+
return invalid(action, "goalId, stateVersion and reason required");
|
|
918
|
+
await goals.cancel(p.goalId, p.stateVersion, req.clientRequestId, p.reason);
|
|
919
|
+
result = await getScheduledGoalBooking(p.goalId, goals);
|
|
920
|
+
}
|
|
921
|
+
else if (action === "scheduledGoalGet") {
|
|
922
|
+
if (Object.keys(p).some((key) => key !== "goalId") || typeof p.goalId !== "string")
|
|
923
|
+
return invalid(action, "goalId required");
|
|
924
|
+
result = await getScheduledGoalBooking(p.goalId, goals);
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
if (Object.keys(p).some((key) => key !== "chatSessionRef") || (p.chatSessionRef !== undefined && typeof p.chatSessionRef !== "string"))
|
|
928
|
+
return invalid(action, "only optional chatSessionRef may be provided");
|
|
929
|
+
const records = (await goals.list()).filter((goal) => !p.chatSessionRef || goal.frozen.chatSessionRef === p.chatSessionRef);
|
|
930
|
+
result = await Promise.all(records.map((goal) => getScheduledGoalBooking(goal.goalId, goals)));
|
|
931
|
+
}
|
|
932
|
+
return { kind: "sync", status: 200, body: operatorSucceeded(action, result) };
|
|
933
|
+
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
return invalid(action, error instanceof Error ? error.message : String(error));
|
|
936
|
+
}
|
|
937
|
+
}
|
|
889
938
|
switch (action) {
|
|
890
939
|
case "operatorCapabilities": {
|
|
891
940
|
const doc = buildOperatorCapabilitiesDocument();
|
|
@@ -1077,6 +1126,7 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
1077
1126
|
const args = ["dag", "doctor"];
|
|
1078
1127
|
if (runId)
|
|
1079
1128
|
args.push("--run-id", runId);
|
|
1129
|
+
args.push("--json");
|
|
1080
1130
|
const body = await runReadCli(ctx, args, "dag doctor");
|
|
1081
1131
|
return { kind: "sync", status: body.ok ? 200 : 400, body };
|
|
1082
1132
|
}
|
|
@@ -119,6 +119,33 @@ export function parseEngineeringBoundaryFromParams(p) {
|
|
|
119
119
|
};
|
|
120
120
|
const allowedPaths = asList(p.allowedPaths ?? p.allowedPath);
|
|
121
121
|
const forbiddenPaths = asList(p.forbiddenPaths ?? p.forbiddenPath);
|
|
122
|
+
const capabilityBoundaryRaw = p.capabilityBoundary;
|
|
123
|
+
const capabilityBoundary = capabilityBoundaryRaw && typeof capabilityBoundaryRaw === "object" && !Array.isArray(capabilityBoundaryRaw)
|
|
124
|
+
? {
|
|
125
|
+
allowed: Array.isArray(capabilityBoundaryRaw.allowed)
|
|
126
|
+
? capabilityBoundaryRaw.allowed.filter((x) => typeof x === "string" && x.trim().length > 0)
|
|
127
|
+
: undefined,
|
|
128
|
+
forbidden: Array.isArray(capabilityBoundaryRaw.forbidden)
|
|
129
|
+
? capabilityBoundaryRaw.forbidden.filter((x) => typeof x === "string" && x.trim().length > 0)
|
|
130
|
+
: undefined,
|
|
131
|
+
}
|
|
132
|
+
: undefined;
|
|
133
|
+
const interactionIds = asList(p.interactionIds ?? p.interactionId);
|
|
134
|
+
const requirementOwnership = Array.isArray(p.requirementOwnership)
|
|
135
|
+
? p.requirementOwnership
|
|
136
|
+
.map((entry) => ({
|
|
137
|
+
requirementIds: Array.isArray(entry.requirementIds)
|
|
138
|
+
? entry.requirementIds.filter((id) => typeof id === "string" && id.trim().length > 0)
|
|
139
|
+
: [],
|
|
140
|
+
implementationFiles: Array.isArray(entry.implementationFiles)
|
|
141
|
+
? entry.implementationFiles.filter((f) => typeof f === "string" && f.trim().length > 0)
|
|
142
|
+
: undefined,
|
|
143
|
+
verificationFiles: Array.isArray(entry.verificationFiles)
|
|
144
|
+
? entry.verificationFiles.filter((f) => typeof f === "string" && f.trim().length > 0)
|
|
145
|
+
: undefined,
|
|
146
|
+
}))
|
|
147
|
+
.filter((entry) => entry.requirementIds.length > 0)
|
|
148
|
+
: undefined;
|
|
122
149
|
const verifyRaw = p.verifyCommands ?? p.verify;
|
|
123
150
|
const verifyCommands = [];
|
|
124
151
|
if (Array.isArray(verifyRaw)) {
|
|
@@ -175,6 +202,11 @@ export function parseEngineeringBoundaryFromParams(p) {
|
|
|
175
202
|
...(allowedPaths.length > 0 ? { allowedPaths } : {}),
|
|
176
203
|
...(forbiddenPaths.length > 0 ? { forbiddenPaths } : {}),
|
|
177
204
|
...(verifyCommands.length > 0 ? { verifyCommands } : {}),
|
|
205
|
+
...(requirementOwnership?.length ? { requirementOwnership } : {}),
|
|
206
|
+
...(capabilityBoundary && ((capabilityBoundary.allowed?.length ?? 0) + (capabilityBoundary.forbidden?.length ?? 0)) > 0
|
|
207
|
+
? { capabilityBoundary }
|
|
208
|
+
: {}),
|
|
209
|
+
...(interactionIds.length > 0 ? { interactionIds } : {}),
|
|
178
210
|
};
|
|
179
211
|
}
|
|
180
212
|
export function appendEngineeringCliArgs(args, boundary) {
|
|
@@ -187,17 +219,38 @@ export function appendEngineeringCliArgs(args, boundary) {
|
|
|
187
219
|
for (const cmd of boundary.verifyCommands ?? []) {
|
|
188
220
|
args.push("--verify", `${cmd.label}:${cmd.command}`);
|
|
189
221
|
}
|
|
222
|
+
if (boundary.requirementOwnership?.length) {
|
|
223
|
+
args.push("--requirement-ownership", JSON.stringify(boundary.requirementOwnership));
|
|
224
|
+
}
|
|
225
|
+
if (boundary.capabilityBoundary) {
|
|
226
|
+
args.push("--capability-boundary", JSON.stringify(boundary.capabilityBoundary));
|
|
227
|
+
}
|
|
228
|
+
for (const id of boundary.interactionIds ?? []) {
|
|
229
|
+
args.push("--interaction-ids", id);
|
|
230
|
+
}
|
|
190
231
|
}
|
|
191
232
|
export function applyEngineeringBoundaryToDraft(draft, boundary) {
|
|
192
233
|
const allowedPaths = filterPlaceholderPaths(boundary.allowedPaths);
|
|
193
234
|
const forbiddenPaths = filterPlaceholderPaths(boundary.forbiddenPaths);
|
|
194
235
|
if (allowedPaths.length === 0 &&
|
|
195
236
|
forbiddenPaths.length === 0 &&
|
|
196
|
-
!(boundary.verifyCommands?.length)
|
|
237
|
+
!(boundary.verifyCommands?.length) &&
|
|
238
|
+
!(boundary.requirementOwnership?.length) &&
|
|
239
|
+
!boundary.capabilityBoundary &&
|
|
240
|
+
!(boundary.interactionIds?.length)) {
|
|
197
241
|
return draft;
|
|
198
242
|
}
|
|
199
243
|
return {
|
|
200
244
|
...draft,
|
|
245
|
+
...(boundary.requirementOwnership?.length
|
|
246
|
+
? { requirementOwnership: boundary.requirementOwnership }
|
|
247
|
+
: {}),
|
|
248
|
+
...(boundary.capabilityBoundary
|
|
249
|
+
? { capabilityBoundary: boundary.capabilityBoundary }
|
|
250
|
+
: {}),
|
|
251
|
+
...(boundary.interactionIds?.length
|
|
252
|
+
? { interactionIds: boundary.interactionIds }
|
|
253
|
+
: {}),
|
|
201
254
|
constraints: {
|
|
202
255
|
invariants: draft.constraints?.invariants ?? [],
|
|
203
256
|
allowedPaths: allowedPaths.length > 0
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { dispatchScheduledGoalOperation } from "./scheduled-goal-operation-adapter.js";
|
|
2
|
+
import { assessScheduledGoal, continueScheduledGoal } from "../scheduler/scheduled-goal-supervisor.js";
|
|
3
|
+
import { evaluateScheduledGoalWake } from "../scheduler/scheduled-goal-time.js";
|
|
4
|
+
/** Background operation ownership, independent of a browser or an interactive Chat turn.
|
|
5
|
+
* Every wake rebuilds decisions from the ledger; timers carry no authorization.
|
|
6
|
+
* No stale takeover: reserved work is observed, never re-issued.
|
|
7
|
+
*/
|
|
8
|
+
export class ScheduledGoalHost {
|
|
9
|
+
deps;
|
|
10
|
+
timer;
|
|
11
|
+
active;
|
|
12
|
+
stopped = true;
|
|
13
|
+
constructor(deps) {
|
|
14
|
+
this.deps = deps;
|
|
15
|
+
if (deps.intervalMs !== undefined && (!Number.isFinite(deps.intervalMs) || deps.intervalMs < 100))
|
|
16
|
+
throw new Error("invalid scheduled goal wake interval");
|
|
17
|
+
}
|
|
18
|
+
start() {
|
|
19
|
+
if (!this.stopped)
|
|
20
|
+
return;
|
|
21
|
+
this.stopped = false;
|
|
22
|
+
this.wake();
|
|
23
|
+
}
|
|
24
|
+
/** Nonblocking. The host retains the operation promise until it settles. */
|
|
25
|
+
wake() {
|
|
26
|
+
if (this.stopped || this.active)
|
|
27
|
+
return;
|
|
28
|
+
if (this.timer) {
|
|
29
|
+
clearTimeout(this.timer);
|
|
30
|
+
this.timer = undefined;
|
|
31
|
+
}
|
|
32
|
+
this.active = this.tick().catch((error) => this.deps.onError?.(undefined, error)).finally(() => {
|
|
33
|
+
this.active = undefined;
|
|
34
|
+
if (!this.stopped) {
|
|
35
|
+
this.timer = setTimeout(() => { this.timer = undefined; this.wake(); }, this.deps.intervalMs ?? 5000);
|
|
36
|
+
this.timer.unref?.();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Stop future admissions. Existing children keep their operation ownership. */
|
|
41
|
+
stop() {
|
|
42
|
+
this.stopped = true;
|
|
43
|
+
if (this.timer)
|
|
44
|
+
clearTimeout(this.timer);
|
|
45
|
+
this.timer = undefined;
|
|
46
|
+
}
|
|
47
|
+
async waitForIdle() { await this.active; }
|
|
48
|
+
async tick() {
|
|
49
|
+
for (const snapshot of await this.deps.goals.list()) {
|
|
50
|
+
if (this.stopped)
|
|
51
|
+
return;
|
|
52
|
+
try {
|
|
53
|
+
await this.advance(snapshot.goalId);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
this.deps.onError?.(snapshot.goalId, error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async advance(goalId) {
|
|
61
|
+
const { goals } = this.deps;
|
|
62
|
+
let goal = await goals.get(goalId);
|
|
63
|
+
if (!goal.approval)
|
|
64
|
+
return;
|
|
65
|
+
const intent = goal.intents.at(-1);
|
|
66
|
+
if (intent && ["dispatching", "associated", "unknown"].includes(intent.state)) {
|
|
67
|
+
await dispatchScheduledGoalOperation(goalId, this.deps);
|
|
68
|
+
goal = await goals.get(goalId);
|
|
69
|
+
}
|
|
70
|
+
if (this.stopped)
|
|
71
|
+
return;
|
|
72
|
+
if (["cancelled", "blocked", "budget_exhausted", "unknown"].includes(goal.state)) {
|
|
73
|
+
await this.deps.deliver?.(goal);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
goal = await goals.refreshAcceptance(goalId);
|
|
77
|
+
goal = await goals.settleIdleGoal(goalId, this.deps.now?.());
|
|
78
|
+
if (["accepted", "cancelled", "blocked", "budget_exhausted", "unknown"].includes(goal.state)) {
|
|
79
|
+
await this.deps.deliver?.(goal);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (goal.assessmentAttempt || goal.supervisions.some((entry) => ["requested", "preparing", "unknown"].includes(entry.state)))
|
|
83
|
+
return;
|
|
84
|
+
const current = goal.intents.at(-1);
|
|
85
|
+
if (!current || current.state === "prepared") {
|
|
86
|
+
if (evaluateScheduledGoalWake(goal.frozen.time, this.deps.now?.() ?? new Date()).kind === "due")
|
|
87
|
+
await dispatchScheduledGoalOperation(goalId, this.deps);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (current.state !== "terminal")
|
|
91
|
+
return;
|
|
92
|
+
if (goal.acceptanceState === "failed") {
|
|
93
|
+
await continueScheduledGoal(goalId, goals, this.deps.client);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
await assessScheduledGoal(goalId, goals, { now: this.deps.now });
|
|
97
|
+
}
|
|
98
|
+
}
|