@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.
Files changed (80) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-task-output.d.ts +29 -0
  3. package/dist/agent-task-output.d.ts.map +1 -0
  4. package/dist/agent-task-output.js +225 -0
  5. package/dist/agent-task-output.js.map +1 -0
  6. package/dist/agent-task-runner.d.ts +34 -0
  7. package/dist/agent-task-runner.d.ts.map +1 -1
  8. package/dist/agent-task-runner.js +634 -84
  9. package/dist/agent-task-runner.js.map +1 -1
  10. package/dist/auto-continue-preview-tool.d.ts +36 -0
  11. package/dist/auto-continue-preview-tool.d.ts.map +1 -0
  12. package/dist/auto-continue-preview-tool.js +119 -0
  13. package/dist/auto-continue-preview-tool.js.map +1 -0
  14. package/dist/completion-ui-cache.d.ts +6 -0
  15. package/dist/completion-ui-cache.d.ts.map +1 -0
  16. package/dist/completion-ui-cache.js +390 -0
  17. package/dist/completion-ui-cache.js.map +1 -0
  18. package/dist/event-hook-observer.d.ts +14 -0
  19. package/dist/event-hook-observer.d.ts.map +1 -0
  20. package/dist/event-hook-observer.js +257 -0
  21. package/dist/event-hook-observer.js.map +1 -0
  22. package/dist/managed-dispatch-adapter.d.ts +62 -0
  23. package/dist/managed-dispatch-adapter.d.ts.map +1 -1
  24. package/dist/managed-dispatch-adapter.js +472 -4
  25. package/dist/managed-dispatch-adapter.js.map +1 -1
  26. package/dist/model-selection-engine.d.ts +60 -0
  27. package/dist/model-selection-engine.d.ts.map +1 -0
  28. package/dist/model-selection-engine.js +242 -0
  29. package/dist/model-selection-engine.js.map +1 -0
  30. package/dist/provider-usage-live-tool.d.ts +10 -0
  31. package/dist/provider-usage-live-tool.d.ts.map +1 -1
  32. package/dist/provider-usage-live-tool.js +262 -33
  33. package/dist/provider-usage-live-tool.js.map +1 -1
  34. package/dist/server.d.ts +36 -1
  35. package/dist/server.d.ts.map +1 -1
  36. package/dist/server.js +497 -20
  37. package/dist/server.js.map +1 -1
  38. package/dist/stall-recovery.d.ts +34 -0
  39. package/dist/stall-recovery.d.ts.map +1 -1
  40. package/dist/stall-recovery.js +680 -3
  41. package/dist/stall-recovery.js.map +1 -1
  42. package/dist/status-live-tool.d.ts +54 -0
  43. package/dist/status-live-tool.d.ts.map +1 -1
  44. package/dist/status-live-tool.js +449 -44
  45. package/dist/status-live-tool.js.map +1 -1
  46. package/dist/tui-subtask-activity.d.ts +73 -0
  47. package/dist/tui-subtask-activity.d.ts.map +1 -0
  48. package/dist/tui-subtask-activity.js +271 -0
  49. package/dist/tui-subtask-activity.js.map +1 -0
  50. package/dist/tui-usage-snapshot.d.ts +14 -0
  51. package/dist/tui-usage-snapshot.d.ts.map +1 -1
  52. package/dist/tui-usage-snapshot.js +275 -8
  53. package/dist/tui-usage-snapshot.js.map +1 -1
  54. package/dist/tui.d.ts.map +1 -1
  55. package/dist/tui.js +102 -44
  56. package/dist/tui.js.map +1 -1
  57. package/dist/workflow-assign-tool.d.ts +23 -0
  58. package/dist/workflow-assign-tool.d.ts.map +1 -0
  59. package/dist/workflow-assign-tool.js +135 -0
  60. package/dist/workflow-assign-tool.js.map +1 -0
  61. package/dist/workflow-author-tool.d.ts +29 -0
  62. package/dist/workflow-author-tool.d.ts.map +1 -0
  63. package/dist/workflow-author-tool.js +227 -0
  64. package/dist/workflow-author-tool.js.map +1 -0
  65. package/dist/workflow-dispatch-tool.d.ts +12 -0
  66. package/dist/workflow-dispatch-tool.d.ts.map +1 -1
  67. package/dist/workflow-dispatch-tool.js +31 -3
  68. package/dist/workflow-dispatch-tool.js.map +1 -1
  69. package/dist/workflow-orchestrator.d.ts +31 -0
  70. package/dist/workflow-orchestrator.d.ts.map +1 -0
  71. package/dist/workflow-orchestrator.js +160 -0
  72. package/dist/workflow-orchestrator.js.map +1 -0
  73. package/dist/workflow-scheduler.d.ts.map +1 -1
  74. package/dist/workflow-scheduler.js +3 -1
  75. package/dist/workflow-scheduler.js.map +1 -1
  76. package/dist/workflow-synthesis-tool.d.ts +31 -0
  77. package/dist/workflow-synthesis-tool.d.ts.map +1 -0
  78. package/dist/workflow-synthesis-tool.js +194 -0
  79. package/dist/workflow-synthesis-tool.js.map +1 -0
  80. package/package.json +2 -2
@@ -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