@tea-agent/loop-agent 0.36.4-beta.0 → 0.37.1-beta.0
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 +2 -6
- package/CHANGELOG.md +72 -163
- package/README.md +1 -1
- package/bin/loop-agent.js +1 -37
- package/dist/application/task-lifecycle/advance.js +0 -1
- package/dist/application/task-lifecycle/observe.js +0 -15
- package/dist/application/task-lifecycle/plan-transitions.js +0 -15
- package/dist/cli/command-definitions.js +0 -7
- package/dist/cli/program.js +3 -8
- package/dist/commands/init-upgrade.js +19 -351
- package/dist/commands/init.js +67 -14
- package/dist/commands/run-dag-progress.js +0 -14
- package/dist/commands/task-advance.js +3 -33
- package/dist/executors/dag-pi-executor.js +30 -47
- package/dist/executors/pi-sdk-executor.js +0 -34
- package/dist/executors/shell-executor.js +95 -6
- package/dist/infrastructure/harness/atomic-write.js +1 -2
- package/dist/shared/operator/capabilities.js +13 -200
- package/dist/shared/package-metadata.js +0 -42
- package/dist/task/frontend-project-capability.js +12 -162
- package/dist/task/source-prepare/completeness.js +0 -17
- package/dist/task/source-prepare/parse-intent.js +1 -15
- package/dist/task/source-prepare/semantic-intake.js +23 -144
- package/dist/worker/console/chat/artifact-card.js +1 -8
- package/dist/worker/console/chat/chat-event-store.js +0 -61
- package/dist/worker/console/chat/human-gate-card.js +1 -9
- package/dist/worker/console/chat/operation-card.js +2 -71
- package/dist/worker/console/chat/pi-runtime.js +62 -86
- package/dist/worker/console/chat/routes.js +8 -36
- package/dist/worker/console/chat/session-store.js +0 -3
- package/dist/worker/console/chat/shortcuts.js +7 -9
- package/dist/worker/console/chat/turn-process.js +23 -97
- package/dist/worker/console/chat/workspace-landing.js +1 -2
- package/dist/worker/console/operation-runner.js +6 -155
- package/dist/worker/console/operator-actions.js +13 -373
- package/dist/worker/console/operator-user-error.js +0 -4
- package/dist/worker/console/recovery-cta.js +3 -50
- package/dist/worker/console/static/assets/index-HX1pbOyl.css +1 -0
- package/dist/worker/console/static/assets/index-M0BLEBfh.js +56 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +9 -13
- package/dist/worker/console/static-src/app/useOperatorActions.js +2 -4
- package/dist/worker/console/static-src/app/useRecoveryActions.js +56 -65
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +1 -73
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +30 -89
- package/dist/worker/console/static-src/operator-chat/refs.js +0 -3
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +11 -30
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -12
- package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -45
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -7
- package/dist/worker/loop-agent/loop-agent-client.js +3 -17
- package/dist/worker/observability/read-model.js +0 -20
- package/dist/worker/observe/spec-evidence.js +8 -3
- package/dist/worker/observe/static/operator-chrome.d.ts +0 -1
- package/dist/worker/observe/static/operator-chrome.js +1 -6
- package/dist/worker/observe/static/views/dag-inspector.js +71 -6
- package/dist/worker/observe/static/views/dag.js +0 -12
- package/dist/worker/preflight.js +1 -2
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +707 -37
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +109 -7
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
- package/dist/workflows/dag/backend-test-scenario-param.js +890 -164
- package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
- package/dist/workflows/dag/failure-routing.js +4 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
- package/dist/workflows/dag/frontend-prewrite-gate.js +62 -394
- package/dist/workflows/dag/frontend-repair.js +18 -219
- package/dist/workflows/dag/frontend-verification-trace.js +32 -47
- package/dist/workflows/dag/init-hybrid.js +86 -238
- package/dist/workflows/dag/lifecycle.js +0 -4
- package/dist/workflows/dag/node-execution.js +6 -105
- package/dist/workflows/dag/recovery-recommendation.js +0 -58
- package/dist/workflows/dag/report.js +0 -6
- package/dist/workflows/dag/retry-policy.js +0 -11
- package/dist/workflows/dag/runner.js +12 -314
- package/dist/workflows/dag/scheduler.js +3 -257
- package/dist/workflows/dag/types.js +9 -132
- package/dist/workflows/dag/validate.js +2 -7
- package/docs/README.md +3 -3
- package/docs/architecture/evolution.md +67 -102
- package/docs/templates/backend-test-dag.json +50 -32
- package/docs/templates/frontend-design-contract.md +14 -34
- package/docs/templates/frontend-task-constraints.md +13 -33
- package/docs/templates/frontend-task-requirement.md +20 -54
- package/docs/templates/init-managed-agents.md +2 -5
- package/harness.json +2 -2
- package/package.json +3 -4
- package/skills/loop-agent/SKILL.md +0 -1
- package/skills/loop-agent/references/command-reference.md +0 -3
- package/dist/build-stamp.json +0 -6
- package/dist/commands/dag-request-interrupt.js +0 -20
- package/dist/worker/console/chat/assistant-content.js +0 -121
- package/dist/worker/console/chat/semantic-activity.js +0 -471
- package/dist/worker/console/operation-run-facts.js +0 -190
- package/dist/worker/console/operation-wait.js +0 -355
- package/dist/worker/console/recovery-error-copy.js +0 -198
- package/dist/worker/console/static/assets/index-D83DYAFG.css +0 -1
- package/dist/worker/console/static/assets/index-IXm7oYjL.js +0 -59
- package/dist/worker/console/static-src/operator-chat/activity-journey.js +0 -125
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/activity-references.js +0 -20
- package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +0 -24
- package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +0 -141
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
- package/dist/worker/observability/interrupt-eligibility.js +0 -264
- package/dist/workflows/dag/contract-output-registry.js +0 -14
- package/dist/workflows/dag/contract-validator-registrations.js +0 -8
- package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
- package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
- package/dist/workflows/dag/frontend-recovery-run.js +0 -539
- package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
- package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
- package/dist/workflows/dag/interrupt-request.js +0 -559
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
import { projectOperationEventSummary, projectOperationForChat, } from "./chat/chat-event-store.js";
|
|
2
|
-
import { isTerminalOperationState, } from "./operation-store.js";
|
|
3
|
-
/**
|
|
4
|
-
* P2: read-only event-driven long poll on the canonical operation event ring
|
|
5
|
-
* (2026-08-13 Operator Chat long-run supervision). Server contract bounds:
|
|
6
|
-
* the model-facing `maxWaitMs` is clamped to [minWaitMs, maxWaitMsBound];
|
|
7
|
-
* defaults allow the 60–180s model supervision cadence and tests may inject
|
|
8
|
-
* short bounds.
|
|
9
|
-
*/
|
|
10
|
-
export const DEFAULT_OPERATION_WAIT_MIN_MS = 60_000;
|
|
11
|
-
export const DEFAULT_OPERATION_WAIT_MAX_MS = 180_000;
|
|
12
|
-
export class OperationWaitError extends Error {
|
|
13
|
-
code;
|
|
14
|
-
constructor(code, message) {
|
|
15
|
-
super(message);
|
|
16
|
-
this.code = code;
|
|
17
|
-
this.name = "OperationWaitError";
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Pure wake classification: heartbeats are liveness telemetry, not progress.
|
|
22
|
-
* Everything else (state/result/stdout/stderr/error/reconcile) is meaningful.
|
|
23
|
-
*
|
|
24
|
-
* Exception (2026-08-15): stderr/stdout events whose payload is a DAG-runner
|
|
25
|
-
* telemetry log line (`[dag execute] heartbeat ...` / `[task advance] heartbeat
|
|
26
|
-
* ...` / bare `heartbeat ...`) are also liveness, not progress. Without this,
|
|
27
|
-
* a long run's periodic stderr heartbeat logs settle every operationWait
|
|
28
|
-
* immediately (55/55 observed instant returns) and force the model back into
|
|
29
|
-
* high-frequency bash polling — defeating the 60-180s backoff contract.
|
|
30
|
-
*/
|
|
31
|
-
const RUNNER_TELEMETRY_LOG = /^\s*\[?(?:dag execute|task advance|run-dag)\]?\s*heartbeat\b/i;
|
|
32
|
-
const BARE_HEARTBEAT_LOG = /^\s*heartbeat\b/i;
|
|
33
|
-
export function isMeaningfulOperationEvent(event) {
|
|
34
|
-
if (event.kind === "heartbeat")
|
|
35
|
-
return false;
|
|
36
|
-
if (event.kind === "stderr" || event.kind === "stdout") {
|
|
37
|
-
const message = typeof event.message === "string"
|
|
38
|
-
? event.message
|
|
39
|
-
: "";
|
|
40
|
-
// Multi-line chunks: only telemetry-only chunks are classified liveness.
|
|
41
|
-
const lines = message.split("\n").filter((line) => line.trim() !== "");
|
|
42
|
-
if (lines.length > 0 && lines.every((line) => RUNNER_TELEMETRY_LOG.test(line) || BARE_HEARTBEAT_LOG.test(line))) {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
function isFocusedOperation(operation) {
|
|
49
|
-
return (isTerminalOperationState(operation.state) ||
|
|
50
|
-
operation.state === "needs-reconcile");
|
|
51
|
-
}
|
|
52
|
-
function settledSummary(input) {
|
|
53
|
-
const projectedEvents = input.newEvents.length > 0
|
|
54
|
-
? input.newEvents.map(projectOperationEventSummary)
|
|
55
|
-
: [];
|
|
56
|
-
// Cursor safety: nextSeq always covers every observed seq so heartbeats
|
|
57
|
-
// filtered out of the payload are never replayed on the next round.
|
|
58
|
-
const nextSeq = Math.max(input.afterSeq, input.observedSeq);
|
|
59
|
-
return {
|
|
60
|
-
operationId: input.operationId,
|
|
61
|
-
state: input.operation.state,
|
|
62
|
-
changed: input.changed,
|
|
63
|
-
timedOut: input.timedOut,
|
|
64
|
-
events: projectedEvents,
|
|
65
|
-
nextSeq,
|
|
66
|
-
recommendedNextCall: {
|
|
67
|
-
operationId: input.operationId,
|
|
68
|
-
afterSeq: nextSeq,
|
|
69
|
-
wakeOn: "meaningful",
|
|
70
|
-
},
|
|
71
|
-
operation: projectOperationForChat(input.operation),
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Deterministic read-only wait over the canonical operation event stream
|
|
76
|
-
* (AC-003 / AC-004). Completion paths (wakeOn=meaningful default):
|
|
77
|
-
* - existing meaningful events (seq > afterSeq) → immediate (changed: true);
|
|
78
|
-
* - operation terminal/needs-reconcile → immediate flush of meaningful events;
|
|
79
|
-
* - first subscribed meaningful event → immediate settle;
|
|
80
|
-
* - maxWaitMs elapsed without meaningful change → timedOut summary whose
|
|
81
|
-
* nextSeq still covers every observed (heartbeat) seq.
|
|
82
|
-
*
|
|
83
|
-
* wakeOn=all preserves the legacy diagnostic semantics: any first event,
|
|
84
|
-
* heartbeat included, settles and is returned.
|
|
85
|
-
*
|
|
86
|
-
* Race safety: the listener is registered BEFORE listFrom, closing the
|
|
87
|
-
* listFrom/subscribe gap; every path settles exactly once through a guarded
|
|
88
|
-
* `finish`, and listener + timer are always cleaned up on settle.
|
|
89
|
-
*/
|
|
90
|
-
export async function waitForOperationChange(input) {
|
|
91
|
-
const operationId = input.operationId?.trim();
|
|
92
|
-
if (!operationId) {
|
|
93
|
-
throw new OperationWaitError("INVALID_INPUT", "operationId is required");
|
|
94
|
-
}
|
|
95
|
-
if (!Number.isInteger(input.afterSeq) || input.afterSeq < 0) {
|
|
96
|
-
throw new OperationWaitError("INVALID_INPUT", "afterSeq must be a non-negative integer");
|
|
97
|
-
}
|
|
98
|
-
const wakeOn = input.wakeOn ?? "meaningful";
|
|
99
|
-
if (wakeOn !== "meaningful" && wakeOn !== "all") {
|
|
100
|
-
throw new OperationWaitError("INVALID_INPUT", "wakeOn must be 'meaningful' or 'all'");
|
|
101
|
-
}
|
|
102
|
-
const minWaitMs = input.minWaitMs ?? DEFAULT_OPERATION_WAIT_MIN_MS;
|
|
103
|
-
const maxWaitMsBound = input.maxWaitMsBound ?? DEFAULT_OPERATION_WAIT_MAX_MS;
|
|
104
|
-
if (!Number.isFinite(minWaitMs) ||
|
|
105
|
-
!Number.isFinite(maxWaitMsBound) ||
|
|
106
|
-
minWaitMs < 0 ||
|
|
107
|
-
maxWaitMsBound < minWaitMs) {
|
|
108
|
-
throw new OperationWaitError("INVALID_INPUT", "invalid wait bounds");
|
|
109
|
-
}
|
|
110
|
-
const rawMaxWaitMs = input.maxWaitMs ?? maxWaitMsBound;
|
|
111
|
-
if (!Number.isFinite(rawMaxWaitMs) || rawMaxWaitMs < 0) {
|
|
112
|
-
throw new OperationWaitError("INVALID_INPUT", "maxWaitMs must be a non-negative number");
|
|
113
|
-
}
|
|
114
|
-
const maxWaitMs = Math.max(minWaitMs, Math.min(maxWaitMsBound, Math.floor(rawMaxWaitMs)));
|
|
115
|
-
const afterSeq = input.afterSeq;
|
|
116
|
-
/** Wake filter per policy: meaningful drops heartbeat payload entirely. */
|
|
117
|
-
const wakeFilter = (events) => wakeOn === "meaningful"
|
|
118
|
-
? events.filter((event) => isMeaningfulOperationEvent(event))
|
|
119
|
-
: events;
|
|
120
|
-
const operation = await input.operations.get(operationId);
|
|
121
|
-
if (!operation) {
|
|
122
|
-
throw new OperationWaitError("NOT_FOUND", `operation not found: ${operationId}`);
|
|
123
|
-
}
|
|
124
|
-
const events = input.events;
|
|
125
|
-
if (isFocusedOperation(operation)) {
|
|
126
|
-
// Flush unconsumed events before the terminal summary: the caller's
|
|
127
|
-
// cursor must advance past every retained canonical event (observed
|
|
128
|
-
// seq includes heartbeats even when their payload is filtered out).
|
|
129
|
-
const listed = events.listFrom(operationId, afterSeq);
|
|
130
|
-
if ("error" in listed) {
|
|
131
|
-
throw new OperationWaitError("EVENT_CURSOR_EXPIRED", "event cursor expired; re-read operation snapshot");
|
|
132
|
-
}
|
|
133
|
-
const wake = wakeFilter(listed.events);
|
|
134
|
-
const observedSeq = listed.events.length > 0
|
|
135
|
-
? listed.events[listed.events.length - 1].seq
|
|
136
|
-
: afterSeq;
|
|
137
|
-
if (wake.length > 0) {
|
|
138
|
-
return settledSummary({
|
|
139
|
-
operationId,
|
|
140
|
-
operation,
|
|
141
|
-
afterSeq,
|
|
142
|
-
changed: true,
|
|
143
|
-
timedOut: false,
|
|
144
|
-
newEvents: wake,
|
|
145
|
-
observedSeq,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
return settledSummary({
|
|
149
|
-
operationId,
|
|
150
|
-
operation,
|
|
151
|
-
afterSeq,
|
|
152
|
-
changed: false,
|
|
153
|
-
timedOut: false,
|
|
154
|
-
newEvents: [],
|
|
155
|
-
observedSeq,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
return new Promise((resolve, reject) => {
|
|
159
|
-
let settled = false;
|
|
160
|
-
let timer;
|
|
161
|
-
let pollTimer;
|
|
162
|
-
let unsubscribe;
|
|
163
|
-
/** Highest seq observed via listener/listFrom during this wait. */
|
|
164
|
-
let observedSeq = afterSeq;
|
|
165
|
-
const cleanup = () => {
|
|
166
|
-
if (timer !== undefined) {
|
|
167
|
-
clearTimeout(timer);
|
|
168
|
-
timer = undefined;
|
|
169
|
-
}
|
|
170
|
-
if (pollTimer !== undefined) {
|
|
171
|
-
clearInterval(pollTimer);
|
|
172
|
-
pollTimer = undefined;
|
|
173
|
-
}
|
|
174
|
-
if (unsubscribe) {
|
|
175
|
-
unsubscribe();
|
|
176
|
-
unsubscribe = undefined;
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
const finish = (result) => {
|
|
180
|
-
if (settled)
|
|
181
|
-
return;
|
|
182
|
-
settled = true;
|
|
183
|
-
cleanup();
|
|
184
|
-
if (result instanceof OperationWaitError)
|
|
185
|
-
reject(result);
|
|
186
|
-
else
|
|
187
|
-
resolve(result);
|
|
188
|
-
};
|
|
189
|
-
const settleWithEvent = (event) => {
|
|
190
|
-
// Future-cursor guard: events at or below the caller's afterSeq are
|
|
191
|
-
// already consumed and must not settle this wait.
|
|
192
|
-
if (event.seq <= afterSeq)
|
|
193
|
-
return;
|
|
194
|
-
// Heartbeats only advance the observed cursor in meaningful mode.
|
|
195
|
-
if (wakeOn === "meaningful" && !isMeaningfulOperationEvent(event)) {
|
|
196
|
-
observedSeq = Math.max(observedSeq, event.seq);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
observedSeq = Math.max(observedSeq, event.seq);
|
|
200
|
-
// Listener path: async re-read of the operation for a fresh summary.
|
|
201
|
-
void (async () => {
|
|
202
|
-
try {
|
|
203
|
-
const current = await input.operations.get(operationId);
|
|
204
|
-
finish(settledSummary({
|
|
205
|
-
operationId,
|
|
206
|
-
operation: current ?? operation,
|
|
207
|
-
afterSeq,
|
|
208
|
-
changed: true,
|
|
209
|
-
timedOut: false,
|
|
210
|
-
newEvents: [event],
|
|
211
|
-
observedSeq,
|
|
212
|
-
}));
|
|
213
|
-
}
|
|
214
|
-
catch (error) {
|
|
215
|
-
finish(error instanceof OperationWaitError
|
|
216
|
-
? error
|
|
217
|
-
: new OperationWaitError("INVALID_INPUT", error instanceof Error ? error.message : String(error)));
|
|
218
|
-
}
|
|
219
|
-
})();
|
|
220
|
-
};
|
|
221
|
-
// Terminal recheck before subscribing: events landing during this await
|
|
222
|
-
// are still in the ring and are caught by listFrom below.
|
|
223
|
-
void (async () => {
|
|
224
|
-
try {
|
|
225
|
-
const current = await input.operations.get(operationId);
|
|
226
|
-
if (current && isFocusedOperation(current)) {
|
|
227
|
-
// Same terminal flush as the initial path: events landing during
|
|
228
|
-
// the await are still in the retained ring.
|
|
229
|
-
const listed = events.listFrom(operationId, afterSeq);
|
|
230
|
-
if ("error" in listed) {
|
|
231
|
-
finish(new OperationWaitError("EVENT_CURSOR_EXPIRED", "event cursor expired; re-read operation snapshot"));
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
const wake = wakeFilter(listed.events);
|
|
235
|
-
observedSeq = Math.max(observedSeq, listed.events.length > 0
|
|
236
|
-
? listed.events[listed.events.length - 1].seq
|
|
237
|
-
: afterSeq);
|
|
238
|
-
if (wake.length > 0) {
|
|
239
|
-
finish(settledSummary({
|
|
240
|
-
operationId,
|
|
241
|
-
operation: current,
|
|
242
|
-
afterSeq,
|
|
243
|
-
changed: true,
|
|
244
|
-
timedOut: false,
|
|
245
|
-
newEvents: wake,
|
|
246
|
-
observedSeq,
|
|
247
|
-
}));
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
finish(settledSummary({
|
|
251
|
-
operationId,
|
|
252
|
-
operation: current,
|
|
253
|
-
afterSeq,
|
|
254
|
-
changed: false,
|
|
255
|
-
timedOut: false,
|
|
256
|
-
newEvents: [],
|
|
257
|
-
observedSeq,
|
|
258
|
-
}));
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
// Subscribe BEFORE listFrom: any event appended after this point
|
|
262
|
-
// reaches the listener, closing the listFrom/subscribe race.
|
|
263
|
-
unsubscribe = events.subscribe(operationId, settleWithEvent);
|
|
264
|
-
const listed = events.listFrom(operationId, afterSeq);
|
|
265
|
-
if ("error" in listed) {
|
|
266
|
-
finish(new OperationWaitError("EVENT_CURSOR_EXPIRED", "event cursor expired; re-read operation snapshot"));
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
observedSeq = Math.max(observedSeq, listed.events.length > 0
|
|
270
|
-
? listed.events[listed.events.length - 1].seq
|
|
271
|
-
: afterSeq);
|
|
272
|
-
const wake = wakeFilter(listed.events);
|
|
273
|
-
if (wake.length > 0) {
|
|
274
|
-
finish(settledSummary({
|
|
275
|
-
operationId,
|
|
276
|
-
operation: current ?? operation,
|
|
277
|
-
afterSeq,
|
|
278
|
-
changed: true,
|
|
279
|
-
timedOut: false,
|
|
280
|
-
newEvents: wake,
|
|
281
|
-
observedSeq,
|
|
282
|
-
}));
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
// No wake events: arm the bounded wait; the listener settles on
|
|
286
|
-
// the first new wake event, the timer settles on timeout. The
|
|
287
|
-
// timeout summary returns observedSeq so heartbeat-only progress
|
|
288
|
-
// advances the cursor without replaying heartbeat payloads.
|
|
289
|
-
timer = setTimeout(() => {
|
|
290
|
-
void (async () => {
|
|
291
|
-
try {
|
|
292
|
-
const latest = await input.operations.get(operationId);
|
|
293
|
-
finish(settledSummary({
|
|
294
|
-
operationId,
|
|
295
|
-
operation: latest ?? operation,
|
|
296
|
-
afterSeq,
|
|
297
|
-
changed: false,
|
|
298
|
-
timedOut: true,
|
|
299
|
-
newEvents: [],
|
|
300
|
-
observedSeq,
|
|
301
|
-
}));
|
|
302
|
-
}
|
|
303
|
-
catch (error) {
|
|
304
|
-
finish(error instanceof OperationWaitError
|
|
305
|
-
? error
|
|
306
|
-
: new OperationWaitError("INVALID_INPUT", error instanceof Error ? error.message : String(error)));
|
|
307
|
-
}
|
|
308
|
-
})();
|
|
309
|
-
}, maxWaitMs);
|
|
310
|
-
timer.unref?.();
|
|
311
|
-
if (input.poll) {
|
|
312
|
-
pollTimer = setInterval(() => {
|
|
313
|
-
void (async () => {
|
|
314
|
-
if (settled)
|
|
315
|
-
return;
|
|
316
|
-
try {
|
|
317
|
-
const changed = await input.poll?.();
|
|
318
|
-
if (!changed || settled)
|
|
319
|
-
return;
|
|
320
|
-
const relisted = events.listFrom(operationId, afterSeq);
|
|
321
|
-
if ("error" in relisted)
|
|
322
|
-
return;
|
|
323
|
-
const nextWake = wakeFilter(relisted.events);
|
|
324
|
-
observedSeq = Math.max(observedSeq, relisted.events.length > 0
|
|
325
|
-
? relisted.events[relisted.events.length - 1].seq
|
|
326
|
-
: afterSeq);
|
|
327
|
-
if (nextWake.length === 0)
|
|
328
|
-
return;
|
|
329
|
-
const latest = await input.operations.get(operationId);
|
|
330
|
-
finish(settledSummary({
|
|
331
|
-
operationId,
|
|
332
|
-
operation: latest ?? operation,
|
|
333
|
-
afterSeq,
|
|
334
|
-
changed: true,
|
|
335
|
-
timedOut: false,
|
|
336
|
-
newEvents: nextWake,
|
|
337
|
-
observedSeq,
|
|
338
|
-
}));
|
|
339
|
-
}
|
|
340
|
-
catch {
|
|
341
|
-
// Poll is best-effort; the wait still times out.
|
|
342
|
-
}
|
|
343
|
-
})();
|
|
344
|
-
}, 1_000);
|
|
345
|
-
pollTimer.unref?.();
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
catch (error) {
|
|
349
|
-
finish(error instanceof OperationWaitError
|
|
350
|
-
? error
|
|
351
|
-
: new OperationWaitError("INVALID_INPUT", error instanceof Error ? error.message : String(error)));
|
|
352
|
-
}
|
|
353
|
-
})();
|
|
354
|
-
});
|
|
355
|
-
}
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Operator-facing Chinese copy for recovery failures.
|
|
3
|
-
* Reason codes stay in plan JSON; flash / chat must not dump them raw.
|
|
4
|
-
*/
|
|
5
|
-
const REASON_LABELS = {
|
|
6
|
-
"worker-managed-rerun-unsupported": "这次运行由 Console/Worker 启动,不能只从某个节点续跑",
|
|
7
|
-
"workspace-drift": "工作区在上次运行后又改过文件,旧结果不能接着用",
|
|
8
|
-
"controller-drift": "控制器版本已变化,不能接到上次运行上继续",
|
|
9
|
-
"binding-drift": "任务契约或源文件已变化,不能只续跑旧计划",
|
|
10
|
-
"restart-subgraph-contains-unsafe-shell": "下游包含会执行命令的验证步骤,不能只重跑这一段",
|
|
11
|
-
"restart-subgraph-contains-writer": "下游包含会改代码的写入步骤,不能只重跑这一段",
|
|
12
|
-
"restart-subgraph-contains-decision-gate": "下游包含需要人工决策的门禁,不能只重跑这一段",
|
|
13
|
-
"dynamic-rerun-unsupported": "这次运行含动态展开节点,不支持从节点续跑",
|
|
14
|
-
"parent-lifecycle-ineligible": "当前运行状态不允许从节点续跑",
|
|
15
|
-
"convergence-rerun-unsupported": "收敛型运行不支持从节点续跑",
|
|
16
|
-
"evaluation-rerun-unsupported": "带评估的运行不支持从节点续跑",
|
|
17
|
-
"budget-rerun-unsupported": "硬预算运行不支持从节点续跑",
|
|
18
|
-
"workspace-checkpoint-missing": "缺少工作区快照,无法安全地只续跑一段",
|
|
19
|
-
"skill-snapshot-invalid": "技能快照缺失或损坏,不能接到上次运行上继续",
|
|
20
|
-
"unresolved-errors-outside-reset-closure": "选中节点之外还有未解决的失败,只重跑这一段不够",
|
|
21
|
-
"selected-node-missing": "找不到要续跑的起始节点",
|
|
22
|
-
"ambiguous-skipped-rewrite": "跳过的节点有多个上游失败,无法自动选定起点",
|
|
23
|
-
"skipped-without-upstream-error": "该节点被跳过且没有明确的上游失败",
|
|
24
|
-
"parent-facts-invalid": "上次运行的证据不完整或已损坏,不能只续跑一段",
|
|
25
|
-
RUN_NOT_ACTIVE: "这次运行不在活动状态,不能协作中止",
|
|
26
|
-
RUN_NOT_CONSOLE_OWNED: "此运行不由当前 Console 持有,暂不支持协作中止",
|
|
27
|
-
RUNNER_IDENTITY_MISMATCH: "运行身份已变化,不能中止这份过期请求",
|
|
28
|
-
INTERRUPT_ALREADY_PENDING: "已有一条中止请求等待结算",
|
|
29
|
-
RUN_ALREADY_TERMINAL: "运行已经结束,没有可中止的活 runner",
|
|
30
|
-
INTERRUPT_NEEDS_RECONCILE: "中止请求已登记但无法确认 runner 已处理,需要对账",
|
|
31
|
-
};
|
|
32
|
-
function knownCodes() {
|
|
33
|
-
return Object.keys(REASON_LABELS);
|
|
34
|
-
}
|
|
35
|
-
export function parseIneligibleReasonToken(token) {
|
|
36
|
-
const trimmed = token.trim().replace(/[.…]+$/, "");
|
|
37
|
-
if (!trimmed)
|
|
38
|
-
return null;
|
|
39
|
-
if (REASON_LABELS[trimmed])
|
|
40
|
-
return { code: trimmed };
|
|
41
|
-
const colon = trimmed.lastIndexOf(":");
|
|
42
|
-
if (colon > 0) {
|
|
43
|
-
const code = trimmed.slice(colon + 1);
|
|
44
|
-
const nodeId = trimmed.slice(0, colon);
|
|
45
|
-
if (REASON_LABELS[code])
|
|
46
|
-
return { code, nodeId };
|
|
47
|
-
const match = knownCodes().find((known) => code.length >= 3 && known.startsWith(code));
|
|
48
|
-
if (match)
|
|
49
|
-
return { code: match, nodeId };
|
|
50
|
-
}
|
|
51
|
-
return REASON_LABELS[trimmed] ? { code: trimmed } : null;
|
|
52
|
-
}
|
|
53
|
-
function collectParsedReasons(input) {
|
|
54
|
-
const parsed = [];
|
|
55
|
-
for (const token of input.blockedReasons ?? []) {
|
|
56
|
-
const item = parseIneligibleReasonToken(String(token));
|
|
57
|
-
if (item)
|
|
58
|
-
parsed.push(item);
|
|
59
|
-
}
|
|
60
|
-
for (const node of input.blockingNodes ?? []) {
|
|
61
|
-
const code = node.reasonCode?.trim();
|
|
62
|
-
if (!code)
|
|
63
|
-
continue;
|
|
64
|
-
const item = parseIneligibleReasonToken(node.nodeId ? `${node.nodeId}:${code}` : code);
|
|
65
|
-
if (item)
|
|
66
|
-
parsed.push(item);
|
|
67
|
-
}
|
|
68
|
-
if (parsed.length === 0 && input.fallbackMessage?.trim()) {
|
|
69
|
-
const raw = input.fallbackMessage
|
|
70
|
-
.trim()
|
|
71
|
-
.replace(/^从节点重跑不具备安全资格[::]?\s*/, "")
|
|
72
|
-
.replace(/[。..]+$/, "");
|
|
73
|
-
for (const part of raw.split(/[;;]/)) {
|
|
74
|
-
const item = parseIneligibleReasonToken(part);
|
|
75
|
-
if (item)
|
|
76
|
-
parsed.push(item);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return parsed;
|
|
80
|
-
}
|
|
81
|
-
function uniqueReasonLines(parsed) {
|
|
82
|
-
const nodesByCode = new Map();
|
|
83
|
-
for (const item of parsed) {
|
|
84
|
-
const nodes = nodesByCode.get(item.code) ?? [];
|
|
85
|
-
if (item.nodeId && !nodes.includes(item.nodeId))
|
|
86
|
-
nodes.push(item.nodeId);
|
|
87
|
-
nodesByCode.set(item.code, nodes);
|
|
88
|
-
}
|
|
89
|
-
const lines = [];
|
|
90
|
-
for (const [code, nodes] of nodesByCode) {
|
|
91
|
-
const label = REASON_LABELS[code];
|
|
92
|
-
if (!label)
|
|
93
|
-
continue;
|
|
94
|
-
if (nodes.length > 0 && code.startsWith("restart-subgraph-contains-")) {
|
|
95
|
-
lines.push(`${label}(${nodes.join("、")})`);
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
lines.push(label);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return lines;
|
|
102
|
-
}
|
|
103
|
-
export function nextActionForIneligibleRerun(reasons) {
|
|
104
|
-
const haystack = reasons.join("; ");
|
|
105
|
-
if (haystack.includes("worker-managed-rerun-unsupported") ||
|
|
106
|
-
reasons.some((reason) => parseIneligibleReasonToken(reason)?.code ===
|
|
107
|
-
"worker-managed-rerun-unsupported")) {
|
|
108
|
-
return "下一步:请点下方「重新排队 Worker Task」,从头完整执行。";
|
|
109
|
-
}
|
|
110
|
-
return "下一步:请改用「完整重跑任务」,从头完整执行。";
|
|
111
|
-
}
|
|
112
|
-
export function formatIneligibleRerunMessage(input) {
|
|
113
|
-
const fallback = input.fallbackMessage?.trim();
|
|
114
|
-
if (fallback?.startsWith("无法从该节点续跑"))
|
|
115
|
-
return fallback;
|
|
116
|
-
const parsed = collectParsedReasons(input);
|
|
117
|
-
const lines = uniqueReasonLines(parsed);
|
|
118
|
-
const reasonTokens = parsed.map((item) => item.code);
|
|
119
|
-
const next = nextActionForIneligibleRerun(reasonTokens.length > 0 ? reasonTokens : [fallback ?? ""]);
|
|
120
|
-
if (lines.length === 0) {
|
|
121
|
-
return [
|
|
122
|
-
"无法从该节点续跑。这次续跑不够安全(工作区、控制器或下游步骤不允许只重跑一段)。",
|
|
123
|
-
next,
|
|
124
|
-
].join("\n");
|
|
125
|
-
}
|
|
126
|
-
return [
|
|
127
|
-
"无法从该节点续跑,因为:",
|
|
128
|
-
...lines.map((line) => `• ${line}`),
|
|
129
|
-
"",
|
|
130
|
-
next,
|
|
131
|
-
].join("\n");
|
|
132
|
-
}
|
|
133
|
-
/** Returns rewritten copy when `raw` is a known recovery failure; otherwise null. */
|
|
134
|
-
export function rewriteRecoveryOperatorError(raw) {
|
|
135
|
-
const text = raw.trim();
|
|
136
|
-
if (!text)
|
|
137
|
-
return null;
|
|
138
|
-
if (text.startsWith("无法从该节点续跑"))
|
|
139
|
-
return text;
|
|
140
|
-
if (/从节点重跑不具备安全资格|worker-managed-rerun-unsupported|restart-subgraph-contains-|workspace-drift|controller-drift|binding-drift/.test(text)) {
|
|
141
|
-
return formatIneligibleRerunMessage({ fallbackMessage: text });
|
|
142
|
-
}
|
|
143
|
-
if (/action must be a mutation-gate target/i.test(text)) {
|
|
144
|
-
return "这个操作不能再走确认门。Worker 启动的任务请直接点「重新排队 Worker Task」;独立运行请点「完整重跑任务」。";
|
|
145
|
-
}
|
|
146
|
-
if (/cannot read dagReport run facts/i.test(text)) {
|
|
147
|
-
return "读不到这次运行的失败报告,不能完整重跑。请先点「查看报告」或「Doctor 诊断」。";
|
|
148
|
-
}
|
|
149
|
-
if (/dagRerunPlan run facts unavailable/i.test(text)) {
|
|
150
|
-
return "读不到从节点续跑的资格结果,不能改走完整重跑。请先点「从节点重跑」查看资格,或先「查看报告」。";
|
|
151
|
-
}
|
|
152
|
-
if (/node rerun plan is eligible/i.test(text)) {
|
|
153
|
-
return "从节点续跑是安全的,请改用「从节点重跑」,不必完整重跑整单。";
|
|
154
|
-
}
|
|
155
|
-
if (/does not support autonomous standaloneTaskRerun/i.test(text)) {
|
|
156
|
-
return "当前失败不适合自动完整重跑。请先点「查看报告」,按建议操作。";
|
|
157
|
-
}
|
|
158
|
-
if (/primaryRecovery requires a human decision/i.test(text)) {
|
|
159
|
-
return "这次失败需要人工决策,不能自动重跑。请先点「查看报告」。";
|
|
160
|
-
}
|
|
161
|
-
if (/is not bound to run/i.test(text)) {
|
|
162
|
-
return "填写的任务 ID 与这次运行不匹配。请使用该运行绑定的原任务 ID。";
|
|
163
|
-
}
|
|
164
|
-
if (/cannot read pool doctor/i.test(text)) {
|
|
165
|
-
return "读不到任务队列状态,暂时不能重新排队。请稍后重试,或先跑 Doctor 诊断。";
|
|
166
|
-
}
|
|
167
|
-
if (/not found in the pool doctor inventory/i.test(text)) {
|
|
168
|
-
return "任务队列里找不到这个任务,不能重新排队。请确认 Task ID 来自 Worker 队列,而不是只填了 DAG 运行 ID。";
|
|
169
|
-
}
|
|
170
|
-
if (/only Failed tasks are retry-eligible/i.test(text)) {
|
|
171
|
-
const status = text.match(/is ([A-Za-z_]+); only Failed/)?.[1];
|
|
172
|
-
return status
|
|
173
|
-
? `该任务当前是「${status}」,只有失败(Failed)的任务才能重新排队。`
|
|
174
|
-
: "只有失败(Failed)的任务才能重新排队。请先确认任务已失败。";
|
|
175
|
-
}
|
|
176
|
-
if (/featureId .+ does not match the pool task/i.test(text)) {
|
|
177
|
-
return "Feature ID 与队列中的任务不一致。请改用队列里记录的 Feature ID,或留空后重试。";
|
|
178
|
-
}
|
|
179
|
-
if (/RUN_NOT_CONSOLE_OWNED|不由当前 Console 持有/.test(text)) {
|
|
180
|
-
return "此运行不由当前 Console 持有,暂不支持协作中止。请用 Doctor / 报告查看。";
|
|
181
|
-
}
|
|
182
|
-
if (/RUNNER_IDENTITY_MISMATCH|运行身份已变化/.test(text)) {
|
|
183
|
-
return "运行身份已变化,不能中止这份过期请求。请刷新后再试。";
|
|
184
|
-
}
|
|
185
|
-
if (/INTERRUPT_ALREADY_PENDING|已有一条中止请求/.test(text)) {
|
|
186
|
-
return "已有一条中止请求等待当前节点安全收敛。请稍候刷新,不要重复提交。";
|
|
187
|
-
}
|
|
188
|
-
if (/RUN_ALREADY_TERMINAL|运行已经结束/.test(text)) {
|
|
189
|
-
return "运行已经结束,没有可中止的活 runner。请改用报告或对账。";
|
|
190
|
-
}
|
|
191
|
-
if (/INTERRUPT_NEEDS_RECONCILE|无法确认 runner 已处理/.test(text)) {
|
|
192
|
-
return "中止请求已登记,但无法确认 runner 已处理。请用 Doctor 对账,不要当成已经中止。";
|
|
193
|
-
}
|
|
194
|
-
if (/RUN_NOT_ACTIVE/.test(text)) {
|
|
195
|
-
return "这次运行不在本机活动目录,或已经不是 running,不能协作中止。";
|
|
196
|
-
}
|
|
197
|
-
return null;
|
|
198
|
-
}
|