@flowdesk/opencode-plugin 0.1.0 → 0.1.2

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 (42) hide show
  1. package/dist/bootstrap-cli.js +0 -0
  2. package/dist/command-handlers.d.ts +5 -1
  3. package/dist/command-handlers.d.ts.map +1 -1
  4. package/dist/command-handlers.js +84 -2
  5. package/dist/command-handlers.js.map +1 -1
  6. package/dist/lane-heartbeat-writer.d.ts +43 -0
  7. package/dist/lane-heartbeat-writer.d.ts.map +1 -0
  8. package/dist/lane-heartbeat-writer.js +133 -0
  9. package/dist/lane-heartbeat-writer.js.map +1 -0
  10. package/dist/local-adapter.d.ts +12 -1
  11. package/dist/local-adapter.d.ts.map +1 -1
  12. package/dist/local-adapter.js +66 -5
  13. package/dist/local-adapter.js.map +1 -1
  14. package/dist/managed-dispatch-adapter.d.ts +506 -1
  15. package/dist/managed-dispatch-adapter.d.ts.map +1 -1
  16. package/dist/managed-dispatch-adapter.js +2624 -38
  17. package/dist/managed-dispatch-adapter.js.map +1 -1
  18. package/dist/provider-usage-live-tool.d.ts +59 -0
  19. package/dist/provider-usage-live-tool.d.ts.map +1 -0
  20. package/dist/provider-usage-live-tool.js +259 -0
  21. package/dist/provider-usage-live-tool.js.map +1 -0
  22. package/dist/quick-fallback-run.d.ts +54 -0
  23. package/dist/quick-fallback-run.d.ts.map +1 -0
  24. package/dist/quick-fallback-run.js +230 -0
  25. package/dist/quick-fallback-run.js.map +1 -0
  26. package/dist/quick-reviewer-run.d.ts +61 -0
  27. package/dist/quick-reviewer-run.d.ts.map +1 -0
  28. package/dist/quick-reviewer-run.js +397 -0
  29. package/dist/quick-reviewer-run.js.map +1 -0
  30. package/dist/runtime-reviewer-execution-bridge.d.ts +43 -0
  31. package/dist/runtime-reviewer-execution-bridge.d.ts.map +1 -0
  32. package/dist/runtime-reviewer-execution-bridge.js +313 -0
  33. package/dist/runtime-reviewer-execution-bridge.js.map +1 -0
  34. package/dist/server.d.ts +104 -6
  35. package/dist/server.d.ts.map +1 -1
  36. package/dist/server.js +1990 -99
  37. package/dist/server.js.map +1 -1
  38. package/dist/status-live-tool.d.ts +55 -0
  39. package/dist/status-live-tool.d.ts.map +1 -0
  40. package/dist/status-live-tool.js +215 -0
  41. package/dist/status-live-tool.js.map +1 -0
  42. package/package.json +3 -3
package/dist/server.js CHANGED
@@ -1,9 +1,15 @@
1
- import { evaluateFlowDeskChatIntakeV1, getFlowDeskPortableCommandToolName, getRelease1SchemaArtifact } from "@flowdesk/core";
1
+ import { applyFlowDeskSessionEvidenceWriteIntentsV1, createFlowDeskChatHookAuthorityProbeV1, evaluateFlowDeskChatIntakeV1, getFlowDeskPortableCommandToolName, getRelease1SchemaArtifact, materializeFlowDeskExactModelCacheEvidenceFromProviderAcquisitionEvidenceV1, materializeFlowDeskRuntimeLaneLaunchPlansFromReviewerFanoutEvidenceV1, planFlowDeskReviewerFanoutFromReloadedCacheEvidenceV1, prepareFlowDeskSessionEvidenceWriteIntentV1, reloadFlowDeskSessionEvidenceV1, validateFlowDeskDefaultManagedDispatchAuthorizationV1, validateRunRequestV1, } from "@flowdesk/core";
2
2
  import { tool } from "@opencode-ai/plugin";
3
- import { flowdeskPluginId, flowdeskPluginScaffold, hasProductionOpenCodeRegistration } from "./index.js";
4
- import { createFlowDeskLocalNonDispatchAdapterSession, flowdeskLocalNonDispatchAdapterProfile } from "./local-adapter.js";
5
- import { dispatchManagedDispatchBetaPromptV1 } from "./managed-dispatch-adapter.js";
6
- import { FLOWDESK_PRE_SPIKE_PLUGIN_TOOL_STUBS, getFlowDeskRelease1HandlerReadinessSummary, getFlowDeskRelease1ProductionReadinessSummary, hasPassingFds1SchemaConversionSpike, runFlowDeskPreSpikePluginToolStub } from "./tool-stubs.js";
3
+ import { flowdeskPluginId, flowdeskPluginScaffold, hasProductionOpenCodeRegistration, } from "./index.js";
4
+ import { createFlowDeskLocalNonDispatchAdapterSession, flowdeskLocalNonDispatchAdapterProfile, } from "./local-adapter.js";
5
+ import { createFlowDeskManagedDispatchBetaDurableReservationStoreV1, createFlowDeskOpenCodeMetadataProviderAcquisitionClientV1, createFlowDeskOpenCodePromptBackedProviderAcquisitionClientV1, dispatchManagedDispatchBetaPromptV1, materializeFlowDeskManagedFallbackRegatePlanEvidenceV1, orchestrateFlowDeskManagedFallbackRegateV1, runFlowDeskExactModelProviderAcquisitionLiveTestV1, } from "./managed-dispatch-adapter.js";
6
+ import { executeFlowDeskRuntimeReviewerExecutionBridgeV1, redactedRuntimeReviewerExecutionBlocked, runtimeReviewerExecutionExpectationsFromValue, } from "./runtime-reviewer-execution-bridge.js";
7
+ import { executeFlowDeskQuickReviewerRunV1, } from "./quick-reviewer-run.js";
8
+ import { executeFlowDeskProviderUsageLiveV1, } from "./provider-usage-live-tool.js";
9
+ import { executeFlowDeskStatusLiveV1, } from "./status-live-tool.js";
10
+ import { executeFlowDeskQuickFallbackRunV1, } from "./quick-fallback-run.js";
11
+ import { recordFlowDeskLaneHeartbeatV1, } from "./lane-heartbeat-writer.js";
12
+ import { FLOWDESK_PRE_SPIKE_PLUGIN_TOOL_STUBS, getFlowDeskRelease1HandlerReadinessSummary, getFlowDeskRelease1ProductionReadinessSummary, hasPassingFds1SchemaConversionSpike, runFlowDeskPreSpikePluginToolStub, } from "./tool-stubs.js";
7
13
  export const flowdeskPreSpikeDoctorToolName = "flowdesk_pre_spike_doctor";
8
14
  export const flowdeskChatIntakeToolName = "flowdesk_chat_intake";
9
15
  export const flowdeskFds1SchemaConversionProbeOption = "fds1SchemaConversionProbe";
@@ -11,8 +17,26 @@ export const flowdeskLocalNonDispatchAdapterOption = "localNonDispatchAdapter";
11
17
  export const flowdeskNaturalLanguageRoutingOption = "naturalLanguageRouting";
12
18
  export const flowdeskDurableStateRootOption = "durableStateRoot";
13
19
  export const flowdeskProductionEnablementOption = "productionEnablement";
20
+ export const flowdeskReviewerFanoutDiagnosticsOption = "reviewerFanoutDiagnostics";
14
21
  export const flowdeskManagedDispatchBetaAdapterOption = "managedDispatchBetaAdapter";
22
+ export const flowdeskExactModelProviderAcquisitionLiveTestOption = "exactModelProviderAcquisitionLiveTest";
23
+ export const flowdeskRuntimeReviewerExecutionOption = "runtimeReviewerExecution";
24
+ export const flowdeskManagedFallbackRegateOption = "managedFallbackRegate";
25
+ export const flowdeskQuickReviewerRunOption = "quickReviewerRun";
26
+ export const flowdeskProviderUsageLiveOption = "providerUsageLive";
27
+ export const flowdeskStatusLiveOption = "statusLive";
28
+ export const flowdeskQuickFallbackRunOption = "quickFallbackRun";
29
+ export const flowdeskLaneHeartbeatWriterOption = "laneHeartbeatWriter";
30
+ export const flowdeskDefaultManagedDispatchAuthorizationOption = "defaultManagedDispatchAuthorization";
15
31
  export const flowdeskManagedDispatchBetaToolName = "flowdesk_managed_dispatch_beta";
32
+ export const flowdeskExactModelProviderAcquisitionLiveTestToolName = "flowdesk_exact_model_provider_acquisition_live_test";
33
+ export const flowdeskRuntimeReviewerExecutionToolName = "flowdesk_runtime_reviewer_execution";
34
+ export const flowdeskManagedFallbackRegateToolName = "flowdesk_managed_fallback_regate";
35
+ export const flowdeskQuickReviewerRunToolName = "flowdesk_quick_reviewer_run";
36
+ export const flowdeskProviderUsageLiveToolName = "flowdesk_provider_usage_live";
37
+ export const flowdeskStatusLiveToolName = "flowdesk_status_live";
38
+ export const flowdeskQuickFallbackRunToolName = "flowdesk_quick_fallback_run";
39
+ export const flowdeskLaneHeartbeatWriterToolName = "flowdesk_lane_heartbeat_record";
16
40
  const flowdeskChatSuggestionDuplicateWindowMs = 10_000;
17
41
  const disabledAuthority = {
18
42
  productionRegistrationEligible: false,
@@ -22,15 +46,47 @@ const disabledAuthority = {
22
46
  providerCall: false,
23
47
  runtimeExecution: false,
24
48
  fallbackAuthority: false,
25
- hardCancelOrNoReplyAuthority: false
49
+ hardCancelOrNoReplyAuthority: false,
26
50
  };
27
51
  function isRecord(value) {
28
52
  return typeof value === "object" && value !== null && !Array.isArray(value);
29
53
  }
54
+ function hasUnsupportedHardChatReturnFields(value) {
55
+ if (!isRecord(value))
56
+ return false;
57
+ return ["noReply", "cancel", "stop"].some((key) => key in value);
58
+ }
59
+ export function createFlowDeskChatHookAuthorityProbeFromObservationV1(input) {
60
+ const unsupportedHardChatReturn = hasUnsupportedHardChatReturnFields(input.returnValue);
61
+ return createFlowDeskChatHookAuthorityProbeV1({
62
+ probeId: safeToken(input.probeId, "chat-hook-probe"),
63
+ chatHookRef: safeToken(input.chatHookRef, "chat-message-hook"),
64
+ observedAt: input.observedAt,
65
+ mutationObserved: input.afterPartsCount > input.beforePartsCount,
66
+ throwBlocksReply: input.hookThrew === true && input.throwBlockedReplyObserved === true,
67
+ noReplySupported: false,
68
+ cancelOrStopSupported: false,
69
+ duplicateAssistantReplyObserved: input.duplicateAssistantReplyObserved === true,
70
+ timeoutOrNullFailClosed: input.timeoutOrNullFailClosed === true,
71
+ malformedReturnFailClosed: unsupportedHardChatReturn
72
+ ? false
73
+ : input.malformedReturnFailClosed === true,
74
+ evidenceRefs: input.evidenceRefs,
75
+ });
76
+ }
30
77
  function isManagedDispatchBetaClient(value) {
31
78
  if (!isRecord(value) || !isRecord(value.session))
32
79
  return false;
33
- return typeof value.session.prompt === "function" || typeof value.session.promptAsync === "function";
80
+ return (typeof value.session.prompt === "function" ||
81
+ typeof value.session.promptAsync === "function");
82
+ }
83
+ function isManagedDispatchBetaReservationStore(value) {
84
+ return (isRecord(value) &&
85
+ typeof value.reserve === "function" &&
86
+ typeof value.recordDispatchFailure === "function");
87
+ }
88
+ function isExactModelProviderAcquisitionClient(value) {
89
+ return isRecord(value) && typeof value.checkExactModelAvailability === "function";
34
90
  }
35
91
  function boundedText(value, fallback) {
36
92
  const trimmed = value.trim();
@@ -42,10 +98,19 @@ function safeToken(value, fallback) {
42
98
  return token.length > 0 ? token : fallback;
43
99
  }
44
100
  function commandNameFromAction(action) {
45
- return action.startsWith("/flowdesk-") && action !== "/flowdesk-explain-route" && action !== "/flowdesk-audit" ? action : undefined;
101
+ return action.startsWith("/flowdesk-") &&
102
+ action !== "/flowdesk-explain-route" &&
103
+ action !== "/flowdesk-audit"
104
+ ? action
105
+ : undefined;
46
106
  }
47
107
  function routedToolName(actions) {
48
- const preferredActions = actions.length > 1 ? [...actions.filter((action) => action !== "/flowdesk-status"), ...actions.filter((action) => action === "/flowdesk-status")] : actions;
108
+ const preferredActions = actions.length > 1
109
+ ? [
110
+ ...actions.filter((action) => action !== "/flowdesk-status"),
111
+ ...actions.filter((action) => action === "/flowdesk-status"),
112
+ ]
113
+ : actions;
49
114
  for (const action of preferredActions) {
50
115
  const commandName = commandNameFromAction(action);
51
116
  if (commandName === undefined)
@@ -63,10 +128,20 @@ function baseToolRequest(request, schemaVersion, optionalFields = chatEnvelopeOp
63
128
  schema_version: schemaVersion,
64
129
  request_id: safeToken(`${schemaVersion.split(".")[1] ?? "tool"}-${request.request_id}`, "request-chat-routed"),
65
130
  input_mode: "chat_routed",
66
- ...(includeOptional.has("workflow_id") && request.workflow_id !== undefined ? { workflow_id: request.workflow_id } : {}),
67
- ...(includeOptional.has("session_ref") && request.session_ref !== undefined ? { session_ref: request.session_ref } : {}),
68
- ...(includeOptional.has("redacted_intake_ref") && request.redacted_intake_ref !== undefined ? { redacted_intake_ref: request.redacted_intake_ref } : {}),
69
- ...(includeOptional.has("user_approval_ref") && request.user_approval_ref !== undefined ? { user_approval_ref: request.user_approval_ref } : {})
131
+ ...(includeOptional.has("workflow_id") && request.workflow_id !== undefined
132
+ ? { workflow_id: request.workflow_id }
133
+ : {}),
134
+ ...(includeOptional.has("session_ref") && request.session_ref !== undefined
135
+ ? { session_ref: request.session_ref }
136
+ : {}),
137
+ ...(includeOptional.has("redacted_intake_ref") &&
138
+ request.redacted_intake_ref !== undefined
139
+ ? { redacted_intake_ref: request.redacted_intake_ref }
140
+ : {}),
141
+ ...(includeOptional.has("user_approval_ref") &&
142
+ request.user_approval_ref !== undefined
143
+ ? { user_approval_ref: request.user_approval_ref }
144
+ : {}),
70
145
  };
71
146
  }
72
147
  function routedToolRequest(toolName, request, options = {}) {
@@ -76,21 +151,25 @@ function routedToolRequest(toolName, request, options = {}) {
76
151
  ...baseToolRequest(request, "flowdesk.plan.request.v1"),
77
152
  goal_summary: summary,
78
153
  scope_summary: "FlowDesk natural-language chat intake routed to command-backed planning.",
79
- risk_hint: options.requiresConfirmation === true ? "execution-like chat intake requires explicit user confirmation before any run" : "ordinary Release 1 command-backed steering only"
154
+ risk_hint: options.requiresConfirmation === true
155
+ ? "execution-like chat intake requires explicit user confirmation before any run"
156
+ : "ordinary Release 1 command-backed steering only",
80
157
  };
81
158
  }
82
159
  if (toolName === "flowdesk_run") {
83
160
  return {
84
161
  ...baseToolRequest(request, "flowdesk.run.request.v1"),
85
- run_mode: /dry[\s_-]*run|드라이\s*런/i.test(summary) ? "guarded-dry-run" : "fake-runtime",
162
+ run_mode: /dry[\s_-]*run|드라이\s*런/i.test(summary)
163
+ ? "guarded-dry-run"
164
+ : "fake-runtime",
86
165
  plan_revision_id: safeToken(`plan-${request.workflow_id ?? request.request_id}`, "plan-chat-routed"),
87
- step_id: safeToken(`step-${request.request_id}`, "step-chat-routed")
166
+ step_id: safeToken(`step-${request.request_id}`, "step-chat-routed"),
88
167
  };
89
168
  }
90
169
  if (toolName === "flowdesk_status") {
91
170
  return {
92
171
  ...baseToolRequest(request, "flowdesk.status.request.v1"),
93
- detail_level: "summary"
172
+ detail_level: "summary",
94
173
  };
95
174
  }
96
175
  if (toolName === "flowdesk_doctor") {
@@ -98,57 +177,67 @@ function routedToolRequest(toolName, request, options = {}) {
98
177
  ...baseToolRequest(request, "flowdesk.doctor.request.v1", []),
99
178
  check_scope: "all",
100
179
  profile: "test",
101
- persist_report: false
180
+ persist_report: false,
102
181
  };
103
182
  }
104
183
  if (toolName === "flowdesk_resume") {
105
184
  return {
106
185
  ...baseToolRequest(request, "flowdesk.resume.request.v1", []),
107
186
  checkpoint_id: safeToken(`checkpoint-${request.workflow_id ?? request.request_id}`, "checkpoint-chat-routed"),
108
- resume_mode: "status_only"
187
+ resume_mode: "status_only",
109
188
  };
110
189
  }
111
190
  if (toolName === "flowdesk_retry") {
112
191
  return {
113
192
  ...baseToolRequest(request, "flowdesk.retry.request.v1"),
114
193
  attempt_id: safeToken(`attempt-${request.workflow_id ?? request.request_id}`, "attempt-chat-routed"),
115
- retry_reason: "FlowDesk chat intake requested a non-dispatch retry diagnostic."
194
+ retry_reason: "FlowDesk chat intake requested a non-dispatch retry diagnostic.",
116
195
  };
117
196
  }
118
197
  if (toolName === "flowdesk_abort") {
119
198
  return {
120
199
  ...baseToolRequest(request, "flowdesk.abort.request.v1"),
121
200
  workflow_id: safeToken(request.workflow_id ?? `workflow-${request.request_id}`, "workflow-chat-routed"),
122
- reason: "FlowDesk chat intake requested a safe abort diagnostic."
201
+ reason: "FlowDesk chat intake requested a safe abort diagnostic.",
123
202
  };
124
203
  }
125
204
  if (toolName === "flowdesk_usage") {
126
205
  return {
127
206
  ...baseToolRequest(request, "flowdesk.usage.request.v1", []),
128
207
  provider_family: "unknown",
129
- refresh: false
208
+ refresh: false,
130
209
  };
131
210
  }
132
211
  if (toolName === "flowdesk_export_debug") {
133
212
  return {
134
213
  ...baseToolRequest(request, "flowdesk.export_debug.request.v1", []),
135
214
  include_sections: ["redaction_summary"],
136
- retention_hint: "keep_until_default_expiry"
215
+ retention_hint: "keep_until_default_expiry",
137
216
  };
138
217
  }
139
218
  return baseToolRequest(request, "flowdesk.tool.request.v1");
140
219
  }
141
220
  function evaluateNaturalLanguageRouting(request, session) {
142
- const evaluation = evaluateFlowDeskChatIntakeV1({ request, chatIntakeMode: "steering", hookHarnessMode: "enforce" });
143
- const toolName = evaluation.response.ok ? routedToolName(evaluation.response.safe_next_actions) : undefined;
144
- const routedToolResult = toolName === undefined ? undefined : session.evaluate(toolName, routedToolRequest(toolName, request, { requiresConfirmation: evaluation.response.route_decision === "ask_clarification" }));
221
+ const evaluation = evaluateFlowDeskChatIntakeV1({
222
+ request,
223
+ chatIntakeMode: "steering",
224
+ hookHarnessMode: "enforce",
225
+ });
226
+ const toolName = evaluation.response.ok
227
+ ? routedToolName(evaluation.response.safe_next_actions)
228
+ : undefined;
229
+ const routedToolResult = toolName === undefined
230
+ ? undefined
231
+ : session.evaluate(toolName, routedToolRequest(toolName, request, {
232
+ requiresConfirmation: evaluation.response.route_decision === "ask_clarification",
233
+ }));
145
234
  return {
146
235
  schema_version: "flowdesk.chat_intake.routing_result.v1",
147
236
  ok: evaluation.ok,
148
237
  evaluation,
149
238
  ...(toolName === undefined ? {} : { routedToolName: toolName }),
150
239
  ...(routedToolResult === undefined ? {} : { routedToolResult }),
151
- ...disabledAuthority
240
+ ...disabledAuthority,
152
241
  };
153
242
  }
154
243
  function extractText(value) {
@@ -157,9 +246,15 @@ function extractText(value) {
157
246
  if (!isRecord(value))
158
247
  return "";
159
248
  const direct = [value.text, value.content, value.message].filter((candidate) => typeof candidate === "string");
160
- const partText = Array.isArray(value.parts) ? value.parts.map(extractText) : [];
161
- const nestedMessage = isRecord(value.message) ? [extractText(value.message)] : [];
162
- return [...direct, ...partText, ...nestedMessage].filter((text) => text.length > 0).join(" ");
249
+ const partText = Array.isArray(value.parts)
250
+ ? value.parts.map(extractText)
251
+ : [];
252
+ const nestedMessage = isRecord(value.message)
253
+ ? [extractText(value.message)]
254
+ : [];
255
+ return [...direct, ...partText, ...nestedMessage]
256
+ .filter((text) => text.length > 0)
257
+ .join(" ");
163
258
  }
164
259
  function intakeRequestFromChatMessage(input) {
165
260
  const record = isRecord(input) ? input : {};
@@ -172,44 +267,68 @@ function intakeRequestFromChatMessage(input) {
172
267
  session_ref: sessionRef,
173
268
  redacted_intake_ref: `intake-${requestId}`,
174
269
  intake_summary: boundedText(extractText(record.message ?? record), "FlowDesk chat message."),
175
- source_surface: "chat.message"
270
+ source_surface: "chat.message",
176
271
  };
177
272
  }
178
273
  function clockMs(clock) {
179
274
  return (typeof clock === "function" ? clock() : clock).getTime();
180
275
  }
181
276
  function previewNaturalLanguageRouting(request) {
182
- const evaluation = evaluateFlowDeskChatIntakeV1({ request, chatIntakeMode: "steering", hookHarnessMode: "enforce" });
183
- const toolName = evaluation.response.ok ? routedToolName(evaluation.response.safe_next_actions) : undefined;
277
+ const evaluation = evaluateFlowDeskChatIntakeV1({
278
+ request,
279
+ chatIntakeMode: "steering",
280
+ hookHarnessMode: "enforce",
281
+ });
282
+ const toolName = evaluation.response.ok
283
+ ? routedToolName(evaluation.response.safe_next_actions)
284
+ : undefined;
184
285
  return { evaluation, toolName };
185
286
  }
186
287
  function mayCreatePendingConfirmation(preview) {
187
- return preview.evaluation.response.route_decision === "ask_clarification" && preview.toolName === "flowdesk_plan";
288
+ return (preview.evaluation.response.route_decision === "ask_clarification" &&
289
+ preview.toolName === "flowdesk_plan");
188
290
  }
189
291
  function suggestionDuplicateKey(request, response) {
190
292
  const suggestedNextStep = response.safe_next_actions[0] ?? "/flowdesk-status";
191
- return [request.session_ref, response.route_decision, suggestedNextStep].map((part, index) => safeToken(part, `chat-card-${index}`)).join("|");
293
+ return [request.session_ref, response.route_decision, suggestedNextStep]
294
+ .map((part, index) => safeToken(part, `chat-card-${index}`))
295
+ .join("|");
192
296
  }
193
297
  function steeringText(result) {
194
298
  const response = result.evaluation.response;
195
- const actions = response.safe_next_actions.map((action) => action === "ask_clarification" ? "Confirm the goal or plan before FlowDesk suggests a run." : action);
299
+ const actions = response.safe_next_actions.map((action) => action === "ask_clarification"
300
+ ? "Confirm the goal or plan before FlowDesk suggests a run."
301
+ : action);
196
302
  const suggestedNextStep = actions[0] ?? "/flowdesk-status";
197
- const localState = isRecord(result.routedToolResult) && isRecord(result.routedToolResult.localState) ? result.routedToolResult.localState : undefined;
198
- const confirmationRef = localState?.pendingConfirmationStatus === "pending" && typeof localState.pendingConfirmationRef === "string" ? localState.pendingConfirmationRef : undefined;
303
+ const localState = isRecord(result.routedToolResult) &&
304
+ isRecord(result.routedToolResult.localState)
305
+ ? result.routedToolResult.localState
306
+ : undefined;
307
+ const confirmationRef = localState?.pendingConfirmationStatus === "pending" &&
308
+ typeof localState.pendingConfirmationRef === "string"
309
+ ? localState.pendingConfirmationRef
310
+ : undefined;
199
311
  const why = response.ok === false
200
312
  ? "This request needs capabilities that are not available in the safe FlowDesk mode."
201
- : response.route_decision === "ask_clarification"
202
- ? "FlowDesk needs confirmation or a clearer goal before suggesting a command-backed workflow."
203
- : response.route_decision === "show_plan"
204
- ? "Your message looks like planning work that FlowDesk can organize as a command-backed plan."
205
- : "Your message matches a safe FlowDesk command-backed workflow.";
313
+ : response.safe_next_actions[0] === "/flowdesk-usage"
314
+ ? "This looks like a larger FlowDesk task, so usage should be checked before planning or running more work."
315
+ : response.route_decision === "ask_clarification"
316
+ ? "FlowDesk needs confirmation or a clearer goal before suggesting a command-backed workflow."
317
+ : response.route_decision === "show_plan"
318
+ ? "Your message looks like planning work that FlowDesk can organize as a command-backed plan."
319
+ : "Your message matches a safe FlowDesk command-backed workflow.";
206
320
  return [
207
321
  "FlowDesk",
208
322
  `Suggested next step: ${suggestedNextStep}`,
209
323
  `Why: ${why}`,
210
- ...(confirmationRef === undefined ? [] : [`Confirmation code: ${confirmationRef}`, "To continue, review the plan and reply with this confirmation code plus explicit approval."]),
324
+ ...(confirmationRef === undefined
325
+ ? []
326
+ : [
327
+ `Confirmation code: ${confirmationRef}`,
328
+ "To continue, review the plan and reply with this confirmation code plus explicit approval.",
329
+ ]),
211
330
  "Actions:",
212
- ...actions.map((action) => `- ${action}`)
331
+ ...actions.map((action) => `- ${action}`),
213
332
  ].join("\n");
214
333
  }
215
334
  function enumValues(values) {
@@ -250,7 +369,10 @@ export function createFlowDeskFds1SchemaConversionProbeTools() {
250
369
  if (artifact === undefined)
251
370
  throw new Error(`missing FDS-1 schema artifact: ${stub.requestSchemaId}`);
252
371
  const required = new Set(artifact.required);
253
- const args = Object.fromEntries(Object.keys(artifact.properties).map((fieldName) => [fieldName, zodForSchemaProperty(stub.requestSchemaId, fieldName, required.has(fieldName))]));
372
+ const args = Object.fromEntries(Object.keys(artifact.properties).map((fieldName) => [
373
+ fieldName,
374
+ zodForSchemaProperty(stub.requestSchemaId, fieldName, required.has(fieldName)),
375
+ ]));
254
376
  return [
255
377
  stub.toolName,
256
378
  tool({
@@ -258,27 +380,35 @@ export function createFlowDeskFds1SchemaConversionProbeTools() {
258
380
  args,
259
381
  async execute(request) {
260
382
  return JSON.stringify(runFlowDeskPreSpikePluginToolStub(stub.toolName, request));
261
- }
262
- })
383
+ },
384
+ }),
263
385
  ];
264
386
  }));
265
387
  }
266
- export function createFlowDeskLocalNonDispatchAdapterTools(now = new Date(), session = createFlowDeskLocalNonDispatchAdapterSession(now)) {
388
+ export function createFlowDeskLocalNonDispatchAdapterTools(now = new Date(), session = createFlowDeskLocalNonDispatchAdapterSession(now), managedDispatchRunRoute = {}) {
267
389
  return Object.fromEntries(FLOWDESK_PRE_SPIKE_PLUGIN_TOOL_STUBS.map((stub) => {
268
390
  const artifact = getRelease1SchemaArtifact(stub.requestSchemaId);
269
391
  if (artifact === undefined)
270
392
  throw new Error(`missing FDS-1 schema artifact: ${stub.requestSchemaId}`);
271
393
  const required = new Set(artifact.required);
272
- const args = Object.fromEntries(Object.keys(artifact.properties).map((fieldName) => [fieldName, zodForSchemaProperty(stub.requestSchemaId, fieldName, required.has(fieldName))]));
394
+ const args = Object.fromEntries(Object.keys(artifact.properties).map((fieldName) => [
395
+ fieldName,
396
+ zodForSchemaProperty(stub.requestSchemaId, fieldName, required.has(fieldName)),
397
+ ]));
273
398
  return [
274
399
  stub.toolName,
275
400
  tool({
276
401
  description: `FlowDesk local non-dispatch command adapter for ${stub.toolName}; no provider call, real dispatch, or lane launch.`,
277
402
  args,
278
403
  async execute(request) {
404
+ const record = isRecord(request) ? request : {};
405
+ if (stub.toolName === "flowdesk_run" &&
406
+ record.run_mode === "managed-dispatch") {
407
+ return JSON.stringify(await evaluateFlowDeskManagedDispatchRunRoute(record, managedDispatchRunRoute));
408
+ }
279
409
  return JSON.stringify(session.evaluate(stub.toolName, request));
280
- }
281
- })
410
+ },
411
+ }),
282
412
  ];
283
413
  }));
284
414
  }
@@ -287,15 +417,18 @@ export function createFlowDeskNaturalLanguageRoutingTools(now = new Date(), sess
287
417
  if (artifact === undefined)
288
418
  throw new Error("missing FDS-1 schema artifact: flowdesk.chat_intake.request.v1");
289
419
  const required = new Set(artifact.required);
290
- const args = Object.fromEntries(Object.keys(artifact.properties).map((fieldName) => [fieldName, zodForSchemaProperty("flowdesk.chat_intake.request.v1", fieldName, required.has(fieldName))]));
420
+ const args = Object.fromEntries(Object.keys(artifact.properties).map((fieldName) => [
421
+ fieldName,
422
+ zodForSchemaProperty("flowdesk.chat_intake.request.v1", fieldName, required.has(fieldName)),
423
+ ]));
291
424
  return {
292
425
  [flowdeskChatIntakeToolName]: tool({
293
426
  description: "FlowDesk natural-language chat intake steering; command-backed only, with no provider call, real dispatch, lane launch, fallback, or hard chat control.",
294
427
  args,
295
428
  async execute(request) {
296
429
  return JSON.stringify(evaluateNaturalLanguageRouting(request, session));
297
- }
298
- })
430
+ },
431
+ }),
299
432
  };
300
433
  }
301
434
  function redactedManagedDispatchBetaToolResult(result) {
@@ -306,72 +439,823 @@ function redactedManagedDispatchBetaToolResult(result) {
306
439
  guardDecision: result.guardDecision,
307
440
  authority: result.authority,
308
441
  verification: result.verification,
309
- ...(result.dispatchAttempted ? {
310
- dispatchMethod: result.dispatchMethod,
311
- sessionId: result.sessionId,
312
- agent: result.agent,
313
- model: result.model,
314
- ...("redactedErrorCategory" in result ? { redactedErrorCategory: result.redactedErrorCategory } : {}),
315
- responseObserved: "response" in result && result.response !== undefined
316
- } : { redactedBlockReason: result.redactedBlockReason })
442
+ ...(result.dispatchAttempted
443
+ ? {
444
+ dispatchMethod: result.dispatchMethod,
445
+ sessionId: result.sessionId,
446
+ agent: result.agent,
447
+ model: result.model,
448
+ ...("redactedErrorCategory" in result
449
+ ? { redactedErrorCategory: result.redactedErrorCategory }
450
+ : {}),
451
+ responseObserved: "response" in result && result.response !== undefined,
452
+ }
453
+ : { redactedBlockReason: result.redactedBlockReason }),
317
454
  };
318
455
  }
319
- export function createFlowDeskManagedDispatchBetaOptInTools(client) {
456
+ function redactedExactModelProviderAcquisitionToolResult(result, cacheMaterialization) {
457
+ return {
458
+ adapterProfile: result.adapterProfile,
459
+ status: result.status,
460
+ providerCallAttempted: result.providerCallAttempted,
461
+ writeAttempted: result.writeAttempted,
462
+ evidenceReloaded: result.evidenceReloaded,
463
+ workflowId: result.workflowId,
464
+ evidenceId: result.evidenceId,
465
+ resultId: result.resultId,
466
+ providerQualifiedModelId: result.providerQualifiedModelId,
467
+ redactedBlockReason: result.redactedBlockReason,
468
+ authority: result.authority,
469
+ resultState: result.result?.state,
470
+ available: result.result?.available,
471
+ highestTierEligible: result.result?.highest_tier_eligible,
472
+ sanitizedProviderResultRef: result.result?.sanitized_provider_result_ref,
473
+ availabilityRef: result.result?.availability_ref,
474
+ blockedLabels: result.result?.blocked_labels,
475
+ ...(cacheMaterialization === undefined
476
+ ? {}
477
+ : {
478
+ cacheMaterialization: {
479
+ state: cacheMaterialization.result.state,
480
+ blockedLabels: cacheMaterialization.result.blocked_labels,
481
+ targetCacheEvidenceId: cacheMaterialization.options.targetCacheEvidenceId,
482
+ targetCacheRefreshPlanEvidenceId: cacheMaterialization.options.targetCacheRefreshPlanEvidenceId,
483
+ cacheId: cacheMaterialization.result.cache?.cache_id ??
484
+ cacheMaterialization.options.cacheId,
485
+ entryId: cacheMaterialization.result.cache?.entries[0]?.entry_id ??
486
+ cacheMaterialization.options.entryId,
487
+ availabilityRef: cacheMaterialization.result.cache?.entries[0]?.availability_ref,
488
+ sanitizedProviderResultRef: cacheMaterialization.result.cache?.entries[0]
489
+ ?.availability_ref === undefined
490
+ ? undefined
491
+ : result.result?.sanitized_provider_result_ref,
492
+ selectionState: cacheMaterialization.result.selection?.state,
493
+ pairSelectionReady: cacheMaterialization.result.selection?.state === "pair_ready",
494
+ ...(cacheMaterialization.fanout === undefined
495
+ ? {}
496
+ : {
497
+ reviewerFanoutPlanning: {
498
+ state: cacheMaterialization.fanout.result.state,
499
+ blockedLabels: cacheMaterialization.fanout.result.blocked_labels,
500
+ fanoutPlanState: cacheMaterialization.fanout.result.fanoutPlan.state,
501
+ plannedPerspectives: cacheMaterialization.fanout.result.fanoutPlan
502
+ .planned_perspectives,
503
+ runtimeLaneLaunchRequests: cacheMaterialization.fanout.result.fanoutPlan
504
+ .runtime_lane_launch_requests.length,
505
+ launchAttempted: cacheMaterialization.fanout.result.fanoutPlan
506
+ .launch_attempted,
507
+ approvalInferred: cacheMaterialization.fanout.result.fanoutPlan
508
+ .approval_inferred,
509
+ actualLaneLaunch: cacheMaterialization.fanout.result.fanoutPlan
510
+ .actualLaneLaunch,
511
+ providerCall: cacheMaterialization.fanout.result.fanoutPlan
512
+ .providerCall,
513
+ runtimeExecution: cacheMaterialization.fanout.result.fanoutPlan
514
+ .runtimeExecution,
515
+ persisted: cacheMaterialization.fanout.persisted,
516
+ persistedEvidenceId: cacheMaterialization.fanout.persistedEvidenceId,
517
+ persistErrors: cacheMaterialization.fanout.persistErrors,
518
+ ...(cacheMaterialization.fanout.runtimeLaunchPlans === undefined
519
+ ? {}
520
+ : {
521
+ runtimeLaunchPlanMaterialization: {
522
+ state: cacheMaterialization.fanout.runtimeLaunchPlans
523
+ .result.state,
524
+ blockedLabels: cacheMaterialization.fanout.runtimeLaunchPlans
525
+ .result.blocked_labels,
526
+ targetLaunchPlanEvidenceIds: cacheMaterialization.fanout.runtimeLaunchPlans
527
+ .options.targetLaunchPlanEvidenceIds,
528
+ launchPlanStates: cacheMaterialization.fanout.runtimeLaunchPlans
529
+ .result.launchPlans.map((plan) => plan.state),
530
+ launchPlanCount: cacheMaterialization.fanout.runtimeLaunchPlans
531
+ .result.launchPlans.length,
532
+ writeIntentCount: cacheMaterialization.fanout.runtimeLaunchPlans
533
+ .result.writeIntents.length,
534
+ launchAttempted: cacheMaterialization.fanout.runtimeLaunchPlans
535
+ .result.launchPlans.some((plan) => plan.launch_attempted),
536
+ actualLaneLaunch: cacheMaterialization.fanout.runtimeLaunchPlans
537
+ .result.actualLaneLaunch,
538
+ providerCall: cacheMaterialization.fanout.runtimeLaunchPlans
539
+ .result.providerCall,
540
+ runtimeExecution: cacheMaterialization.fanout.runtimeLaunchPlans
541
+ .result.runtimeExecution,
542
+ ...(cacheMaterialization.fanout.runtimeLaunchPlans
543
+ .runtimeReviewerExecution === undefined
544
+ ? {}
545
+ : {
546
+ runtimeReviewerExecution: {
547
+ status: cacheMaterialization.fanout
548
+ .runtimeLaunchPlans
549
+ .runtimeReviewerExecution.result
550
+ .status,
551
+ laneCount: cacheMaterialization.fanout
552
+ .runtimeLaunchPlans
553
+ .runtimeReviewerExecution.result
554
+ .laneCount,
555
+ acceptanceStatus: cacheMaterialization.fanout
556
+ .runtimeLaunchPlans
557
+ .runtimeReviewerExecution.result
558
+ .acceptanceStatus,
559
+ acceptedPerspectives: cacheMaterialization.fanout
560
+ .runtimeLaunchPlans
561
+ .runtimeReviewerExecution.result
562
+ .acceptedPerspectives,
563
+ durableLinkageStatus: cacheMaterialization.fanout
564
+ .runtimeLaunchPlans
565
+ .runtimeReviewerExecution.result
566
+ .durableLinkageStatus,
567
+ linkedVerdictCount: cacheMaterialization.fanout
568
+ .runtimeLaunchPlans
569
+ .runtimeReviewerExecution.result
570
+ .linkedVerdictCount,
571
+ linkedLifecycleCount: cacheMaterialization.fanout
572
+ .runtimeLaunchPlans
573
+ .runtimeReviewerExecution.result
574
+ .linkedLifecycleCount,
575
+ redactedBlockReason: cacheMaterialization.fanout
576
+ .runtimeLaunchPlans
577
+ .runtimeReviewerExecution.result
578
+ .redactedBlockReason,
579
+ },
580
+ }),
581
+ },
582
+ }),
583
+ },
584
+ }),
585
+ },
586
+ }),
587
+ };
588
+ }
589
+ function providerAcquisitionRuntimeReviewerExecutionFromValue(value) {
590
+ if (!isRecord(value) || value.enabled !== true)
591
+ return undefined;
592
+ if (typeof value.attemptId !== "string" ||
593
+ value.attemptId.trim().length === 0 ||
594
+ typeof value.parentSessionId !== "string" ||
595
+ value.parentSessionId.trim().length === 0 ||
596
+ !isRecord(value.consumedReviewerFanoutApproval))
597
+ return undefined;
598
+ const expectations = runtimeReviewerExecutionExpectationsFromValue(value.verdictExpectations);
599
+ if (expectations === undefined || expectations.length === 0)
600
+ return undefined;
601
+ return {
602
+ enabled: true,
603
+ attemptId: value.attemptId,
604
+ parentSessionId: value.parentSessionId,
605
+ ...(typeof value.observedAt === "string" && value.observedAt.trim().length > 0
606
+ ? { observedAt: value.observedAt }
607
+ : {}),
608
+ consumedReviewerFanoutApproval: value.consumedReviewerFanoutApproval,
609
+ verdictExpectations: expectations,
610
+ };
611
+ }
612
+ function providerAcquisitionRuntimeLaunchPlanMaterializationFromValue(value) {
613
+ if (!isRecord(value) || value.enabled !== true)
614
+ return undefined;
615
+ if (!Array.isArray(value.targetLaunchPlanEvidenceIds) ||
616
+ value.targetLaunchPlanEvidenceIds.length === 0 ||
617
+ !value.targetLaunchPlanEvidenceIds.every((entry) => typeof entry === "string" && entry.trim().length > 0))
618
+ return undefined;
619
+ const runtimeReviewerExecution = providerAcquisitionRuntimeReviewerExecutionFromValue(value.runtimeReviewerExecution);
620
+ return {
621
+ enabled: true,
622
+ targetLaunchPlanEvidenceIds: value.targetLaunchPlanEvidenceIds,
623
+ ...(typeof value.sdkClientAvailable === "boolean"
624
+ ? { sdkClientAvailable: value.sdkClientAvailable }
625
+ : {}),
626
+ ...(typeof value.durableEvidenceRootRef === "string" &&
627
+ value.durableEvidenceRootRef.trim().length > 0
628
+ ? { durableEvidenceRootRef: value.durableEvidenceRootRef }
629
+ : {}),
630
+ ...(runtimeReviewerExecution === undefined
631
+ ? {}
632
+ : { runtimeReviewerExecution }),
633
+ };
634
+ }
635
+ function providerAcquisitionReviewerFanoutPlanningFromValue(value) {
636
+ if (!isRecord(value) || value.enabled !== true)
637
+ return undefined;
638
+ if (typeof value.attemptId !== "string" ||
639
+ value.attemptId.trim().length === 0 ||
640
+ typeof value.parentSessionRef !== "string" ||
641
+ value.parentSessionRef.trim().length === 0 ||
642
+ typeof value.agentRef !== "string" ||
643
+ value.agentRef.trim().length === 0)
644
+ return undefined;
645
+ const runtimeLaunchPlanMaterialization = providerAcquisitionRuntimeLaunchPlanMaterializationFromValue(value.runtimeLaunchPlanMaterialization);
646
+ return {
647
+ enabled: true,
648
+ attemptId: value.attemptId,
649
+ parentSessionRef: value.parentSessionRef,
650
+ agentRef: value.agentRef,
651
+ ...(typeof value.requestedAt === "string" && value.requestedAt.trim().length > 0
652
+ ? { requestedAt: value.requestedAt }
653
+ : {}),
654
+ ...(Array.isArray(value.requestedPerspectives) &&
655
+ value.requestedPerspectives.every((entry) => typeof entry === "string")
656
+ ? {
657
+ requestedPerspectives: value.requestedPerspectives,
658
+ }
659
+ : {}),
660
+ ...(typeof value.maxConcurrentLaneCount === "number"
661
+ ? { maxConcurrentLaneCount: value.maxConcurrentLaneCount }
662
+ : {}),
663
+ ...(typeof value.timeoutMs === "number" ? { timeoutMs: value.timeoutMs } : {}),
664
+ ...(typeof value.orphanMaxAgeMs === "number"
665
+ ? { orphanMaxAgeMs: value.orphanMaxAgeMs }
666
+ : {}),
667
+ ...(typeof value.retryBudget === "number"
668
+ ? { retryBudget: value.retryBudget }
669
+ : {}),
670
+ ...(typeof value.preLaunchAuditRef === "string"
671
+ ? { preLaunchAuditRef: value.preLaunchAuditRef }
672
+ : {}),
673
+ ...(typeof value.laneLaunchApprovalRef === "string"
674
+ ? { laneLaunchApprovalRef: value.laneLaunchApprovalRef }
675
+ : {}),
676
+ ...(value.persistDerivedFanoutPlanEvidence === true
677
+ ? { persistDerivedFanoutPlanEvidence: true }
678
+ : {}),
679
+ ...(typeof value.fanoutPlanEvidenceId === "string" &&
680
+ value.fanoutPlanEvidenceId.trim().length > 0
681
+ ? { fanoutPlanEvidenceId: value.fanoutPlanEvidenceId }
682
+ : {}),
683
+ ...(runtimeLaunchPlanMaterialization === undefined
684
+ ? {}
685
+ : { runtimeLaunchPlanMaterialization }),
686
+ };
687
+ }
688
+ function exactModelProviderAcquisitionCacheMaterializationFromOptions(options) {
689
+ const value = options?.[flowdeskExactModelProviderAcquisitionLiveTestOption];
690
+ if (!isRecord(value) || !isRecord(value.cacheMaterialization))
691
+ return undefined;
692
+ const cacheMaterialization = value.cacheMaterialization;
693
+ if (cacheMaterialization.enabled !== true ||
694
+ typeof cacheMaterialization.targetCacheEvidenceId !== "string" ||
695
+ cacheMaterialization.targetCacheEvidenceId.trim().length === 0 ||
696
+ typeof cacheMaterialization.targetCacheRefreshPlanEvidenceId !== "string" ||
697
+ cacheMaterialization.targetCacheRefreshPlanEvidenceId.trim().length === 0)
698
+ return undefined;
699
+ const reviewerFanoutPlanning = providerAcquisitionReviewerFanoutPlanningFromValue(cacheMaterialization.reviewerFanoutPlanning);
700
+ return {
701
+ enabled: true,
702
+ targetCacheEvidenceId: cacheMaterialization.targetCacheEvidenceId,
703
+ targetCacheRefreshPlanEvidenceId: cacheMaterialization.targetCacheRefreshPlanEvidenceId,
704
+ ...(typeof cacheMaterialization.cacheId === "string" &&
705
+ cacheMaterialization.cacheId.trim().length > 0
706
+ ? { cacheId: cacheMaterialization.cacheId }
707
+ : {}),
708
+ ...(typeof cacheMaterialization.entryId === "string" &&
709
+ cacheMaterialization.entryId.trim().length > 0
710
+ ? { entryId: cacheMaterialization.entryId }
711
+ : {}),
712
+ ...(reviewerFanoutPlanning === undefined
713
+ ? {}
714
+ : { reviewerFanoutPlanning }),
715
+ };
716
+ }
717
+ function providerAcquisitionFanoutPlanEvidenceId(workflowId, plan) {
718
+ return safeToken(`reviewer-fanout-plan-${workflowId}-${plan.fanoutPlan.attempt_id}-${plan.fanoutPlan.cache_id ?? "cache"}`, "reviewer-fanout-plan");
719
+ }
720
+ function persistProviderAcquisitionReviewerFanoutPlanEvidence(input) {
721
+ if (input.plan.state !== "fanout_ready" || !input.plan.ok)
722
+ return { persisted: false, errors: ["reviewer fanout plan is not ready"] };
723
+ const prepared = prepareFlowDeskSessionEvidenceWriteIntentV1({
724
+ workflowId: input.workflowId,
725
+ evidenceId: input.evidenceId,
726
+ record: input.plan.fanoutPlan,
727
+ });
728
+ if (!prepared.ok || prepared.writeIntent === undefined)
729
+ return { persisted: false, errors: prepared.errors };
730
+ const applied = applyFlowDeskSessionEvidenceWriteIntentsV1(input.rootDir, [
731
+ prepared.writeIntent,
732
+ ]);
733
+ return { persisted: applied.ok, errors: applied.errors };
734
+ }
735
+ function blockedManagedDispatchRunRoute(redactedBlockReason, defaultAuthorization) {
736
+ return {
737
+ runRouteProfile: "flowdesk_run_default_managed_dispatch_route",
738
+ status: "blocked_before_dispatch",
739
+ dispatchAttempted: false,
740
+ redactedBlockReason,
741
+ ...(defaultAuthorization === undefined
742
+ ? {}
743
+ : {
744
+ defaultManagedDispatchAuthorizationRef: defaultAuthorization.authorization_id,
745
+ defaultManagedDispatchReadinessRef: defaultAuthorization.readiness_ref,
746
+ }),
747
+ authority: { ...disabledAuthority, toolAuthority: false },
748
+ verification: {
749
+ defaultManagedDispatchAuthorizationRequired: true,
750
+ managedDispatchAdapterGateRequired: true,
751
+ defaultRelease1NonDispatchModesUnchanged: true,
752
+ },
753
+ };
754
+ }
755
+ async function evaluateFlowDeskManagedDispatchRunRoute(request, options = {}) {
756
+ const requestValidation = validateRunRequestV1(request);
757
+ if (!requestValidation.ok) {
758
+ return blockedManagedDispatchRunRoute("/flowdesk-run managed-dispatch requires a valid flowdesk.run.request.v1 envelope before dispatch routing.", options.defaultAuthorization);
759
+ }
760
+ const authorization = options.defaultAuthorization;
761
+ const authorizationResult = authorization === undefined
762
+ ? undefined
763
+ : validateFlowDeskDefaultManagedDispatchAuthorizationV1(authorization);
764
+ if (authorization === undefined ||
765
+ authorizationResult?.ok !== true ||
766
+ authorization.state !== "authorized" ||
767
+ authorization.default_managed_dispatch_authority_enabled !== true) {
768
+ return blockedManagedDispatchRunRoute("/flowdesk-run managed-dispatch requires a valid default managed-dispatch authorization.", authorization);
769
+ }
770
+ if (options.client === undefined) {
771
+ return blockedManagedDispatchRunRoute("/flowdesk-run managed-dispatch requires an injected OpenCode SDK client.", authorization);
772
+ }
773
+ if (!isRecord(request.managed_dispatch_boundary_input) ||
774
+ !isRecord(request.managed_dispatch_request) ||
775
+ !isRecord(request.managed_dispatch_manifest)) {
776
+ return blockedManagedDispatchRunRoute("/flowdesk-run managed-dispatch requires boundary input, dispatch request, and dispatch manifest records.", authorization);
777
+ }
778
+ const boundaryInput = request.managed_dispatch_boundary_input;
779
+ const reloadedEvidence = isRecord(request.managed_dispatch_reloaded_evidence)
780
+ ? request.managed_dispatch_reloaded_evidence
781
+ : options.durableStateRootDir !== undefined &&
782
+ typeof boundaryInput.workflowId === "string"
783
+ ? reloadFlowDeskSessionEvidenceV1({
784
+ workflowId: boundaryInput.workflowId,
785
+ rootDir: options.durableStateRootDir,
786
+ })
787
+ : undefined;
788
+ const result = await dispatchManagedDispatchBetaPromptV1({
789
+ client: options.client,
790
+ boundaryInput,
791
+ request: request.managed_dispatch_request,
792
+ dispatchManifest: request.managed_dispatch_manifest,
793
+ ...(reloadedEvidence === undefined ? {} : { reloadedEvidence }),
794
+ ...(options.reservationStore === undefined
795
+ ? {}
796
+ : { reservationStore: options.reservationStore }),
797
+ });
798
+ return {
799
+ runRouteProfile: "flowdesk_run_default_managed_dispatch_route",
800
+ defaultManagedDispatchAuthorizationRef: authorization.authorization_id,
801
+ defaultManagedDispatchReadinessRef: authorization.readiness_ref,
802
+ ...redactedManagedDispatchBetaToolResult(result),
803
+ };
804
+ }
805
+ export function createFlowDeskManagedDispatchBetaOptInTools(client, reservationStore, durableStateRootDir) {
320
806
  return {
321
807
  [flowdeskManagedDispatchBetaToolName]: tool({
322
808
  description: "FlowDesk Release 2 managed-dispatch beta opt-in tool; requires full Guard evidence and calls only the injected OpenCode SDK client without fallback.",
323
809
  args: {
324
- boundaryInput: tool.schema.record(tool.schema.string(), tool.schema.unknown()).describe("Complete ManagedDispatchBetaBoundaryInputV1 evidence bundle; no raw prompts or provider payloads."),
325
- request: tool.schema.record(tool.schema.string(), tool.schema.unknown()).describe("Bounded managed dispatch request with session, agent, provider-qualified model id, and approved prompt summary/text.")
810
+ boundaryInput: tool.schema
811
+ .record(tool.schema.string(), tool.schema.unknown())
812
+ .describe("Complete ManagedDispatchBetaBoundaryInputV1 evidence bundle; no raw prompts or provider payloads."),
813
+ request: tool.schema
814
+ .record(tool.schema.string(), tool.schema.unknown())
815
+ .describe("Bounded managed dispatch request with session, agent, provider-qualified model id, and approved prompt summary/text."),
816
+ dispatchManifest: tool.schema
817
+ .record(tool.schema.string(), tool.schema.unknown())
818
+ .describe("Committed dispatch attempt manifest binding approval, audit, idempotency, and disabled authority flags."),
819
+ reloadedEvidence: tool.schema
820
+ .record(tool.schema.string(), tool.schema.unknown())
821
+ .optional()
822
+ .describe("Optional preloaded durable session evidence. If omitted, FlowDesk reloads evidence from the configured durableStateRoot."),
326
823
  },
327
824
  async execute(input) {
328
825
  const record = isRecord(input) ? input : {};
329
- if (!isRecord(record.boundaryInput) || !isRecord(record.request)) {
826
+ if (!isRecord(record.boundaryInput) ||
827
+ !isRecord(record.request) ||
828
+ !isRecord(record.dispatchManifest)) {
330
829
  return JSON.stringify({
331
830
  adapterProfile: "managed_dispatch_beta_real_opencode_dispatch_adapter",
332
831
  status: "blocked_before_dispatch",
333
832
  dispatchAttempted: false,
334
- redactedBlockReason: "Managed-dispatch beta requires boundaryInput and request records.",
833
+ redactedBlockReason: "Managed-dispatch beta requires boundaryInput, request, and dispatchManifest records.",
335
834
  authority: { ...disabledAuthority, toolAuthority: false },
336
- verification: { defaultRelease1ServerBehaviorUnchanged: true }
835
+ verification: { defaultRelease1ServerBehaviorUnchanged: true },
337
836
  });
338
837
  }
838
+ const boundaryInput = record.boundaryInput;
839
+ const reloadedEvidence = isRecord(record.reloadedEvidence)
840
+ ? record.reloadedEvidence
841
+ : durableStateRootDir !== undefined &&
842
+ typeof boundaryInput.workflowId === "string"
843
+ ? reloadFlowDeskSessionEvidenceV1({
844
+ workflowId: boundaryInput.workflowId,
845
+ rootDir: durableStateRootDir,
846
+ })
847
+ : undefined;
339
848
  const result = await dispatchManagedDispatchBetaPromptV1({
340
849
  client,
341
- boundaryInput: record.boundaryInput,
342
- request: record.request
850
+ boundaryInput,
851
+ request: record.request,
852
+ dispatchManifest: record.dispatchManifest,
853
+ ...(reloadedEvidence === undefined ? {} : { reloadedEvidence }),
854
+ ...(reservationStore === undefined ? {} : { reservationStore }),
343
855
  });
344
856
  return JSON.stringify(redactedManagedDispatchBetaToolResult(result));
345
- }
346
- })
857
+ },
858
+ }),
347
859
  };
348
860
  }
349
- export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Date(), session = createFlowDeskLocalNonDispatchAdapterSession(now)) {
861
+ export function createFlowDeskExactModelProviderAcquisitionLiveTestOptInTools(client, rootDir, cacheMaterialization, runtimeReviewerExecutionClient) {
862
+ return {
863
+ [flowdeskExactModelProviderAcquisitionLiveTestToolName]: tool({
864
+ description: "FlowDesk exact-model provider acquisition live-test opt-in tool; performs one bounded provider availability check and writes sanitized session evidence without dispatch or reviewer launch.",
865
+ args: {
866
+ request: tool.schema
867
+ .record(tool.schema.string(), tool.schema.unknown())
868
+ .describe("Complete FlowDeskExactModelProviderAcquisitionLiveTestRequestV1 with acquisition_plan, profile/auth boundary refs, redaction proof, pre-call audit, idempotency, and exact provider-qualified model."),
869
+ },
870
+ async execute(input) {
871
+ const record = isRecord(input) ? input : {};
872
+ if (!isRecord(record.request)) {
873
+ return JSON.stringify({
874
+ adapterProfile: "exact_model_provider_acquisition_live_test_adapter",
875
+ status: "blocked_before_provider_acquisition",
876
+ providerCallAttempted: false,
877
+ writeAttempted: false,
878
+ evidenceReloaded: false,
879
+ redactedBlockReason: "Exact-model provider acquisition live-test requires a request record.",
880
+ authority: { ...disabledAuthority, toolAuthority: false },
881
+ });
882
+ }
883
+ const request = record.request;
884
+ const result = await runFlowDeskExactModelProviderAcquisitionLiveTestV1({
885
+ client,
886
+ rootDir,
887
+ request,
888
+ });
889
+ const materializationResult = cacheMaterialization !== undefined &&
890
+ result.evidenceReloaded === true
891
+ ? materializeFlowDeskExactModelCacheEvidenceFromProviderAcquisitionEvidenceV1({
892
+ reloadedEvidence: reloadFlowDeskSessionEvidenceV1({
893
+ workflowId: request.workflowId,
894
+ rootDir,
895
+ }),
896
+ workflowId: request.workflowId,
897
+ providerAcquisitionEvidenceId: request.evidenceId,
898
+ targetCacheEvidenceId: cacheMaterialization.targetCacheEvidenceId,
899
+ targetCacheRefreshPlanEvidenceId: cacheMaterialization.targetCacheRefreshPlanEvidenceId,
900
+ ...(cacheMaterialization.cacheId === undefined
901
+ ? {}
902
+ : { cacheId: cacheMaterialization.cacheId }),
903
+ ...(cacheMaterialization.entryId === undefined
904
+ ? {}
905
+ : { entryId: cacheMaterialization.entryId }),
906
+ rootDir,
907
+ localDate: request.localDate,
908
+ activeProfileRef: request.activeProfileRef,
909
+ opencodeVersionRef: request.opencodeVersionRef,
910
+ flowdeskPackageVersionRef: request.flowdeskPackageVersionRef,
911
+ registryHash: request.registryHash,
912
+ policyPackHash: request.policyPackHash,
913
+ authAccountBoundaryRef: request.authAccountBoundaryRef,
914
+ })
915
+ : undefined;
916
+ const fanoutResult = cacheMaterialization?.reviewerFanoutPlanning !== undefined &&
917
+ materializationResult?.state === "materialized" &&
918
+ materializationResult.reloadedEvidence !== undefined
919
+ ? planFlowDeskReviewerFanoutFromReloadedCacheEvidenceV1({
920
+ reloadedEvidence: materializationResult.reloadedEvidence,
921
+ workflowId: request.workflowId,
922
+ localDate: request.localDate,
923
+ activeProfileRef: request.activeProfileRef,
924
+ opencodeVersionRef: request.opencodeVersionRef,
925
+ flowdeskPackageVersionRef: request.flowdeskPackageVersionRef,
926
+ registryHash: request.registryHash,
927
+ policyPackHash: request.policyPackHash,
928
+ authAccountBoundaryRef: request.authAccountBoundaryRef,
929
+ attemptId: cacheMaterialization.reviewerFanoutPlanning.attemptId,
930
+ parentSessionRef: cacheMaterialization.reviewerFanoutPlanning
931
+ .parentSessionRef,
932
+ agentRef: cacheMaterialization.reviewerFanoutPlanning.agentRef,
933
+ requestedAt: cacheMaterialization.reviewerFanoutPlanning.requestedAt ??
934
+ request.observedAt,
935
+ ...(cacheMaterialization.reviewerFanoutPlanning
936
+ .requestedPerspectives === undefined
937
+ ? {}
938
+ : {
939
+ requestedPerspectives: cacheMaterialization.reviewerFanoutPlanning
940
+ .requestedPerspectives,
941
+ }),
942
+ ...(cacheMaterialization.reviewerFanoutPlanning
943
+ .maxConcurrentLaneCount === undefined
944
+ ? {}
945
+ : {
946
+ maxConcurrentLaneCount: cacheMaterialization.reviewerFanoutPlanning
947
+ .maxConcurrentLaneCount,
948
+ }),
949
+ ...(cacheMaterialization.reviewerFanoutPlanning.timeoutMs ===
950
+ undefined
951
+ ? {}
952
+ : {
953
+ timeoutMs: cacheMaterialization.reviewerFanoutPlanning.timeoutMs,
954
+ }),
955
+ ...(cacheMaterialization.reviewerFanoutPlanning.orphanMaxAgeMs ===
956
+ undefined
957
+ ? {}
958
+ : {
959
+ orphanMaxAgeMs: cacheMaterialization.reviewerFanoutPlanning.orphanMaxAgeMs,
960
+ }),
961
+ ...(cacheMaterialization.reviewerFanoutPlanning.retryBudget ===
962
+ undefined
963
+ ? {}
964
+ : {
965
+ retryBudget: cacheMaterialization.reviewerFanoutPlanning.retryBudget,
966
+ }),
967
+ ...(cacheMaterialization.reviewerFanoutPlanning
968
+ .preLaunchAuditRef === undefined
969
+ ? {}
970
+ : {
971
+ preLaunchAuditRef: cacheMaterialization.reviewerFanoutPlanning
972
+ .preLaunchAuditRef,
973
+ }),
974
+ ...(cacheMaterialization.reviewerFanoutPlanning
975
+ .laneLaunchApprovalRef === undefined
976
+ ? {}
977
+ : {
978
+ laneLaunchApprovalRef: cacheMaterialization.reviewerFanoutPlanning
979
+ .laneLaunchApprovalRef,
980
+ }),
981
+ })
982
+ : undefined;
983
+ const fanoutPersistedEvidenceId = fanoutResult !== undefined &&
984
+ cacheMaterialization?.reviewerFanoutPlanning
985
+ ?.persistDerivedFanoutPlanEvidence === true
986
+ ? (cacheMaterialization.reviewerFanoutPlanning.fanoutPlanEvidenceId ??
987
+ providerAcquisitionFanoutPlanEvidenceId(request.workflowId, fanoutResult))
988
+ : undefined;
989
+ const fanoutPersistence = fanoutResult !== undefined && fanoutPersistedEvidenceId !== undefined
990
+ ? persistProviderAcquisitionReviewerFanoutPlanEvidence({
991
+ rootDir,
992
+ workflowId: request.workflowId,
993
+ evidenceId: fanoutPersistedEvidenceId,
994
+ plan: fanoutResult,
995
+ })
996
+ : { persisted: false, errors: [] };
997
+ const runtimeLaunchPlanOptions = cacheMaterialization?.reviewerFanoutPlanning
998
+ ?.runtimeLaunchPlanMaterialization;
999
+ const runtimeLaunchPlanResult = runtimeLaunchPlanOptions !== undefined &&
1000
+ fanoutPersistedEvidenceId !== undefined &&
1001
+ fanoutPersistence.persisted === true
1002
+ ? materializeFlowDeskRuntimeLaneLaunchPlansFromReviewerFanoutEvidenceV1({
1003
+ reloadedEvidence: reloadFlowDeskSessionEvidenceV1({
1004
+ workflowId: request.workflowId,
1005
+ rootDir,
1006
+ }),
1007
+ workflowId: request.workflowId,
1008
+ reviewerFanoutEvidenceId: fanoutPersistedEvidenceId,
1009
+ targetLaunchPlanEvidenceIds: runtimeLaunchPlanOptions.targetLaunchPlanEvidenceIds,
1010
+ ...(runtimeLaunchPlanOptions.sdkClientAvailable === undefined
1011
+ ? {}
1012
+ : {
1013
+ sdkClientAvailable: runtimeLaunchPlanOptions.sdkClientAvailable,
1014
+ }),
1015
+ ...(runtimeLaunchPlanOptions.durableEvidenceRootRef === undefined
1016
+ ? {}
1017
+ : {
1018
+ durableEvidenceRootRef: runtimeLaunchPlanOptions.durableEvidenceRootRef,
1019
+ }),
1020
+ rootDir,
1021
+ })
1022
+ : undefined;
1023
+ const runtimeReviewerExecutionOptions = runtimeLaunchPlanOptions?.runtimeReviewerExecution;
1024
+ const runtimeReviewerExecutionResult = runtimeReviewerExecutionOptions !== undefined &&
1025
+ runtimeLaunchPlanResult?.state === "materialized" &&
1026
+ runtimeReviewerExecutionClient !== undefined
1027
+ ? await executeFlowDeskRuntimeReviewerExecutionBridgeV1({
1028
+ client: runtimeReviewerExecutionClient,
1029
+ rootDir,
1030
+ request: {
1031
+ workflowId: request.workflowId,
1032
+ attemptId: runtimeReviewerExecutionOptions.attemptId,
1033
+ parentSessionId: runtimeReviewerExecutionOptions.parentSessionId,
1034
+ allowActualLaneLaunch: true,
1035
+ observedAt: runtimeReviewerExecutionOptions.observedAt ??
1036
+ request.observedAt,
1037
+ consumedReviewerFanoutApproval: runtimeReviewerExecutionOptions.consumedReviewerFanoutApproval,
1038
+ verdictExpectations: runtimeReviewerExecutionOptions.verdictExpectations,
1039
+ },
1040
+ })
1041
+ : undefined;
1042
+ const redactedMaterialization = cacheMaterialization !== undefined && materializationResult !== undefined
1043
+ ? {
1044
+ options: cacheMaterialization,
1045
+ result: materializationResult,
1046
+ ...(cacheMaterialization.reviewerFanoutPlanning !== undefined &&
1047
+ fanoutResult !== undefined
1048
+ ? {
1049
+ fanout: {
1050
+ options: cacheMaterialization.reviewerFanoutPlanning,
1051
+ result: fanoutResult,
1052
+ persistedEvidenceId: fanoutPersistedEvidenceId,
1053
+ persisted: fanoutPersistence.persisted,
1054
+ persistErrors: fanoutPersistence.errors,
1055
+ ...(runtimeLaunchPlanOptions !== undefined &&
1056
+ runtimeLaunchPlanResult !== undefined
1057
+ ? {
1058
+ runtimeLaunchPlans: {
1059
+ options: runtimeLaunchPlanOptions,
1060
+ result: runtimeLaunchPlanResult,
1061
+ ...(runtimeReviewerExecutionOptions !==
1062
+ undefined &&
1063
+ runtimeReviewerExecutionResult !==
1064
+ undefined
1065
+ ? {
1066
+ runtimeReviewerExecution: {
1067
+ options: runtimeReviewerExecutionOptions,
1068
+ result: runtimeReviewerExecutionResult,
1069
+ },
1070
+ }
1071
+ : {}),
1072
+ },
1073
+ }
1074
+ : {}),
1075
+ },
1076
+ }
1077
+ : {}),
1078
+ }
1079
+ : undefined;
1080
+ return JSON.stringify(redactedExactModelProviderAcquisitionToolResult(result, redactedMaterialization));
1081
+ },
1082
+ }),
1083
+ };
1084
+ }
1085
+ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Date(), session = createFlowDeskLocalNonDispatchAdapterSession(now), stallAlert) {
350
1086
  const recentSuggestionCards = new Map();
1087
+ const recentStallAlerts = new Map();
351
1088
  return async function message(input, output) {
352
1089
  const inputRecord = isRecord(input) ? input : {};
353
1090
  const request = intakeRequestFromChatMessage({ ...inputRecord, ...output });
354
1091
  const preview = previewNaturalLanguageRouting(request);
355
- if (preview.evaluation.response.route_decision === "continue_chat")
356
- return;
357
1092
  const nowMs = clockMs(now);
358
1093
  for (const [key, recordedAtMs] of recentSuggestionCards) {
359
- if (nowMs - recordedAtMs > flowdeskChatSuggestionDuplicateWindowMs || nowMs < recordedAtMs)
1094
+ if (nowMs - recordedAtMs > flowdeskChatSuggestionDuplicateWindowMs ||
1095
+ nowMs < recordedAtMs)
360
1096
  recentSuggestionCards.delete(key);
361
1097
  }
1098
+ for (const [key, recordedAtMs] of recentStallAlerts) {
1099
+ if (nowMs - recordedAtMs > flowdeskChatSuggestionDuplicateWindowMs ||
1100
+ nowMs < recordedAtMs)
1101
+ recentStallAlerts.delete(key);
1102
+ }
1103
+ const stallSummary = stallAlert
1104
+ ? await collectStallAlertSummary(stallAlert, now)
1105
+ : undefined;
1106
+ const stalledAlertReady = stallSummary !== undefined &&
1107
+ stallSummary.worstClassification === "stalled" &&
1108
+ stallSummary.totalStalled > 0;
1109
+ const lateAlertReady = stallSummary !== undefined &&
1110
+ stallAlert?.includeProgressingLate === true &&
1111
+ stallSummary.worstClassification === "progressing_late" &&
1112
+ stallSummary.totalLate > 0;
1113
+ const shouldAppendStallCard = stalledAlertReady || lateAlertReady;
1114
+ if (preview.evaluation.response.route_decision === "continue_chat") {
1115
+ if (shouldAppendStallCard && stallSummary !== undefined) {
1116
+ const key = stallAlertDuplicateKey(request, stallSummary);
1117
+ const previous = recentStallAlerts.get(key);
1118
+ recentStallAlerts.set(key, nowMs);
1119
+ if (previous === undefined ||
1120
+ nowMs - previous > flowdeskChatSuggestionDuplicateWindowMs) {
1121
+ if (!Array.isArray(output.parts))
1122
+ output.parts = [];
1123
+ output.parts.push({
1124
+ type: "text",
1125
+ text: stallAlertText(stallSummary),
1126
+ });
1127
+ }
1128
+ }
1129
+ return;
1130
+ }
362
1131
  if (!mayCreatePendingConfirmation(preview)) {
363
1132
  const duplicateKey = suggestionDuplicateKey(request, preview.evaluation.response);
364
1133
  const previousAtMs = recentSuggestionCards.get(duplicateKey);
365
1134
  recentSuggestionCards.set(duplicateKey, nowMs);
366
- if (previousAtMs !== undefined && nowMs - previousAtMs <= flowdeskChatSuggestionDuplicateWindowMs)
1135
+ if (previousAtMs !== undefined &&
1136
+ nowMs - previousAtMs <= flowdeskChatSuggestionDuplicateWindowMs)
367
1137
  return;
368
1138
  }
369
1139
  const result = evaluateNaturalLanguageRouting(request, session);
370
1140
  if (!Array.isArray(output.parts))
371
1141
  output.parts = [];
372
1142
  output.parts.push({ type: "text", text: steeringText(result) });
1143
+ if (shouldAppendStallCard && stallSummary !== undefined) {
1144
+ const key = stallAlertDuplicateKey(request, stallSummary);
1145
+ const previous = recentStallAlerts.get(key);
1146
+ recentStallAlerts.set(key, nowMs);
1147
+ if (previous === undefined ||
1148
+ nowMs - previous > flowdeskChatSuggestionDuplicateWindowMs)
1149
+ output.parts.push({
1150
+ type: "text",
1151
+ text: stallAlertText(stallSummary),
1152
+ });
1153
+ }
373
1154
  };
374
1155
  }
1156
+ async function collectStallAlertSummary(stallAlert, clock) {
1157
+ try {
1158
+ const observedAt = (typeof clock === "function" ? clock() : clock).toISOString();
1159
+ const config = {
1160
+ rootDir: stallAlert.rootDir,
1161
+ ...(stallAlert.maxWorkflows === undefined
1162
+ ? {}
1163
+ : { maxWorkflows: stallAlert.maxWorkflows }),
1164
+ ...(stallAlert.laneHeartbeatLateThresholdMs === undefined
1165
+ ? {}
1166
+ : {
1167
+ laneHeartbeatLateThresholdMs: stallAlert.laneHeartbeatLateThresholdMs,
1168
+ }),
1169
+ ...(stallAlert.laneHeartbeatStallThresholdMs === undefined
1170
+ ? {}
1171
+ : {
1172
+ laneHeartbeatStallThresholdMs: stallAlert.laneHeartbeatStallThresholdMs,
1173
+ }),
1174
+ };
1175
+ const result = await executeFlowDeskStatusLiveV1({
1176
+ config,
1177
+ now: () => new Date(observedAt),
1178
+ });
1179
+ if (result.status !== "status_live_collected")
1180
+ return undefined;
1181
+ const workflowSummaries = result.workflows
1182
+ .filter((workflow) => (workflow.stalledLaneCount ?? 0) > 0 ||
1183
+ (stallAlert.includeProgressingLate === true &&
1184
+ (workflow.progressingLateLaneCount ?? 0) > 0))
1185
+ .slice(0, 3)
1186
+ .map((workflow) => {
1187
+ const stalledEntry = workflow.laneStallProjection?.entries.find((entry) => entry.classification === "stalled");
1188
+ const lateEntry = workflow.laneStallProjection?.entries.find((entry) => entry.classification === "progressing_late");
1189
+ const primary = stalledEntry ?? lateEntry;
1190
+ return {
1191
+ workflowId: workflow.workflowId,
1192
+ stalledLaneCount: workflow.stalledLaneCount ?? 0,
1193
+ lateLaneCount: workflow.progressingLateLaneCount ?? 0,
1194
+ ...(primary?.secondsSinceLastSignal === undefined
1195
+ ? {}
1196
+ : { secondsSinceLastSignal: primary.secondsSinceLastSignal }),
1197
+ ...(primary?.laneId === undefined ? {} : { laneId: primary.laneId }),
1198
+ ...(primary?.failureHint === undefined
1199
+ ? {}
1200
+ : { failureHint: primary.failureHint }),
1201
+ };
1202
+ });
1203
+ return {
1204
+ worstClassification: result.worstLaneStallClassification ?? "unknown",
1205
+ totalStalled: result.totalStalledLaneCount ?? 0,
1206
+ totalLate: result.totalProgressingLateLaneCount ?? 0,
1207
+ workflowSummaries,
1208
+ };
1209
+ }
1210
+ catch {
1211
+ return undefined;
1212
+ }
1213
+ }
1214
+ function stallAlertDuplicateKey(request, summary) {
1215
+ const wf = summary.workflowSummaries
1216
+ .map((entry) => {
1217
+ const ageMinutes = typeof entry.secondsSinceLastSignal === "number"
1218
+ ? Math.floor(entry.secondsSinceLastSignal / 60)
1219
+ : -1;
1220
+ return `${entry.workflowId}:${entry.stalledLaneCount}:${ageMinutes}`;
1221
+ })
1222
+ .join("|");
1223
+ return `${safeToken(request.session_ref, "session")}|stall|${wf}|worst:${summary.worstClassification}`;
1224
+ }
1225
+ function stallAlertText(summary) {
1226
+ const lines = [];
1227
+ lines.push("FlowDesk");
1228
+ if (summary.worstClassification === "stalled") {
1229
+ lines.push(`Stalled lanes detected: ${summary.totalStalled} stalled, ${summary.totalLate} progressing-late.`);
1230
+ }
1231
+ else if (summary.worstClassification === "progressing_late") {
1232
+ lines.push(`Late-progressing lanes detected: ${summary.totalLate} late, ${summary.totalStalled} stalled.`);
1233
+ }
1234
+ else {
1235
+ lines.push(`Lane progress check: ${summary.totalStalled} stalled, ${summary.totalLate} progressing-late.`);
1236
+ }
1237
+ for (const workflow of summary.workflowSummaries.slice(0, 3)) {
1238
+ const secs = workflow.secondsSinceLastSignal ?? 0;
1239
+ const minutes = Math.floor(secs / 60);
1240
+ const hint = workflow.failureHint ?? "no recent heartbeat";
1241
+ const counts = workflow.stalledLaneCount > 0
1242
+ ? `${workflow.stalledLaneCount} stalled`
1243
+ : `${workflow.lateLaneCount} progressing-late`;
1244
+ lines.push(`- workflow ${workflow.workflowId}: ${counts} (last signal ~${minutes}m ago, ${hint}).`);
1245
+ }
1246
+ lines.push("FlowDesk does not auto-retry, auto-abort, or auto-fallback on stall.");
1247
+ lines.push("Safe next actions:");
1248
+ for (const action of [
1249
+ "/flowdesk-status",
1250
+ "/flowdesk-retry",
1251
+ "/flowdesk-resume",
1252
+ "/flowdesk-abort",
1253
+ "/flowdesk-doctor",
1254
+ "/flowdesk-export-debug",
1255
+ ])
1256
+ lines.push(`- ${action}`);
1257
+ return lines.join("\n");
1258
+ }
375
1259
  function isFds1SchemaConversionProbeEnabled(options) {
376
1260
  return options?.[flowdeskFds1SchemaConversionProbeOption] === true;
377
1261
  }
@@ -389,7 +1273,9 @@ function isNaturalLanguageRoutingEnabled(options) {
389
1273
  }
390
1274
  function durableStateRootFromOptions(options) {
391
1275
  const value = options?.[flowdeskDurableStateRootOption];
392
- return typeof value === "string" && value.trim().length > 0 ? value : undefined;
1276
+ return typeof value === "string" && value.trim().length > 0
1277
+ ? value
1278
+ : undefined;
393
1279
  }
394
1280
  function productionEnablementFromOptions(options) {
395
1281
  const value = options?.[flowdeskProductionEnablementOption];
@@ -397,39 +1283,963 @@ function productionEnablementFromOptions(options) {
397
1283
  return undefined;
398
1284
  return {
399
1285
  enabled: true,
400
- ...(typeof value.preDispatchAuditRef === "string" ? { preDispatchAuditRef: value.preDispatchAuditRef } : {}),
401
- ...(typeof value.configuredVerificationRef === "string" ? { configuredVerificationRef: value.configuredVerificationRef } : {}),
402
- ...(typeof value.externalAuthPolicyRef === "string" ? { externalAuthPolicyRef: value.externalAuthPolicyRef } : {}),
403
- ...(typeof value.providerPolicyRef === "string" ? { providerPolicyRef: value.providerPolicyRef } : {}),
404
- ...(Array.isArray(value.laneConformanceRefs) && value.laneConformanceRefs.every((ref) => typeof ref === "string") ? { laneConformanceRefs: value.laneConformanceRefs } : {}),
405
- ...(typeof value.allowIncompleteConformance === "boolean" ? { allowIncompleteConformance: value.allowIncompleteConformance } : {})
1286
+ ...(typeof value.preDispatchAuditRef === "string"
1287
+ ? { preDispatchAuditRef: value.preDispatchAuditRef }
1288
+ : {}),
1289
+ ...(typeof value.configuredVerificationRef === "string"
1290
+ ? { configuredVerificationRef: value.configuredVerificationRef }
1291
+ : {}),
1292
+ ...(isRecord(value.configuredVerificationResult)
1293
+ ? {
1294
+ configuredVerificationResult: value.configuredVerificationResult,
1295
+ }
1296
+ : {}),
1297
+ ...(typeof value.sanitizedAuthCaptureRef === "string"
1298
+ ? { sanitizedAuthCaptureRef: value.sanitizedAuthCaptureRef }
1299
+ : {}),
1300
+ ...(isRecord(value.sanitizedAuthCaptureResult)
1301
+ ? {
1302
+ sanitizedAuthCaptureResult: value.sanitizedAuthCaptureResult,
1303
+ }
1304
+ : {}),
1305
+ ...(typeof value.externalAuthPolicyRef === "string"
1306
+ ? { externalAuthPolicyRef: value.externalAuthPolicyRef }
1307
+ : {}),
1308
+ ...(typeof value.providerPolicyRef === "string"
1309
+ ? { providerPolicyRef: value.providerPolicyRef }
1310
+ : {}),
1311
+ ...(isRecord(value.externalAuthProviderPolicyResult)
1312
+ ? {
1313
+ externalAuthProviderPolicyResult: value.externalAuthProviderPolicyResult,
1314
+ }
1315
+ : {}),
1316
+ ...(Array.isArray(value.laneConformanceRefs) &&
1317
+ value.laneConformanceRefs.every((ref) => typeof ref === "string")
1318
+ ? { laneConformanceRefs: value.laneConformanceRefs }
1319
+ : {}),
1320
+ ...(typeof value.allowIncompleteConformance === "boolean"
1321
+ ? { allowIncompleteConformance: value.allowIncompleteConformance }
1322
+ : {}),
1323
+ ...(isRecord(value.approvalDecision)
1324
+ ? {
1325
+ approvalDecision: value.approvalDecision,
1326
+ }
1327
+ : {}),
406
1328
  };
407
1329
  }
1330
+ function reviewerFanoutDiagnosticsFromOptions(options) {
1331
+ const value = options?.[flowdeskReviewerFanoutDiagnosticsOption];
1332
+ if (!isRecord(value) || value.enabled !== true)
1333
+ return undefined;
1334
+ return value;
1335
+ }
408
1336
  function isManagedDispatchBetaAdapterEnabled(options) {
409
1337
  const value = options?.[flowdeskManagedDispatchBetaAdapterOption];
410
1338
  return value === true || (isRecord(value) && value.enabled === true);
411
1339
  }
1340
+ function isExactModelProviderAcquisitionLiveTestEnabled(options) {
1341
+ const value = options?.[flowdeskExactModelProviderAcquisitionLiveTestOption];
1342
+ return isRecord(value) && value.enabled === true;
1343
+ }
1344
+ function isRuntimeReviewerExecutionEnabled(options) {
1345
+ const value = options?.[flowdeskRuntimeReviewerExecutionOption];
1346
+ return isRecord(value) && value.enabled === true;
1347
+ }
1348
+ function defaultManagedDispatchAuthorizationFromOptions(options) {
1349
+ const value = options?.[flowdeskDefaultManagedDispatchAuthorizationOption];
1350
+ if (!isRecord(value))
1351
+ return undefined;
1352
+ const authorization = value;
1353
+ const validation = validateFlowDeskDefaultManagedDispatchAuthorizationV1(authorization);
1354
+ return validation.ok &&
1355
+ authorization.state === "authorized" &&
1356
+ authorization.default_managed_dispatch_authority_enabled === true
1357
+ ? authorization
1358
+ : undefined;
1359
+ }
1360
+ function isDefaultManagedDispatchAuthorized(options) {
1361
+ return defaultManagedDispatchAuthorizationFromOptions(options) !== undefined;
1362
+ }
412
1363
  function managedDispatchBetaClientFrom(input, options) {
413
1364
  const option = options?.[flowdeskManagedDispatchBetaAdapterOption];
414
1365
  if (isRecord(option) && isManagedDispatchBetaClient(option.client))
415
1366
  return option.client;
416
- return isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
1367
+ return isRecord(input) && isManagedDispatchBetaClient(input.client)
1368
+ ? input.client
1369
+ : undefined;
1370
+ }
1371
+ function managedDispatchBetaReservationStoreFrom(input, options) {
1372
+ const option = options?.[flowdeskManagedDispatchBetaAdapterOption];
1373
+ if (isRecord(option) &&
1374
+ isManagedDispatchBetaReservationStore(option.reservationStore))
1375
+ return option.reservationStore;
1376
+ return isRecord(input) &&
1377
+ isManagedDispatchBetaReservationStore(input.reservationStore)
1378
+ ? input.reservationStore
1379
+ : undefined;
1380
+ }
1381
+ function managedDispatchBetaDurableReservationStoreFrom(options) {
1382
+ const option = options?.[flowdeskManagedDispatchBetaAdapterOption];
1383
+ const optionRoot = isRecord(option) &&
1384
+ typeof option.durableStateRoot === "string" &&
1385
+ option.durableStateRoot.trim().length > 0
1386
+ ? option.durableStateRoot
1387
+ : undefined;
1388
+ const rootDir = optionRoot ?? durableStateRootFromOptions(options);
1389
+ return rootDir === undefined
1390
+ ? undefined
1391
+ : createFlowDeskManagedDispatchBetaDurableReservationStoreV1({ rootDir });
1392
+ }
1393
+ function exactModelProviderAcquisitionClientFrom(input, options) {
1394
+ const option = options?.[flowdeskExactModelProviderAcquisitionLiveTestOption];
1395
+ if (isRecord(option) && isExactModelProviderAcquisitionClient(option.client))
1396
+ return option.client;
1397
+ if (isRecord(input) && isExactModelProviderAcquisitionClient(input.exactModelProviderAcquisitionClient))
1398
+ return input.exactModelProviderAcquisitionClient;
1399
+ if (!isRecord(input))
1400
+ return undefined;
1401
+ const promptBackedCheck = isRecord(option) && isRecord(option.promptBackedCheck)
1402
+ ? option.promptBackedCheck
1403
+ : undefined;
1404
+ const commonOptions = {
1405
+ client: input.client,
1406
+ ...(typeof input.directory === "string" ? { directory: input.directory } : {}),
1407
+ ...(typeof input.workspace === "string" ? { workspace: input.workspace } : {}),
1408
+ };
1409
+ if (promptBackedCheck?.enabled === true) {
1410
+ const allowedProviderQualifiedModelIds = Array.isArray(promptBackedCheck.allowedProviderQualifiedModelIds)
1411
+ ? promptBackedCheck.allowedProviderQualifiedModelIds.filter((value) => typeof value === "string")
1412
+ : [];
1413
+ return createFlowDeskOpenCodePromptBackedProviderAcquisitionClientV1({
1414
+ ...commonOptions,
1415
+ allowProviderCall: promptBackedCheck.allowProviderCall === true,
1416
+ allowedProviderQualifiedModelIds,
1417
+ ...(typeof promptBackedCheck.sessionId === "string"
1418
+ ? { sessionId: promptBackedCheck.sessionId }
1419
+ : {}),
1420
+ ...(typeof promptBackedCheck.agent === "string"
1421
+ ? { agent: promptBackedCheck.agent }
1422
+ : {}),
1423
+ });
1424
+ }
1425
+ return createFlowDeskOpenCodeMetadataProviderAcquisitionClientV1(commonOptions);
1426
+ }
1427
+ function exactModelProviderAcquisitionRootFrom(options) {
1428
+ const option = options?.[flowdeskExactModelProviderAcquisitionLiveTestOption];
1429
+ const optionRoot = isRecord(option) && typeof option.durableStateRoot === "string" && option.durableStateRoot.trim().length > 0
1430
+ ? option.durableStateRoot
1431
+ : undefined;
1432
+ return optionRoot ?? durableStateRootFromOptions(options);
1433
+ }
1434
+ function exactModelProviderAcquisitionRuntimeReviewerExecutionClientFrom(input, options) {
1435
+ const option = options?.[flowdeskExactModelProviderAcquisitionLiveTestOption];
1436
+ if (isRecord(option) &&
1437
+ isManagedDispatchBetaClient(option.runtimeReviewerExecutionClient))
1438
+ return option.runtimeReviewerExecutionClient;
1439
+ if (isRecord(option) &&
1440
+ isManagedDispatchBetaClient(option.sdkClient))
1441
+ return option.sdkClient;
1442
+ return isRecord(input) && isManagedDispatchBetaClient(input.client)
1443
+ ? input.client
1444
+ : undefined;
1445
+ }
1446
+ function runtimeReviewerExecutionOptionsFrom(options) {
1447
+ const value = options?.[flowdeskRuntimeReviewerExecutionOption];
1448
+ if (!isRecord(value) || value.enabled !== true)
1449
+ return undefined;
1450
+ return {
1451
+ enabled: true,
1452
+ ...(typeof value.durableStateRoot === "string" &&
1453
+ value.durableStateRoot.trim().length > 0
1454
+ ? { durableStateRoot: value.durableStateRoot }
1455
+ : {}),
1456
+ ...(isManagedDispatchBetaClient(value.client) ? { client: value.client } : {}),
1457
+ };
1458
+ }
1459
+ function runtimeReviewerExecutionClientFrom(input, options) {
1460
+ const runtimeOptions = runtimeReviewerExecutionOptionsFrom(options);
1461
+ if (runtimeOptions?.client !== undefined)
1462
+ return runtimeOptions.client;
1463
+ return isRecord(input) && isManagedDispatchBetaClient(input.client)
1464
+ ? input.client
1465
+ : undefined;
1466
+ }
1467
+ function runtimeReviewerExecutionRootFrom(options) {
1468
+ const runtimeOptions = runtimeReviewerExecutionOptionsFrom(options);
1469
+ return runtimeOptions?.durableStateRoot ?? durableStateRootFromOptions(options);
1470
+ }
1471
+ export function createFlowDeskRuntimeReviewerExecutionOptInTools(client, rootDir) {
1472
+ return {
1473
+ [flowdeskRuntimeReviewerExecutionToolName]: tool({
1474
+ description: "FlowDesk explicit runtime reviewer execution bridge; launches persisted runtime lane plans through an injected SDK client and persists typed reviewer verdict/lifecycle evidence without default dispatch promotion.",
1475
+ args: {
1476
+ request: tool.schema
1477
+ .record(tool.schema.string(), tool.schema.unknown())
1478
+ .describe("Explicit runtime reviewer execution request with workflow/attempt/session refs, consumed reviewer-fanout approval, and one verdict expectation per persisted launch-plan evidence id."),
1479
+ },
1480
+ async execute(input) {
1481
+ const record = isRecord(input) ? input : {};
1482
+ if (!isRecord(record.request)) {
1483
+ return JSON.stringify(redactedRuntimeReviewerExecutionBlocked("Runtime reviewer execution requires a request record."));
1484
+ }
1485
+ const result = await executeFlowDeskRuntimeReviewerExecutionBridgeV1({
1486
+ client,
1487
+ rootDir,
1488
+ request: record.request,
1489
+ });
1490
+ return JSON.stringify(result);
1491
+ },
1492
+ }),
1493
+ };
1494
+ }
1495
+ function redactedQuickReviewerRunBlocked(reason) {
1496
+ return {
1497
+ adapterProfile: "quick_reviewer_run_helper",
1498
+ status: "blocked_before_quick_reviewer_run",
1499
+ laneCount: 0,
1500
+ lanes: [],
1501
+ redactedBlockReason: reason,
1502
+ safeNextActions: ["/flowdesk-status"],
1503
+ authority: {
1504
+ realOpenCodeDispatch: false,
1505
+ fallbackAuthority: false,
1506
+ hardCancelOrNoReplyAuthority: false,
1507
+ toolAuthority: false,
1508
+ providerCall: false,
1509
+ runtimeExecution: false,
1510
+ actualLaneLaunch: false,
1511
+ dispatchAuthorityEnabled: false,
1512
+ developerModeAcknowledged: false,
1513
+ quickReviewerRunExecuted: false,
1514
+ },
1515
+ };
1516
+ }
1517
+ function redactedQuickReviewerRunToolResult(result) {
1518
+ return {
1519
+ adapterProfile: result.adapterProfile,
1520
+ status: result.status,
1521
+ workflowId: result.workflowId,
1522
+ attemptId: result.attemptId,
1523
+ parentSessionId: result.parentSessionId,
1524
+ rootDir: result.rootDir,
1525
+ providerQualifiedModelId: result.providerQualifiedModelId,
1526
+ runtimeAgent: result.runtimeAgent,
1527
+ laneCount: result.laneCount,
1528
+ lanes: result.lanes,
1529
+ acceptanceStatus: result.acceptanceStatus,
1530
+ durableLinkageStatus: result.durableLinkageStatus,
1531
+ linkedVerdictCount: result.linkedVerdictCount,
1532
+ linkedLifecycleCount: result.linkedLifecycleCount,
1533
+ acceptedPerspectives: result.acceptedPerspectives,
1534
+ redactedBlockReason: result.redactedBlockReason,
1535
+ safeNextActions: result.safeNextActions,
1536
+ authority: result.authority,
1537
+ };
1538
+ }
1539
+ export function createFlowDeskQuickReviewerRunOptInTools(client, defaults) {
1540
+ return {
1541
+ [flowdeskQuickReviewerRunToolName]: tool({
1542
+ description: [
1543
+ "Run a 3-perspective FlowDesk reviewer fan-out (policy_security, architecture, verification_implementation) on a user-supplied prompt, pasted content, or the current conversation context, returning typed reviewer verdicts.",
1544
+ "WHEN TO USE: the user explicitly asks for a code review, security audit, multi-perspective check, critical review, design review, or quality review. Trigger on English phrases such as 'multi-perspective review', 'multi-angle review', 'critical review', 'review from multiple perspectives', 'audit', 'critique', 'assess', or 'evaluate'. Trigger on Korean phrases such as '다관점 리뷰', '다관점리뷰', '다관점 비판적리뷰', '다관점 비판적 리뷰', '다각도 리뷰', '다각도 검토', '여러 관점 리뷰', '여러 관점에서 검토', '복수 관점 리뷰', '비판적 리뷰', '비판적 검토', '심층 리뷰', '아키텍처 리뷰', '보안 리뷰', '품질 리뷰', '검토', or '점검'.",
1545
+ "CONTENT SELECTION: code is not required. If the user provides no explicit snippet after the trigger phrase, pass a concise prompt that includes the user's request plus the relevant current conversation context or target they are asking to review.",
1546
+ "WHEN NOT TO USE: general questions, plain chat, or any request that does not explicitly ask for a review, critique, audit, assessment, evaluation, 검토, 리뷰, 점검, or 비판.",
1547
+ "INVOKE WITH: developerModeAcknowledged=true and allowProviderCall=true. The plugin user has already opted into this tool at configuration time, so these flags must be set on every call. Do not ask the user for extra confirmation before calling; just call.",
1548
+ "AFTER CALLING: summarize the lane verdicts back to the user. acceptanceStatus=verdicts_accepted and durableLinkageStatus=durable_verdicts_accepted mean all three perspectives passed. Any lane with a redactedBlockReason indicates that perspective did not return a matching typed verdict.",
1549
+ "LANE HEARTBEAT: each reviewer lane automatically records one durable flowdesk.lane_heartbeat.v1 evidence record on launch through the runtime reviewer execution bridge; status_live and the chat.message stall card consume that heartbeat as the latest progress signal so the lane shows as progressing_normal while it is still working.",
1550
+ ].join(" "),
1551
+ args: {
1552
+ prompt: tool.schema
1553
+ .string()
1554
+ .describe("Plain-text review target to send to the reviewer lanes as evidence to review. This can be pasted code/content or a concise summary of the relevant current conversation context when no snippet is provided."),
1555
+ developerModeAcknowledged: tool.schema
1556
+ .boolean()
1557
+ .describe("Must be true to explicitly acknowledge that this is a developer-mode synthetic approval, not a production-grade reviewer fan-out."),
1558
+ allowProviderCall: tool.schema
1559
+ .boolean()
1560
+ .describe("Must be true to explicitly allow real provider calls for the reviewer lanes."),
1561
+ providerQualifiedModelId: tool.schema
1562
+ .string()
1563
+ .optional()
1564
+ .describe("Optional concrete provider/model id override. Defaults to the value configured in the quickReviewerRun plugin option."),
1565
+ runtimeAgent: tool.schema
1566
+ .string()
1567
+ .optional()
1568
+ .describe("Optional reviewer agent override. Defaults to the value configured in the quickReviewerRun plugin option."),
1569
+ perspectives: tool.schema
1570
+ .array(tool.schema.string())
1571
+ .optional()
1572
+ .describe("Optional subset of reviewer perspectives (policy_security, architecture, verification_implementation). Defaults to all three."),
1573
+ parentSessionId: tool.schema
1574
+ .string()
1575
+ .optional()
1576
+ .describe("Optional existing parent session id. If omitted, the tool creates a new top-level session via the injected SDK client."),
1577
+ },
1578
+ async execute(input) {
1579
+ const record = isRecord(input) ? input : {};
1580
+ const prompt = typeof record.prompt === "string" ? record.prompt : undefined;
1581
+ if (prompt === undefined)
1582
+ return JSON.stringify(redactedQuickReviewerRunBlocked("Quick reviewer run requires a prompt string."));
1583
+ const providerQualifiedModelId = typeof record.providerQualifiedModelId === "string" &&
1584
+ record.providerQualifiedModelId.trim().length > 0
1585
+ ? record.providerQualifiedModelId
1586
+ : defaults.providerQualifiedModelId;
1587
+ const runtimeAgent = typeof record.runtimeAgent === "string" &&
1588
+ record.runtimeAgent.trim().length > 0
1589
+ ? record.runtimeAgent
1590
+ : defaults.runtimeAgent;
1591
+ if (typeof providerQualifiedModelId !== "string" ||
1592
+ typeof runtimeAgent !== "string")
1593
+ return JSON.stringify(redactedQuickReviewerRunBlocked("Quick reviewer run requires providerQualifiedModelId and runtimeAgent (either as args or plugin defaults)."));
1594
+ const perspectives = Array.isArray(record.perspectives)
1595
+ ? record.perspectives.filter((value) => typeof value === "string")
1596
+ : undefined;
1597
+ const result = await executeFlowDeskQuickReviewerRunV1({
1598
+ client,
1599
+ prompt,
1600
+ providerQualifiedModelId,
1601
+ runtimeAgent,
1602
+ allowProviderCall: record.allowProviderCall === true,
1603
+ developerModeAcknowledged: record.developerModeAcknowledged === true,
1604
+ ...(perspectives === undefined ? {} : { perspectives }),
1605
+ ...(typeof record.parentSessionId === "string" &&
1606
+ record.parentSessionId.length > 0
1607
+ ? { parentSessionId: record.parentSessionId }
1608
+ : {}),
1609
+ ...(defaults.rootDir === undefined
1610
+ ? {}
1611
+ : { rootDir: defaults.rootDir }),
1612
+ ...(defaults.sourceLabel === undefined
1613
+ ? {}
1614
+ : { sourceLabel: defaults.sourceLabel }),
1615
+ });
1616
+ return JSON.stringify(redactedQuickReviewerRunToolResult(result));
1617
+ },
1618
+ }),
1619
+ };
1620
+ }
1621
+ function redactedManagedFallbackRegateBlocked(reason) {
1622
+ return {
1623
+ adapterProfile: "managed_fallback_regate_orchestrator",
1624
+ status: "blocked_before_regate_plan",
1625
+ dispatchAttempted: false,
1626
+ providerSwitchAttempted: false,
1627
+ sdkCallAttempted: false,
1628
+ redactedBlockReason: reason,
1629
+ safeNextActions: ["/flowdesk-status"],
1630
+ authority: {
1631
+ ...disabledAuthority,
1632
+ toolAuthority: false,
1633
+ automaticFallbackAuthorized: false,
1634
+ freshRegatePlanPrepared: false,
1635
+ },
1636
+ };
1637
+ }
1638
+ function redactedManagedFallbackRegateToolResult(result) {
1639
+ return {
1640
+ adapterProfile: result.adapterProfile,
1641
+ status: result.status,
1642
+ dispatchAttempted: result.dispatchAttempted,
1643
+ providerSwitchAttempted: result.providerSwitchAttempted,
1644
+ sdkCallAttempted: result.sdkCallAttempted,
1645
+ workflowId: result.workflowId,
1646
+ parentAttemptId: result.parentAttemptId,
1647
+ newAttemptId: result.newAttemptId,
1648
+ fromProviderQualifiedModelId: result.fromProviderQualifiedModelId,
1649
+ toProviderQualifiedModelId: result.toProviderQualifiedModelId,
1650
+ regatePlanState: result.regatePlan?.state,
1651
+ regatePlanOk: result.regatePlan?.ok,
1652
+ regatePlanErrors: result.regatePlan?.errors,
1653
+ requiredFreshEvidenceRefCount: Array.isArray(result.regatePlan?.required_fresh_evidence_refs)
1654
+ ? result.regatePlan.required_fresh_evidence_refs.length
1655
+ : undefined,
1656
+ requiredGuardDecisionRef: result.regatePlan?.required_guard_decision_ref,
1657
+ requiredApprovalRef: result.regatePlan?.required_approval_ref,
1658
+ requiredPreDispatchAuditRef: result.regatePlan?.required_pre_dispatch_audit_ref,
1659
+ policyEligibilityRef: result.regatePlan?.policy_eligibility_ref,
1660
+ runtimeCompatibilityRef: result.regatePlan?.runtime_compatibility_ref,
1661
+ consumedFallbackApprovalRef: result.regatePlan?.consumed_fallback_approval_ref,
1662
+ safeNextActions: result.safeNextActions,
1663
+ redactedBlockReason: result.redactedBlockReason,
1664
+ authority: { ...result.authority, toolAuthority: false },
1665
+ };
1666
+ }
1667
+ export function createFlowDeskManagedFallbackRegateOptInTools(rootDir) {
1668
+ return {
1669
+ [flowdeskManagedFallbackRegateToolName]: tool({
1670
+ description: "FlowDesk explicit opt-in managed fallback regate planning tool; converts a valid fallback decision plus consumed reviewer fallback approval into a fresh full re-gate plan without dispatch, provider switching, SDK calls, or runtime authority.",
1671
+ args: {
1672
+ decision: tool.schema
1673
+ .record(tool.schema.string(), tool.schema.unknown())
1674
+ .describe("Complete FlowDeskFallbackDecisionV1 with new attempt id, fresh evidence refs, fresh guard/approval/audit/policy/runtime-compatibility refs, and explicit automatic_fallback_authorized=false."),
1675
+ consumedApproval: tool.schema
1676
+ .record(tool.schema.string(), tool.schema.unknown())
1677
+ .describe("Consumed FlowDeskProductionApprovalSourceV1 with action_type=fallback_reselection bound to the new attempt id."),
1678
+ persistRegatePlanEvidence: tool.schema
1679
+ .boolean()
1680
+ .optional()
1681
+ .describe("When true and a configured durable state root is available, persist the resulting regate plan as fallback_regate_plan session evidence."),
1682
+ regatePlanEvidenceId: tool.schema
1683
+ .string()
1684
+ .optional()
1685
+ .describe("Evidence id to use when persisting the regate plan. Required when persistRegatePlanEvidence is true."),
1686
+ },
1687
+ async execute(input) {
1688
+ const record = isRecord(input) ? input : {};
1689
+ if (!isRecord(record.decision) ||
1690
+ !isRecord(record.consumedApproval)) {
1691
+ return JSON.stringify(redactedManagedFallbackRegateBlocked("Managed fallback regate requires both decision and consumedApproval records."));
1692
+ }
1693
+ const result = orchestrateFlowDeskManagedFallbackRegateV1({
1694
+ decision: record.decision,
1695
+ consumedApproval: record.consumedApproval,
1696
+ });
1697
+ const redacted = redactedManagedFallbackRegateToolResult(result);
1698
+ const persistRequested = record.persistRegatePlanEvidence === true;
1699
+ const evidenceId = typeof record.regatePlanEvidenceId === "string"
1700
+ ? record.regatePlanEvidenceId
1701
+ : undefined;
1702
+ if (persistRequested &&
1703
+ rootDir !== undefined &&
1704
+ result.regatePlan !== undefined &&
1705
+ result.status === "regate_plan_ready" &&
1706
+ evidenceId !== undefined &&
1707
+ evidenceId.trim().length > 0) {
1708
+ const persistResult = materializeFlowDeskManagedFallbackRegatePlanEvidenceV1({
1709
+ rootDir,
1710
+ regatePlan: result.regatePlan,
1711
+ evidenceId,
1712
+ });
1713
+ redacted.regatePlanEvidence = {
1714
+ status: persistResult.status,
1715
+ writeAttempted: persistResult.writeAttempted,
1716
+ evidenceReloaded: persistResult.evidenceReloaded,
1717
+ evidenceId: persistResult.evidenceId,
1718
+ redactedBlockReason: persistResult.redactedBlockReason,
1719
+ authority: persistResult.authority,
1720
+ };
1721
+ }
1722
+ return JSON.stringify(redacted);
1723
+ },
1724
+ }),
1725
+ };
1726
+ }
1727
+ function isManagedFallbackRegateEnabled(options) {
1728
+ const value = options?.[flowdeskManagedFallbackRegateOption];
1729
+ return value === true || (isRecord(value) && value.enabled === true);
1730
+ }
1731
+ function isQuickReviewerRunEnabled(options) {
1732
+ const value = options?.[flowdeskQuickReviewerRunOption];
1733
+ return value === true || (isRecord(value) && value.enabled === true);
1734
+ }
1735
+ function isProviderUsageLiveEnabled(options) {
1736
+ const value = options?.[flowdeskProviderUsageLiveOption];
1737
+ return value === true || (isRecord(value) && value.enabled === true);
1738
+ }
1739
+ function providerUsageLiveConfigFromOptions(options) {
1740
+ const value = options?.[flowdeskProviderUsageLiveOption];
1741
+ if (!isRecord(value) || value.enabled !== true)
1742
+ return undefined;
1743
+ const config = {};
1744
+ if (typeof value.homeDir === "string" && value.homeDir.trim().length > 0)
1745
+ config.homeDir = value.homeDir;
1746
+ if (Array.isArray(value.providers)) {
1747
+ const allowed = value.providers.filter((family) => family === "claude" || family === "openai" || family === "gemini");
1748
+ config.providers = allowed;
1749
+ }
1750
+ if (typeof value.claudeOAuthUsage === "boolean")
1751
+ config.claudeOAuthUsage = value.claudeOAuthUsage;
1752
+ if (typeof value.codexLiveUsage === "boolean")
1753
+ config.codexLiveUsage = value.codexLiveUsage;
1754
+ if (typeof value.geminiQuota === "boolean")
1755
+ config.geminiQuota = value.geminiQuota;
1756
+ if (typeof value.geminiOAuthClientId === "string" &&
1757
+ value.geminiOAuthClientId.trim().length > 0)
1758
+ config.geminiOAuthClientId = value.geminiOAuthClientId;
1759
+ if (typeof value.geminiOAuthClientSecret === "string" &&
1760
+ value.geminiOAuthClientSecret.trim().length > 0)
1761
+ config.geminiOAuthClientSecret = value.geminiOAuthClientSecret;
1762
+ if (typeof value.geminiProjectId === "string" &&
1763
+ value.geminiProjectId.trim().length > 0)
1764
+ config.geminiProjectId = value.geminiProjectId;
1765
+ return config;
1766
+ }
1767
+ function isQuickFallbackRunEnabled(options) {
1768
+ const value = options?.[flowdeskQuickFallbackRunOption];
1769
+ return value === true || (isRecord(value) && value.enabled === true);
1770
+ }
1771
+ function quickFallbackRunConfigFromOptions(options) {
1772
+ const value = options?.[flowdeskQuickFallbackRunOption];
1773
+ if (!isRecord(value) || value.enabled !== true)
1774
+ return undefined;
1775
+ const config = {};
1776
+ if (typeof value.defaultFromProvider === "string" &&
1777
+ value.defaultFromProvider.trim().length > 0)
1778
+ config.defaultFromProvider = value.defaultFromProvider;
1779
+ if (typeof value.defaultToProvider === "string" &&
1780
+ value.defaultToProvider.trim().length > 0)
1781
+ config.defaultToProvider = value.defaultToProvider;
1782
+ if (typeof value.sourceLabel === "string" && value.sourceLabel.trim().length > 0)
1783
+ config.sourceLabel = value.sourceLabel;
1784
+ const explicitRoot = typeof value.rootDir === "string" && value.rootDir.trim().length > 0
1785
+ ? value.rootDir
1786
+ : undefined;
1787
+ const root = explicitRoot ?? durableStateRootFromOptions(options);
1788
+ if (root !== undefined)
1789
+ config.rootDir = root;
1790
+ return config;
1791
+ }
1792
+ export function createFlowDeskQuickFallbackRunOptInTools(config) {
1793
+ return {
1794
+ [flowdeskQuickFallbackRunToolName]: tool({
1795
+ description: [
1796
+ "Plan a FlowDesk fallback regate from one provider to another by auto-building a developer-mode synthetic fallback decision and consumed fallback_reselection approval, then running the FlowDesk fallback regate orchestrator to produce a redacted regate plan. This tool plans, it does not switch providers or dispatch real lanes; FlowDesk default dispatch authority remains disabled.",
1797
+ "WHEN TO USE: the user explicitly says one provider is blocked, exhausted, slow, or otherwise unwanted and asks to retry the work on a different provider. Trigger on English phrases such as 'fallback to', 'switch to', 'retry with', 'try with another provider', 'use a different provider', 'this provider is blocked', and on Korean phrases such as '막혔어', '다른 걸로 다시', '다른 provider 로', '다른 모델로 재시도', '재시도 해줘', '바꿔서 다시', 'fallback 해줘', '다른 곳으로 돌려', 'OpenAI 로 다시', 'Claude 로 다시', 'Gemini 로 다시'.",
1798
+ "WHEN NOT TO USE: general usage/quota questions (use flowdesk_provider_usage_live), code review/audit requests (use flowdesk_quick_reviewer_run), workflow status questions (use flowdesk_status_live), or any request that does not explicitly ask to retry on a different provider.",
1799
+ "INVOKE WITH: fromProvider (concrete provider-qualified model id, e.g. 'claude/sonnet-4'), toProvider (concrete provider-qualified model id, e.g. 'openai/gpt-5.5'), optional reason ('provider_unhealthy', 'quota_exhausted', 'runtime_incompatible', 'policy_ineligible', or 'manual_reselection_requested'; defaults to 'manual_reselection_requested'), optional workflowId (auto-generated otherwise), and developerModeAcknowledged=true. The plugin user has already opted into this tool at configuration time, so the assistant should set developerModeAcknowledged=true automatically and call the tool directly without per-call confirmation. Optionally set persistRegatePlanEvidence=true to persist the resulting plan as durable session evidence.",
1800
+ "AFTER CALLING: summarize the orchestrator status to the user. On status=quick_fallback_run_completed, surface the new attempt id, regate plan state (typically 'full_regate_required'), required fresh evidence count, and remind the user that the actual provider switch is still blocked behind managed-dispatch promotion. On status=quick_fallback_run_incomplete, surface the regatePlanRedactedErrors or redactedBlockReason and suggest what evidence to refresh. Never echo raw provider/auth/token payloads.",
1801
+ ].join(" "),
1802
+ args: {
1803
+ fromProvider: tool.schema
1804
+ .string()
1805
+ .optional()
1806
+ .describe("Concrete provider-qualified model id the current attempt is on (e.g. 'claude/sonnet-4')."),
1807
+ toProvider: tool.schema
1808
+ .string()
1809
+ .optional()
1810
+ .describe("Concrete provider-qualified model id to switch to (e.g. 'openai/gpt-5.5')."),
1811
+ reason: tool.schema
1812
+ .string()
1813
+ .optional()
1814
+ .describe("Fallback reason label: 'provider_unhealthy', 'quota_exhausted', 'runtime_incompatible', 'policy_ineligible', or 'manual_reselection_requested'."),
1815
+ workflowId: tool.schema
1816
+ .string()
1817
+ .optional()
1818
+ .describe("Optional workflow id to bind the regate plan to. Auto-generated when omitted."),
1819
+ developerModeAcknowledged: tool.schema
1820
+ .boolean()
1821
+ .describe("Must be true to acknowledge that this tool synthesizes a developer-mode fallback_reselection approval. Not a production-grade approval."),
1822
+ persistRegatePlanEvidence: tool.schema
1823
+ .boolean()
1824
+ .optional()
1825
+ .describe("When true and a durable state root is configured, persist the resulting regate plan as fallback_regate_plan session evidence."),
1826
+ },
1827
+ async execute(input) {
1828
+ const request = isRecord(input)
1829
+ ? {
1830
+ fromProvider: typeof input.fromProvider === "string"
1831
+ ? input.fromProvider
1832
+ : undefined,
1833
+ toProvider: typeof input.toProvider === "string"
1834
+ ? input.toProvider
1835
+ : undefined,
1836
+ reason: typeof input.reason === "string" ? input.reason : undefined,
1837
+ workflowId: typeof input.workflowId === "string"
1838
+ ? input.workflowId
1839
+ : undefined,
1840
+ developerModeAcknowledged: typeof input.developerModeAcknowledged === "boolean"
1841
+ ? input.developerModeAcknowledged
1842
+ : undefined,
1843
+ persistRegatePlanEvidence: typeof input.persistRegatePlanEvidence === "boolean"
1844
+ ? input.persistRegatePlanEvidence
1845
+ : undefined,
1846
+ }
1847
+ : {};
1848
+ const result = await executeFlowDeskQuickFallbackRunV1({
1849
+ config,
1850
+ request,
1851
+ });
1852
+ return JSON.stringify(result);
1853
+ },
1854
+ }),
1855
+ };
1856
+ }
1857
+ function isLaneHeartbeatWriterEnabled(options) {
1858
+ const value = options?.[flowdeskLaneHeartbeatWriterOption];
1859
+ return value === true || (isRecord(value) && value.enabled === true);
1860
+ }
1861
+ function laneHeartbeatWriterConfigFromOptions(options) {
1862
+ const value = options?.[flowdeskLaneHeartbeatWriterOption];
1863
+ const enabledFromBool = value === true;
1864
+ const enabledFromRecord = isRecord(value) && value.enabled === true;
1865
+ if (!enabledFromBool && !enabledFromRecord)
1866
+ return undefined;
1867
+ const explicitRoot = isRecord(value) &&
1868
+ typeof value.rootDir === "string" &&
1869
+ value.rootDir.trim().length > 0
1870
+ ? value.rootDir
1871
+ : undefined;
1872
+ const root = explicitRoot ?? durableStateRootFromOptions(options);
1873
+ if (root === undefined)
1874
+ return undefined;
1875
+ const config = { rootDir: root };
1876
+ if (isRecord(value) &&
1877
+ typeof value.defaultExpectedIntervalMs === "number" &&
1878
+ value.defaultExpectedIntervalMs > 0)
1879
+ config.defaultExpectedIntervalMs = Math.floor(value.defaultExpectedIntervalMs);
1880
+ return config;
1881
+ }
1882
+ export function createFlowDeskLaneHeartbeatWriterOptInTools(config) {
1883
+ return {
1884
+ [flowdeskLaneHeartbeatWriterToolName]: tool({
1885
+ description: [
1886
+ "Record a durable FlowDesk lane heartbeat for a FlowDesk-owned lane (reviewer lane, runtime lane launch, provider acquisition lane, managed-dispatch attempt, fallback regate plan). Each call produces one validated flowdesk.lane_heartbeat.v1 record with a monotonically increasing heartbeat_seq per lane id, persisted as durable session evidence. Heartbeats are diagnostic evidence only and never approve dispatch, widen scope, or replace Guard. Default soft heartbeat interval is about 2 minutes; the 5-minute stall threshold lives in the stall projection.",
1887
+ "WHEN TO USE: a FlowDesk coordinator that owns the lane needs to prove it is still active during a long-running step. Trigger when the assistant is coordinating a FlowDesk lane and the previous heartbeat or lifecycle update was emitted close to the soft heartbeat interval (about 2 minutes by default), OR when the user explicitly asks to record/refresh a heartbeat. Also trigger on English phrases such as 'heartbeat', 'record heartbeat', 'emit heartbeat', 'mark progress', 'I'm still alive', 'lane is still progressing', 'heartbeat for the lane', and Korean phrases such as '하트비트 남겨줘', '하트비트 기록해줘', '심박 남겨줘', '심장박동 기록', '레인 살아 있다고 표시', '진행 신호 남겨줘', '진행 표시 해줘', '아직 살아 있다고 알려줘'.",
1888
+ "WHEN NOT TO USE: lifecycle transitions to terminal states (use lane_lifecycle materializers), reviewer verdict observations, provider-call evidence, dispatch authority changes, arbitrary OpenCode user-driven tool calls that FlowDesk did not launch, or any case where you do not have a stable FlowDesk lane id and parent session id.",
1889
+ "INVOKE WITH: workflowId, attemptId, laneId, parentSessionRef (must start with 'ses-'), agentRef (must start with 'agent-'), providerQualifiedModelId (concrete provider/model id), state (one of 'created', 'running', 'awaiting_dependency', 'cooldown'), and optional progressSummaryLabel (<=120 chars and redaction-safe), progressRef (starts with 'progress-' or 'heartbeat-progress-'), expectedIntervalMs, heartbeatSeq, observedAt. When heartbeatSeq is omitted the writer derives it from the latest heartbeat for the lane id. The plugin user already opted into this tool at configuration time, so do not ask the user for extra confirmation; just call.",
1890
+ "AFTER CALLING: confirm status=lane_heartbeat_recorded with the heartbeat_seq, observed_at, and expected_next_heartbeat_at. On status=blocked_before_lane_heartbeat surface the redactedBlockReason and suggest /flowdesk-status or /flowdesk-doctor. Never echo raw prompts, transcripts, provider payloads, runtime echo, or any other forbidden raw markers.",
1891
+ ].join(" "),
1892
+ args: {
1893
+ workflowId: tool.schema
1894
+ .string()
1895
+ .describe("Stable opaque FlowDesk workflow id bound to this lane."),
1896
+ attemptId: tool.schema
1897
+ .string()
1898
+ .describe("Stable opaque attempt id bound to this lane."),
1899
+ laneId: tool.schema
1900
+ .string()
1901
+ .describe("Stable opaque FlowDesk lane id (e.g. 'lane-quick-policy_security-...')."),
1902
+ parentSessionRef: tool.schema
1903
+ .string()
1904
+ .describe("Opaque ses-* ref for the parent session that owns this lane."),
1905
+ agentRef: tool.schema
1906
+ .string()
1907
+ .describe("Opaque agent-* ref for the FlowDesk agent profile."),
1908
+ providerQualifiedModelId: tool.schema
1909
+ .string()
1910
+ .describe("Concrete provider-qualified model id (e.g. 'openai/gpt-5.4-mini-fast')."),
1911
+ state: tool.schema
1912
+ .enum([
1913
+ "created",
1914
+ "running",
1915
+ "awaiting_dependency",
1916
+ "cooldown",
1917
+ ])
1918
+ .describe("Active lane state at heartbeat time."),
1919
+ progressSummaryLabel: tool.schema
1920
+ .string()
1921
+ .optional()
1922
+ .describe("Bounded redacted progress label (<=120 chars). No raw prompts, tool args, or transcripts."),
1923
+ progressRef: tool.schema
1924
+ .string()
1925
+ .optional()
1926
+ .describe("Opaque progress reference starting with 'progress-' or 'heartbeat-progress-'."),
1927
+ expectedIntervalMs: tool.schema
1928
+ .number()
1929
+ .optional()
1930
+ .describe("Override for expected next heartbeat interval in milliseconds; clamped to >=10s and <=24h."),
1931
+ heartbeatSeq: tool.schema
1932
+ .number()
1933
+ .optional()
1934
+ .describe("Optional explicit heartbeat sequence. When omitted, the writer increments from the latest persisted heartbeat for this lane id."),
1935
+ observedAt: tool.schema
1936
+ .string()
1937
+ .optional()
1938
+ .describe("ISO timestamp; defaults to now."),
1939
+ },
1940
+ async execute(input) {
1941
+ const record = isRecord(input) ? input : {};
1942
+ const request = {
1943
+ rootDir: config.rootDir,
1944
+ workflowId: typeof record.workflowId === "string" ? record.workflowId : "",
1945
+ attemptId: typeof record.attemptId === "string" ? record.attemptId : "",
1946
+ laneId: typeof record.laneId === "string" ? record.laneId : "",
1947
+ parentSessionRef: typeof record.parentSessionRef === "string"
1948
+ ? record.parentSessionRef
1949
+ : "",
1950
+ agentRef: typeof record.agentRef === "string" ? record.agentRef : "",
1951
+ providerQualifiedModelId: typeof record.providerQualifiedModelId === "string"
1952
+ ? record.providerQualifiedModelId
1953
+ : "",
1954
+ state: record.state === "created" ||
1955
+ record.state === "running" ||
1956
+ record.state === "awaiting_dependency" ||
1957
+ record.state === "cooldown"
1958
+ ? record.state
1959
+ : "running",
1960
+ ...(typeof record.progressSummaryLabel === "string"
1961
+ ? { progressSummaryLabel: record.progressSummaryLabel }
1962
+ : {}),
1963
+ ...(typeof record.progressRef === "string"
1964
+ ? { progressRef: record.progressRef }
1965
+ : {}),
1966
+ ...(typeof record.expectedIntervalMs === "number"
1967
+ ? { expectedIntervalMs: record.expectedIntervalMs }
1968
+ : config.defaultExpectedIntervalMs === undefined
1969
+ ? {}
1970
+ : { expectedIntervalMs: config.defaultExpectedIntervalMs }),
1971
+ ...(typeof record.heartbeatSeq === "number"
1972
+ ? { heartbeatSeq: record.heartbeatSeq }
1973
+ : {}),
1974
+ ...(typeof record.observedAt === "string"
1975
+ ? { observedAt: record.observedAt }
1976
+ : {}),
1977
+ };
1978
+ const result = recordFlowDeskLaneHeartbeatV1(request);
1979
+ return JSON.stringify(result);
1980
+ },
1981
+ }),
1982
+ };
1983
+ }
1984
+ function isStatusLiveEnabled(options) {
1985
+ const value = options?.[flowdeskStatusLiveOption];
1986
+ return value === true || (isRecord(value) && value.enabled === true);
1987
+ }
1988
+ function statusLiveConfigFromOptions(options) {
1989
+ const value = options?.[flowdeskStatusLiveOption];
1990
+ if (!isRecord(value) || value.enabled !== true)
1991
+ return undefined;
1992
+ const explicitRoot = typeof value.rootDir === "string" && value.rootDir.trim().length > 0
1993
+ ? value.rootDir
1994
+ : undefined;
1995
+ const fallbackRoot = durableStateRootFromOptions(options);
1996
+ const rootDir = explicitRoot ?? fallbackRoot;
1997
+ if (rootDir === undefined)
1998
+ return undefined;
1999
+ const config = { rootDir };
2000
+ if (typeof value.maxWorkflows === "number" && value.maxWorkflows > 0)
2001
+ config.maxWorkflows = Math.floor(value.maxWorkflows);
2002
+ if (typeof value.maxRecentEvidencePerClass === "number" &&
2003
+ value.maxRecentEvidencePerClass > 0)
2004
+ config.maxRecentEvidencePerClass = Math.floor(value.maxRecentEvidencePerClass);
2005
+ if (typeof value.laneHeartbeatLateThresholdMs === "number" &&
2006
+ value.laneHeartbeatLateThresholdMs > 0)
2007
+ config.laneHeartbeatLateThresholdMs = Math.floor(value.laneHeartbeatLateThresholdMs);
2008
+ if (typeof value.laneHeartbeatStallThresholdMs === "number" &&
2009
+ value.laneHeartbeatStallThresholdMs > 0)
2010
+ config.laneHeartbeatStallThresholdMs = Math.floor(value.laneHeartbeatStallThresholdMs);
2011
+ return config;
2012
+ }
2013
+ export function createFlowDeskStatusLiveOptInTools(config) {
2014
+ return {
2015
+ [flowdeskStatusLiveToolName]: tool({
2016
+ description: [
2017
+ "Return a live FlowDesk status summary by reloading durable session evidence under the configured FlowDesk state root, including reviewer verdict counts, reviewer fan-out plans, runtime lane lifecycle records, fallback regate plans, exact-model availability cache entries, provider acquisition results, and a lane heartbeat stall projection that classifies each FlowDesk-owned lane as progressing_normal, progressing_late, stalled, terminal, or unknown based on the most recent lifecycle update.",
2018
+ "WHEN TO USE: the user asks about recent FlowDesk activity, current workflow progress, recent reviewer results, ongoing or stalled runs, lanes that have stopped logging, lanes that look stuck, or what has been recorded so far. Trigger on English phrases such as 'status', 'what happened', 'recent activity', 'progress', 'where are we', 'how is it going', 'recent reviews', 'recent runs', 'is it stuck', 'stalled', 'no log', 'no update', 'is anything frozen', and Korean phrases such as '상태', '어디까지', '진행 상황', '진행됐', '오늘 작업', '오늘 뭐했', '최근 활동', '최근 리뷰', '지금 어디', '상태 요약', '워크플로우 상태', '멈춘 것 같아', '멈췄어', '응답이 없어', '아무 로그도 없', '오래 걸리는', '진행이 안돼'.",
2019
+ "WHEN NOT TO USE: provider usage/quota questions (use flowdesk_provider_usage_live), multi-perspective code reviews (use flowdesk_quick_reviewer_run), or unrelated general chat.",
2020
+ "INVOKE WITH: optional workflowId. When omitted, the tool lists the most recently modified durable workflows (default up to 5). The plugin user already opted in to durable status evidence reload at configuration time, so this tool can be called automatically without per-call confirmation.",
2021
+ "AFTER CALLING: summarize per-workflow durable evidence counts in plain language for the user. Mention reviewer verdict labels (pass / changes_required / blocked / inconclusive), lane lifecycle states (running, complete, invocation_failed), the most recent fallback_regate_plan state, the most recent provider acquisition status, and any stalled or progressing_late lanes reported in worstLaneStallClassification with totalStalledLaneCount and totalProgressingLateLaneCount. Per-lane entries inside laneStallProjection.entries can also carry expectedNextHeartbeatOverdue=true plus secondsPastExpectedNextHeartbeat to indicate that the heartbeat's own expected_next_heartbeat_at has passed, which is a diagnostic hint independent of the configurable stall threshold. If stalled lanes are present, surface the laneStallProjection safe next actions (/flowdesk-status, /flowdesk-retry, /flowdesk-resume, /flowdesk-abort, /flowdesk-doctor, /flowdesk-export-debug) without auto-retrying, auto-aborting, or auto-fallbacking on the user's behalf. If no workflow returned evidence, say so plainly. Never echo raw provider/auth/token payloads.",
2022
+ ].join(" "),
2023
+ args: {
2024
+ workflowId: tool.schema
2025
+ .string()
2026
+ .optional()
2027
+ .describe("Optional specific workflow id to summarize. When omitted, the most recently modified durable workflows are summarized."),
2028
+ },
2029
+ async execute(input) {
2030
+ const request = isRecord(input)
2031
+ ? {
2032
+ workflowId: typeof input.workflowId === "string"
2033
+ ? input.workflowId
2034
+ : undefined,
2035
+ }
2036
+ : {};
2037
+ const result = await executeFlowDeskStatusLiveV1({
2038
+ config,
2039
+ request,
2040
+ });
2041
+ return JSON.stringify(result);
2042
+ },
2043
+ }),
2044
+ };
2045
+ }
2046
+ export function createFlowDeskProviderUsageLiveOptInTools(config) {
2047
+ return {
2048
+ [flowdeskProviderUsageLiveToolName]: tool({
2049
+ description: [
2050
+ "Return live FlowDesk provider usage availability for Claude, OpenAI/Codex, and Gemini Code Assist using provider-native usage collectors. No estimation; reads OAuth credentials and provider rate-limit/quota APIs. Each provider row carries remainingPercent, alertLevel (ok/warning/critical/exhausted/stale/unknown), and a short recommendation. The top-level worstAlertLevel and overallRecommendation summarize the riskiest provider.",
2051
+ "WHEN TO USE: the user asks how much usage, quota, credit, limit, or budget they have left, when their quota resets, or whether a provider is available right now. Trigger on English phrases such as 'usage', 'quota', 'remaining', 'how much left', 'rate limit', 'reset', 'budget left', and on Korean phrases such as '사용량', '잔량', '남은 사용량', '얼마 남았어', '쿼터', '한도', '리셋', '남은거 얼마야', '남은 토큰', '사용 가능량'.",
2052
+ "ALSO PROACTIVELY USE: before starting a large multi-step task that depends on a specific provider (e.g. extensive refactor, long agentic loop, multi-perspective review), call this tool first to check whether the chosen provider has enough headroom; if worstAlertLevel is critical or exhausted, warn the user and suggest switching providers or waiting for reset.",
2053
+ "WHEN NOT TO USE: general chat, status of an in-progress workflow (use status instead), or any non-usage question.",
2054
+ "INVOKE WITH: optional providerFamily ('claude', 'openai', 'gemini', or 'all'; default 'all'). The plugin user has already opted in to provider-native usage collection at configuration time, so this tool can be called automatically without per-call confirmation.",
2055
+ "AFTER CALLING: summarize per-provider availability for the user in plain language. For each provider include the bucket label (claude-5h, claude-weekly, openai-gpt-5h, gemini-pro-5h, gemini-pro-weekly, gemini-flash-daily, gemini-flash-lite-daily), remainingPercent, reset time, alertLevel, and recommendation. If any provider returned non_dispatchable, exhausted, critical, stale, or unknown, note it explicitly and surface the overallRecommendation. Never echo raw tokens or raw payloads.",
2056
+ ].join(" "),
2057
+ args: {
2058
+ providerFamily: tool.schema
2059
+ .string()
2060
+ .optional()
2061
+ .describe("Provider to query: 'claude', 'openai', 'gemini', or 'all' (default)."),
2062
+ },
2063
+ async execute(input) {
2064
+ const request = isRecord(input)
2065
+ ? { providerFamily: typeof input.providerFamily === "string" ? input.providerFamily : undefined }
2066
+ : {};
2067
+ const result = await executeFlowDeskProviderUsageLiveV1({
2068
+ config,
2069
+ request,
2070
+ });
2071
+ return JSON.stringify(result);
2072
+ },
2073
+ }),
2074
+ };
2075
+ }
2076
+ function quickReviewerRunClientFrom(input, options) {
2077
+ const option = options?.[flowdeskQuickReviewerRunOption];
2078
+ if (isRecord(option) && isManagedDispatchBetaClient(option.client))
2079
+ return option.client;
2080
+ return isRecord(input) && isManagedDispatchBetaClient(input.client)
2081
+ ? input.client
2082
+ : undefined;
2083
+ }
2084
+ function quickReviewerRunDefaultsFromOptions(options) {
2085
+ const option = options?.[flowdeskQuickReviewerRunOption];
2086
+ if (!isRecord(option))
2087
+ return {};
2088
+ return {
2089
+ ...(typeof option.providerQualifiedModelId === "string" &&
2090
+ option.providerQualifiedModelId.trim().length > 0
2091
+ ? { providerQualifiedModelId: option.providerQualifiedModelId }
2092
+ : {}),
2093
+ ...(typeof option.runtimeAgent === "string" &&
2094
+ option.runtimeAgent.trim().length > 0
2095
+ ? { runtimeAgent: option.runtimeAgent }
2096
+ : {}),
2097
+ ...(typeof option.sourceLabel === "string" &&
2098
+ option.sourceLabel.trim().length > 0
2099
+ ? { sourceLabel: option.sourceLabel }
2100
+ : {}),
2101
+ ...(typeof option.durableStateRoot === "string" &&
2102
+ option.durableStateRoot.trim().length > 0
2103
+ ? { rootDir: option.durableStateRoot }
2104
+ : {}),
2105
+ };
417
2106
  }
418
2107
  const flowdeskServerPlugin = async (input, options) => {
419
- const localSession = isLocalNonDispatchAdapterEnabled(options) || isNaturalLanguageRoutingEnabled(options) ? createFlowDeskLocalNonDispatchAdapterSession(new Date(), undefined, { durableStateRootDir: durableStateRootFromOptions(options), productionEnablement: productionEnablementFromOptions(options) }) : undefined;
420
- const managedDispatchBetaClient = isManagedDispatchBetaAdapterEnabled(options) ? managedDispatchBetaClientFrom(input, options) : undefined;
2108
+ const localSession = isLocalNonDispatchAdapterEnabled(options) ||
2109
+ isNaturalLanguageRoutingEnabled(options)
2110
+ ? createFlowDeskLocalNonDispatchAdapterSession(new Date(), undefined, {
2111
+ durableStateRootDir: durableStateRootFromOptions(options),
2112
+ productionEnablement: productionEnablementFromOptions(options),
2113
+ reviewerFanoutDiagnostics: reviewerFanoutDiagnosticsFromOptions(options),
2114
+ })
2115
+ : undefined;
2116
+ const managedDispatchBetaClient = isManagedDispatchBetaAdapterEnabled(options)
2117
+ || isDefaultManagedDispatchAuthorized(options)
2118
+ ? managedDispatchBetaClientFrom(input, options)
2119
+ : undefined;
2120
+ const managedDispatchBetaReservationStore = isManagedDispatchBetaAdapterEnabled(options) || isDefaultManagedDispatchAuthorized(options)
2121
+ ? (managedDispatchBetaReservationStoreFrom(input, options) ??
2122
+ managedDispatchBetaDurableReservationStoreFrom(options))
2123
+ : undefined;
2124
+ const defaultAuthorization = defaultManagedDispatchAuthorizationFromOptions(options);
2125
+ const exactModelProviderAcquisitionClient = isExactModelProviderAcquisitionLiveTestEnabled(options)
2126
+ ? exactModelProviderAcquisitionClientFrom(input, options)
2127
+ : undefined;
2128
+ const exactModelProviderAcquisitionRoot = exactModelProviderAcquisitionRootFrom(options);
2129
+ const exactModelProviderAcquisitionCacheMaterialization = exactModelProviderAcquisitionCacheMaterializationFromOptions(options);
2130
+ const runtimeReviewerExecutionClient = isRuntimeReviewerExecutionEnabled(options)
2131
+ ? runtimeReviewerExecutionClientFrom(input, options)
2132
+ : undefined;
2133
+ const runtimeReviewerExecutionRoot = runtimeReviewerExecutionRootFrom(options);
421
2134
  const tools = {
422
2135
  [flowdeskPreSpikeDoctorToolName]: tool({
423
2136
  description: "Report FlowDesk plugin load status without enabling real dispatch, provider calls, or runtime execution.",
424
2137
  args: {},
425
2138
  async execute() {
2139
+ const providerUsageLiveConfigForDoctor = isProviderUsageLiveEnabled(options)
2140
+ ? providerUsageLiveConfigFromOptions(options)
2141
+ : undefined;
2142
+ const statusLiveConfigForDoctor = isStatusLiveEnabled(options)
2143
+ ? statusLiveConfigFromOptions(options)
2144
+ : undefined;
2145
+ const quickFallbackRunConfigForDoctor = isQuickFallbackRunEnabled(options)
2146
+ ? quickFallbackRunConfigFromOptions(options)
2147
+ : undefined;
2148
+ const laneHeartbeatWriterConfigForDoctor = isLaneHeartbeatWriterEnabled(options)
2149
+ ? laneHeartbeatWriterConfigFromOptions(options)
2150
+ : undefined;
2151
+ const quickReviewerRunRegistered = isQuickReviewerRunEnabled(options) &&
2152
+ quickReviewerRunClientFrom(input, options) !== undefined;
2153
+ const naturalLanguageTools = {
2154
+ quickReviewerRun: {
2155
+ enabled: isQuickReviewerRunEnabled(options),
2156
+ registered: quickReviewerRunRegistered,
2157
+ missingClient: isQuickReviewerRunEnabled(options) && !quickReviewerRunRegistered
2158
+ ? "injected OpenCode SDK client (input.client) is missing"
2159
+ : undefined,
2160
+ },
2161
+ providerUsageLive: {
2162
+ enabled: isProviderUsageLiveEnabled(options),
2163
+ registered: providerUsageLiveConfigForDoctor !== undefined,
2164
+ providers: providerUsageLiveConfigForDoctor?.providers,
2165
+ geminiOAuthConfigured: providerUsageLiveConfigForDoctor !== undefined &&
2166
+ ((providerUsageLiveConfigForDoctor.geminiOAuthClientId !==
2167
+ undefined &&
2168
+ providerUsageLiveConfigForDoctor.geminiOAuthClientSecret !==
2169
+ undefined) ||
2170
+ typeof process.env.FLOWDESK_GEMINI_OAUTH_CLIENT_ID ===
2171
+ "string" ||
2172
+ typeof process.env.FLOWDESK_GEMINI_OAUTH_CLIENT_SECRET ===
2173
+ "string"),
2174
+ hint: isProviderUsageLiveEnabled(options) &&
2175
+ providerUsageLiveConfigForDoctor === undefined
2176
+ ? "providerUsageLive.enabled=true but no provider family configured; set providers=['claude','openai','gemini']"
2177
+ : undefined,
2178
+ },
2179
+ statusLive: {
2180
+ enabled: isStatusLiveEnabled(options),
2181
+ registered: statusLiveConfigForDoctor !== undefined,
2182
+ rootDir: statusLiveConfigForDoctor?.rootDir,
2183
+ laneHeartbeatLateThresholdMs: statusLiveConfigForDoctor?.laneHeartbeatLateThresholdMs,
2184
+ laneHeartbeatStallThresholdMs: statusLiveConfigForDoctor?.laneHeartbeatStallThresholdMs,
2185
+ exposesLaneStallProjection: statusLiveConfigForDoctor !== undefined,
2186
+ exposesExpectedNextHeartbeatOverdueHint: statusLiveConfigForDoctor !== undefined,
2187
+ hint: isStatusLiveEnabled(options) &&
2188
+ statusLiveConfigForDoctor === undefined
2189
+ ? "statusLive.enabled=true but no durable state root resolved; set statusLive.rootDir or top-level durableStateRoot"
2190
+ : undefined,
2191
+ },
2192
+ quickFallbackRun: {
2193
+ enabled: isQuickFallbackRunEnabled(options),
2194
+ registered: quickFallbackRunConfigForDoctor !== undefined,
2195
+ defaultFromProvider: quickFallbackRunConfigForDoctor?.defaultFromProvider,
2196
+ defaultToProvider: quickFallbackRunConfigForDoctor?.defaultToProvider,
2197
+ persistsRegatePlanEvidence: quickFallbackRunConfigForDoctor?.rootDir !== undefined,
2198
+ },
2199
+ laneHeartbeatWriter: {
2200
+ enabled: isLaneHeartbeatWriterEnabled(options),
2201
+ registered: laneHeartbeatWriterConfigForDoctor !== undefined,
2202
+ rootDir: laneHeartbeatWriterConfigForDoctor?.rootDir,
2203
+ defaultExpectedIntervalMs: laneHeartbeatWriterConfigForDoctor?.defaultExpectedIntervalMs,
2204
+ hint: isLaneHeartbeatWriterEnabled(options) &&
2205
+ laneHeartbeatWriterConfigForDoctor === undefined
2206
+ ? "laneHeartbeatWriter.enabled=true but no durable state root resolved; set laneHeartbeatWriter.rootDir or top-level durableStateRoot"
2207
+ : undefined,
2208
+ },
2209
+ chatMessageStallAlert: {
2210
+ enabled: options?.[flowdeskChatMessageStallAlertOption] === true ||
2211
+ (isRecord(options?.[flowdeskChatMessageStallAlertOption]) &&
2212
+ (options?.[flowdeskChatMessageStallAlertOption])
2213
+ .enabled === true),
2214
+ registered: chatMessageStallAlertOptionsFrom(options, statusLiveConfigForDoctor) !==
2215
+ undefined,
2216
+ requires: "statusLive.enabled=true and durableStateRoot (top-level or chatMessageStallAlert.rootDir)",
2217
+ note: "chat.message hook appends a passive stall card listing stalled lanes and safe next actions; no auto-retry, auto-abort, or auto-fallback.",
2218
+ },
2219
+ };
426
2220
  return JSON.stringify({
427
2221
  pluginId: flowdeskPluginId,
428
2222
  loaded: true,
429
- probeRegistrationProfile: isFds1SchemaConversionProbeEnabled(options) ? "sandbox_conformance_probe_only" : "disabled",
430
- localNonDispatchAdapterProfile: isLocalNonDispatchAdapterEnabled(options) ? flowdeskLocalNonDispatchAdapterProfile : "disabled",
431
- naturalLanguageRoutingProfile: isNaturalLanguageRoutingEnabled(options) ? "chat_steering_command_backed_non_dispatch" : "disabled",
432
- productionPromotionGate: "release1_non_dispatch_command_registration_ready",
2223
+ probeRegistrationProfile: isFds1SchemaConversionProbeEnabled(options)
2224
+ ? "sandbox_conformance_probe_only"
2225
+ : "disabled",
2226
+ localNonDispatchAdapterProfile: isLocalNonDispatchAdapterEnabled(options)
2227
+ ? flowdeskLocalNonDispatchAdapterProfile
2228
+ : "disabled",
2229
+ naturalLanguageRoutingProfile: isNaturalLanguageRoutingEnabled(options)
2230
+ ? "chat_steering_command_backed_non_dispatch"
2231
+ : "disabled",
2232
+ naturalLanguageTools,
2233
+ productionPromotionGate: defaultAuthorization === undefined
2234
+ ? "release1_non_dispatch_command_registration_ready"
2235
+ : "default_managed_dispatch_authorized_registration_ready",
2236
+ defaultManagedDispatchRegistrationAuthorized: defaultAuthorization !== undefined,
2237
+ ...(defaultAuthorization === undefined
2238
+ ? {}
2239
+ : {
2240
+ defaultManagedDispatchAuthorizationRef: defaultAuthorization.authorization_id,
2241
+ defaultManagedDispatchReadinessRef: defaultAuthorization.readiness_ref,
2242
+ }),
433
2243
  productionOpenCodeRegistration: hasProductionOpenCodeRegistration(),
434
2244
  productionToolRegistration: flowdeskPluginScaffold.productionToolRegistration,
435
2245
  release1HandlerReadiness: getFlowDeskRelease1HandlerReadinessSummary(),
@@ -440,26 +2250,107 @@ const flowdeskServerPlugin = async (input, options) => {
440
2250
  runtimeExecution: false,
441
2251
  actualLaneLaunch: false,
442
2252
  fallbackAuthority: false,
443
- hardCancelOrNoReplyAuthority: false
2253
+ hardCancelOrNoReplyAuthority: false,
444
2254
  });
445
- }
446
- })
2255
+ },
2256
+ }),
447
2257
  };
448
2258
  if (isFds1SchemaConversionProbeEnabled(options))
449
2259
  Object.assign(tools, createFlowDeskFds1SchemaConversionProbeTools());
450
2260
  if (isLocalNonDispatchAdapterEnabled(options))
451
- Object.assign(tools, createFlowDeskLocalNonDispatchAdapterTools(new Date(), localSession));
2261
+ Object.assign(tools, createFlowDeskLocalNonDispatchAdapterTools(new Date(), localSession, {
2262
+ client: managedDispatchBetaClient,
2263
+ reservationStore: managedDispatchBetaReservationStore,
2264
+ durableStateRootDir: durableStateRootFromOptions(options),
2265
+ defaultAuthorization,
2266
+ }));
452
2267
  if (isNaturalLanguageRoutingEnabled(options))
453
2268
  Object.assign(tools, createFlowDeskNaturalLanguageRoutingTools(new Date(), localSession));
454
2269
  if (managedDispatchBetaClient !== undefined)
455
- Object.assign(tools, createFlowDeskManagedDispatchBetaOptInTools(managedDispatchBetaClient));
2270
+ Object.assign(tools, createFlowDeskManagedDispatchBetaOptInTools(managedDispatchBetaClient, managedDispatchBetaReservationStore, durableStateRootFromOptions(options)));
2271
+ if (exactModelProviderAcquisitionClient !== undefined && exactModelProviderAcquisitionRoot !== undefined)
2272
+ Object.assign(tools, createFlowDeskExactModelProviderAcquisitionLiveTestOptInTools(exactModelProviderAcquisitionClient, exactModelProviderAcquisitionRoot, exactModelProviderAcquisitionCacheMaterialization, runtimeReviewerExecutionClient ??
2273
+ exactModelProviderAcquisitionRuntimeReviewerExecutionClientFrom(input, options)));
2274
+ if (runtimeReviewerExecutionClient !== undefined &&
2275
+ runtimeReviewerExecutionRoot !== undefined)
2276
+ Object.assign(tools, createFlowDeskRuntimeReviewerExecutionOptInTools(runtimeReviewerExecutionClient, runtimeReviewerExecutionRoot));
2277
+ if (isManagedFallbackRegateEnabled(options))
2278
+ Object.assign(tools, createFlowDeskManagedFallbackRegateOptInTools(durableStateRootFromOptions(options)));
2279
+ const quickReviewerRunClient = isQuickReviewerRunEnabled(options)
2280
+ ? quickReviewerRunClientFrom(input, options)
2281
+ : undefined;
2282
+ if (quickReviewerRunClient !== undefined)
2283
+ Object.assign(tools, createFlowDeskQuickReviewerRunOptInTools(quickReviewerRunClient, quickReviewerRunDefaultsFromOptions(options)));
2284
+ const providerUsageLiveConfig = isProviderUsageLiveEnabled(options)
2285
+ ? providerUsageLiveConfigFromOptions(options)
2286
+ : undefined;
2287
+ if (providerUsageLiveConfig !== undefined)
2288
+ Object.assign(tools, createFlowDeskProviderUsageLiveOptInTools(providerUsageLiveConfig));
2289
+ const statusLiveConfig = isStatusLiveEnabled(options)
2290
+ ? statusLiveConfigFromOptions(options)
2291
+ : undefined;
2292
+ if (statusLiveConfig !== undefined)
2293
+ Object.assign(tools, createFlowDeskStatusLiveOptInTools(statusLiveConfig));
2294
+ const quickFallbackRunConfig = isQuickFallbackRunEnabled(options)
2295
+ ? quickFallbackRunConfigFromOptions(options)
2296
+ : undefined;
2297
+ if (quickFallbackRunConfig !== undefined)
2298
+ Object.assign(tools, createFlowDeskQuickFallbackRunOptInTools(quickFallbackRunConfig));
2299
+ const laneHeartbeatWriterConfig = isLaneHeartbeatWriterEnabled(options)
2300
+ ? laneHeartbeatWriterConfigFromOptions(options)
2301
+ : undefined;
2302
+ if (laneHeartbeatWriterConfig !== undefined)
2303
+ Object.assign(tools, createFlowDeskLaneHeartbeatWriterOptInTools(laneHeartbeatWriterConfig));
456
2304
  if (!isNaturalLanguageRoutingEnabled(options))
457
2305
  return { tool: tools };
458
- return { tool: tools, "chat.message": createFlowDeskNaturalLanguageChatMessageHook(() => new Date(), localSession) };
2306
+ const stallAlertOption = chatMessageStallAlertOptionsFrom(options, statusLiveConfig);
2307
+ return {
2308
+ tool: tools,
2309
+ "chat.message": createFlowDeskNaturalLanguageChatMessageHook(() => new Date(), localSession, stallAlertOption),
2310
+ };
459
2311
  };
2312
+ export const flowdeskChatMessageStallAlertOption = "chatMessageStallAlert";
2313
+ function chatMessageStallAlertOptionsFrom(options, statusLiveConfig) {
2314
+ const raw = options?.[flowdeskChatMessageStallAlertOption];
2315
+ if (raw === false)
2316
+ return undefined;
2317
+ const recordRaw = isRecord(raw) ? raw : undefined;
2318
+ const explicitEnabled = recordRaw?.enabled === true || raw === true;
2319
+ const explicitDisabled = recordRaw?.enabled === false;
2320
+ if (explicitDisabled)
2321
+ return undefined;
2322
+ const explicitRoot = recordRaw !== undefined &&
2323
+ typeof recordRaw.rootDir === "string" &&
2324
+ recordRaw.rootDir.trim().length > 0
2325
+ ? recordRaw.rootDir
2326
+ : undefined;
2327
+ const fallbackRoot = statusLiveConfig?.rootDir ?? durableStateRootFromOptions(options);
2328
+ const rootDir = explicitRoot ?? fallbackRoot;
2329
+ if (rootDir === undefined)
2330
+ return undefined;
2331
+ if (!explicitEnabled && statusLiveConfig === undefined)
2332
+ return undefined;
2333
+ const config = { rootDir };
2334
+ if (recordRaw !== undefined &&
2335
+ typeof recordRaw.maxWorkflows === "number" &&
2336
+ recordRaw.maxWorkflows > 0)
2337
+ config.maxWorkflows = Math.floor(recordRaw.maxWorkflows);
2338
+ if (recordRaw !== undefined &&
2339
+ typeof recordRaw.laneHeartbeatLateThresholdMs === "number" &&
2340
+ recordRaw.laneHeartbeatLateThresholdMs > 0)
2341
+ config.laneHeartbeatLateThresholdMs = Math.floor(recordRaw.laneHeartbeatLateThresholdMs);
2342
+ if (recordRaw !== undefined &&
2343
+ typeof recordRaw.laneHeartbeatStallThresholdMs === "number" &&
2344
+ recordRaw.laneHeartbeatStallThresholdMs > 0)
2345
+ config.laneHeartbeatStallThresholdMs = Math.floor(recordRaw.laneHeartbeatStallThresholdMs);
2346
+ if (recordRaw !== undefined &&
2347
+ typeof recordRaw.includeProgressingLate === "boolean")
2348
+ config.includeProgressingLate = recordRaw.includeProgressingLate;
2349
+ return config;
2350
+ }
460
2351
  export const flowdeskOpenCodeServerPlugin = {
461
2352
  id: flowdeskPluginId,
462
- server: flowdeskServerPlugin
2353
+ server: flowdeskServerPlugin,
463
2354
  };
464
2355
  export default flowdeskOpenCodeServerPlugin;
465
2356
  //# sourceMappingURL=server.js.map