@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,6 +1,6 @@
1
1
  import { existsSync, readdirSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { FLOWDESK_SESSION_EVIDENCE_CLASSES, projectFlowDeskLaneStallV1, reloadFlowDeskSessionEvidenceV1, } from "@flowdesk/core";
3
+ import { applyFlowDeskSessionEvidenceWriteIntentsV1, FLOWDESK_SESSION_EVIDENCE_CLASSES, prepareFlowDeskSessionEvidenceWriteIntentV1, projectFlowDeskLaneStallV1, reloadFlowDeskSessionEvidenceV1, } from "@flowdesk/core";
4
4
  import { backfillTerminalAgentTaskFailedLanesV1 } from "./stall-recovery.js";
5
5
  const FLOWDESK_LANE_STALL_TERMINAL_STATES = new Set([
6
6
  "complete",
@@ -90,6 +90,27 @@ function pruneNonTerminalLifecycleSnapshotsNoLongerValid(reload) {
90
90
  return reload;
91
91
  return { ...reload, entries };
92
92
  }
93
+ function pruneInconsistencySnapshotsNoLongerValid(reload) {
94
+ const terminalLaneIds = new Set();
95
+ for (const entry of reload.entries) {
96
+ if (entry.evidenceClass === "task_result" || entry.evidenceClass === "task_failed") {
97
+ const laneId = getLaneLifecycleRecordField(entry.record, "lane_id");
98
+ if (laneId !== undefined)
99
+ terminalLaneIds.add(laneId);
100
+ }
101
+ }
102
+ if (terminalLaneIds.size === 0)
103
+ return reload;
104
+ const entries = reload.entries.filter((entry) => {
105
+ if (entry.evidenceClass !== "agent_task_inconsistency")
106
+ return true;
107
+ const laneId = getLaneLifecycleRecordField(entry.record, "lane_id");
108
+ return laneId === undefined || !terminalLaneIds.has(laneId);
109
+ });
110
+ if (entries.length === reload.entries.length)
111
+ return reload;
112
+ return { ...reload, entries };
113
+ }
93
114
  const FLOWDESK_SESSION_RECORD_ROOT = ".flowdesk/sessions";
94
115
  function blockedAuthority() {
95
116
  return {
@@ -106,6 +127,14 @@ function blockedAuthority() {
106
127
  function safeNextActions() {
107
128
  return ["/flowdesk-status", "/flowdesk-doctor", "/flowdesk-export-debug"];
108
129
  }
130
+ function authorityCapabilitySummary() {
131
+ return {
132
+ availableNow: ["display_only", "local_preview", "command_backed_guarded"],
133
+ explicitDevBeta: ["provider_task_lane", "controlled_workspace_write"],
134
+ laterGated: ["managed_dispatch", "managed_fallback", "tui_actions", "hard_chat_control"],
135
+ unsupportedByDefault: ["auto_provider_execution", "automatic_reselection"],
136
+ };
137
+ }
109
138
  function listSessionWorkflowIds(rootDir, max) {
110
139
  const sessionsDir = join(rootDir, FLOWDESK_SESSION_RECORD_ROOT);
111
140
  if (!existsSync(sessionsDir))
@@ -138,6 +167,165 @@ function getStringField(record, key) {
138
167
  const value = record[key];
139
168
  return typeof value === "string" ? value : undefined;
140
169
  }
170
+ function getPositiveIntegerField(record, key) {
171
+ const value = record[key];
172
+ return typeof value === "number" && Number.isInteger(value) && value > 0
173
+ ? value
174
+ : undefined;
175
+ }
176
+ const FORBIDDEN_SYNTHESIS_SUMMARY_MARKERS = /system prompt|provider payload|raw token|hidden injection|opencode\srun|dispatch|fallback|reselect/i;
177
+ function synthesisSummaryPreview(value) {
178
+ if (value === undefined)
179
+ return undefined;
180
+ const compact = value.replace(/\s+/g, " ").trim();
181
+ if (compact.length === 0 || FORBIDDEN_SYNTHESIS_SUMMARY_MARKERS.test(compact))
182
+ return undefined;
183
+ return compact.length > 160 ? `${compact.slice(0, 159)}…` : compact;
184
+ }
185
+ const DEFAULT_AGENT_TASK_FINALIZING_INCONSISTENCY_GRACE_MS = 90_000;
186
+ const MIN_AGENT_TASK_FINALIZING_INCONSISTENCY_GRACE_MS = 30_000;
187
+ const MAX_AGENT_TASK_FINALIZING_INCONSISTENCY_GRACE_MS = 600_000;
188
+ function clampFinalizingInconsistencyGraceMs(value) {
189
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
190
+ return DEFAULT_AGENT_TASK_FINALIZING_INCONSISTENCY_GRACE_MS;
191
+ return Math.min(MAX_AGENT_TASK_FINALIZING_INCONSISTENCY_GRACE_MS, Math.max(MIN_AGENT_TASK_FINALIZING_INCONSISTENCY_GRACE_MS, Math.floor(value)));
192
+ }
193
+ function finalizingInconsistencyEvidenceId(laneId) {
194
+ return `agent-task-inconsistency-${laneId}-finalizing-without-terminal`;
195
+ }
196
+ function materializeFinalizingWithoutTerminalInconsistencies(input) {
197
+ const observedAtMs = Date.parse(input.observedAt);
198
+ if (!Number.isFinite(observedAtMs))
199
+ return false;
200
+ const terminalLaneIds = new Set();
201
+ const existingInconsistencyIds = new Set();
202
+ const attemptIdByLane = new Map();
203
+ const taskIdByLane = new Map();
204
+ const latestActiveSignalByLane = new Map();
205
+ const latestFinalizingByLane = new Map();
206
+ for (const entry of input.reload.entries) {
207
+ const laneId = getStringField(entry.record, "lane_id");
208
+ if (laneId !== undefined) {
209
+ if (entry.evidenceClass === "lane_lifecycle" || entry.evidenceClass === "lane_heartbeat") {
210
+ const attemptId = getStringField(entry.record, "attempt_id");
211
+ if (attemptId !== undefined)
212
+ attemptIdByLane.set(laneId, attemptId);
213
+ const state = getStringField(entry.record, "state");
214
+ const observedAt = getStringField(entry.record, "updated_at") ??
215
+ getStringField(entry.record, "observed_at") ??
216
+ getStringField(entry.record, "created_at");
217
+ const observedAtMs = observedAt === undefined ? NaN : Date.parse(observedAt);
218
+ if ((state === "created" ||
219
+ state === "running" ||
220
+ state === "awaiting_dependency" ||
221
+ state === "cooldown") &&
222
+ Number.isFinite(observedAtMs)) {
223
+ const current = latestActiveSignalByLane.get(laneId);
224
+ if (current === undefined || observedAtMs > current)
225
+ latestActiveSignalByLane.set(laneId, observedAtMs);
226
+ }
227
+ }
228
+ if (entry.evidenceClass === "agent_task_context") {
229
+ const taskId = getStringField(entry.record, "task_id");
230
+ if (taskId !== undefined)
231
+ taskIdByLane.set(laneId, taskId);
232
+ }
233
+ }
234
+ if (entry.evidenceClass === "task_result" || entry.evidenceClass === "task_failed") {
235
+ if (laneId !== undefined)
236
+ terminalLaneIds.add(laneId);
237
+ continue;
238
+ }
239
+ if (entry.evidenceClass === "agent_task_inconsistency") {
240
+ existingInconsistencyIds.add(entry.evidenceId);
241
+ continue;
242
+ }
243
+ if (entry.evidenceClass !== "agent_task_progress")
244
+ continue;
245
+ if (laneId === undefined)
246
+ continue;
247
+ if (getStringField(entry.record, "phase") !== "finalizing")
248
+ continue;
249
+ const progressObservedAt = getStringField(entry.record, "observed_at");
250
+ if (progressObservedAt === undefined)
251
+ continue;
252
+ const progressObservedAtMs = Date.parse(progressObservedAt);
253
+ if (!Number.isFinite(progressObservedAtMs))
254
+ continue;
255
+ const current = latestFinalizingByLane.get(laneId);
256
+ if (current !== undefined && current.observedAtMs > progressObservedAtMs)
257
+ continue;
258
+ latestFinalizingByLane.set(laneId, {
259
+ laneId,
260
+ attemptId: attemptIdByLane.get(laneId),
261
+ taskId: getStringField(entry.record, "task_id"),
262
+ progressSeq: getPositiveIntegerField(entry.record, "progress_seq"),
263
+ observedAt: progressObservedAt,
264
+ observedAtMs: progressObservedAtMs,
265
+ });
266
+ }
267
+ const intents = [];
268
+ for (const progress of latestFinalizingByLane.values()) {
269
+ if (terminalLaneIds.has(progress.laneId))
270
+ continue;
271
+ const activeSignalMs = latestActiveSignalByLane.get(progress.laneId);
272
+ const finalizingAgeMs = observedAtMs - progress.observedAtMs;
273
+ const activeSignalAgeMs = activeSignalMs === undefined ? 0 : observedAtMs - activeSignalMs;
274
+ if (finalizingAgeMs < input.graceMs && activeSignalAgeMs < input.graceMs)
275
+ continue;
276
+ const attemptId = progress.attemptId ?? attemptIdByLane.get(progress.laneId);
277
+ const taskId = progress.taskId ?? taskIdByLane.get(progress.laneId);
278
+ const progressSeq = progress.progressSeq;
279
+ if (attemptId === undefined || taskId === undefined || progressSeq === undefined)
280
+ continue;
281
+ const evidenceId = finalizingInconsistencyEvidenceId(progress.laneId);
282
+ if (existingInconsistencyIds.has(evidenceId))
283
+ continue;
284
+ const record = {
285
+ schema_version: "flowdesk.agent_task_inconsistency.v1",
286
+ workflow_id: input.workflowId,
287
+ attempt_id: attemptId,
288
+ lane_id: progress.laneId,
289
+ task_id: taskId,
290
+ last_progress_seq: progressSeq,
291
+ last_progress_observed_at: progress.observedAt,
292
+ inconsistency_kind: "finalizing_without_terminal",
293
+ grace_window_ms: input.graceMs,
294
+ grace_source_label: input.graceMs === DEFAULT_AGENT_TASK_FINALIZING_INCONSISTENCY_GRACE_MS
295
+ ? "default_status_live_finalizing_inconsistency_grace"
296
+ : "configured_status_live_finalizing_inconsistency_grace",
297
+ observed_at: input.observedAt,
298
+ safe_next_actions: [
299
+ "/flowdesk-status",
300
+ "/flowdesk-abort",
301
+ "/flowdesk-retry",
302
+ "/flowdesk-doctor",
303
+ "/flowdesk-export-debug",
304
+ ],
305
+ redaction_version: "v1",
306
+ dispatch_authority_enabled: false,
307
+ };
308
+ const prepared = prepareFlowDeskSessionEvidenceWriteIntentV1({
309
+ workflowId: input.workflowId,
310
+ evidenceId,
311
+ record,
312
+ });
313
+ if (prepared.ok && prepared.writeIntent !== undefined)
314
+ intents.push(prepared.writeIntent);
315
+ }
316
+ if (intents.length === 0)
317
+ return false;
318
+ const applied = applyFlowDeskSessionEvidenceWriteIntentsV1(input.rootDir, intents);
319
+ return applied.ok && applied.writtenPaths.length > 0;
320
+ }
321
+ function compactPromptPreview(value, max = 96) {
322
+ if (value === undefined)
323
+ return undefined;
324
+ const compact = value.replace(/\s+/g, " ").trim();
325
+ if (compact.length === 0)
326
+ return undefined;
327
+ return compact.length > max ? `${compact.slice(0, Math.max(0, max - 1))}…` : compact;
328
+ }
141
329
  function summarizeWorkflow(workflowId, reload, maxRecent) {
142
330
  const counts = {};
143
331
  const recent = {};
@@ -152,8 +340,10 @@ function summarizeWorkflow(workflowId, reload, maxRecent) {
152
340
  let taskAgentAssignmentCount;
153
341
  let taskModelSelectionStatus;
154
342
  let taskModelSelectionBlockedLabels;
343
+ let workflowSynthesisId;
155
344
  let workflowSynthesisTasksSummarized;
156
345
  let workflowSynthesisConflictDetected;
346
+ let workflowSynthesisSummaryPreview;
157
347
  let workflowDispatchPlanRevisionId;
158
348
  let workflowDispatchPlanTaskCount;
159
349
  for (const evidenceClass of FLOWDESK_SESSION_EVIDENCE_CLASSES) {
@@ -233,10 +423,12 @@ function summarizeWorkflow(workflowId, reload, maxRecent) {
233
423
  if (evidenceClass === "workflow_synthesis_result") {
234
424
  const last = classEntries[classEntries.length - 1];
235
425
  if (last !== undefined) {
426
+ workflowSynthesisId = getStringField(last.record, "synthesis_id") ?? last.evidenceId;
236
427
  const tasks = last.record.tasks_summarized;
237
428
  if (typeof tasks === "number")
238
429
  workflowSynthesisTasksSummarized = tasks;
239
430
  workflowSynthesisConflictDetected = last.record.conflict_detected === true;
431
+ workflowSynthesisSummaryPreview = synthesisSummaryPreview(getStringField(last.record, "synthesis_summary"));
240
432
  }
241
433
  }
242
434
  if (evidenceClass === "workflow_dispatch_plan") {
@@ -299,12 +491,18 @@ function summarizeWorkflow(workflowId, reload, maxRecent) {
299
491
  ...(taskModelSelectionBlockedLabels !== undefined
300
492
  ? { latestTaskModelSelectionBlockedLabels: taskModelSelectionBlockedLabels }
301
493
  : {}),
494
+ ...(workflowSynthesisId !== undefined
495
+ ? { latestWorkflowSynthesisId: workflowSynthesisId }
496
+ : {}),
302
497
  ...(workflowSynthesisTasksSummarized !== undefined
303
498
  ? { latestWorkflowSynthesisTasksSummarized: workflowSynthesisTasksSummarized }
304
499
  : {}),
305
500
  ...(workflowSynthesisConflictDetected !== undefined
306
501
  ? { latestWorkflowSynthesisConflictDetected: workflowSynthesisConflictDetected }
307
502
  : {}),
503
+ ...(workflowSynthesisSummaryPreview !== undefined
504
+ ? { latestWorkflowSynthesisSummaryPreview: workflowSynthesisSummaryPreview }
505
+ : {}),
308
506
  ...(workflowDispatchPlanRevisionId !== undefined
309
507
  ? {
310
508
  latestWorkflowDispatchPlanRevisionId: workflowDispatchPlanRevisionId,
@@ -318,7 +516,67 @@ function summarizeWorkflow(workflowId, reload, maxRecent) {
318
516
  function buildLaneProgressCards(workflowId, reload, projection) {
319
517
  const lifecycleMeta = new Map();
320
518
  const verdictLabels = new Map();
519
+ const taskResultLaneIds = new Set();
520
+ const taskResultByLane = new Map();
521
+ const agentTaskContextByLane = new Map();
522
+ const childSessionByLane = new Map();
523
+ const agentTaskProgressByLane = new Map();
321
524
  for (const entry of reload.entries) {
525
+ if (entry.evidenceClass === "agent_task_progress") {
526
+ const laneId = getStringField(entry.record, "lane_id");
527
+ if (laneId !== undefined) {
528
+ const observedAt = getStringField(entry.record, "observed_at");
529
+ const observedAtMs = observedAt === undefined ? 0 : Date.parse(observedAt);
530
+ const current = agentTaskProgressByLane.get(laneId);
531
+ if (current === undefined || current.observedAtMs <= observedAtMs) {
532
+ agentTaskProgressByLane.set(laneId, {
533
+ observedAtMs: Number.isFinite(observedAtMs) ? observedAtMs : 0,
534
+ phase: getStringField(entry.record, "phase"),
535
+ label: getStringField(entry.record, "progress_label"),
536
+ observedAt,
537
+ });
538
+ }
539
+ }
540
+ continue;
541
+ }
542
+ if (entry.evidenceClass === "agent_task_context") {
543
+ const laneId = getStringField(entry.record, "lane_id");
544
+ if (laneId !== undefined) {
545
+ agentTaskContextByLane.set(laneId, {
546
+ taskId: getStringField(entry.record, "task_id"),
547
+ promptPreview: compactPromptPreview(getStringField(entry.record, "prompt_text")),
548
+ promptTextTruncated: entry.record.prompt_text_truncated === true,
549
+ });
550
+ }
551
+ continue;
552
+ }
553
+ if (entry.evidenceClass === "agent_task_child_session") {
554
+ const laneId = getStringField(entry.record, "lane_id");
555
+ const nudgeCount = entry.record.nudge_count;
556
+ if (laneId !== undefined) {
557
+ childSessionByLane.set(laneId, {
558
+ ...(typeof nudgeCount === "number" && Number.isFinite(nudgeCount)
559
+ ? { nudgeCount }
560
+ : {}),
561
+ });
562
+ }
563
+ continue;
564
+ }
565
+ if (entry.evidenceClass === "task_result") {
566
+ const laneId = getStringField(entry.record, "lane_id");
567
+ if (laneId !== undefined) {
568
+ taskResultLaneIds.add(laneId);
569
+ taskResultByLane.set(laneId, {
570
+ taskId: getStringField(entry.record, "task_id"),
571
+ completionStatus: getStringField(entry.record, "completion_status"),
572
+ outputKind: getStringField(entry.record, "output_kind"),
573
+ ...(typeof entry.record.usable_for_synthesis === "boolean"
574
+ ? { usableForSynthesis: entry.record.usable_for_synthesis }
575
+ : {}),
576
+ });
577
+ }
578
+ continue;
579
+ }
322
580
  if (entry.evidenceClass === "reviewer_verdict") {
323
581
  const label = getStringField(entry.record, "verdict_label");
324
582
  if (label === "pass" ||
@@ -348,14 +606,23 @@ function buildLaneProgressCards(workflowId, reload, projection) {
348
606
  }
349
607
  return projection.entries.slice(0, 6).map((entry) => {
350
608
  const meta = lifecycleMeta.get(entry.laneId);
609
+ const context = agentTaskContextByLane.get(entry.laneId);
610
+ const childSession = childSessionByLane.get(entry.laneId);
611
+ const progress = agentTaskProgressByLane.get(entry.laneId);
612
+ const taskResult = taskResultByLane.get(entry.laneId);
613
+ const projectedState = meta?.state === "incomplete" && taskResultLaneIds.has(entry.laneId)
614
+ ? "task_result"
615
+ : (meta?.state ?? entry.lifecycleState);
616
+ const taskId = context?.taskId ?? taskResult?.taskId;
351
617
  const verdictLabel = meta?.verdictRef === undefined
352
618
  ? undefined
353
619
  : verdictLabels.get(meta.verdictRef);
354
620
  return {
355
621
  workflowId,
356
622
  laneId: entry.laneId,
623
+ ...(taskId === undefined ? {} : { taskId }),
357
624
  attemptId: entry.attemptId,
358
- state: meta?.state ?? entry.lifecycleState,
625
+ state: projectedState,
359
626
  classification: entry.classification,
360
627
  ...(entry.secondsSinceLastSignal === undefined
361
628
  ? {}
@@ -367,7 +634,22 @@ function buildLaneProgressCards(workflowId, reload, projection) {
367
634
  ...(meta?.providerQualifiedModelId === undefined
368
635
  ? {}
369
636
  : { providerQualifiedModelId: meta.providerQualifiedModelId }),
637
+ ...(context?.promptPreview === undefined ? {} : { promptPreview: context.promptPreview }),
638
+ ...(context?.promptTextTruncated === undefined ? {} : { promptTextTruncated: context.promptTextTruncated }),
639
+ ...(childSession?.nudgeCount === undefined ? {} : { nudgeCount: childSession.nudgeCount }),
640
+ ...(progress?.phase === undefined ? {} : { progressPhase: progress.phase }),
641
+ ...(progress?.label === undefined ? {} : { progressLabel: progress.label }),
642
+ ...(progress?.observedAt === undefined ? {} : { progressObservedAt: progress.observedAt }),
370
643
  ...(verdictLabel === undefined ? {} : { verdictLabel }),
644
+ ...(taskResult?.completionStatus === undefined
645
+ ? {}
646
+ : { completionStatus: taskResult.completionStatus }),
647
+ ...(taskResult?.outputKind === undefined
648
+ ? {}
649
+ : { outputKind: taskResult.outputKind }),
650
+ ...(taskResult?.usableForSynthesis === undefined
651
+ ? {}
652
+ : { usableForSynthesis: taskResult.usableForSynthesis }),
371
653
  ...(entry.failureHint === undefined
372
654
  ? {}
373
655
  : { failureHint: entry.failureHint }),
@@ -376,6 +658,60 @@ function buildLaneProgressCards(workflowId, reload, projection) {
376
658
  };
377
659
  });
378
660
  }
661
+ function buildLaneProgressAggregate(cards, synthesisAlreadyRecorded = false) {
662
+ const expected = cards.length;
663
+ const terminal = cards.filter(card => card.classification === "terminal").length;
664
+ const taskResult = cards.filter(card => card.state === "task_result").length;
665
+ const failed = cards.filter(card => card.failureHint !== undefined || card.state === "invocation_failed" || card.state === "task_failed").length;
666
+ const awaitingPermission = cards.filter(card => card.progressPhase === "awaiting_permission").length;
667
+ const normalCompleted = cards.filter(card => card.state === "task_result" &&
668
+ card.classification === "terminal" &&
669
+ card.completionStatus !== "partial" &&
670
+ card.usableForSynthesis !== false &&
671
+ card.failureHint === undefined).length;
672
+ const nextActionReady = expected > 0 && normalCompleted === expected && !synthesisAlreadyRecorded;
673
+ return {
674
+ expected,
675
+ terminal,
676
+ taskResult,
677
+ failed,
678
+ awaitingPermission,
679
+ normalCompleted,
680
+ autoNextStepEligible: nextActionReady,
681
+ nextActionAvailable: nextActionReady,
682
+ ...(nextActionReady ? { nextActionKind: "synthesis" } : {}),
683
+ nextActionRefs: ["/flowdesk-status", "/flowdesk-export-debug"],
684
+ };
685
+ }
686
+ function buildSubtaskActivityRows(cards) {
687
+ return cards.map((card) => {
688
+ const recoveryActionRefs = card.failureHint !== undefined ||
689
+ card.classification === "stalled" ||
690
+ card.classification === "progressing_late"
691
+ ? ["/flowdesk-status", "/flowdesk-retry", "/flowdesk-resume", "/flowdesk-abort", "/flowdesk-export-debug"]
692
+ : ["/flowdesk-status", "/flowdesk-export-debug"];
693
+ return {
694
+ workflowId: card.workflowId,
695
+ laneId: card.laneId,
696
+ ...(card.taskId === undefined ? {} : { taskId: card.taskId }),
697
+ ...(card.attemptId === undefined ? {} : { attemptId: card.attemptId }),
698
+ ...(card.state === undefined ? {} : { state: card.state }),
699
+ classification: card.classification,
700
+ ...(card.progressPhase === undefined ? {} : { progressPhase: card.progressPhase }),
701
+ ...(card.progressLabel === undefined ? {} : { progressLabel: card.progressLabel }),
702
+ ...(card.progressObservedAt === undefined ? {} : { lastObservedAt: card.progressObservedAt }),
703
+ ...(card.nudgeCount === undefined ? {} : { nudgeCount: card.nudgeCount }),
704
+ ...(card.completionStatus === undefined ? {} : { completionStatus: card.completionStatus }),
705
+ ...(card.outputKind === undefined ? {} : { outputKind: card.outputKind }),
706
+ ...(card.usableForSynthesis === undefined ? {} : { usableForSynthesis: card.usableForSynthesis }),
707
+ ...(card.verdictLabel === undefined ? {} : { verdictLabel: card.verdictLabel }),
708
+ ...(card.failureHint === undefined ? {} : { failureHint: card.failureHint }),
709
+ recoveryActionRefs,
710
+ statusCommandRef: "/flowdesk-status",
711
+ debugCommandRef: "/flowdesk-export-debug",
712
+ };
713
+ });
714
+ }
379
715
  function summarizeLatestProviderUsageSnapshot(entries) {
380
716
  const snapshots = entries.filter((entry) => entry.evidenceClass === "provider_usage_snapshot");
381
717
  if (snapshots.length === 0)
@@ -406,6 +742,7 @@ export async function executeFlowDeskStatusLiveV1(input) {
406
742
  workflows: [],
407
743
  redactedBlockReason: "status live tool requires a durable state root directory",
408
744
  safeNextActions: safeNextActions(),
745
+ authorityCapabilitySummary: authorityCapabilitySummary(),
409
746
  authority: blockedAuthority(),
410
747
  };
411
748
  }
@@ -424,6 +761,7 @@ export async function executeFlowDeskStatusLiveV1(input) {
424
761
  ? `no durable session evidence for workflow ${requestedWorkflowId}`
425
762
  : "no durable session workflows found under the configured durable state root",
426
763
  safeNextActions: safeNextActions(),
764
+ authorityCapabilitySummary: authorityCapabilitySummary(),
427
765
  authority: blockedAuthority(),
428
766
  };
429
767
  }
@@ -433,48 +771,84 @@ export async function executeFlowDeskStatusLiveV1(input) {
433
771
  workflowId,
434
772
  rootDir,
435
773
  now: new Date(observedAt),
774
+ refreshCompletionUiCaches: false,
436
775
  });
437
- const reload = reloadFlowDeskSessionEvidenceV1({
776
+ let reload = reloadFlowDeskSessionEvidenceV1({
438
777
  workflowId,
439
778
  rootDir,
440
779
  });
441
- const summary = summarizeWorkflow(workflowId, reload, maxRecentEvidencePerClass);
442
- const projectionReload = pruneNonTerminalLifecycleSnapshotsNoLongerValid(reload);
443
- const projection = projectFlowDeskLaneStallV1({
780
+ let summary;
781
+ let projectionReload;
782
+ let projection;
783
+ const buildWorkflowSummary = () => {
784
+ summary = summarizeWorkflow(workflowId, reload, maxRecentEvidencePerClass);
785
+ projectionReload = pruneInconsistencySnapshotsNoLongerValid(pruneNonTerminalLifecycleSnapshotsNoLongerValid(reload));
786
+ projection = projectFlowDeskLaneStallV1({
787
+ workflowId,
788
+ reload: projectionReload,
789
+ observedAt,
790
+ ...(input.config.laneHeartbeatLateThresholdMs === undefined
791
+ ? {}
792
+ : { lateThresholdMs: input.config.laneHeartbeatLateThresholdMs }),
793
+ ...(input.config.laneHeartbeatStallThresholdMs === undefined
794
+ ? {}
795
+ : { stallThresholdMs: input.config.laneHeartbeatStallThresholdMs }),
796
+ });
797
+ summary.laneStallProjection = projection;
798
+ const projectedLifecycleStates = projection.entries
799
+ .map((entry) => entry.lifecycleState)
800
+ .filter((state) => typeof state === "string" && state.length > 0);
801
+ if (projectedLifecycleStates.length > 0) {
802
+ summary.latestLaneLifecycleStates = projectedLifecycleStates;
803
+ }
804
+ summary.worstLaneStallClassification = projection.worstClassification;
805
+ summary.stalledLaneCount = projection.totalStalledLanes;
806
+ summary.progressingLateLaneCount = projection.totalLateLanes;
807
+ summary.inconsistentFinalizingWithoutTerminalLaneCount =
808
+ projection.totalInconsistentLanes;
809
+ summary.laneProgressCards = buildLaneProgressCards(workflowId, projectionReload, projection);
810
+ summary.subtaskActivityRows = buildSubtaskActivityRows(summary.laneProgressCards);
811
+ summary.laneProgressAggregate = buildLaneProgressAggregate(summary.laneProgressCards, summary.latestWorkflowSynthesisTasksSummarized !== undefined);
812
+ };
813
+ const graceMs = clampFinalizingInconsistencyGraceMs(input.config.agentTaskFinalizingInconsistencyGraceMs);
814
+ const wroteInconsistency = materializeFinalizingWithoutTerminalInconsistencies({
444
815
  workflowId,
445
- reload: projectionReload,
816
+ rootDir,
817
+ reload,
446
818
  observedAt,
447
- ...(input.config.laneHeartbeatLateThresholdMs === undefined
448
- ? {}
449
- : { lateThresholdMs: input.config.laneHeartbeatLateThresholdMs }),
450
- ...(input.config.laneHeartbeatStallThresholdMs === undefined
451
- ? {}
452
- : { stallThresholdMs: input.config.laneHeartbeatStallThresholdMs }),
819
+ graceMs,
453
820
  });
454
- summary.laneStallProjection = projection;
455
- summary.worstLaneStallClassification = projection.worstClassification;
456
- summary.stalledLaneCount = projection.totalStalledLanes;
457
- summary.progressingLateLaneCount = projection.totalLateLanes;
458
- summary.laneProgressCards = buildLaneProgressCards(workflowId, projectionReload, projection);
821
+ if (wroteInconsistency) {
822
+ reload = reloadFlowDeskSessionEvidenceV1({
823
+ workflowId,
824
+ rootDir,
825
+ });
826
+ }
827
+ buildWorkflowSummary();
459
828
  workflows.push(summary);
460
829
  }
461
830
  const observed = workflows.some((summary) => Object.values(summary.evidenceCounts).some((count) => typeof count === "number" && count > 0));
462
831
  const totalStalled = workflows.reduce((sum, summary) => sum + (summary.stalledLaneCount ?? 0), 0);
463
832
  const totalLate = workflows.reduce((sum, summary) => sum + (summary.progressingLateLaneCount ?? 0), 0);
464
- const worstLaneStallClassification = workflows.some((summary) => summary.worstLaneStallClassification === "stalled")
465
- ? "stalled"
466
- : workflows.some((summary) => summary.worstLaneStallClassification === "progressing_late")
467
- ? "progressing_late"
468
- : workflows.some((summary) => summary.worstLaneStallClassification === "progressing_normal")
469
- ? "progressing_normal"
470
- : workflows.some((summary) => summary.worstLaneStallClassification === "terminal")
471
- ? "terminal"
472
- : "unknown";
833
+ const totalInconsistent = workflows.reduce((sum, summary) => sum + (summary.inconsistentFinalizingWithoutTerminalLaneCount ?? 0), 0);
834
+ const worstLaneStallClassification = workflows.some((summary) => summary.worstLaneStallClassification ===
835
+ "inconsistent_finalizing_without_terminal")
836
+ ? "inconsistent_finalizing_without_terminal"
837
+ : workflows.some((summary) => summary.worstLaneStallClassification === "stalled")
838
+ ? "stalled"
839
+ : workflows.some((summary) => summary.worstLaneStallClassification === "progressing_late")
840
+ ? "progressing_late"
841
+ : workflows.some((summary) => summary.worstLaneStallClassification === "progressing_normal")
842
+ ? "progressing_normal"
843
+ : workflows.some((summary) => summary.worstLaneStallClassification === "terminal")
844
+ ? "terminal"
845
+ : "unknown";
473
846
  const summaryForUser = buildStatusLiveSummaryForUser({
474
847
  workflows,
475
848
  worstLaneStallClassification,
476
849
  totalStalled,
477
850
  totalLate,
851
+ totalInconsistent,
478
852
  requestedWorkflowId,
479
853
  });
480
854
  return {
@@ -487,8 +861,10 @@ export async function executeFlowDeskStatusLiveV1(input) {
487
861
  worstLaneStallClassification,
488
862
  totalStalledLaneCount: totalStalled,
489
863
  totalProgressingLateLaneCount: totalLate,
864
+ totalInconsistentFinalizingWithoutTerminalLaneCount: totalInconsistent,
490
865
  summaryForUser,
491
866
  safeNextActions: safeNextActions(),
867
+ authorityCapabilitySummary: authorityCapabilitySummary(),
492
868
  authority: {
493
869
  realOpenCodeDispatch: false,
494
870
  providerCall: false,
@@ -508,29 +884,46 @@ function buildStatusLiveSummaryForUser(input) {
508
884
  ? `FlowDesk status: no durable evidence for workflow ${input.requestedWorkflowId}.`
509
885
  : "FlowDesk status: no durable workflows found.";
510
886
  }
511
- const headline = input.totalStalled > 0
512
- ? `FlowDesk status: ${workflowsCount} workflow(s); worst classification stalled (${input.totalStalled} stalled, ${input.totalLate} progressing-late).`
513
- : input.totalLate > 0
514
- ? `FlowDesk status: ${workflowsCount} workflow(s); ${input.totalLate} progressing-late, no stalled lanes.`
515
- : input.worstLaneStallClassification === "terminal"
516
- ? `FlowDesk status: ${workflowsCount} workflow(s); all observed lanes terminal/complete.`
517
- : input.worstLaneStallClassification === "progressing_normal"
518
- ? `FlowDesk status: ${workflowsCount} workflow(s); active lanes progressing_normal.`
519
- : `FlowDesk status: ${workflowsCount} workflow(s); worst classification ${input.worstLaneStallClassification}.`;
887
+ const headline = input.totalInconsistent > 0
888
+ ? `FlowDesk status: ${workflowsCount} workflow(s); ${input.totalInconsistent} finalizing-without-terminal inconsistent lane(s) require manual recovery.`
889
+ : input.totalStalled > 0
890
+ ? `FlowDesk status: ${workflowsCount} workflow(s); worst classification stalled (${input.totalStalled} stalled, ${input.totalLate} progressing-late).`
891
+ : input.totalLate > 0
892
+ ? `FlowDesk status: ${workflowsCount} workflow(s); ${input.totalLate} progressing-late, no stalled lanes.`
893
+ : input.worstLaneStallClassification === "terminal"
894
+ ? `FlowDesk status: ${workflowsCount} workflow(s); all observed lanes terminal/complete.`
895
+ : input.worstLaneStallClassification === "progressing_normal"
896
+ ? `FlowDesk status: ${workflowsCount} workflow(s); active lanes progressing_normal.`
897
+ : `FlowDesk status: ${workflowsCount} workflow(s); worst classification ${input.worstLaneStallClassification}.`;
520
898
  const perWorkflow = input.workflows.slice(0, 3).map((workflow) => {
521
899
  const verdictLabels = workflow.latestReviewerVerdictLabels;
522
900
  const verdictText = verdictLabels.length > 0
523
901
  ? `verdicts=${verdictLabels.join("/")}`
524
902
  : "verdicts=(none)";
525
- const lifecycleStates = workflow.latestLaneLifecycleStates;
903
+ const projectedLaneStates = (workflow.laneProgressCards ?? [])
904
+ .slice(0, 3)
905
+ .map((lane) => `${lane.state ?? "unknown"}/${lane.classification}`);
906
+ const lifecycleStates = projectedLaneStates.length > 0 ? projectedLaneStates : workflow.latestLaneLifecycleStates;
526
907
  const lifecycleText = lifecycleStates.length > 0
527
- ? `lifecycle=${lifecycleStates.slice(0, 3).join("/")}`
528
- : "lifecycle=(none)";
908
+ ? `${projectedLaneStates.length > 0 ? "lane_state" : "lifecycle"}=${lifecycleStates.slice(0, 3).join("/")}`
909
+ : "lane_state=(none)";
910
+ const synthesisPreview = workflow.latestWorkflowSynthesisSummaryPreview === undefined
911
+ ? ""
912
+ : ` preview="${workflow.latestWorkflowSynthesisSummaryPreview.replace(/"/g, "'").slice(0, 96)}"`;
913
+ const synthesisText = workflow.latestWorkflowSynthesisTasksSummarized !== undefined
914
+ ? ` synthesis=${workflow.latestWorkflowSynthesisTasksSummarized} tasks${workflow.latestWorkflowSynthesisConflictDetected ? " conflict=detected" : ""}${synthesisPreview}`
915
+ : "";
916
+ const autoNextText = workflow.laneProgressAggregate?.autoNextStepEligible === true ? " auto_next=ready" : "";
917
+ const nextActionText = workflow.laneProgressAggregate?.nextActionAvailable === true
918
+ ? ` next_action=${workflow.laneProgressAggregate.nextActionKind ?? "available"}_ready`
919
+ : "";
529
920
  const planText = workflow.latestWorkflowDispatchPlanRevisionId !== undefined
530
- ? `workflow_plan=${workflow.latestWorkflowDispatchPlanRevisionId} tasks=${workflow.latestWorkflowDispatchPlanTaskCount ?? "unknown"}`
921
+ ? `workflow_plan=${workflow.latestWorkflowDispatchPlanRevisionId} tasks=${workflow.latestWorkflowDispatchPlanTaskCount ?? "unknown"}${synthesisText}${autoNextText}${nextActionText}`
531
922
  : workflow.latestTaskGraphTaskCount !== undefined
532
- ? `planning_slice=${workflow.latestWorkflowAuthoringStatus ?? "unknown"} tasks=${workflow.latestTaskGraphTaskCount} assignments=${workflow.latestTaskAgentAssignmentCount ?? 0} model=${workflow.latestTaskModelSelectionStatus ?? "unknown"}${workflow.latestWorkflowSynthesisTasksSummarized !== undefined ? ` synthesis=${workflow.latestWorkflowSynthesisTasksSummarized} tasks${workflow.latestWorkflowSynthesisConflictDetected ? " (conflict)" : ""}` : ""}`
533
- : "workflow_plan=(none)";
923
+ ? `planning_slice=${workflow.latestWorkflowAuthoringStatus ?? "unknown"} tasks=${workflow.latestTaskGraphTaskCount} assignments=${workflow.latestTaskAgentAssignmentCount ?? 0} model=${workflow.latestTaskModelSelectionStatus ?? "unknown"}${synthesisText}${autoNextText}${nextActionText}`
924
+ : synthesisText.length > 0
925
+ ? `${synthesisText.trim()}${autoNextText}${nextActionText}`
926
+ : `workflow_plan=(none)${autoNextText}${nextActionText}`;
534
927
  const classification = workflow.worstLaneStallClassification ?? "unknown";
535
928
  const lanePreview = (workflow.laneProgressCards ?? [])
536
929
  .slice(0, 2)
@@ -541,8 +934,20 @@ function buildStatusLiveSummaryForUser(input) {
541
934
  return `${lane.laneId}:${lane.state ?? "unknown"}/${lane.classification}/last=${age}`;
542
935
  })
543
936
  .join(", ");
544
- const laneText = lanePreview.length > 0 ? `, lanes=${lanePreview}` : "";
545
- return `- ${workflow.workflowId}: ${classification}, ${verdictText}, ${lifecycleText}, ${planText}${laneText}`;
937
+ const subtaskPreview = (workflow.subtaskActivityRows ?? [])
938
+ .slice(0, 3)
939
+ .map((row) => {
940
+ const actions = (row.recoveryActionRefs ?? [])
941
+ .slice(0, 5)
942
+ .map((action) => action.replace("/flowdesk-", ""))
943
+ .join("|");
944
+ const compact = `${row.laneId}:${row.state ?? "unknown"}/${row.classification}${actions.length > 0 ? `[${actions}]` : ""}`;
945
+ return compact.length > 96 ? `${compact.slice(0, 95)}…` : compact;
946
+ })
947
+ .join("; ");
948
+ const laneText = lanePreview.length > 0 && subtaskPreview.length === 0 ? `, lanes=${lanePreview}` : "";
949
+ const subtaskText = subtaskPreview.length > 0 ? `, subtasks=${subtaskPreview}` : "";
950
+ return `- ${workflow.workflowId}: ${classification}, ${verdictText}, ${lifecycleText}, ${planText}${laneText}${subtaskText}`;
546
951
  });
547
952
  return [headline, ...perWorkflow].join("\n");
548
953
  }