@flowdesk/opencode-plugin 0.1.13 → 0.1.15
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/README.md +1 -1
- package/dist/agent-task-output.d.ts +29 -0
- package/dist/agent-task-output.d.ts.map +1 -0
- package/dist/agent-task-output.js +225 -0
- package/dist/agent-task-output.js.map +1 -0
- package/dist/agent-task-runner.d.ts +34 -0
- package/dist/agent-task-runner.d.ts.map +1 -1
- package/dist/agent-task-runner.js +634 -84
- package/dist/agent-task-runner.js.map +1 -1
- package/dist/auto-continue-preview-tool.d.ts +36 -0
- package/dist/auto-continue-preview-tool.d.ts.map +1 -0
- package/dist/auto-continue-preview-tool.js +119 -0
- package/dist/auto-continue-preview-tool.js.map +1 -0
- package/dist/completion-ui-cache.d.ts +6 -0
- package/dist/completion-ui-cache.d.ts.map +1 -0
- package/dist/completion-ui-cache.js +390 -0
- package/dist/completion-ui-cache.js.map +1 -0
- package/dist/event-hook-observer.d.ts +14 -0
- package/dist/event-hook-observer.d.ts.map +1 -0
- package/dist/event-hook-observer.js +257 -0
- package/dist/event-hook-observer.js.map +1 -0
- package/dist/managed-dispatch-adapter.d.ts +62 -0
- package/dist/managed-dispatch-adapter.d.ts.map +1 -1
- package/dist/managed-dispatch-adapter.js +472 -4
- package/dist/managed-dispatch-adapter.js.map +1 -1
- package/dist/model-selection-engine.d.ts +60 -0
- package/dist/model-selection-engine.d.ts.map +1 -0
- package/dist/model-selection-engine.js +242 -0
- package/dist/model-selection-engine.js.map +1 -0
- package/dist/provider-usage-live-tool.d.ts +10 -0
- package/dist/provider-usage-live-tool.d.ts.map +1 -1
- package/dist/provider-usage-live-tool.js +262 -33
- package/dist/provider-usage-live-tool.js.map +1 -1
- package/dist/server.d.ts +36 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +497 -20
- package/dist/server.js.map +1 -1
- package/dist/stall-recovery.d.ts +34 -0
- package/dist/stall-recovery.d.ts.map +1 -1
- package/dist/stall-recovery.js +680 -3
- package/dist/stall-recovery.js.map +1 -1
- package/dist/status-live-tool.d.ts +54 -0
- package/dist/status-live-tool.d.ts.map +1 -1
- package/dist/status-live-tool.js +449 -44
- package/dist/status-live-tool.js.map +1 -1
- package/dist/tui-subtask-activity.d.ts +73 -0
- package/dist/tui-subtask-activity.d.ts.map +1 -0
- package/dist/tui-subtask-activity.js +271 -0
- package/dist/tui-subtask-activity.js.map +1 -0
- package/dist/tui-usage-snapshot.d.ts +14 -0
- package/dist/tui-usage-snapshot.d.ts.map +1 -1
- package/dist/tui-usage-snapshot.js +275 -8
- package/dist/tui-usage-snapshot.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +102 -44
- package/dist/tui.js.map +1 -1
- package/dist/workflow-assign-tool.d.ts +23 -0
- package/dist/workflow-assign-tool.d.ts.map +1 -0
- package/dist/workflow-assign-tool.js +135 -0
- package/dist/workflow-assign-tool.js.map +1 -0
- package/dist/workflow-author-tool.d.ts +29 -0
- package/dist/workflow-author-tool.d.ts.map +1 -0
- package/dist/workflow-author-tool.js +227 -0
- package/dist/workflow-author-tool.js.map +1 -0
- package/dist/workflow-dispatch-tool.d.ts +12 -0
- package/dist/workflow-dispatch-tool.d.ts.map +1 -1
- package/dist/workflow-dispatch-tool.js +31 -3
- package/dist/workflow-dispatch-tool.js.map +1 -1
- package/dist/workflow-orchestrator.d.ts +31 -0
- package/dist/workflow-orchestrator.d.ts.map +1 -0
- package/dist/workflow-orchestrator.js +160 -0
- package/dist/workflow-orchestrator.js.map +1 -0
- package/dist/workflow-scheduler.d.ts.map +1 -1
- package/dist/workflow-scheduler.js +3 -1
- package/dist/workflow-scheduler.js.map +1 -1
- package/dist/workflow-synthesis-tool.d.ts +31 -0
- package/dist/workflow-synthesis-tool.d.ts.map +1 -0
- package/dist/workflow-synthesis-tool.js +194 -0
- package/dist/workflow-synthesis-tool.js.map +1 -0
- package/package.json +2 -2
package/dist/stall-recovery.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { applyFlowDeskSessionEvidenceWriteIntentsV1, prepareFlowDeskSessionEvidenceWriteIntentV1, reloadFlowDeskSessionEvidenceV1, projectFlowDeskLaneStallV1, } from "@flowdesk/core";
|
|
2
|
-
import { createHmac, createHash, timingSafeEqual } from "node:crypto";
|
|
1
|
+
import { applyFlowDeskSessionEvidenceWriteIntentsV1, prepareFlowDeskSessionEvidenceWriteIntentV1, reloadFlowDeskSessionEvidenceV1, projectFlowDeskLaneStallV1, validateTopTierReviewVerdictV1, } from "@flowdesk/core";
|
|
2
|
+
import { createHmac, createHash, timingSafeEqual, randomBytes } from "node:crypto";
|
|
3
3
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { launchFlowDeskInjectedSdkRuntimeLaneFromPlanV1, } from "./managed-dispatch-adapter.js";
|
|
6
6
|
import { FLOWDESK_TIMEOUT_DEFAULTS, FlowDeskTimeoutError, withTimeout, } from "./shared/with-timeout.js";
|
|
7
|
-
import { executeFlowDeskAgentTaskV1 } from "./agent-task-runner.js";
|
|
7
|
+
import { executeFlowDeskAgentTaskV1, AGENT_TASK_CHILD_SESSION_SCHEMA_VERSION, sanitizeFlowDeskTaskResultTextV1 } from "./agent-task-runner.js";
|
|
8
|
+
import { observeFlowDeskAgentTaskOutputV1 } from "./agent-task-output.js";
|
|
9
|
+
import { refreshFlowDeskCompletionUiCachesV1 } from "./completion-ui-cache.js";
|
|
8
10
|
export async function checkSdkSessionApiHealthV1(client, sessionId, timeouts = {}) {
|
|
9
11
|
if (typeof client.session.messages !== "function") {
|
|
10
12
|
return { status: "unknown", reason: "sdk_messages_not_available" };
|
|
@@ -252,6 +254,11 @@ export function validateAndAbortFlowDeskLaneEvidenceV1(input) {
|
|
|
252
254
|
entry.record.state === "aborted");
|
|
253
255
|
if (!persisted)
|
|
254
256
|
return { status: "write_failed", reason: "abort_evidence_not_persisted" };
|
|
257
|
+
refreshFlowDeskCompletionUiCachesV1({
|
|
258
|
+
rootDir: input.rootDir,
|
|
259
|
+
workflowId: input.workflow_id,
|
|
260
|
+
observedAt: observedAt,
|
|
261
|
+
});
|
|
255
262
|
return {
|
|
256
263
|
status: "aborted",
|
|
257
264
|
lane_id: input.lane_id,
|
|
@@ -580,6 +587,14 @@ export function backfillTerminalAgentTaskFailedLanesV1(input) {
|
|
|
580
587
|
terminalEvidenceIds.push(evidenceId);
|
|
581
588
|
}
|
|
582
589
|
}
|
|
590
|
+
if ((input.refreshCompletionUiCaches ?? true) &&
|
|
591
|
+
(terminalEvidenceIds.length > 0 || latestFailure.size > 0)) {
|
|
592
|
+
refreshFlowDeskCompletionUiCachesV1({
|
|
593
|
+
rootDir: input.rootDir,
|
|
594
|
+
workflowId: input.workflowId,
|
|
595
|
+
observedAt,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
583
598
|
return {
|
|
584
599
|
status: "backfill_completed",
|
|
585
600
|
workflowId: input.workflowId,
|
|
@@ -900,6 +915,8 @@ export async function evaluateGuardedAutoRetryHookV1(input) {
|
|
|
900
915
|
rootDir: input.rootDir,
|
|
901
916
|
client: input.client,
|
|
902
917
|
timeoutMs: timeoutMs,
|
|
918
|
+
_nudgeQuietPeriodMs: input._nudgeQuietPeriodMs,
|
|
919
|
+
_messagesTimeoutMs: input._messagesTimeoutMs,
|
|
903
920
|
});
|
|
904
921
|
if (taskResult.status === "task_failed") {
|
|
905
922
|
const failedId = `retry-failed-agent-task-${safeToken(newLaneId)}-${retryToken}`;
|
|
@@ -1169,6 +1186,18 @@ export async function runFlowDeskWatchdogCycleV1(input) {
|
|
|
1169
1186
|
const workflowIds = listWatchdogWorkflowIds(input.rootDir);
|
|
1170
1187
|
const now = input.now ?? new Date();
|
|
1171
1188
|
for (const workflowId of workflowIds) {
|
|
1189
|
+
// Monitor async-mode child sessions (nudge + abort + result collection)
|
|
1190
|
+
if (input.client !== undefined) {
|
|
1191
|
+
try {
|
|
1192
|
+
await monitorChildSessionsV1({
|
|
1193
|
+
rootDir: input.rootDir,
|
|
1194
|
+
workflowId,
|
|
1195
|
+
client: input.client,
|
|
1196
|
+
now,
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
catch { /* best-effort, must not crash watchdog */ }
|
|
1200
|
+
}
|
|
1172
1201
|
backfillTerminalAgentTaskFailedLanesV1({
|
|
1173
1202
|
rootDir: input.rootDir,
|
|
1174
1203
|
workflowId,
|
|
@@ -1218,6 +1247,8 @@ export async function runFlowDeskWatchdogCycleV1(input) {
|
|
|
1218
1247
|
client: input.client,
|
|
1219
1248
|
parentSessionId: input.parentSessionId,
|
|
1220
1249
|
now,
|
|
1250
|
+
_nudgeQuietPeriodMs: input._nudgeQuietPeriodMs,
|
|
1251
|
+
_messagesTimeoutMs: input._messagesTimeoutMs,
|
|
1221
1252
|
});
|
|
1222
1253
|
if (retryResult.status === "retry_launched") {
|
|
1223
1254
|
lanesRetried++;
|
|
@@ -1254,4 +1285,650 @@ export async function runFlowDeskWatchdogCycleV1(input) {
|
|
|
1254
1285
|
lanesFailed,
|
|
1255
1286
|
};
|
|
1256
1287
|
}
|
|
1288
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1289
|
+
// Child session monitor (async-mode lanes)
|
|
1290
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1291
|
+
const AGENT_TASK_NUDGE_TEXT_WATCHDOG = "Please provide your final answer now. If you have completed your analysis, output your complete response.";
|
|
1292
|
+
/** Poll result from one session.messages call */
|
|
1293
|
+
function monitorRecord(value) {
|
|
1294
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
1295
|
+
? value
|
|
1296
|
+
: undefined;
|
|
1297
|
+
}
|
|
1298
|
+
function monitorResponseData(value) {
|
|
1299
|
+
const record = monitorRecord(value);
|
|
1300
|
+
return record !== undefined && "data" in record ? record.data : value;
|
|
1301
|
+
}
|
|
1302
|
+
function monitorSdkErrorResponse(value) {
|
|
1303
|
+
const record = monitorRecord(value);
|
|
1304
|
+
const data = monitorRecord(monitorResponseData(value));
|
|
1305
|
+
return record?.error !== undefined || data?.error !== undefined;
|
|
1306
|
+
}
|
|
1307
|
+
async function pollChildSessionOutput(client, childSessionId, messagesTimeoutMs = 3_000) {
|
|
1308
|
+
const messages = client.session.messages;
|
|
1309
|
+
if (typeof messages !== "function")
|
|
1310
|
+
return null;
|
|
1311
|
+
try {
|
|
1312
|
+
const readMessages = async () => {
|
|
1313
|
+
const current = await messages.call(client.session, {
|
|
1314
|
+
sessionID: childSessionId,
|
|
1315
|
+
});
|
|
1316
|
+
if (!monitorSdkErrorResponse(current))
|
|
1317
|
+
return current;
|
|
1318
|
+
return messages.call(client.session, {
|
|
1319
|
+
path: { id: childSessionId },
|
|
1320
|
+
});
|
|
1321
|
+
};
|
|
1322
|
+
const raw = await Promise.race([
|
|
1323
|
+
readMessages(),
|
|
1324
|
+
new Promise(resolve => setTimeout(() => resolve(null), messagesTimeoutMs)),
|
|
1325
|
+
]);
|
|
1326
|
+
if (raw === null)
|
|
1327
|
+
return null;
|
|
1328
|
+
const observed = observeFlowDeskAgentTaskOutputV1(raw);
|
|
1329
|
+
if (observed.terminalObserved && observed.latestText !== undefined && observed.latestText.trim().length > 0)
|
|
1330
|
+
return { text: observed.latestText, completionStatus: "final", outputKind: observed.outputKind, usableForSynthesis: observed.usableForSynthesis, looksLikeRefusalOrError: observed.looksLikeRefusalOrError };
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
catch {
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
async function pollChildSessionCandidate(client, childSessionId, messagesTimeoutMs = 3_000) {
|
|
1338
|
+
const messages = client.session.messages;
|
|
1339
|
+
if (typeof messages !== "function")
|
|
1340
|
+
return null;
|
|
1341
|
+
try {
|
|
1342
|
+
const readMessages = async () => {
|
|
1343
|
+
const current = await messages.call(client.session, { sessionID: childSessionId });
|
|
1344
|
+
if (!monitorSdkErrorResponse(current))
|
|
1345
|
+
return current;
|
|
1346
|
+
return messages.call(client.session, { path: { id: childSessionId } });
|
|
1347
|
+
};
|
|
1348
|
+
const raw = await Promise.race([
|
|
1349
|
+
readMessages(),
|
|
1350
|
+
new Promise(resolve => setTimeout(() => resolve(null), messagesTimeoutMs)),
|
|
1351
|
+
]);
|
|
1352
|
+
if (raw === null)
|
|
1353
|
+
return null;
|
|
1354
|
+
const observed = observeFlowDeskAgentTaskOutputV1(raw);
|
|
1355
|
+
if (observed.latestText !== undefined && observed.latestText.trim().length > 0)
|
|
1356
|
+
return { text: observed.latestText, completionStatus: observed.terminalObserved ? "final" : "partial", outputKind: observed.outputKind, usableForSynthesis: observed.usableForSynthesis, looksLikeRefusalOrError: observed.looksLikeRefusalOrError };
|
|
1357
|
+
return null;
|
|
1358
|
+
}
|
|
1359
|
+
catch {
|
|
1360
|
+
return null;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
/** Send a noReply nudge to a child session — best effort with hard timeout */
|
|
1364
|
+
async function sendWatchdogNudge(client, childSessionId, timeoutMs = 5_000) {
|
|
1365
|
+
const promptFn = client.session.promptAsync ?? client.session.prompt;
|
|
1366
|
+
if (promptFn === undefined)
|
|
1367
|
+
return "skipped";
|
|
1368
|
+
try {
|
|
1369
|
+
await Promise.race([
|
|
1370
|
+
promptFn.call(client.session, {
|
|
1371
|
+
sessionID: childSessionId,
|
|
1372
|
+
noReply: true,
|
|
1373
|
+
parts: [{ type: "text", text: AGENT_TASK_NUDGE_TEXT_WATCHDOG }],
|
|
1374
|
+
}),
|
|
1375
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("nudge timeout")), timeoutMs)),
|
|
1376
|
+
]);
|
|
1377
|
+
return "sent";
|
|
1378
|
+
}
|
|
1379
|
+
catch {
|
|
1380
|
+
return "timeout";
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
/** Abort a child session via the injected SDK client */
|
|
1384
|
+
async function abortChildSession(client, childSessionId) {
|
|
1385
|
+
const abort = client.session.abort;
|
|
1386
|
+
if (typeof abort !== "function")
|
|
1387
|
+
return;
|
|
1388
|
+
try {
|
|
1389
|
+
await abort.call(client.session, {
|
|
1390
|
+
path: { id: childSessionId },
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
catch { /* best-effort */ }
|
|
1394
|
+
}
|
|
1395
|
+
function writeChildSessionEvidence(rootDir, workflowId, evidenceId, record) {
|
|
1396
|
+
const prepared = prepareFlowDeskSessionEvidenceWriteIntentV1({ workflowId, evidenceId, record });
|
|
1397
|
+
if (!prepared.ok || prepared.writeIntent === undefined)
|
|
1398
|
+
return false;
|
|
1399
|
+
const applied = applyFlowDeskSessionEvidenceWriteIntentsV1(rootDir, [prepared.writeIntent]);
|
|
1400
|
+
return applied.ok && applied.writtenPaths.length > 0;
|
|
1401
|
+
}
|
|
1402
|
+
function extractJsonBlocksFromText(raw) {
|
|
1403
|
+
const trimmed = raw.trim();
|
|
1404
|
+
const results = [];
|
|
1405
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}"))
|
|
1406
|
+
return [trimmed];
|
|
1407
|
+
const fencePattern = /```(?:json)?\s*\n?(\{[\s\S]*?\})\s*\n?```/g;
|
|
1408
|
+
for (const match of trimmed.matchAll(fencePattern)) {
|
|
1409
|
+
if (match[1])
|
|
1410
|
+
results.push(match[1].trim());
|
|
1411
|
+
}
|
|
1412
|
+
if (results.length > 0)
|
|
1413
|
+
return results;
|
|
1414
|
+
let depth = 0;
|
|
1415
|
+
let start = -1;
|
|
1416
|
+
let lastBlock;
|
|
1417
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
1418
|
+
const ch = trimmed[i];
|
|
1419
|
+
if (ch === "{") {
|
|
1420
|
+
if (depth === 0)
|
|
1421
|
+
start = i;
|
|
1422
|
+
depth++;
|
|
1423
|
+
}
|
|
1424
|
+
else if (ch === "}") {
|
|
1425
|
+
depth--;
|
|
1426
|
+
if (depth === 0 && start !== -1) {
|
|
1427
|
+
lastBlock = trimmed.slice(start, i + 1).trim();
|
|
1428
|
+
start = -1;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return lastBlock === undefined ? [] : [lastBlock];
|
|
1433
|
+
}
|
|
1434
|
+
function observedTopTierReviewerVerdictFromText(input) {
|
|
1435
|
+
for (const block of extractJsonBlocksFromText(input.text)) {
|
|
1436
|
+
try {
|
|
1437
|
+
const candidate = JSON.parse(block);
|
|
1438
|
+
const validation = validateTopTierReviewVerdictV1(candidate);
|
|
1439
|
+
if (!validation.ok)
|
|
1440
|
+
continue;
|
|
1441
|
+
const verdict = candidate;
|
|
1442
|
+
if (verdict.workflow_id === input.workflowId)
|
|
1443
|
+
return verdict;
|
|
1444
|
+
}
|
|
1445
|
+
catch {
|
|
1446
|
+
// Keep scanning candidates.
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
return undefined;
|
|
1450
|
+
}
|
|
1451
|
+
function persistObservedReviewerVerdict(input) {
|
|
1452
|
+
const evidenceId = input.verdict.verdict_id;
|
|
1453
|
+
if (!writeChildSessionEvidence(input.rootDir, input.workflowId, evidenceId, input.verdict))
|
|
1454
|
+
return false;
|
|
1455
|
+
const reloaded = reloadFlowDeskSessionEvidenceV1({ rootDir: input.rootDir, workflowId: input.workflowId });
|
|
1456
|
+
return reloaded.ok && reloaded.blocked.length === 0 && reloaded.entries.some((entry) => entry.evidenceClass === "reviewer_verdict" &&
|
|
1457
|
+
entry.evidenceId === evidenceId &&
|
|
1458
|
+
entry.record.verdict_id === input.verdict.verdict_id);
|
|
1459
|
+
}
|
|
1460
|
+
function writeAgentTaskCompleteLifecycleForVerdict(input) {
|
|
1461
|
+
return writeChildSessionEvidence(input.rootDir, input.workflowId, `lifecycle-agent-task-complete-${input.laneId}-${input.verdictId}`, {
|
|
1462
|
+
schema_version: "flowdesk.lane_lifecycle_record.v1",
|
|
1463
|
+
lane_id: input.laneId,
|
|
1464
|
+
workflow_id: input.workflowId,
|
|
1465
|
+
attempt_id: input.attemptId,
|
|
1466
|
+
parent_session_ref: input.parentSessionRef,
|
|
1467
|
+
child_session_ref: input.childSessionId.startsWith("ses-") ? input.childSessionId : `ses-${input.childSessionId}`,
|
|
1468
|
+
message_ref: `msg-${input.laneId}`,
|
|
1469
|
+
agent_ref: input.agentRef,
|
|
1470
|
+
provider_qualified_model_id: input.providerQualifiedModelId,
|
|
1471
|
+
state: "complete",
|
|
1472
|
+
verdict_ref: input.verdictId,
|
|
1473
|
+
output_ref: `output-${input.taskResultEvidenceId}`,
|
|
1474
|
+
runtime_echo_ref: `runtime-echo-${input.laneId}`,
|
|
1475
|
+
telemetry_ref: `telemetry-${input.laneId}`,
|
|
1476
|
+
timeout_ms: 0,
|
|
1477
|
+
orphan_max_age_ms: 0,
|
|
1478
|
+
retry_count: 0,
|
|
1479
|
+
created_at: input.observedAt,
|
|
1480
|
+
updated_at: input.observedAt,
|
|
1481
|
+
dispatch_authority_enabled: false,
|
|
1482
|
+
providerCall: false,
|
|
1483
|
+
actualLaneLaunch: false,
|
|
1484
|
+
runtimeExecution: false,
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
function laneAlreadyHasTerminalTaskEvidence(input) {
|
|
1488
|
+
const reloaded = reloadFlowDeskSessionEvidenceV1({
|
|
1489
|
+
rootDir: input.rootDir,
|
|
1490
|
+
workflowId: input.workflowId,
|
|
1491
|
+
});
|
|
1492
|
+
if (!reloaded.ok)
|
|
1493
|
+
return false;
|
|
1494
|
+
return reloaded.entries.some((entry) => {
|
|
1495
|
+
if (entry.evidenceClass !== "task_result" && entry.evidenceClass !== "task_failed")
|
|
1496
|
+
return false;
|
|
1497
|
+
const record = entry.record;
|
|
1498
|
+
return record.lane_id === input.laneId;
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
function terminalEvidenceObservedAtMs(record) {
|
|
1502
|
+
const value = typeof record.updated_at === "string"
|
|
1503
|
+
? record.updated_at
|
|
1504
|
+
: typeof record.created_at === "string"
|
|
1505
|
+
? record.created_at
|
|
1506
|
+
: typeof record.observed_at === "string"
|
|
1507
|
+
? record.observed_at
|
|
1508
|
+
: undefined;
|
|
1509
|
+
const parsed = value === undefined ? Number.NaN : Date.parse(value);
|
|
1510
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
1511
|
+
}
|
|
1512
|
+
function chooseLaterTerminalEndState(existing, candidate) {
|
|
1513
|
+
if (existing === undefined)
|
|
1514
|
+
return candidate;
|
|
1515
|
+
if (candidate.observedAtMs > existing.observedAtMs)
|
|
1516
|
+
return candidate;
|
|
1517
|
+
if (candidate.observedAtMs < existing.observedAtMs)
|
|
1518
|
+
return existing;
|
|
1519
|
+
// Prefer task_result for equal timestamps; otherwise keep the existing entry so
|
|
1520
|
+
// duplicate event-session-error / failed-child evidence is idempotent.
|
|
1521
|
+
return candidate.hasTaskResult && !existing.hasTaskResult ? candidate : existing;
|
|
1522
|
+
}
|
|
1523
|
+
function collectTerminalLaneEndStatesV1(entries) {
|
|
1524
|
+
const terminalByLane = new Map();
|
|
1525
|
+
for (const entry of entries) {
|
|
1526
|
+
const rec = entry.record;
|
|
1527
|
+
const laneId = typeof rec.lane_id === "string" ? rec.lane_id : undefined;
|
|
1528
|
+
if (laneId === undefined)
|
|
1529
|
+
continue;
|
|
1530
|
+
let state;
|
|
1531
|
+
let hasTaskResult = false;
|
|
1532
|
+
if (entry.evidenceClass === "lane_lifecycle") {
|
|
1533
|
+
state = typeof rec.state === "string" && TERMINAL_LANE_STATES.has(rec.state) ? rec.state : undefined;
|
|
1534
|
+
}
|
|
1535
|
+
else if (entry.evidenceClass === "task_result") {
|
|
1536
|
+
state = "complete";
|
|
1537
|
+
hasTaskResult = true;
|
|
1538
|
+
}
|
|
1539
|
+
else if (entry.evidenceClass === "task_failed") {
|
|
1540
|
+
state = rec.failure_category === "no_response" ? "no_output" : "invocation_failed";
|
|
1541
|
+
}
|
|
1542
|
+
if (state === undefined)
|
|
1543
|
+
continue;
|
|
1544
|
+
terminalByLane.set(laneId, chooseLaterTerminalEndState(terminalByLane.get(laneId), {
|
|
1545
|
+
laneId,
|
|
1546
|
+
state,
|
|
1547
|
+
observedAtMs: terminalEvidenceObservedAtMs(rec),
|
|
1548
|
+
hasTaskResult,
|
|
1549
|
+
}));
|
|
1550
|
+
}
|
|
1551
|
+
return terminalByLane;
|
|
1552
|
+
}
|
|
1553
|
+
function latestTerminalObservedAtIso(endStates, fallbackMs) {
|
|
1554
|
+
let latest = 0;
|
|
1555
|
+
for (const state of endStates)
|
|
1556
|
+
latest = Math.max(latest, state.observedAtMs);
|
|
1557
|
+
return new Date(latest > 0 ? latest : fallbackMs).toISOString();
|
|
1558
|
+
}
|
|
1559
|
+
function childProgressLabel(value) {
|
|
1560
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
1561
|
+
return compact.length > 120 ? `${compact.slice(0, 119)}…` : compact;
|
|
1562
|
+
}
|
|
1563
|
+
function writeAgentTaskProgressEvidence(input) {
|
|
1564
|
+
const record = {
|
|
1565
|
+
schema_version: "flowdesk.agent_task_progress.v1",
|
|
1566
|
+
workflow_id: input.workflowId,
|
|
1567
|
+
lane_id: input.laneId,
|
|
1568
|
+
task_id: input.taskId,
|
|
1569
|
+
agent_ref: input.agentRef,
|
|
1570
|
+
provider_qualified_model_id: input.providerQualifiedModelId,
|
|
1571
|
+
progress_seq: input.progressSeq,
|
|
1572
|
+
observed_at: input.observedAt,
|
|
1573
|
+
phase: input.phase,
|
|
1574
|
+
progress_label: childProgressLabel(input.progressLabel),
|
|
1575
|
+
progress_ref: `progress-${input.laneId}-${input.progressSeq}`,
|
|
1576
|
+
redaction_version: "v1",
|
|
1577
|
+
dispatch_authority_enabled: false,
|
|
1578
|
+
};
|
|
1579
|
+
writeChildSessionEvidence(input.rootDir, input.workflowId, `agent-task-progress-${input.laneId}-${input.progressSeq}`, record);
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Monitor all async-mode agent task lanes in the given workflow.
|
|
1583
|
+
* Called from the watchdog cycle once per interval:
|
|
1584
|
+
* - Poll session.messages for each running child session
|
|
1585
|
+
* - On result: write task_result evidence + terminal lifecycle
|
|
1586
|
+
* - At 10s silence: nudge with noReply: true
|
|
1587
|
+
* - At 20s: second nudge
|
|
1588
|
+
* - At 30s+: session.abort + task_failed + terminal lifecycle
|
|
1589
|
+
*/
|
|
1590
|
+
export async function monitorChildSessionsV1(input) {
|
|
1591
|
+
const nowMs = (input.now ?? new Date()).getTime();
|
|
1592
|
+
const nudgeQuietPeriodMs = input.nudgeQuietPeriodMs ?? 10_000;
|
|
1593
|
+
const maxNudges = input.maxNudges ?? 2;
|
|
1594
|
+
const abortThresholdMs = input.abortThresholdMs ?? 30_000;
|
|
1595
|
+
const result = { lanesPolled: 0, lanesCompleted: 0, lanesNudged: 0, lanesAborted: 0 };
|
|
1596
|
+
const reloaded = reloadFlowDeskSessionEvidenceV1({ rootDir: input.rootDir, workflowId: input.workflowId });
|
|
1597
|
+
if (!reloaded.ok)
|
|
1598
|
+
return result;
|
|
1599
|
+
// Find all child session records for running lanes
|
|
1600
|
+
const childRecords = reloaded.entries
|
|
1601
|
+
.filter(e => e.evidenceClass === "agent_task_child_session")
|
|
1602
|
+
.map(e => e.record)
|
|
1603
|
+
.filter(r => r.schema_version === AGENT_TASK_CHILD_SESSION_SCHEMA_VERSION);
|
|
1604
|
+
// Find lanes that are NOT yet terminal. Terminal evidence can come from
|
|
1605
|
+
// task_result, task_failed (including event-session-error records), or a
|
|
1606
|
+
// lifecycle terminal state. The map keeps the extracted end-state timestamp so
|
|
1607
|
+
// cache refreshes are monotonic/idempotent even when no task_result exists.
|
|
1608
|
+
const terminalEndStates = collectTerminalLaneEndStatesV1(reloaded.entries);
|
|
1609
|
+
const terminalLaneIds = new Set(terminalEndStates.keys());
|
|
1610
|
+
const awaitingPermissionLaneIds = new Set();
|
|
1611
|
+
const latestProgressByLane = new Map();
|
|
1612
|
+
for (const entry of reloaded.entries) {
|
|
1613
|
+
const rec = entry.record;
|
|
1614
|
+
const laneIdVal = typeof rec.lane_id === "string" ? rec.lane_id : undefined;
|
|
1615
|
+
if (!laneIdVal)
|
|
1616
|
+
continue;
|
|
1617
|
+
if (entry.evidenceClass === "agent_task_progress") {
|
|
1618
|
+
const observedAtMs = typeof rec.observed_at === "string" ? Date.parse(rec.observed_at) : NaN;
|
|
1619
|
+
const phase = typeof rec.phase === "string" ? rec.phase : undefined;
|
|
1620
|
+
const current = latestProgressByLane.get(laneIdVal);
|
|
1621
|
+
if (phase !== undefined && Number.isFinite(observedAtMs) && (current === undefined || current.observedAtMs <= observedAtMs)) {
|
|
1622
|
+
latestProgressByLane.set(laneIdVal, { observedAtMs, phase });
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
const terminalRefreshObservedAt = latestTerminalObservedAtIso(terminalEndStates.values(), nowMs);
|
|
1627
|
+
for (const [laneId, progress] of latestProgressByLane) {
|
|
1628
|
+
if (progress.phase === "awaiting_permission")
|
|
1629
|
+
awaitingPermissionLaneIds.add(laneId);
|
|
1630
|
+
}
|
|
1631
|
+
// Defensive fallback for older/event-written progress records: if any explicit
|
|
1632
|
+
// awaiting_permission marker is present and no later permission response has
|
|
1633
|
+
// been observed, suspend watchdog nudge/abort for the lane.
|
|
1634
|
+
for (const entry of reloaded.entries) {
|
|
1635
|
+
if (entry.evidenceClass !== "agent_task_progress")
|
|
1636
|
+
continue;
|
|
1637
|
+
const rec = entry.record;
|
|
1638
|
+
if (rec.phase === "awaiting_permission" && typeof rec.lane_id === "string")
|
|
1639
|
+
awaitingPermissionLaneIds.add(rec.lane_id);
|
|
1640
|
+
if (rec.phase === "waiting" && typeof rec.progress_label === "string" && rec.progress_label.includes("permission response") && typeof rec.lane_id === "string")
|
|
1641
|
+
awaitingPermissionLaneIds.delete(rec.lane_id);
|
|
1642
|
+
}
|
|
1643
|
+
let uiCacheRefreshed = false;
|
|
1644
|
+
for (const record of childRecords) {
|
|
1645
|
+
const laneId = typeof record.lane_id === "string" ? record.lane_id : "";
|
|
1646
|
+
const childSessionId = typeof record.child_session_id === "string" ? record.child_session_id : "";
|
|
1647
|
+
if (!laneId || !childSessionId)
|
|
1648
|
+
continue;
|
|
1649
|
+
if (terminalLaneIds.has(laneId)) {
|
|
1650
|
+
// Refresh the completion UI cache for terminal lanes so stale "running"
|
|
1651
|
+
// rows are promoted to terminal. This must run for any terminal lane,
|
|
1652
|
+
// including lanes that only have lane_lifecycle/task_failed evidence and
|
|
1653
|
+
// no task_result (e.g. reviewer execution bridge writing only
|
|
1654
|
+
// lane_lifecycle=invocation_failed). Without this, the sidebar row stays
|
|
1655
|
+
// stuck at progressing_normal/running until a task_result appears.
|
|
1656
|
+
if (!uiCacheRefreshed) {
|
|
1657
|
+
refreshFlowDeskCompletionUiCachesV1({
|
|
1658
|
+
rootDir: input.rootDir,
|
|
1659
|
+
workflowId: input.workflowId,
|
|
1660
|
+
observedAt: terminalRefreshObservedAt,
|
|
1661
|
+
});
|
|
1662
|
+
uiCacheRefreshed = true;
|
|
1663
|
+
}
|
|
1664
|
+
continue; // already done
|
|
1665
|
+
}
|
|
1666
|
+
if (awaitingPermissionLaneIds.has(laneId))
|
|
1667
|
+
continue; // user permission is outstanding; do not nudge or abort
|
|
1668
|
+
result.lanesPolled++;
|
|
1669
|
+
const createdAtMs = typeof record.created_at === "string" ? Date.parse(record.created_at) : nowMs;
|
|
1670
|
+
const nudgeCount = typeof record.nudge_count === "number" ? record.nudge_count : 0;
|
|
1671
|
+
const lastNudgeAtMs = typeof record.last_nudge_at === "string" ? Date.parse(record.last_nudge_at) : createdAtMs;
|
|
1672
|
+
const silenceMs = nowMs - lastNudgeAtMs;
|
|
1673
|
+
const totalAgeMs = nowMs - createdAtMs;
|
|
1674
|
+
// 1. Try to collect terminal result text. Candidate text without terminal is kept for abort-time partial capture.
|
|
1675
|
+
const resultObservation = await pollChildSessionOutput(input.client, childSessionId);
|
|
1676
|
+
if (resultObservation !== null && resultObservation.text.trim().length > 0) {
|
|
1677
|
+
const taskId = typeof record.task_id === "string" ? record.task_id : laneId;
|
|
1678
|
+
const agentRef = typeof record.agent_ref === "string" ? record.agent_ref : "agent-unknown";
|
|
1679
|
+
const modelId = typeof record.provider_qualified_model_id === "string" ? record.provider_qualified_model_id : "unknown/unknown";
|
|
1680
|
+
if (laneAlreadyHasTerminalTaskEvidence({ rootDir: input.rootDir, workflowId: input.workflowId, laneId })) {
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1683
|
+
const token = randomBytes(4).toString("hex");
|
|
1684
|
+
const completedAt = new Date(nowMs).toISOString();
|
|
1685
|
+
const sanitizedResult = sanitizeFlowDeskTaskResultTextV1(resultObservation.text);
|
|
1686
|
+
const finalText = sanitizedResult.text;
|
|
1687
|
+
const taskResultEvidenceId = `task-result-${taskId}-watchdog-${token}`;
|
|
1688
|
+
const taskResultWritten = input._forceTaskResultWriteFailureForTest === true
|
|
1689
|
+
? false
|
|
1690
|
+
: writeChildSessionEvidence(input.rootDir, input.workflowId, taskResultEvidenceId, {
|
|
1691
|
+
schema_version: "flowdesk.task_result.v1",
|
|
1692
|
+
workflow_id: input.workflowId,
|
|
1693
|
+
lane_id: laneId,
|
|
1694
|
+
task_id: taskId,
|
|
1695
|
+
agent_ref: agentRef,
|
|
1696
|
+
provider_qualified_model_id: modelId,
|
|
1697
|
+
task_prompt_sha256: createHash("sha256").update("watchdog-collected").digest("hex"),
|
|
1698
|
+
result_text: finalText,
|
|
1699
|
+
result_text_truncated: sanitizedResult.truncated,
|
|
1700
|
+
result_text_sha256: createHash("sha256").update(resultObservation.text).digest("hex"),
|
|
1701
|
+
completion_status: resultObservation.completionStatus,
|
|
1702
|
+
output_kind: resultObservation.outputKind,
|
|
1703
|
+
usable_for_synthesis: resultObservation.usableForSynthesis,
|
|
1704
|
+
missing_contract: false,
|
|
1705
|
+
finalization_reason: "terminal_marker",
|
|
1706
|
+
looks_like_refusal_or_error: resultObservation.looksLikeRefusalOrError,
|
|
1707
|
+
created_at: completedAt,
|
|
1708
|
+
dispatch_authority_enabled: false,
|
|
1709
|
+
});
|
|
1710
|
+
if (!taskResultWritten) {
|
|
1711
|
+
writeChildSessionEvidence(input.rootDir, input.workflowId, `task-failed-${taskId}-watchdog-result-write-${token}`, {
|
|
1712
|
+
schema_version: "flowdesk.task_failed.v1",
|
|
1713
|
+
workflow_id: input.workflowId,
|
|
1714
|
+
lane_id: laneId,
|
|
1715
|
+
task_id: taskId,
|
|
1716
|
+
agent_ref: agentRef,
|
|
1717
|
+
provider_qualified_model_id: modelId,
|
|
1718
|
+
failure_category: "unknown",
|
|
1719
|
+
redacted_reason: "watchdog could not persist task_result evidence",
|
|
1720
|
+
created_at: completedAt,
|
|
1721
|
+
dispatch_authority_enabled: false,
|
|
1722
|
+
});
|
|
1723
|
+
writeAgentTaskProgressEvidence({
|
|
1724
|
+
rootDir: input.rootDir,
|
|
1725
|
+
workflowId: input.workflowId,
|
|
1726
|
+
laneId,
|
|
1727
|
+
taskId,
|
|
1728
|
+
agentRef,
|
|
1729
|
+
providerQualifiedModelId: modelId,
|
|
1730
|
+
phase: "failed",
|
|
1731
|
+
progressSeq: 20 + nudgeCount,
|
|
1732
|
+
progressLabel: "async agent task result persistence failed",
|
|
1733
|
+
observedAt: completedAt,
|
|
1734
|
+
});
|
|
1735
|
+
refreshFlowDeskCompletionUiCachesV1({
|
|
1736
|
+
rootDir: input.rootDir,
|
|
1737
|
+
workflowId: input.workflowId,
|
|
1738
|
+
observedAt: completedAt,
|
|
1739
|
+
});
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
const observedReviewerVerdict = observedTopTierReviewerVerdictFromText({
|
|
1743
|
+
text: resultObservation.text,
|
|
1744
|
+
workflowId: input.workflowId,
|
|
1745
|
+
});
|
|
1746
|
+
const reviewerVerdictPersisted = observedReviewerVerdict === undefined
|
|
1747
|
+
? false
|
|
1748
|
+
: persistObservedReviewerVerdict({
|
|
1749
|
+
rootDir: input.rootDir,
|
|
1750
|
+
workflowId: input.workflowId,
|
|
1751
|
+
verdict: observedReviewerVerdict,
|
|
1752
|
+
});
|
|
1753
|
+
const runningLifecycle = reloaded.entries.find((entry) => {
|
|
1754
|
+
const lifecycle = entry.record;
|
|
1755
|
+
return entry.evidenceClass === "lane_lifecycle" && lifecycle.lane_id === laneId && typeof lifecycle.attempt_id === "string";
|
|
1756
|
+
})?.record;
|
|
1757
|
+
if (reviewerVerdictPersisted && observedReviewerVerdict !== undefined) {
|
|
1758
|
+
writeAgentTaskCompleteLifecycleForVerdict({
|
|
1759
|
+
rootDir: input.rootDir,
|
|
1760
|
+
workflowId: input.workflowId,
|
|
1761
|
+
laneId,
|
|
1762
|
+
attemptId: typeof runningLifecycle?.attempt_id === "string" ? runningLifecycle.attempt_id : `attempt-${laneId}`,
|
|
1763
|
+
parentSessionRef: typeof record.parent_session_ref === "string" ? record.parent_session_ref : "ses-agent-task-parent",
|
|
1764
|
+
childSessionId,
|
|
1765
|
+
taskResultEvidenceId,
|
|
1766
|
+
agentRef,
|
|
1767
|
+
providerQualifiedModelId: modelId,
|
|
1768
|
+
verdictId: observedReviewerVerdict.verdict_id,
|
|
1769
|
+
observedAt: completedAt,
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
writeAgentTaskProgressEvidence({
|
|
1773
|
+
rootDir: input.rootDir,
|
|
1774
|
+
workflowId: input.workflowId,
|
|
1775
|
+
laneId,
|
|
1776
|
+
taskId,
|
|
1777
|
+
agentRef,
|
|
1778
|
+
providerQualifiedModelId: modelId,
|
|
1779
|
+
phase: "finalizing",
|
|
1780
|
+
progressSeq: 10 + nudgeCount,
|
|
1781
|
+
progressLabel: reviewerVerdictPersisted
|
|
1782
|
+
? "async agent task result captured with reviewer verdict evidence"
|
|
1783
|
+
: "async agent task result captured by watchdog",
|
|
1784
|
+
observedAt: completedAt,
|
|
1785
|
+
});
|
|
1786
|
+
refreshFlowDeskCompletionUiCachesV1({
|
|
1787
|
+
rootDir: input.rootDir,
|
|
1788
|
+
workflowId: input.workflowId,
|
|
1789
|
+
observedAt: completedAt,
|
|
1790
|
+
});
|
|
1791
|
+
result.lanesCompleted++;
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
// 2. Abort threshold exceeded
|
|
1795
|
+
if (totalAgeMs >= abortThresholdMs && nudgeCount >= maxNudges) {
|
|
1796
|
+
await abortChildSession(input.client, childSessionId);
|
|
1797
|
+
const taskId = typeof record.task_id === "string" ? record.task_id : laneId;
|
|
1798
|
+
const agentRef = typeof record.agent_ref === "string" ? record.agent_ref : "agent-unknown";
|
|
1799
|
+
const modelId = typeof record.provider_qualified_model_id === "string" ? record.provider_qualified_model_id : "unknown/unknown";
|
|
1800
|
+
if (laneAlreadyHasTerminalTaskEvidence({ rootDir: input.rootDir, workflowId: input.workflowId, laneId })) {
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
const token = randomBytes(4).toString("hex");
|
|
1804
|
+
const abortedAt = new Date(nowMs).toISOString();
|
|
1805
|
+
const partialObservation = await pollChildSessionCandidate(input.client, childSessionId);
|
|
1806
|
+
if (partialObservation !== null && partialObservation.text.trim().length > 0) {
|
|
1807
|
+
const sanitizedResult = sanitizeFlowDeskTaskResultTextV1(partialObservation.text);
|
|
1808
|
+
const taskResultWritten = writeChildSessionEvidence(input.rootDir, input.workflowId, `task-result-${taskId}-watchdog-partial-${token}`, {
|
|
1809
|
+
schema_version: "flowdesk.task_result.v1",
|
|
1810
|
+
workflow_id: input.workflowId,
|
|
1811
|
+
lane_id: laneId,
|
|
1812
|
+
task_id: taskId,
|
|
1813
|
+
agent_ref: agentRef,
|
|
1814
|
+
provider_qualified_model_id: modelId,
|
|
1815
|
+
task_prompt_sha256: createHash("sha256").update("watchdog-collected").digest("hex"),
|
|
1816
|
+
result_text: sanitizedResult.text,
|
|
1817
|
+
result_text_truncated: sanitizedResult.truncated,
|
|
1818
|
+
result_text_sha256: createHash("sha256").update(partialObservation.text).digest("hex"),
|
|
1819
|
+
completion_status: "partial",
|
|
1820
|
+
output_kind: partialObservation.outputKind,
|
|
1821
|
+
usable_for_synthesis: partialObservation.usableForSynthesis,
|
|
1822
|
+
// Captured partial text is still a usable result, not a contract
|
|
1823
|
+
// failure. The coordinator judges substance from the advisory fields.
|
|
1824
|
+
missing_contract: false,
|
|
1825
|
+
finalization_reason: "timeout_partial",
|
|
1826
|
+
looks_like_refusal_or_error: partialObservation.looksLikeRefusalOrError,
|
|
1827
|
+
created_at: abortedAt,
|
|
1828
|
+
dispatch_authority_enabled: false,
|
|
1829
|
+
});
|
|
1830
|
+
if (taskResultWritten) {
|
|
1831
|
+
writeAgentTaskProgressEvidence({
|
|
1832
|
+
rootDir: input.rootDir,
|
|
1833
|
+
workflowId: input.workflowId,
|
|
1834
|
+
laneId,
|
|
1835
|
+
taskId,
|
|
1836
|
+
agentRef,
|
|
1837
|
+
providerQualifiedModelId: modelId,
|
|
1838
|
+
phase: "finalizing",
|
|
1839
|
+
progressSeq: 20 + nudgeCount,
|
|
1840
|
+
progressLabel: "async agent task partial result captured before abort",
|
|
1841
|
+
observedAt: abortedAt,
|
|
1842
|
+
});
|
|
1843
|
+
refreshFlowDeskCompletionUiCachesV1({
|
|
1844
|
+
rootDir: input.rootDir,
|
|
1845
|
+
workflowId: input.workflowId,
|
|
1846
|
+
observedAt: abortedAt,
|
|
1847
|
+
});
|
|
1848
|
+
result.lanesCompleted++;
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
const failureCategory = totalAgeMs > abortThresholdMs * 2
|
|
1853
|
+
? "network_interrupted"
|
|
1854
|
+
: "sdk_prompt_timeout";
|
|
1855
|
+
writeChildSessionEvidence(input.rootDir, input.workflowId, `task-failed-${taskId}-watchdog-abort-${token}`, {
|
|
1856
|
+
schema_version: "flowdesk.task_failed.v1",
|
|
1857
|
+
workflow_id: input.workflowId,
|
|
1858
|
+
lane_id: laneId,
|
|
1859
|
+
task_id: taskId,
|
|
1860
|
+
agent_ref: agentRef,
|
|
1861
|
+
provider_qualified_model_id: modelId,
|
|
1862
|
+
failure_category: failureCategory,
|
|
1863
|
+
redacted_reason: `watchdog aborted child session after ${Math.round(totalAgeMs / 1000)}s with no response`,
|
|
1864
|
+
created_at: abortedAt,
|
|
1865
|
+
dispatch_authority_enabled: false,
|
|
1866
|
+
});
|
|
1867
|
+
writeAgentTaskProgressEvidence({
|
|
1868
|
+
rootDir: input.rootDir,
|
|
1869
|
+
workflowId: input.workflowId,
|
|
1870
|
+
laneId,
|
|
1871
|
+
taskId,
|
|
1872
|
+
agentRef,
|
|
1873
|
+
providerQualifiedModelId: modelId,
|
|
1874
|
+
phase: "failed",
|
|
1875
|
+
progressSeq: 20 + nudgeCount,
|
|
1876
|
+
progressLabel: "async agent task aborted after no response",
|
|
1877
|
+
observedAt: abortedAt,
|
|
1878
|
+
});
|
|
1879
|
+
refreshFlowDeskCompletionUiCachesV1({
|
|
1880
|
+
rootDir: input.rootDir,
|
|
1881
|
+
workflowId: input.workflowId,
|
|
1882
|
+
observedAt: abortedAt,
|
|
1883
|
+
});
|
|
1884
|
+
result.lanesAborted++;
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
1887
|
+
// 3. Nudge if silence threshold exceeded
|
|
1888
|
+
if (silenceMs >= nudgeQuietPeriodMs && nudgeCount < maxNudges) {
|
|
1889
|
+
await sendWatchdogNudge(input.client, childSessionId);
|
|
1890
|
+
result.lanesNudged++;
|
|
1891
|
+
const taskId = typeof record.task_id === "string" ? record.task_id : laneId;
|
|
1892
|
+
const agentRef = typeof record.agent_ref === "string" ? record.agent_ref : "agent-unknown";
|
|
1893
|
+
const modelId = typeof record.provider_qualified_model_id === "string" ? record.provider_qualified_model_id : "unknown/unknown";
|
|
1894
|
+
const nudgedAt = new Date(nowMs).toISOString();
|
|
1895
|
+
writeAgentTaskProgressEvidence({
|
|
1896
|
+
rootDir: input.rootDir,
|
|
1897
|
+
workflowId: input.workflowId,
|
|
1898
|
+
laneId,
|
|
1899
|
+
taskId,
|
|
1900
|
+
agentRef,
|
|
1901
|
+
providerQualifiedModelId: modelId,
|
|
1902
|
+
phase: "nudged",
|
|
1903
|
+
progressSeq: 2 + nudgeCount,
|
|
1904
|
+
progressLabel: "async agent task nudged after quiet period",
|
|
1905
|
+
observedAt: nudgedAt,
|
|
1906
|
+
});
|
|
1907
|
+
// Update nudge_count in evidence (overwrite record)
|
|
1908
|
+
const evidenceId = reloaded.entries
|
|
1909
|
+
.find(e => e.evidenceClass === "agent_task_child_session" && e.record.lane_id === laneId)
|
|
1910
|
+
?.evidenceId ?? `agent-task-child-session-${laneId}-watchdog`;
|
|
1911
|
+
writeChildSessionEvidence(input.rootDir, input.workflowId, evidenceId, {
|
|
1912
|
+
...record,
|
|
1913
|
+
nudge_count: nudgeCount + 1,
|
|
1914
|
+
last_nudge_at: new Date(nowMs).toISOString(),
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
// Invariant: terminal/failure lanes observed during this cycle MUST cause a
|
|
1919
|
+
// completion UI cache refresh, even when the failed lane has no paired
|
|
1920
|
+
// `agent_task_child_session` record (e.g. reviewer execution bridge wrote
|
|
1921
|
+
// only `lane_lifecycle=invocation_failed`, or `task_failed`/`no_output`
|
|
1922
|
+
// arrived via the event hook without a task_result). Without this defensive
|
|
1923
|
+
// refresh, the `subtask-activity-sidebar` and `auto-next-ready` caches stay
|
|
1924
|
+
// stuck at `progressing_normal/running` until the next backfill pass.
|
|
1925
|
+
if (!uiCacheRefreshed && terminalLaneIds.size > 0) {
|
|
1926
|
+
refreshFlowDeskCompletionUiCachesV1({
|
|
1927
|
+
rootDir: input.rootDir,
|
|
1928
|
+
workflowId: input.workflowId,
|
|
1929
|
+
observedAt: terminalRefreshObservedAt,
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
return result;
|
|
1933
|
+
}
|
|
1257
1934
|
//# sourceMappingURL=stall-recovery.js.map
|