@flowdesk/opencode-plugin 0.1.12 → 0.1.14
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 +17 -0
- package/dist/agent-task-output.d.ts.map +1 -0
- package/dist/agent-task-output.js +119 -0
- package/dist/agent-task-output.js.map +1 -0
- package/dist/agent-task-runner.d.ts +24 -0
- package/dist/agent-task-runner.d.ts.map +1 -1
- package/dist/agent-task-runner.js +536 -61
- 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 +260 -0
- package/dist/completion-ui-cache.js.map +1 -0
- package/dist/controlled-write-tool.d.ts +49 -0
- package/dist/controlled-write-tool.d.ts.map +1 -0
- package/dist/controlled-write-tool.js +296 -0
- package/dist/controlled-write-tool.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 +193 -0
- package/dist/event-hook-observer.js.map +1 -0
- package/dist/managed-dispatch-adapter.d.ts +1 -0
- package/dist/managed-dispatch-adapter.d.ts.map +1 -1
- package/dist/managed-dispatch-adapter.js +100 -29
- package/dist/managed-dispatch-adapter.js.map +1 -1
- package/dist/model-selection-engine.d.ts +47 -0
- package/dist/model-selection-engine.d.ts.map +1 -0
- package/dist/model-selection-engine.js +175 -0
- package/dist/model-selection-engine.js.map +1 -0
- package/dist/provider-usage-live-tool.d.ts +27 -0
- package/dist/provider-usage-live-tool.d.ts.map +1 -1
- package/dist/provider-usage-live-tool.js +443 -4
- package/dist/provider-usage-live-tool.js.map +1 -1
- package/dist/quick-reviewer-run.d.ts +3 -0
- package/dist/quick-reviewer-run.d.ts.map +1 -1
- package/dist/quick-reviewer-run.js +20 -8
- package/dist/quick-reviewer-run.js.map +1 -1
- package/dist/runtime-reviewer-execution-bridge.d.ts +21 -0
- package/dist/runtime-reviewer-execution-bridge.d.ts.map +1 -1
- package/dist/runtime-reviewer-execution-bridge.js +238 -0
- package/dist/runtime-reviewer-execution-bridge.js.map +1 -1
- package/dist/server.d.ts +60 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +800 -41
- package/dist/server.js.map +1 -1
- package/dist/stall-recovery.d.ts +60 -0
- package/dist/stall-recovery.d.ts.map +1 -1
- package/dist/stall-recovery.js +763 -11
- package/dist/stall-recovery.js.map +1 -1
- package/dist/status-live-tool.d.ts +81 -0
- package/dist/status-live-tool.d.ts.map +1 -1
- package/dist/status-live-tool.js +620 -38
- package/dist/status-live-tool.js.map +1 -1
- package/dist/tui-subtask-activity.d.ts +69 -0
- package/dist/tui-subtask-activity.d.ts.map +1 -0
- package/dist/tui-subtask-activity.js +266 -0
- package/dist/tui-subtask-activity.js.map +1 -0
- package/dist/tui-usage-snapshot.d.ts +44 -0
- package/dist/tui-usage-snapshot.d.ts.map +1 -0
- package/dist/tui-usage-snapshot.js +397 -0
- package/dist/tui-usage-snapshot.js.map +1 -0
- package/dist/tui.d.ts +7 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +134 -0
- package/dist/tui.js.map +1 -0
- 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 +117 -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-plan-tool.d.ts +47 -0
- package/dist/workflow-dispatch-plan-tool.d.ts.map +1 -0
- package/dist/workflow-dispatch-plan-tool.js +251 -0
- package/dist/workflow-dispatch-plan-tool.js.map +1 -0
- package/dist/workflow-dispatch-tool.d.ts +56 -0
- package/dist/workflow-dispatch-tool.d.ts.map +1 -0
- package/dist/workflow-dispatch-tool.js +306 -0
- package/dist/workflow-dispatch-tool.js.map +1 -0
- 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 +19 -0
- package/dist/workflow-scheduler.d.ts.map +1 -0
- package/dist/workflow-scheduler.js +45 -0
- package/dist/workflow-scheduler.js.map +1 -0
- 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 +10 -2
package/dist/stall-recovery.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { applyFlowDeskSessionEvidenceWriteIntentsV1, prepareFlowDeskSessionEvidenceWriteIntentV1, reloadFlowDeskSessionEvidenceV1, projectFlowDeskLaneStallV1, } from "@flowdesk/core";
|
|
2
|
-
import { createHmac, createHash, timingSafeEqual } from "node:crypto";
|
|
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, 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";
|
|
7
10
|
export async function checkSdkSessionApiHealthV1(client, sessionId, timeouts = {}) {
|
|
8
11
|
if (typeof client.session.messages !== "function") {
|
|
9
12
|
return { status: "unknown", reason: "sdk_messages_not_available" };
|
|
@@ -423,6 +426,15 @@ export function evaluateGuardedAutoAbortHookV1(input) {
|
|
|
423
426
|
function isReviewerLaneContext(value) {
|
|
424
427
|
return isRecord(value) && value.schema_version === "flowdesk.reviewer_lane_context.v1";
|
|
425
428
|
}
|
|
429
|
+
function isAgentTaskContext(value) {
|
|
430
|
+
return isRecord(value) && value.schema_version === "flowdesk.agent_task_context.v1";
|
|
431
|
+
}
|
|
432
|
+
function isTaskFailed(value) {
|
|
433
|
+
return isRecord(value) && value.schema_version === "flowdesk.task_failed.v1";
|
|
434
|
+
}
|
|
435
|
+
function isTaskResult(value) {
|
|
436
|
+
return isRecord(value) && value.schema_version === "flowdesk.task_result.v1";
|
|
437
|
+
}
|
|
426
438
|
function isPendingRetryPlan(value) {
|
|
427
439
|
return isRecord(value) && value.schema_version === "flowdesk.pending_retry_plan.v1";
|
|
428
440
|
}
|
|
@@ -432,6 +444,152 @@ function isRetryExecuted(value) {
|
|
|
432
444
|
function isRetryFailed(value) {
|
|
433
445
|
return isRecord(value) && value.schema_version === "flowdesk.retry_failed.v1";
|
|
434
446
|
}
|
|
447
|
+
function latestLifecycleByLane(records) {
|
|
448
|
+
const byLane = new Map();
|
|
449
|
+
for (const record of records) {
|
|
450
|
+
const existing = byLane.get(record.lane_id);
|
|
451
|
+
if (existing === undefined ||
|
|
452
|
+
Date.parse(record.updated_at) > Date.parse(existing.updated_at) ||
|
|
453
|
+
(Date.parse(record.updated_at) === Date.parse(existing.updated_at) && record.state > existing.state)) {
|
|
454
|
+
byLane.set(record.lane_id, record);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return byLane;
|
|
458
|
+
}
|
|
459
|
+
function latestTaskFailedByLane(records) {
|
|
460
|
+
const byLane = new Map();
|
|
461
|
+
for (const record of records) {
|
|
462
|
+
const existing = byLane.get(record.lane_id);
|
|
463
|
+
if (existing === undefined || Date.parse(record.created_at) >= Date.parse(existing.created_at)) {
|
|
464
|
+
byLane.set(record.lane_id, record);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return byLane;
|
|
468
|
+
}
|
|
469
|
+
function latestTaskResultByLane(records) {
|
|
470
|
+
const byLane = new Map();
|
|
471
|
+
for (const record of records) {
|
|
472
|
+
const existing = byLane.get(record.lane_id);
|
|
473
|
+
if (existing === undefined || Date.parse(record.created_at) >= Date.parse(existing.created_at)) {
|
|
474
|
+
byLane.set(record.lane_id, record);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return byLane;
|
|
478
|
+
}
|
|
479
|
+
function agentTaskContextByLane(records) {
|
|
480
|
+
const byLane = new Map();
|
|
481
|
+
for (const record of records)
|
|
482
|
+
byLane.set(record.lane_id, record);
|
|
483
|
+
return byLane;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Safe local cleanup/backfill for legacy `flowdesk_agent_task_run` lanes.
|
|
487
|
+
*
|
|
488
|
+
* Older agent-task results/failures could persist `task_result.v1` or
|
|
489
|
+
* `task_failed.v1` while the latest `lane_lifecycle` remained
|
|
490
|
+
* `created`/`running`, which made status cards keep reporting stale active
|
|
491
|
+
* lanes. This helper only writes terminal lifecycle evidence when existing
|
|
492
|
+
* durable evidence already proves the agent-task ended.
|
|
493
|
+
* It never launches, aborts, retries, enables dispatch authority, or rewrites
|
|
494
|
+
* existing evidence.
|
|
495
|
+
*/
|
|
496
|
+
export function backfillTerminalAgentTaskFailedLanesV1(input) {
|
|
497
|
+
const reloaded = reloadFlowDeskSessionEvidenceV1({ rootDir: input.rootDir, workflowId: input.workflowId });
|
|
498
|
+
if (!reloaded.ok)
|
|
499
|
+
return { status: "backfill_skipped", workflowId: input.workflowId, reason: "session_evidence_reload_failed" };
|
|
500
|
+
const lifecycles = [];
|
|
501
|
+
const taskResults = [];
|
|
502
|
+
const taskFailures = [];
|
|
503
|
+
const contexts = [];
|
|
504
|
+
for (const entry of reloaded.entries) {
|
|
505
|
+
if (isLaneLifecycleRecord(entry.record))
|
|
506
|
+
lifecycles.push(entry.record);
|
|
507
|
+
else if (isTaskResult(entry.record))
|
|
508
|
+
taskResults.push(entry.record);
|
|
509
|
+
else if (isTaskFailed(entry.record))
|
|
510
|
+
taskFailures.push(entry.record);
|
|
511
|
+
else if (isAgentTaskContext(entry.record))
|
|
512
|
+
contexts.push(entry.record);
|
|
513
|
+
}
|
|
514
|
+
const latestLifecycle = latestLifecycleByLane(lifecycles);
|
|
515
|
+
const latestResult = latestTaskResultByLane(taskResults);
|
|
516
|
+
const latestFailure = latestTaskFailedByLane(taskFailures);
|
|
517
|
+
const contextByLane = agentTaskContextByLane(contexts);
|
|
518
|
+
const observedAt = (input.now ?? new Date()).toISOString();
|
|
519
|
+
const token = timestampToken(input.now ?? new Date());
|
|
520
|
+
const terminalEvidenceIds = [];
|
|
521
|
+
let lanesScanned = 0;
|
|
522
|
+
for (const [laneId, result] of latestResult) {
|
|
523
|
+
const latest = latestLifecycle.get(laneId);
|
|
524
|
+
if (latest === undefined)
|
|
525
|
+
continue;
|
|
526
|
+
lanesScanned++;
|
|
527
|
+
if (!ABORT_ELIGIBLE_LANE_STATES.has(latest.state))
|
|
528
|
+
continue;
|
|
529
|
+
const context = contextByLane.get(laneId);
|
|
530
|
+
const evidenceId = `lifecycle-agent-task-terminal-backfill-${safeToken(laneId)}-${token}`;
|
|
531
|
+
const terminalRecord = {
|
|
532
|
+
...latest,
|
|
533
|
+
workflow_id: result.workflow_id,
|
|
534
|
+
lane_id: laneId,
|
|
535
|
+
agent_ref: context?.agent_ref ?? result.agent_ref ?? latest.agent_ref,
|
|
536
|
+
provider_qualified_model_id: context?.provider_qualified_model_id ?? result.provider_qualified_model_id ?? latest.provider_qualified_model_id,
|
|
537
|
+
parent_session_ref: context?.parent_session_ref ?? latest.parent_session_ref,
|
|
538
|
+
state: "incomplete",
|
|
539
|
+
verdict_ref: undefined,
|
|
540
|
+
output_ref: `output-${safeToken(result.task_id)}`,
|
|
541
|
+
updated_at: observedAt,
|
|
542
|
+
spawned_by: latest.spawned_by ?? "flowdesk",
|
|
543
|
+
dispatch_authority_enabled: false,
|
|
544
|
+
providerCall: false,
|
|
545
|
+
actualLaneLaunch: false,
|
|
546
|
+
runtimeExecution: false,
|
|
547
|
+
};
|
|
548
|
+
if (writeEvidence({ rootDir: input.rootDir, workflowId: input.workflowId, evidenceId, record: terminalRecord })) {
|
|
549
|
+
terminalEvidenceIds.push(evidenceId);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
for (const [laneId, failed] of latestFailure) {
|
|
553
|
+
if (latestResult.has(laneId))
|
|
554
|
+
continue;
|
|
555
|
+
const latest = latestLifecycle.get(laneId);
|
|
556
|
+
if (latest === undefined)
|
|
557
|
+
continue;
|
|
558
|
+
lanesScanned++;
|
|
559
|
+
if (!ABORT_ELIGIBLE_LANE_STATES.has(latest.state))
|
|
560
|
+
continue;
|
|
561
|
+
const context = contextByLane.get(laneId);
|
|
562
|
+
const state = failed.failure_category === "no_response" ? "no_output" : "invocation_failed";
|
|
563
|
+
const evidenceId = `lifecycle-agent-task-terminal-backfill-${safeToken(laneId)}-${token}`;
|
|
564
|
+
const terminalRecord = {
|
|
565
|
+
...latest,
|
|
566
|
+
workflow_id: failed.workflow_id,
|
|
567
|
+
lane_id: laneId,
|
|
568
|
+
agent_ref: context?.agent_ref ?? failed.agent_ref ?? latest.agent_ref,
|
|
569
|
+
provider_qualified_model_id: context?.provider_qualified_model_id ?? failed.provider_qualified_model_id ?? latest.provider_qualified_model_id,
|
|
570
|
+
parent_session_ref: context?.parent_session_ref ?? latest.parent_session_ref,
|
|
571
|
+
state,
|
|
572
|
+
verdict_ref: undefined,
|
|
573
|
+
output_ref: undefined,
|
|
574
|
+
updated_at: observedAt,
|
|
575
|
+
spawned_by: latest.spawned_by ?? "flowdesk",
|
|
576
|
+
dispatch_authority_enabled: false,
|
|
577
|
+
providerCall: false,
|
|
578
|
+
actualLaneLaunch: false,
|
|
579
|
+
runtimeExecution: false,
|
|
580
|
+
};
|
|
581
|
+
if (writeEvidence({ rootDir: input.rootDir, workflowId: input.workflowId, evidenceId, record: terminalRecord })) {
|
|
582
|
+
terminalEvidenceIds.push(evidenceId);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return {
|
|
586
|
+
status: "backfill_completed",
|
|
587
|
+
workflowId: input.workflowId,
|
|
588
|
+
lanesScanned,
|
|
589
|
+
lanesTerminalized: terminalEvidenceIds.length,
|
|
590
|
+
terminalLifecycleEvidenceIds: terminalEvidenceIds,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
435
593
|
/**
|
|
436
594
|
* On startup/reload: any `pending_retry_plan` in `launched` state with no matching
|
|
437
595
|
* `retry_executed` or `retry_failed` within 10 minutes of `created_at` is reconciled
|
|
@@ -522,6 +680,19 @@ function buildRetryLaunchPlanFromContextV1(context, newLaneId, parentSessionId)
|
|
|
522
680
|
runtimeExecution: false,
|
|
523
681
|
};
|
|
524
682
|
}
|
|
683
|
+
function parentSessionIdFromRef(parentSessionRef) {
|
|
684
|
+
return parentSessionRef.startsWith("ses-") && parentSessionRef.length > "ses-".length
|
|
685
|
+
? parentSessionRef.slice("ses-".length)
|
|
686
|
+
: undefined;
|
|
687
|
+
}
|
|
688
|
+
function writeRetryFailedV1(input) {
|
|
689
|
+
writeEvidence({
|
|
690
|
+
rootDir: input.rootDir,
|
|
691
|
+
workflowId: input.workflowId,
|
|
692
|
+
evidenceId: input.evidenceId,
|
|
693
|
+
record: input.record,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
525
696
|
/**
|
|
526
697
|
* Evaluate guarded auto-retry hook following the execution order from the design doc exactly.
|
|
527
698
|
* Called after `evaluateGuardedAutoAbortHookV1` returns `auto_abort_executed`.
|
|
@@ -572,7 +743,8 @@ export async function evaluateGuardedAutoRetryHookV1(input) {
|
|
|
572
743
|
redactedReason: "sdk_client_missing_or_session_create_unavailable",
|
|
573
744
|
};
|
|
574
745
|
}
|
|
575
|
-
// Step 4: Load
|
|
746
|
+
// Step 4: Load retry context for laneId. Reviewer context remains the
|
|
747
|
+
// preferred P7 path; generic agent tasks fall back to agent_task_context.v1.
|
|
576
748
|
const reloaded = reloadFlowDeskSessionEvidenceV1({
|
|
577
749
|
rootDir: input.rootDir,
|
|
578
750
|
workflowId: input.workflowId,
|
|
@@ -580,18 +752,31 @@ export async function evaluateGuardedAutoRetryHookV1(input) {
|
|
|
580
752
|
if (!reloaded.ok) {
|
|
581
753
|
return { status: "auto_retry_disabled", reason: "context_missing" };
|
|
582
754
|
}
|
|
583
|
-
const
|
|
755
|
+
const reviewerContextEntry = reloaded.entries.find((e) => isReviewerLaneContext(e.record) && e.record.lane_id === input.laneId);
|
|
756
|
+
const agentTaskContextEntry = reviewerContextEntry === undefined
|
|
757
|
+
? reloaded.entries.find((e) => isAgentTaskContext(e.record) && e.record.lane_id === input.laneId)
|
|
758
|
+
: undefined;
|
|
759
|
+
const contextEntry = reviewerContextEntry ?? agentTaskContextEntry;
|
|
584
760
|
if (contextEntry === undefined) {
|
|
585
761
|
return { status: "auto_retry_disabled", reason: "context_missing" };
|
|
586
762
|
}
|
|
587
|
-
const
|
|
763
|
+
const isAgentTaskRetry = reviewerContextEntry === undefined;
|
|
764
|
+
const reviewerContext = isAgentTaskRetry ? undefined : contextEntry.record;
|
|
765
|
+
const agentTaskContext = isAgentTaskRetry ? contextEntry.record : undefined;
|
|
766
|
+
const context = reviewerContext ?? agentTaskContext;
|
|
588
767
|
// Step 5: Verify context.redaction_version present
|
|
589
768
|
if (!context.redaction_version || typeof context.redaction_version !== "string" || context.redaction_version.trim().length === 0) {
|
|
590
769
|
return { status: "auto_retry_disabled", reason: "context_redaction_invalid" };
|
|
591
770
|
}
|
|
592
|
-
// Step 6: Verify context.workflow_id === workflowId
|
|
771
|
+
// Step 6: Verify context.workflow_id === workflowId and context-specific invariants.
|
|
593
772
|
const VALID_PERSPECTIVES = new Set(["policy_security", "architecture", "verification_implementation"]);
|
|
594
|
-
if (context.workflow_id !== input.workflowId
|
|
773
|
+
if (context.workflow_id !== input.workflowId) {
|
|
774
|
+
return { status: "auto_retry_disabled", reason: "invariant_violated" };
|
|
775
|
+
}
|
|
776
|
+
if (reviewerContext !== undefined && !VALID_PERSPECTIVES.has(reviewerContext.perspective)) {
|
|
777
|
+
return { status: "auto_retry_disabled", reason: "invariant_violated" };
|
|
778
|
+
}
|
|
779
|
+
if (agentTaskContext !== undefined && agentTaskContext.prompt_text_truncated === true) {
|
|
595
780
|
return { status: "auto_retry_disabled", reason: "invariant_violated" };
|
|
596
781
|
}
|
|
597
782
|
// Step 7: Count cap — retry_executed + retry_failed + pending_retry_plan(pending|launched) for laneId
|
|
@@ -674,8 +859,116 @@ export async function evaluateGuardedAutoRetryHookV1(input) {
|
|
|
674
859
|
redactedReason: "pending_retry_plan_write_failed",
|
|
675
860
|
};
|
|
676
861
|
}
|
|
862
|
+
if (agentTaskContext !== undefined) {
|
|
863
|
+
const retryParentSessionId = parentSessionIdFromRef(agentTaskContext.parent_session_ref);
|
|
864
|
+
if (retryParentSessionId === undefined) {
|
|
865
|
+
const failedId = `retry-failed-agent-task-parent-${safeToken(newLaneId)}-${retryToken}`;
|
|
866
|
+
writeRetryFailedV1({
|
|
867
|
+
rootDir: input.rootDir,
|
|
868
|
+
workflowId: input.workflowId,
|
|
869
|
+
evidenceId: failedId,
|
|
870
|
+
record: {
|
|
871
|
+
schema_version: "flowdesk.retry_failed.v1",
|
|
872
|
+
workflow_id: input.workflowId,
|
|
873
|
+
original_lane_id: input.laneId,
|
|
874
|
+
new_lane_id: newLaneId,
|
|
875
|
+
retry_attempt: retryAttempt,
|
|
876
|
+
failure_category: "invariant_violated",
|
|
877
|
+
redacted_reason: "agent_task_context_parent_session_ref_invalid",
|
|
878
|
+
created_at: now.toISOString(),
|
|
879
|
+
dispatch_authority_enabled: false,
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
writeEvidence({
|
|
883
|
+
rootDir: input.rootDir,
|
|
884
|
+
workflowId: input.workflowId,
|
|
885
|
+
evidenceId: `${pendingRetryEvidenceId}-failed`,
|
|
886
|
+
record: { ...pendingRecord, status: "failed" },
|
|
887
|
+
});
|
|
888
|
+
return {
|
|
889
|
+
status: "retry_failed",
|
|
890
|
+
failureCategory: "invariant_violated",
|
|
891
|
+
redactedReason: "agent_task_context_parent_session_ref_invalid",
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
const taskResult = await executeFlowDeskAgentTaskV1({
|
|
895
|
+
workflowId: input.workflowId,
|
|
896
|
+
taskId: agentTaskContext.task_id,
|
|
897
|
+
laneId: newLaneId,
|
|
898
|
+
agentRef: agentTaskContext.agent_ref,
|
|
899
|
+
providerQualifiedModelId: agentTaskContext.provider_qualified_model_id,
|
|
900
|
+
promptText: agentTaskContext.prompt_text,
|
|
901
|
+
parentSessionId: retryParentSessionId,
|
|
902
|
+
rootDir: input.rootDir,
|
|
903
|
+
client: input.client,
|
|
904
|
+
timeoutMs: timeoutMs,
|
|
905
|
+
_nudgeQuietPeriodMs: input._nudgeQuietPeriodMs,
|
|
906
|
+
_messagesTimeoutMs: input._messagesTimeoutMs,
|
|
907
|
+
});
|
|
908
|
+
if (taskResult.status === "task_failed") {
|
|
909
|
+
const failedId = `retry-failed-agent-task-${safeToken(newLaneId)}-${retryToken}`;
|
|
910
|
+
writeRetryFailedV1({
|
|
911
|
+
rootDir: input.rootDir,
|
|
912
|
+
workflowId: input.workflowId,
|
|
913
|
+
evidenceId: failedId,
|
|
914
|
+
record: {
|
|
915
|
+
schema_version: "flowdesk.retry_failed.v1",
|
|
916
|
+
workflow_id: input.workflowId,
|
|
917
|
+
original_lane_id: input.laneId,
|
|
918
|
+
new_lane_id: newLaneId,
|
|
919
|
+
retry_attempt: retryAttempt,
|
|
920
|
+
failure_category: taskResult.failureCategory === "no_response" ? "indeterminate_launch" : "sdk_create_failed",
|
|
921
|
+
redacted_reason: `agent_task_retry_${taskResult.failureCategory}`,
|
|
922
|
+
created_at: now.toISOString(),
|
|
923
|
+
dispatch_authority_enabled: false,
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
writeEvidence({
|
|
927
|
+
rootDir: input.rootDir,
|
|
928
|
+
workflowId: input.workflowId,
|
|
929
|
+
evidenceId: `${pendingRetryEvidenceId}-failed`,
|
|
930
|
+
record: { ...pendingRecord, status: "failed" },
|
|
931
|
+
});
|
|
932
|
+
return {
|
|
933
|
+
status: "retry_failed",
|
|
934
|
+
failureCategory: taskResult.failureCategory,
|
|
935
|
+
redactedReason: taskResult.redactedReason,
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
const executedId = `retry-executed-${safeToken(newLaneId)}-${retryToken}`;
|
|
939
|
+
const executedRecord = {
|
|
940
|
+
schema_version: "flowdesk.retry_executed.v1",
|
|
941
|
+
workflow_id: input.workflowId,
|
|
942
|
+
original_lane_id: input.laneId,
|
|
943
|
+
new_lane_id: newLaneId,
|
|
944
|
+
retry_attempt: retryAttempt,
|
|
945
|
+
retry_kind: "agent_task",
|
|
946
|
+
task_id: agentTaskContext.task_id,
|
|
947
|
+
provider_qualified_model_id: agentTaskContext.provider_qualified_model_id,
|
|
948
|
+
new_parent_session_ref: agentTaskContext.parent_session_ref,
|
|
949
|
+
created_at: now.toISOString(),
|
|
950
|
+
dispatch_authority_enabled: false,
|
|
951
|
+
};
|
|
952
|
+
writeEvidence({
|
|
953
|
+
rootDir: input.rootDir,
|
|
954
|
+
workflowId: input.workflowId,
|
|
955
|
+
evidenceId: executedId,
|
|
956
|
+
record: executedRecord,
|
|
957
|
+
});
|
|
958
|
+
writeEvidence({
|
|
959
|
+
rootDir: input.rootDir,
|
|
960
|
+
workflowId: input.workflowId,
|
|
961
|
+
evidenceId: `${pendingRetryEvidenceId}-launched`,
|
|
962
|
+
record: { ...pendingRecord, status: "launched" },
|
|
963
|
+
});
|
|
964
|
+
return {
|
|
965
|
+
status: "retry_launched",
|
|
966
|
+
newLaneId,
|
|
967
|
+
pendingRetryEvidenceId,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
677
970
|
// Step 12: Call launchFlowDeskInjectedSdkRuntimeLaneFromPlanV1
|
|
678
|
-
const launchPlan = buildRetryLaunchPlanFromContextV1(
|
|
971
|
+
const launchPlan = buildRetryLaunchPlanFromContextV1(reviewerContext, newLaneId, input.parentSessionId);
|
|
679
972
|
let launchResult;
|
|
680
973
|
try {
|
|
681
974
|
const launchPromise = launchFlowDeskInjectedSdkRuntimeLaneFromPlanV1({
|
|
@@ -684,7 +977,7 @@ export async function evaluateGuardedAutoRetryHookV1(input) {
|
|
|
684
977
|
request: {
|
|
685
978
|
allowActualLaneLaunch: true,
|
|
686
979
|
parentSessionId: input.parentSessionId,
|
|
687
|
-
promptText:
|
|
980
|
+
promptText: reviewerContext.prompt_text,
|
|
688
981
|
dispatchMethod: "prompt",
|
|
689
982
|
},
|
|
690
983
|
});
|
|
@@ -771,10 +1064,11 @@ export async function evaluateGuardedAutoRetryHookV1(input) {
|
|
|
771
1064
|
original_lane_id: input.laneId,
|
|
772
1065
|
new_lane_id: newLaneId,
|
|
773
1066
|
retry_attempt: retryAttempt,
|
|
774
|
-
|
|
775
|
-
|
|
1067
|
+
retry_kind: "reviewer_lane",
|
|
1068
|
+
perspective: reviewerContext.perspective,
|
|
1069
|
+
provider_qualified_model_id: reviewerContext.provider_qualified_model_id,
|
|
776
1070
|
new_parent_session_ref: newParentSessionRef,
|
|
777
|
-
original_attempt_id:
|
|
1071
|
+
original_attempt_id: reviewerContext.original_attempt_id,
|
|
778
1072
|
created_at: now.toISOString(),
|
|
779
1073
|
dispatch_authority_enabled: false,
|
|
780
1074
|
};
|
|
@@ -879,6 +1173,23 @@ export async function runFlowDeskWatchdogCycleV1(input) {
|
|
|
879
1173
|
const workflowIds = listWatchdogWorkflowIds(input.rootDir);
|
|
880
1174
|
const now = input.now ?? new Date();
|
|
881
1175
|
for (const workflowId of workflowIds) {
|
|
1176
|
+
// Monitor async-mode child sessions (nudge + abort + result collection)
|
|
1177
|
+
if (input.client !== undefined) {
|
|
1178
|
+
try {
|
|
1179
|
+
await monitorChildSessionsV1({
|
|
1180
|
+
rootDir: input.rootDir,
|
|
1181
|
+
workflowId,
|
|
1182
|
+
client: input.client,
|
|
1183
|
+
now,
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
catch { /* best-effort, must not crash watchdog */ }
|
|
1187
|
+
}
|
|
1188
|
+
backfillTerminalAgentTaskFailedLanesV1({
|
|
1189
|
+
rootDir: input.rootDir,
|
|
1190
|
+
workflowId,
|
|
1191
|
+
now,
|
|
1192
|
+
});
|
|
882
1193
|
// Reload evidence for this workflow
|
|
883
1194
|
const reloaded = reloadFlowDeskSessionEvidenceV1({
|
|
884
1195
|
rootDir: input.rootDir,
|
|
@@ -923,6 +1234,8 @@ export async function runFlowDeskWatchdogCycleV1(input) {
|
|
|
923
1234
|
client: input.client,
|
|
924
1235
|
parentSessionId: input.parentSessionId,
|
|
925
1236
|
now,
|
|
1237
|
+
_nudgeQuietPeriodMs: input._nudgeQuietPeriodMs,
|
|
1238
|
+
_messagesTimeoutMs: input._messagesTimeoutMs,
|
|
926
1239
|
});
|
|
927
1240
|
if (retryResult.status === "retry_launched") {
|
|
928
1241
|
lanesRetried++;
|
|
@@ -959,4 +1272,443 @@ export async function runFlowDeskWatchdogCycleV1(input) {
|
|
|
959
1272
|
lanesFailed,
|
|
960
1273
|
};
|
|
961
1274
|
}
|
|
1275
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1276
|
+
// Child session monitor (async-mode lanes)
|
|
1277
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1278
|
+
const AGENT_TASK_NUDGE_TEXT_WATCHDOG = "Please provide your final answer now. If you have completed your analysis, output your complete response.";
|
|
1279
|
+
/** Poll result from one session.messages call */
|
|
1280
|
+
function monitorRecord(value) {
|
|
1281
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
1282
|
+
? value
|
|
1283
|
+
: undefined;
|
|
1284
|
+
}
|
|
1285
|
+
function monitorResponseData(value) {
|
|
1286
|
+
const record = monitorRecord(value);
|
|
1287
|
+
return record !== undefined && "data" in record ? record.data : value;
|
|
1288
|
+
}
|
|
1289
|
+
function monitorSdkErrorResponse(value) {
|
|
1290
|
+
const record = monitorRecord(value);
|
|
1291
|
+
const data = monitorRecord(monitorResponseData(value));
|
|
1292
|
+
return record?.error !== undefined || data?.error !== undefined;
|
|
1293
|
+
}
|
|
1294
|
+
async function pollChildSessionOutput(client, childSessionId, messagesTimeoutMs = 3_000) {
|
|
1295
|
+
const messages = client.session.messages;
|
|
1296
|
+
if (typeof messages !== "function")
|
|
1297
|
+
return null;
|
|
1298
|
+
try {
|
|
1299
|
+
const readMessages = async () => {
|
|
1300
|
+
const current = await messages.call(client.session, {
|
|
1301
|
+
sessionID: childSessionId,
|
|
1302
|
+
});
|
|
1303
|
+
if (!monitorSdkErrorResponse(current))
|
|
1304
|
+
return current;
|
|
1305
|
+
return messages.call(client.session, {
|
|
1306
|
+
path: { id: childSessionId },
|
|
1307
|
+
});
|
|
1308
|
+
};
|
|
1309
|
+
const raw = await Promise.race([
|
|
1310
|
+
readMessages(),
|
|
1311
|
+
new Promise(resolve => setTimeout(() => resolve(null), messagesTimeoutMs)),
|
|
1312
|
+
]);
|
|
1313
|
+
if (raw === null)
|
|
1314
|
+
return null;
|
|
1315
|
+
const observed = observeFlowDeskAgentTaskOutputV1(raw);
|
|
1316
|
+
if (observed.terminalObserved && observed.latestText !== undefined && observed.latestText.trim().length > 0)
|
|
1317
|
+
return { text: observed.latestText, completionStatus: "final", outputKind: observed.outputKind, usableForSynthesis: observed.usableForSynthesis };
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
catch {
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
async function pollChildSessionCandidate(client, childSessionId, messagesTimeoutMs = 3_000) {
|
|
1325
|
+
const messages = client.session.messages;
|
|
1326
|
+
if (typeof messages !== "function")
|
|
1327
|
+
return null;
|
|
1328
|
+
try {
|
|
1329
|
+
const readMessages = async () => {
|
|
1330
|
+
const current = await messages.call(client.session, { sessionID: childSessionId });
|
|
1331
|
+
if (!monitorSdkErrorResponse(current))
|
|
1332
|
+
return current;
|
|
1333
|
+
return messages.call(client.session, { path: { id: childSessionId } });
|
|
1334
|
+
};
|
|
1335
|
+
const raw = await Promise.race([
|
|
1336
|
+
readMessages(),
|
|
1337
|
+
new Promise(resolve => setTimeout(() => resolve(null), messagesTimeoutMs)),
|
|
1338
|
+
]);
|
|
1339
|
+
if (raw === null)
|
|
1340
|
+
return null;
|
|
1341
|
+
const observed = observeFlowDeskAgentTaskOutputV1(raw);
|
|
1342
|
+
if (observed.latestText !== undefined && observed.latestText.trim().length > 0)
|
|
1343
|
+
return { text: observed.latestText, completionStatus: observed.terminalObserved ? "final" : "partial", outputKind: observed.outputKind, usableForSynthesis: observed.usableForSynthesis };
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
catch {
|
|
1347
|
+
return null;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
/** Send a noReply nudge to a child session — best effort with hard timeout */
|
|
1351
|
+
async function sendWatchdogNudge(client, childSessionId, timeoutMs = 5_000) {
|
|
1352
|
+
const promptFn = client.session.promptAsync ?? client.session.prompt;
|
|
1353
|
+
if (promptFn === undefined)
|
|
1354
|
+
return "skipped";
|
|
1355
|
+
try {
|
|
1356
|
+
await Promise.race([
|
|
1357
|
+
promptFn.call(client.session, {
|
|
1358
|
+
sessionID: childSessionId,
|
|
1359
|
+
noReply: true,
|
|
1360
|
+
parts: [{ type: "text", text: AGENT_TASK_NUDGE_TEXT_WATCHDOG }],
|
|
1361
|
+
}),
|
|
1362
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("nudge timeout")), timeoutMs)),
|
|
1363
|
+
]);
|
|
1364
|
+
return "sent";
|
|
1365
|
+
}
|
|
1366
|
+
catch {
|
|
1367
|
+
return "timeout";
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
/** Abort a child session via the injected SDK client */
|
|
1371
|
+
async function abortChildSession(client, childSessionId) {
|
|
1372
|
+
const abort = client.session.abort;
|
|
1373
|
+
if (typeof abort !== "function")
|
|
1374
|
+
return;
|
|
1375
|
+
try {
|
|
1376
|
+
await abort.call(client.session, {
|
|
1377
|
+
path: { id: childSessionId },
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
catch { /* best-effort */ }
|
|
1381
|
+
}
|
|
1382
|
+
function writeChildSessionEvidence(rootDir, workflowId, evidenceId, record) {
|
|
1383
|
+
const prepared = prepareFlowDeskSessionEvidenceWriteIntentV1({ workflowId, evidenceId, record });
|
|
1384
|
+
if (!prepared.ok || prepared.writeIntent === undefined)
|
|
1385
|
+
return false;
|
|
1386
|
+
const applied = applyFlowDeskSessionEvidenceWriteIntentsV1(rootDir, [prepared.writeIntent]);
|
|
1387
|
+
return applied.ok && applied.writtenPaths.length > 0;
|
|
1388
|
+
}
|
|
1389
|
+
function laneAlreadyHasTerminalTaskEvidence(input) {
|
|
1390
|
+
const reloaded = reloadFlowDeskSessionEvidenceV1({
|
|
1391
|
+
rootDir: input.rootDir,
|
|
1392
|
+
workflowId: input.workflowId,
|
|
1393
|
+
});
|
|
1394
|
+
if (!reloaded.ok)
|
|
1395
|
+
return false;
|
|
1396
|
+
return reloaded.entries.some((entry) => {
|
|
1397
|
+
if (entry.evidenceClass !== "task_result" && entry.evidenceClass !== "task_failed")
|
|
1398
|
+
return false;
|
|
1399
|
+
const record = entry.record;
|
|
1400
|
+
return record.lane_id === input.laneId;
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
function childProgressLabel(value) {
|
|
1404
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
1405
|
+
return compact.length > 120 ? `${compact.slice(0, 119)}…` : compact;
|
|
1406
|
+
}
|
|
1407
|
+
function writeAgentTaskProgressEvidence(input) {
|
|
1408
|
+
const record = {
|
|
1409
|
+
schema_version: "flowdesk.agent_task_progress.v1",
|
|
1410
|
+
workflow_id: input.workflowId,
|
|
1411
|
+
lane_id: input.laneId,
|
|
1412
|
+
task_id: input.taskId,
|
|
1413
|
+
agent_ref: input.agentRef,
|
|
1414
|
+
provider_qualified_model_id: input.providerQualifiedModelId,
|
|
1415
|
+
progress_seq: input.progressSeq,
|
|
1416
|
+
observed_at: input.observedAt,
|
|
1417
|
+
phase: input.phase,
|
|
1418
|
+
progress_label: childProgressLabel(input.progressLabel),
|
|
1419
|
+
progress_ref: `progress-${input.laneId}-${input.progressSeq}`,
|
|
1420
|
+
redaction_version: "v1",
|
|
1421
|
+
dispatch_authority_enabled: false,
|
|
1422
|
+
};
|
|
1423
|
+
writeChildSessionEvidence(input.rootDir, input.workflowId, `agent-task-progress-${input.laneId}-${input.progressSeq}`, record);
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Monitor all async-mode agent task lanes in the given workflow.
|
|
1427
|
+
* Called from the watchdog cycle once per interval:
|
|
1428
|
+
* - Poll session.messages for each running child session
|
|
1429
|
+
* - On result: write task_result evidence + terminal lifecycle
|
|
1430
|
+
* - At 20s silence: nudge with noReply: true
|
|
1431
|
+
* - At 40s: second nudge
|
|
1432
|
+
* - At 60s+: session.abort + task_failed + terminal lifecycle
|
|
1433
|
+
*/
|
|
1434
|
+
export async function monitorChildSessionsV1(input) {
|
|
1435
|
+
const nowMs = (input.now ?? new Date()).getTime();
|
|
1436
|
+
const nudgeQuietPeriodMs = input.nudgeQuietPeriodMs ?? 20_000;
|
|
1437
|
+
const maxNudges = input.maxNudges ?? 2;
|
|
1438
|
+
const abortThresholdMs = input.abortThresholdMs ?? 60_000;
|
|
1439
|
+
const result = { lanesPolled: 0, lanesCompleted: 0, lanesNudged: 0, lanesAborted: 0 };
|
|
1440
|
+
const reloaded = reloadFlowDeskSessionEvidenceV1({ rootDir: input.rootDir, workflowId: input.workflowId });
|
|
1441
|
+
if (!reloaded.ok)
|
|
1442
|
+
return result;
|
|
1443
|
+
// Find all child session records for running lanes
|
|
1444
|
+
const childRecords = reloaded.entries
|
|
1445
|
+
.filter(e => e.evidenceClass === "agent_task_child_session")
|
|
1446
|
+
.map(e => e.record)
|
|
1447
|
+
.filter(r => r.schema_version === AGENT_TASK_CHILD_SESSION_SCHEMA_VERSION);
|
|
1448
|
+
// Find lanes that are NOT yet terminal (by lifecycle state or by task_result/task_failed evidence)
|
|
1449
|
+
const terminalLaneIds = new Set();
|
|
1450
|
+
const terminalTaskResultLaneIds = new Set();
|
|
1451
|
+
const awaitingPermissionLaneIds = new Set();
|
|
1452
|
+
const latestProgressByLane = new Map();
|
|
1453
|
+
for (const entry of reloaded.entries) {
|
|
1454
|
+
const rec = entry.record;
|
|
1455
|
+
const laneIdVal = typeof rec.lane_id === "string" ? rec.lane_id : undefined;
|
|
1456
|
+
if (!laneIdVal)
|
|
1457
|
+
continue;
|
|
1458
|
+
if (entry.evidenceClass === "agent_task_progress") {
|
|
1459
|
+
const observedAtMs = typeof rec.observed_at === "string" ? Date.parse(rec.observed_at) : NaN;
|
|
1460
|
+
const phase = typeof rec.phase === "string" ? rec.phase : undefined;
|
|
1461
|
+
const current = latestProgressByLane.get(laneIdVal);
|
|
1462
|
+
if (phase !== undefined && Number.isFinite(observedAtMs) && (current === undefined || current.observedAtMs <= observedAtMs)) {
|
|
1463
|
+
latestProgressByLane.set(laneIdVal, { observedAtMs, phase });
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
if (entry.evidenceClass === "lane_lifecycle") {
|
|
1467
|
+
const s = rec.state;
|
|
1468
|
+
if (s === "complete" || s === "invocation_failed" || s === "no_output" || s === "incomplete")
|
|
1469
|
+
terminalLaneIds.add(laneIdVal);
|
|
1470
|
+
}
|
|
1471
|
+
else if (entry.evidenceClass === "task_result" || entry.evidenceClass === "task_failed") {
|
|
1472
|
+
terminalLaneIds.add(laneIdVal);
|
|
1473
|
+
if (entry.evidenceClass === "task_result")
|
|
1474
|
+
terminalTaskResultLaneIds.add(laneIdVal);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
for (const [laneId, progress] of latestProgressByLane) {
|
|
1478
|
+
if (progress.phase === "awaiting_permission")
|
|
1479
|
+
awaitingPermissionLaneIds.add(laneId);
|
|
1480
|
+
}
|
|
1481
|
+
// Defensive fallback for older/event-written progress records: if any explicit
|
|
1482
|
+
// awaiting_permission marker is present and no later permission response has
|
|
1483
|
+
// been observed, suspend watchdog nudge/abort for the lane.
|
|
1484
|
+
for (const entry of reloaded.entries) {
|
|
1485
|
+
if (entry.evidenceClass !== "agent_task_progress")
|
|
1486
|
+
continue;
|
|
1487
|
+
const rec = entry.record;
|
|
1488
|
+
if (rec.phase === "awaiting_permission" && typeof rec.lane_id === "string")
|
|
1489
|
+
awaitingPermissionLaneIds.add(rec.lane_id);
|
|
1490
|
+
if (rec.phase === "waiting" && typeof rec.progress_label === "string" && rec.progress_label.includes("permission response") && typeof rec.lane_id === "string")
|
|
1491
|
+
awaitingPermissionLaneIds.delete(rec.lane_id);
|
|
1492
|
+
}
|
|
1493
|
+
for (const record of childRecords) {
|
|
1494
|
+
const laneId = typeof record.lane_id === "string" ? record.lane_id : "";
|
|
1495
|
+
const childSessionId = typeof record.child_session_id === "string" ? record.child_session_id : "";
|
|
1496
|
+
if (!laneId || !childSessionId)
|
|
1497
|
+
continue;
|
|
1498
|
+
if (terminalLaneIds.has(laneId)) {
|
|
1499
|
+
if (terminalTaskResultLaneIds.has(laneId)) {
|
|
1500
|
+
refreshFlowDeskCompletionUiCachesV1({
|
|
1501
|
+
rootDir: input.rootDir,
|
|
1502
|
+
workflowId: input.workflowId,
|
|
1503
|
+
observedAt: new Date(nowMs).toISOString(),
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
continue; // already done
|
|
1507
|
+
}
|
|
1508
|
+
if (awaitingPermissionLaneIds.has(laneId))
|
|
1509
|
+
continue; // user permission is outstanding; do not nudge or abort
|
|
1510
|
+
result.lanesPolled++;
|
|
1511
|
+
const createdAtMs = typeof record.created_at === "string" ? Date.parse(record.created_at) : nowMs;
|
|
1512
|
+
const nudgeCount = typeof record.nudge_count === "number" ? record.nudge_count : 0;
|
|
1513
|
+
const lastNudgeAtMs = typeof record.last_nudge_at === "string" ? Date.parse(record.last_nudge_at) : createdAtMs;
|
|
1514
|
+
const silenceMs = nowMs - lastNudgeAtMs;
|
|
1515
|
+
const totalAgeMs = nowMs - createdAtMs;
|
|
1516
|
+
// 1. Try to collect terminal result text. Candidate text without terminal is kept for abort-time partial capture.
|
|
1517
|
+
const resultObservation = await pollChildSessionOutput(input.client, childSessionId);
|
|
1518
|
+
if (resultObservation !== null && resultObservation.text.trim().length > 0) {
|
|
1519
|
+
const taskId = typeof record.task_id === "string" ? record.task_id : laneId;
|
|
1520
|
+
const agentRef = typeof record.agent_ref === "string" ? record.agent_ref : "agent-unknown";
|
|
1521
|
+
const modelId = typeof record.provider_qualified_model_id === "string" ? record.provider_qualified_model_id : "unknown/unknown";
|
|
1522
|
+
if (laneAlreadyHasTerminalTaskEvidence({ rootDir: input.rootDir, workflowId: input.workflowId, laneId })) {
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
const token = randomBytes(4).toString("hex");
|
|
1526
|
+
const completedAt = new Date(nowMs).toISOString();
|
|
1527
|
+
const sanitizedResult = sanitizeFlowDeskTaskResultTextV1(resultObservation.text);
|
|
1528
|
+
const finalText = sanitizedResult.text;
|
|
1529
|
+
const taskResultEvidenceId = `task-result-${taskId}-watchdog-${token}`;
|
|
1530
|
+
const taskResultWritten = input._forceTaskResultWriteFailureForTest === true
|
|
1531
|
+
? false
|
|
1532
|
+
: writeChildSessionEvidence(input.rootDir, input.workflowId, taskResultEvidenceId, {
|
|
1533
|
+
schema_version: "flowdesk.task_result.v1",
|
|
1534
|
+
workflow_id: input.workflowId,
|
|
1535
|
+
lane_id: laneId,
|
|
1536
|
+
task_id: taskId,
|
|
1537
|
+
agent_ref: agentRef,
|
|
1538
|
+
provider_qualified_model_id: modelId,
|
|
1539
|
+
task_prompt_sha256: createHash("sha256").update("watchdog-collected").digest("hex"),
|
|
1540
|
+
result_text: finalText,
|
|
1541
|
+
result_text_truncated: sanitizedResult.truncated,
|
|
1542
|
+
result_text_sha256: createHash("sha256").update(resultObservation.text).digest("hex"),
|
|
1543
|
+
completion_status: resultObservation.completionStatus,
|
|
1544
|
+
output_kind: resultObservation.outputKind,
|
|
1545
|
+
usable_for_synthesis: resultObservation.usableForSynthesis,
|
|
1546
|
+
missing_contract: false,
|
|
1547
|
+
created_at: completedAt,
|
|
1548
|
+
dispatch_authority_enabled: false,
|
|
1549
|
+
});
|
|
1550
|
+
if (!taskResultWritten) {
|
|
1551
|
+
writeChildSessionEvidence(input.rootDir, input.workflowId, `task-failed-${taskId}-watchdog-result-write-${token}`, {
|
|
1552
|
+
schema_version: "flowdesk.task_failed.v1",
|
|
1553
|
+
workflow_id: input.workflowId,
|
|
1554
|
+
lane_id: laneId,
|
|
1555
|
+
task_id: taskId,
|
|
1556
|
+
agent_ref: agentRef,
|
|
1557
|
+
provider_qualified_model_id: modelId,
|
|
1558
|
+
failure_category: "unknown",
|
|
1559
|
+
redacted_reason: "watchdog could not persist task_result evidence",
|
|
1560
|
+
created_at: completedAt,
|
|
1561
|
+
dispatch_authority_enabled: false,
|
|
1562
|
+
});
|
|
1563
|
+
writeAgentTaskProgressEvidence({
|
|
1564
|
+
rootDir: input.rootDir,
|
|
1565
|
+
workflowId: input.workflowId,
|
|
1566
|
+
laneId,
|
|
1567
|
+
taskId,
|
|
1568
|
+
agentRef,
|
|
1569
|
+
providerQualifiedModelId: modelId,
|
|
1570
|
+
phase: "failed",
|
|
1571
|
+
progressSeq: 20 + nudgeCount,
|
|
1572
|
+
progressLabel: "async agent task result persistence failed",
|
|
1573
|
+
observedAt: completedAt,
|
|
1574
|
+
});
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
writeAgentTaskProgressEvidence({
|
|
1578
|
+
rootDir: input.rootDir,
|
|
1579
|
+
workflowId: input.workflowId,
|
|
1580
|
+
laneId,
|
|
1581
|
+
taskId,
|
|
1582
|
+
agentRef,
|
|
1583
|
+
providerQualifiedModelId: modelId,
|
|
1584
|
+
phase: "finalizing",
|
|
1585
|
+
progressSeq: 10 + nudgeCount,
|
|
1586
|
+
progressLabel: "async agent task result captured by watchdog",
|
|
1587
|
+
observedAt: completedAt,
|
|
1588
|
+
});
|
|
1589
|
+
refreshFlowDeskCompletionUiCachesV1({
|
|
1590
|
+
rootDir: input.rootDir,
|
|
1591
|
+
workflowId: input.workflowId,
|
|
1592
|
+
observedAt: completedAt,
|
|
1593
|
+
});
|
|
1594
|
+
result.lanesCompleted++;
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
// 2. Abort threshold exceeded
|
|
1598
|
+
if (totalAgeMs >= abortThresholdMs && nudgeCount >= maxNudges) {
|
|
1599
|
+
await abortChildSession(input.client, childSessionId);
|
|
1600
|
+
const taskId = typeof record.task_id === "string" ? record.task_id : laneId;
|
|
1601
|
+
const agentRef = typeof record.agent_ref === "string" ? record.agent_ref : "agent-unknown";
|
|
1602
|
+
const modelId = typeof record.provider_qualified_model_id === "string" ? record.provider_qualified_model_id : "unknown/unknown";
|
|
1603
|
+
if (laneAlreadyHasTerminalTaskEvidence({ rootDir: input.rootDir, workflowId: input.workflowId, laneId })) {
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
const token = randomBytes(4).toString("hex");
|
|
1607
|
+
const abortedAt = new Date(nowMs).toISOString();
|
|
1608
|
+
const partialObservation = await pollChildSessionCandidate(input.client, childSessionId);
|
|
1609
|
+
if (partialObservation !== null && partialObservation.text.trim().length > 0) {
|
|
1610
|
+
const sanitizedResult = sanitizeFlowDeskTaskResultTextV1(partialObservation.text);
|
|
1611
|
+
const taskResultWritten = writeChildSessionEvidence(input.rootDir, input.workflowId, `task-result-${taskId}-watchdog-partial-${token}`, {
|
|
1612
|
+
schema_version: "flowdesk.task_result.v1",
|
|
1613
|
+
workflow_id: input.workflowId,
|
|
1614
|
+
lane_id: laneId,
|
|
1615
|
+
task_id: taskId,
|
|
1616
|
+
agent_ref: agentRef,
|
|
1617
|
+
provider_qualified_model_id: modelId,
|
|
1618
|
+
task_prompt_sha256: createHash("sha256").update("watchdog-collected").digest("hex"),
|
|
1619
|
+
result_text: sanitizedResult.text,
|
|
1620
|
+
result_text_truncated: sanitizedResult.truncated,
|
|
1621
|
+
result_text_sha256: createHash("sha256").update(partialObservation.text).digest("hex"),
|
|
1622
|
+
completion_status: "partial",
|
|
1623
|
+
output_kind: partialObservation.outputKind,
|
|
1624
|
+
usable_for_synthesis: partialObservation.usableForSynthesis,
|
|
1625
|
+
missing_contract: true,
|
|
1626
|
+
created_at: abortedAt,
|
|
1627
|
+
dispatch_authority_enabled: false,
|
|
1628
|
+
});
|
|
1629
|
+
if (taskResultWritten) {
|
|
1630
|
+
writeAgentTaskProgressEvidence({
|
|
1631
|
+
rootDir: input.rootDir,
|
|
1632
|
+
workflowId: input.workflowId,
|
|
1633
|
+
laneId,
|
|
1634
|
+
taskId,
|
|
1635
|
+
agentRef,
|
|
1636
|
+
providerQualifiedModelId: modelId,
|
|
1637
|
+
phase: "finalizing",
|
|
1638
|
+
progressSeq: 20 + nudgeCount,
|
|
1639
|
+
progressLabel: "async agent task partial result captured before abort",
|
|
1640
|
+
observedAt: abortedAt,
|
|
1641
|
+
});
|
|
1642
|
+
refreshFlowDeskCompletionUiCachesV1({
|
|
1643
|
+
rootDir: input.rootDir,
|
|
1644
|
+
workflowId: input.workflowId,
|
|
1645
|
+
observedAt: abortedAt,
|
|
1646
|
+
});
|
|
1647
|
+
result.lanesCompleted++;
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
const failureCategory = totalAgeMs > abortThresholdMs * 2
|
|
1652
|
+
? "network_interrupted"
|
|
1653
|
+
: "sdk_prompt_timeout";
|
|
1654
|
+
writeChildSessionEvidence(input.rootDir, input.workflowId, `task-failed-${taskId}-watchdog-abort-${token}`, {
|
|
1655
|
+
schema_version: "flowdesk.task_failed.v1",
|
|
1656
|
+
workflow_id: input.workflowId,
|
|
1657
|
+
lane_id: laneId,
|
|
1658
|
+
task_id: taskId,
|
|
1659
|
+
agent_ref: agentRef,
|
|
1660
|
+
provider_qualified_model_id: modelId,
|
|
1661
|
+
failure_category: failureCategory,
|
|
1662
|
+
redacted_reason: `watchdog aborted child session after ${Math.round(totalAgeMs / 1000)}s with no response`,
|
|
1663
|
+
created_at: abortedAt,
|
|
1664
|
+
dispatch_authority_enabled: false,
|
|
1665
|
+
});
|
|
1666
|
+
writeAgentTaskProgressEvidence({
|
|
1667
|
+
rootDir: input.rootDir,
|
|
1668
|
+
workflowId: input.workflowId,
|
|
1669
|
+
laneId,
|
|
1670
|
+
taskId,
|
|
1671
|
+
agentRef,
|
|
1672
|
+
providerQualifiedModelId: modelId,
|
|
1673
|
+
phase: "failed",
|
|
1674
|
+
progressSeq: 20 + nudgeCount,
|
|
1675
|
+
progressLabel: "async agent task aborted after no response",
|
|
1676
|
+
observedAt: abortedAt,
|
|
1677
|
+
});
|
|
1678
|
+
result.lanesAborted++;
|
|
1679
|
+
continue;
|
|
1680
|
+
}
|
|
1681
|
+
// 3. Nudge if silence threshold exceeded
|
|
1682
|
+
if (silenceMs >= nudgeQuietPeriodMs && nudgeCount < maxNudges) {
|
|
1683
|
+
await sendWatchdogNudge(input.client, childSessionId);
|
|
1684
|
+
result.lanesNudged++;
|
|
1685
|
+
const taskId = typeof record.task_id === "string" ? record.task_id : laneId;
|
|
1686
|
+
const agentRef = typeof record.agent_ref === "string" ? record.agent_ref : "agent-unknown";
|
|
1687
|
+
const modelId = typeof record.provider_qualified_model_id === "string" ? record.provider_qualified_model_id : "unknown/unknown";
|
|
1688
|
+
const nudgedAt = new Date(nowMs).toISOString();
|
|
1689
|
+
writeAgentTaskProgressEvidence({
|
|
1690
|
+
rootDir: input.rootDir,
|
|
1691
|
+
workflowId: input.workflowId,
|
|
1692
|
+
laneId,
|
|
1693
|
+
taskId,
|
|
1694
|
+
agentRef,
|
|
1695
|
+
providerQualifiedModelId: modelId,
|
|
1696
|
+
phase: "nudged",
|
|
1697
|
+
progressSeq: 2 + nudgeCount,
|
|
1698
|
+
progressLabel: "async agent task nudged after quiet period",
|
|
1699
|
+
observedAt: nudgedAt,
|
|
1700
|
+
});
|
|
1701
|
+
// Update nudge_count in evidence (overwrite record)
|
|
1702
|
+
const evidenceId = reloaded.entries
|
|
1703
|
+
.find(e => e.evidenceClass === "agent_task_child_session" && e.record.lane_id === laneId)
|
|
1704
|
+
?.evidenceId ?? `agent-task-child-session-${laneId}-watchdog`;
|
|
1705
|
+
writeChildSessionEvidence(input.rootDir, input.workflowId, evidenceId, {
|
|
1706
|
+
...record,
|
|
1707
|
+
nudge_count: nudgeCount + 1,
|
|
1708
|
+
last_nudge_at: new Date(nowMs).toISOString(),
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
return result;
|
|
1713
|
+
}
|
|
962
1714
|
//# sourceMappingURL=stall-recovery.js.map
|