@flowdesk/opencode-plugin 0.1.12 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agent-task-output.d.ts +17 -0
- package/dist/agent-task-output.d.ts.map +1 -0
- package/dist/agent-task-output.js +119 -0
- package/dist/agent-task-output.js.map +1 -0
- package/dist/agent-task-runner.d.ts +24 -0
- package/dist/agent-task-runner.d.ts.map +1 -1
- package/dist/agent-task-runner.js +536 -61
- package/dist/agent-task-runner.js.map +1 -1
- package/dist/auto-continue-preview-tool.d.ts +36 -0
- package/dist/auto-continue-preview-tool.d.ts.map +1 -0
- package/dist/auto-continue-preview-tool.js +119 -0
- package/dist/auto-continue-preview-tool.js.map +1 -0
- package/dist/completion-ui-cache.d.ts +6 -0
- package/dist/completion-ui-cache.d.ts.map +1 -0
- package/dist/completion-ui-cache.js +260 -0
- package/dist/completion-ui-cache.js.map +1 -0
- package/dist/controlled-write-tool.d.ts +49 -0
- package/dist/controlled-write-tool.d.ts.map +1 -0
- package/dist/controlled-write-tool.js +296 -0
- package/dist/controlled-write-tool.js.map +1 -0
- package/dist/event-hook-observer.d.ts +14 -0
- package/dist/event-hook-observer.d.ts.map +1 -0
- package/dist/event-hook-observer.js +193 -0
- package/dist/event-hook-observer.js.map +1 -0
- package/dist/managed-dispatch-adapter.d.ts +1 -0
- package/dist/managed-dispatch-adapter.d.ts.map +1 -1
- package/dist/managed-dispatch-adapter.js +100 -29
- package/dist/managed-dispatch-adapter.js.map +1 -1
- package/dist/model-selection-engine.d.ts +47 -0
- package/dist/model-selection-engine.d.ts.map +1 -0
- package/dist/model-selection-engine.js +175 -0
- package/dist/model-selection-engine.js.map +1 -0
- package/dist/provider-usage-live-tool.d.ts +27 -0
- package/dist/provider-usage-live-tool.d.ts.map +1 -1
- package/dist/provider-usage-live-tool.js +443 -4
- package/dist/provider-usage-live-tool.js.map +1 -1
- package/dist/quick-reviewer-run.d.ts +3 -0
- package/dist/quick-reviewer-run.d.ts.map +1 -1
- package/dist/quick-reviewer-run.js +20 -8
- package/dist/quick-reviewer-run.js.map +1 -1
- package/dist/runtime-reviewer-execution-bridge.d.ts +21 -0
- package/dist/runtime-reviewer-execution-bridge.d.ts.map +1 -1
- package/dist/runtime-reviewer-execution-bridge.js +238 -0
- package/dist/runtime-reviewer-execution-bridge.js.map +1 -1
- package/dist/server.d.ts +60 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +800 -41
- package/dist/server.js.map +1 -1
- package/dist/stall-recovery.d.ts +60 -0
- package/dist/stall-recovery.d.ts.map +1 -1
- package/dist/stall-recovery.js +763 -11
- package/dist/stall-recovery.js.map +1 -1
- package/dist/status-live-tool.d.ts +81 -0
- package/dist/status-live-tool.d.ts.map +1 -1
- package/dist/status-live-tool.js +620 -38
- package/dist/status-live-tool.js.map +1 -1
- package/dist/tui-subtask-activity.d.ts +69 -0
- package/dist/tui-subtask-activity.d.ts.map +1 -0
- package/dist/tui-subtask-activity.js +266 -0
- package/dist/tui-subtask-activity.js.map +1 -0
- package/dist/tui-usage-snapshot.d.ts +44 -0
- package/dist/tui-usage-snapshot.d.ts.map +1 -0
- package/dist/tui-usage-snapshot.js +397 -0
- package/dist/tui-usage-snapshot.js.map +1 -0
- package/dist/tui.d.ts +7 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +134 -0
- package/dist/tui.js.map +1 -0
- package/dist/workflow-assign-tool.d.ts +23 -0
- package/dist/workflow-assign-tool.d.ts.map +1 -0
- package/dist/workflow-assign-tool.js +117 -0
- package/dist/workflow-assign-tool.js.map +1 -0
- package/dist/workflow-author-tool.d.ts +29 -0
- package/dist/workflow-author-tool.d.ts.map +1 -0
- package/dist/workflow-author-tool.js +227 -0
- package/dist/workflow-author-tool.js.map +1 -0
- package/dist/workflow-dispatch-plan-tool.d.ts +47 -0
- package/dist/workflow-dispatch-plan-tool.d.ts.map +1 -0
- package/dist/workflow-dispatch-plan-tool.js +251 -0
- package/dist/workflow-dispatch-plan-tool.js.map +1 -0
- package/dist/workflow-dispatch-tool.d.ts +56 -0
- package/dist/workflow-dispatch-tool.d.ts.map +1 -0
- package/dist/workflow-dispatch-tool.js +306 -0
- package/dist/workflow-dispatch-tool.js.map +1 -0
- package/dist/workflow-orchestrator.d.ts +31 -0
- package/dist/workflow-orchestrator.d.ts.map +1 -0
- package/dist/workflow-orchestrator.js +160 -0
- package/dist/workflow-orchestrator.js.map +1 -0
- package/dist/workflow-scheduler.d.ts +19 -0
- package/dist/workflow-scheduler.d.ts.map +1 -0
- package/dist/workflow-scheduler.js +45 -0
- package/dist/workflow-scheduler.js.map +1 -0
- package/dist/workflow-synthesis-tool.d.ts +31 -0
- package/dist/workflow-synthesis-tool.d.ts.map +1 -0
- package/dist/workflow-synthesis-tool.js +194 -0
- package/dist/workflow-synthesis-tool.js.map +1 -0
- package/package.json +10 -2
package/dist/server.js
CHANGED
|
@@ -4,15 +4,23 @@ import { join, resolve, sep } from "node:path";
|
|
|
4
4
|
import { applyFlowDeskSessionEvidenceWriteIntentsV1, createFlowDeskChatHookAuthorityProbeV1, evaluateFlowDeskChatIntakeV1, getFlowDeskPortableCommandToolName, getRelease1SchemaArtifact, materializeFlowDeskExactModelCacheEvidenceFromProviderAcquisitionEvidenceV1, materializeFlowDeskRuntimeLaneLaunchPlansFromReviewerFanoutEvidenceV1, planFlowDeskReviewerFanoutFromReloadedCacheEvidenceV1, prepareFlowDeskSessionEvidenceWriteIntentV1, reloadFlowDeskSessionEvidenceV1, validateFlowDeskDefaultManagedDispatchAuthorizationV1, validateProjectConfigV1, validateRunRequestV1, } from "@flowdesk/core";
|
|
5
5
|
import { tool, } from "@opencode-ai/plugin";
|
|
6
6
|
import { flowdeskPluginId, flowdeskPluginScaffold, hasProductionOpenCodeRegistration, } from "./index.js";
|
|
7
|
+
import { observeFlowDeskOpenCodeEventV1 } from "./event-hook-observer.js";
|
|
7
8
|
import { recordFlowDeskLaneHeartbeatV1, } from "./lane-heartbeat-writer.js";
|
|
8
9
|
import { createFlowDeskLocalNonDispatchAdapterSession, flowdeskLocalNonDispatchAdapterProfile, hasFlowDeskLocalPlanningEvidenceV1, } from "./local-adapter.js";
|
|
9
10
|
import { createFlowDeskManagedDispatchBetaDurableReservationStoreV1, createFlowDeskOpenCodeMetadataProviderAcquisitionClientV1, createFlowDeskOpenCodePromptBackedProviderAcquisitionClientV1, dispatchManagedDispatchBetaPromptV1, materializeFlowDeskManagedFallbackRegatePlanEvidenceV1, orchestrateFlowDeskManagedFallbackRegateV1, runFlowDeskExactModelProviderAcquisitionLiveTestV1, } from "./managed-dispatch-adapter.js";
|
|
10
11
|
import { executeFlowDeskProviderUsageLiveV1, } from "./provider-usage-live-tool.js";
|
|
12
|
+
import { formatFlowDeskTuiUsageSnapshotCompactText, loadFlowDeskTuiUsageSnapshotViewV1, } from "./tui-usage-snapshot.js";
|
|
11
13
|
import { executeFlowDeskQuickFallbackRunV1, } from "./quick-fallback-run.js";
|
|
12
14
|
import { executeFlowDeskQuickReviewerRunV1, } from "./quick-reviewer-run.js";
|
|
13
15
|
import { executeFlowDeskRuntimeReviewerExecutionBridgeV1, redactedRuntimeReviewerExecutionBlocked, runtimeReviewerExecutionExpectationsFromValue, } from "./runtime-reviewer-execution-bridge.js";
|
|
14
16
|
import { executeFlowDeskAgentTaskV1 } from "./agent-task-runner.js";
|
|
17
|
+
import { executeFlowDeskAutoContinuePreviewToolV1, } from "./auto-continue-preview-tool.js";
|
|
18
|
+
import { executeFlowDeskControlledWriteApplyToolV1, } from "./controlled-write-tool.js";
|
|
15
19
|
import { executeFlowDeskStatusLiveV1, } from "./status-live-tool.js";
|
|
20
|
+
import { executeFlowDeskWorkflowDispatchPlanToolV1, } from "./workflow-dispatch-plan-tool.js";
|
|
21
|
+
import { executeFlowDeskWorkflowDispatchToolV1, } from "./workflow-dispatch-tool.js";
|
|
22
|
+
import { executeFlowDeskWorkflowOrchestratorV1 } from "./workflow-orchestrator.js";
|
|
23
|
+
import { executeFlowDeskWorkflowSynthesisPreviewV1 } from "./workflow-synthesis-tool.js";
|
|
16
24
|
import { evaluateGuardedAutoAbortHookV1, evaluateGuardedAutoRetryHookV1, reconcileStalePendingRetryPlansV1, checkSdkSessionApiHealthV1, runFlowDeskWatchdogCycleV1, } from "./stall-recovery.js";
|
|
17
25
|
import { withTimeout, FlowDeskTimeoutError } from "./shared/with-timeout.js";
|
|
18
26
|
import { FLOWDESK_PRE_SPIKE_PLUGIN_TOOL_STUBS, getFlowDeskRelease1HandlerReadinessSummary, getFlowDeskRelease1ProductionReadinessSummary, hasPassingFds1SchemaConversionSpike, runFlowDeskPreSpikePluginToolStub, } from "./tool-stubs.js";
|
|
@@ -34,6 +42,10 @@ export const flowdeskProviderUsageLiveOption = "providerUsageLive";
|
|
|
34
42
|
export const flowdeskStatusLiveOption = "statusLive";
|
|
35
43
|
export const flowdeskQuickFallbackRunOption = "quickFallbackRun";
|
|
36
44
|
export const flowdeskLaneHeartbeatWriterOption = "laneHeartbeatWriter";
|
|
45
|
+
export const flowdeskWorkflowDispatchPlanToolOption = "workflowDispatchPlanTool";
|
|
46
|
+
export const flowdeskWorkflowDispatchOption = "workflowDispatch";
|
|
47
|
+
export const flowdeskControlledWriteApplyOption = "controlledWriteApply";
|
|
48
|
+
export const flowdeskUiProbeOption = "uiProbe";
|
|
37
49
|
export const flowdeskDefaultManagedDispatchAuthorizationOption = "defaultManagedDispatchAuthorization";
|
|
38
50
|
export const flowdeskWatchdogOption = "watchdog";
|
|
39
51
|
export const flowdeskWatchdogTriggerToolName = "flowdesk_watchdog_trigger";
|
|
@@ -46,8 +58,14 @@ export const flowdeskProviderUsageLiveToolName = "flowdesk_provider_usage_live";
|
|
|
46
58
|
export const flowdeskStatusLiveToolName = "flowdesk_status_live";
|
|
47
59
|
export const flowdeskQuickFallbackRunToolName = "flowdesk_quick_fallback_run";
|
|
48
60
|
export const flowdeskLaneHeartbeatWriterToolName = "flowdesk_lane_heartbeat_record";
|
|
61
|
+
export const flowdeskWorkflowDispatchPlanToolName = "flowdesk_workflow_dispatch_plan";
|
|
62
|
+
export const flowdeskWorkflowDispatchToolName = "flowdesk_workflow_dispatch";
|
|
63
|
+
export const flowdeskControlledWriteApplyToolName = "flowdesk_controlled_write_apply";
|
|
49
64
|
export const flowdeskAgentTaskRunOption = "agentTaskRun";
|
|
50
65
|
export const flowdeskAgentTaskRunToolName = "flowdesk_agent_task_run";
|
|
66
|
+
export const flowdeskWorkflowSynthesisPreviewToolName = "flowdesk_workflow_synthesis_preview";
|
|
67
|
+
export const flowdeskAutoContinuePreviewToolName = "flowdesk_auto_continue_preview";
|
|
68
|
+
export const flowdeskUiProbeToolName = "flowdesk_ui_probe";
|
|
51
69
|
const flowdeskChatSuggestionDuplicateWindowMs = 10_000;
|
|
52
70
|
const disabledAuthority = {
|
|
53
71
|
productionRegistrationEligible: false,
|
|
@@ -1166,9 +1184,11 @@ export function createFlowDeskExactModelProviderAcquisitionLiveTestOptInTools(cl
|
|
|
1166
1184
|
}),
|
|
1167
1185
|
};
|
|
1168
1186
|
}
|
|
1169
|
-
export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Date(), session = createFlowDeskLocalNonDispatchAdapterSession(now), stallAlert, durableSuggestionRoot) {
|
|
1187
|
+
export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Date(), session = createFlowDeskLocalNonDispatchAdapterSession(now), stallAlert, durableSuggestionRoot, providerUsageLiveConfig) {
|
|
1170
1188
|
const recentSuggestionCards = new Map();
|
|
1171
1189
|
const recentStallAlerts = new Map();
|
|
1190
|
+
const usageAutoRefreshMaxAgeMs = 3 * 60_000;
|
|
1191
|
+
let lastUsageRefreshAttemptAtMs = 0;
|
|
1172
1192
|
return async function message(input, output) {
|
|
1173
1193
|
const inputRecord = isRecord(input) ? input : {};
|
|
1174
1194
|
const partSessionID = typeof inputRecord.sessionID === "string" ? inputRecord.sessionID : "";
|
|
@@ -1183,6 +1203,40 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
|
|
|
1183
1203
|
const request = intakeRequestFromChatMessage({ ...inputRecord, ...output });
|
|
1184
1204
|
const preview = previewNaturalLanguageRouting(request, session);
|
|
1185
1205
|
const nowMs = clockMs(now);
|
|
1206
|
+
if (providerUsageLiveConfig?.durableStateRootDir && nowMs - lastUsageRefreshAttemptAtMs > 30_000) {
|
|
1207
|
+
lastUsageRefreshAttemptAtMs = nowMs;
|
|
1208
|
+
let isStale = false;
|
|
1209
|
+
try {
|
|
1210
|
+
const cachePath = join(providerUsageLiveConfig.durableStateRootDir, ".flowdesk", "ui", "provider-usage-sidebar.json");
|
|
1211
|
+
const cacheContent = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
1212
|
+
if (typeof cacheContent.observed_at === "string" && nowMs - Date.parse(cacheContent.observed_at) > usageAutoRefreshMaxAgeMs)
|
|
1213
|
+
isStale = true;
|
|
1214
|
+
}
|
|
1215
|
+
catch {
|
|
1216
|
+
isStale = true;
|
|
1217
|
+
}
|
|
1218
|
+
if (isStale) {
|
|
1219
|
+
try {
|
|
1220
|
+
await executeFlowDeskProviderUsageLiveV1({ config: { ...providerUsageLiveConfig, persistSidebarCache: true }, request: { providerFamily: "all" } });
|
|
1221
|
+
}
|
|
1222
|
+
catch { }
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
const usageTextToAppend = providerUsageLiveConfig?.durableStateRootDir &&
|
|
1226
|
+
providerUsageLiveConfig.appendToChat === true
|
|
1227
|
+
? formatFlowDeskTuiUsageSnapshotCompactText(loadFlowDeskTuiUsageSnapshotViewV1({
|
|
1228
|
+
rootDir: providerUsageLiveConfig.durableStateRootDir,
|
|
1229
|
+
workflowId: providerUsageLiveConfig.persistWorkflowId,
|
|
1230
|
+
now: () => (typeof now === "function" ? now() : now),
|
|
1231
|
+
}))
|
|
1232
|
+
: undefined;
|
|
1233
|
+
const appendUsageCard = () => {
|
|
1234
|
+
if (usageTextToAppend === undefined)
|
|
1235
|
+
return;
|
|
1236
|
+
if (!Array.isArray(output.parts))
|
|
1237
|
+
output.parts = [];
|
|
1238
|
+
output.parts.push(buildTextPart(usageTextToAppend));
|
|
1239
|
+
};
|
|
1186
1240
|
for (const [key, recordedAtMs] of recentSuggestionCards) {
|
|
1187
1241
|
if (nowMs - recordedAtMs > flowdeskChatSuggestionDuplicateWindowMs ||
|
|
1188
1242
|
nowMs < recordedAtMs)
|
|
@@ -1194,7 +1248,9 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
|
|
|
1194
1248
|
recentStallAlerts.delete(key);
|
|
1195
1249
|
}
|
|
1196
1250
|
const stallResult = stallAlert
|
|
1197
|
-
? await collectStallAlertResult(stallAlert, now
|
|
1251
|
+
? await collectStallAlertResult(stallAlert, now, {
|
|
1252
|
+
currentSessionRef: partSessionID || request.session_ref,
|
|
1253
|
+
})
|
|
1198
1254
|
: { status: "none" };
|
|
1199
1255
|
let stallTextToAppend = undefined;
|
|
1200
1256
|
let stallDedupKey = undefined;
|
|
@@ -1215,7 +1271,10 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
|
|
|
1215
1271
|
const lateAlertReady = stallAlert?.includeProgressingLate === true &&
|
|
1216
1272
|
summary.worstClassification === "progressing_late" &&
|
|
1217
1273
|
summary.totalLate > 0;
|
|
1218
|
-
|
|
1274
|
+
const progressCardReady = stallAlert?.includeProgressCards === true &&
|
|
1275
|
+
summary.workflowSummaries.some((workflow) => (workflow.laneCards?.length ?? 0) > 0);
|
|
1276
|
+
const autoNextReady = summary.workflowSummaries.some((workflow) => workflow.autoNextReady === true);
|
|
1277
|
+
if (stalledAlertReady || lateAlertReady || progressCardReady || autoNextReady) {
|
|
1219
1278
|
stallDedupKey = stallAlertDuplicateKey(request, summary);
|
|
1220
1279
|
stallTextToAppend = stallAlertText(summary);
|
|
1221
1280
|
}
|
|
@@ -1233,6 +1292,7 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
|
|
|
1233
1292
|
}
|
|
1234
1293
|
};
|
|
1235
1294
|
if (preview.evaluation.response.route_decision === "continue_chat") {
|
|
1295
|
+
appendUsageCard();
|
|
1236
1296
|
appendStallCard();
|
|
1237
1297
|
return;
|
|
1238
1298
|
}
|
|
@@ -1243,13 +1303,16 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
|
|
|
1243
1303
|
recentSuggestionCards.set(duplicateKey, nowMs);
|
|
1244
1304
|
writeDurableSuggestionPreference(durableSuggestionRoot, duplicateKey, request, preview.evaluation.response, nowMs);
|
|
1245
1305
|
if (previousAtMs !== undefined &&
|
|
1246
|
-
nowMs - previousAtMs <= flowdeskChatSuggestionDuplicateWindowMs)
|
|
1306
|
+
nowMs - previousAtMs <= flowdeskChatSuggestionDuplicateWindowMs) {
|
|
1307
|
+
appendUsageCard();
|
|
1247
1308
|
return;
|
|
1309
|
+
}
|
|
1248
1310
|
}
|
|
1249
1311
|
const result = evaluateNaturalLanguageRouting(request, session);
|
|
1250
1312
|
if (!Array.isArray(output.parts))
|
|
1251
1313
|
output.parts = [];
|
|
1252
1314
|
output.parts.push(buildTextPart(steeringText(result)));
|
|
1315
|
+
appendUsageCard();
|
|
1253
1316
|
appendStallCard();
|
|
1254
1317
|
};
|
|
1255
1318
|
}
|
|
@@ -1257,6 +1320,28 @@ const ALLOWED_ERROR_NAMES = new Set(["FlowDeskDiskError", "FlowDeskStateError"])
|
|
|
1257
1320
|
export function assertNever(x) {
|
|
1258
1321
|
throw new Error("Unexpected object: " + x);
|
|
1259
1322
|
}
|
|
1323
|
+
function sessionRefVariants(value) {
|
|
1324
|
+
const variants = new Set();
|
|
1325
|
+
if (value === undefined || value.trim().length === 0)
|
|
1326
|
+
return variants;
|
|
1327
|
+
const token = safeToken(value, "session");
|
|
1328
|
+
variants.add(token);
|
|
1329
|
+
if (token.startsWith("ses_"))
|
|
1330
|
+
variants.add(`ses-${token.slice(4)}`);
|
|
1331
|
+
if (token.startsWith("ses-"))
|
|
1332
|
+
variants.add(`ses_${token.slice(4)}`);
|
|
1333
|
+
return variants;
|
|
1334
|
+
}
|
|
1335
|
+
function sessionRefsMatch(left, right) {
|
|
1336
|
+
const leftVariants = sessionRefVariants(left);
|
|
1337
|
+
if (leftVariants.size === 0)
|
|
1338
|
+
return false;
|
|
1339
|
+
for (const candidate of sessionRefVariants(right)) {
|
|
1340
|
+
if (leftVariants.has(candidate))
|
|
1341
|
+
return true;
|
|
1342
|
+
}
|
|
1343
|
+
return false;
|
|
1344
|
+
}
|
|
1260
1345
|
function latestParentSessionRefForLane(rootDir, workflowId, laneId) {
|
|
1261
1346
|
const reload = reloadFlowDeskSessionEvidenceV1({ rootDir, workflowId });
|
|
1262
1347
|
if (!reload.ok)
|
|
@@ -1310,18 +1395,36 @@ export async function collectStallAlertResult(stallAlert, clock, deps = {}) {
|
|
|
1310
1395
|
if (result.status !== "status_live_collected")
|
|
1311
1396
|
return { status: "none" };
|
|
1312
1397
|
const autoAbortSummaries = [];
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
(stallAlert.includeProgressingLate === true &&
|
|
1316
|
-
(workflow.progressingLateLaneCount ?? 0) > 0))
|
|
1317
|
-
.slice(0, 3)
|
|
1398
|
+
const currentSessionRef = deps.currentSessionRef;
|
|
1399
|
+
const workflowSummariesWithEmpty = await Promise.all(result.workflows
|
|
1318
1400
|
.map(async (workflow) => {
|
|
1319
|
-
const
|
|
1320
|
-
const
|
|
1401
|
+
const parentRefCache = new Map();
|
|
1402
|
+
const latestParentRef = (laneId) => {
|
|
1403
|
+
if (!parentRefCache.has(laneId)) {
|
|
1404
|
+
parentRefCache.set(laneId, latestParentSessionRefForLane(stallAlert.rootDir, workflow.workflowId, laneId));
|
|
1405
|
+
}
|
|
1406
|
+
return parentRefCache.get(laneId);
|
|
1407
|
+
};
|
|
1408
|
+
const laneInCurrentSession = (laneId) => currentSessionRef === undefined ||
|
|
1409
|
+
sessionRefsMatch(latestParentRef(laneId), currentSessionRef);
|
|
1410
|
+
const scopedEntries = (workflow.laneStallProjection?.entries ?? []).filter((entry) => laneInCurrentSession(entry.laneId));
|
|
1411
|
+
const stalledEntry = scopedEntries.find((entry) => entry.classification === "stalled");
|
|
1412
|
+
const lateEntry = scopedEntries.find((entry) => entry.classification === "progressing_late");
|
|
1321
1413
|
const primary = stalledEntry ?? lateEntry;
|
|
1414
|
+
const autoNextReady = workflow.laneProgressAggregate?.autoNextStepEligible === true;
|
|
1415
|
+
const scopedLaneCards = (workflow.laneProgressCards ?? []).filter((lane) => (autoNextReady || lane.classification !== "terminal") &&
|
|
1416
|
+
laneInCurrentSession(lane.laneId));
|
|
1417
|
+
const scopedStalledCount = scopedEntries.filter((entry) => entry.classification === "stalled").length;
|
|
1418
|
+
const scopedLateCount = scopedEntries.filter((entry) => entry.classification === "progressing_late").length;
|
|
1419
|
+
const shouldShowWorkflow = scopedStalledCount > 0 ||
|
|
1420
|
+
(stallAlert.includeProgressingLate === true && scopedLateCount > 0) ||
|
|
1421
|
+
(autoNextReady && scopedLaneCards.length > 0) ||
|
|
1422
|
+
(stallAlert.includeProgressCards === true && scopedLaneCards.length > 0);
|
|
1423
|
+
if (!shouldShowWorkflow)
|
|
1424
|
+
return undefined;
|
|
1322
1425
|
if (stallAlert.guardedAutoAbort !== undefined &&
|
|
1323
1426
|
stalledEntry !== undefined &&
|
|
1324
|
-
|
|
1427
|
+
scopedStalledCount > 0) {
|
|
1325
1428
|
// Reconcile stale pending retry plans on each stall check
|
|
1326
1429
|
try {
|
|
1327
1430
|
reconcileStalePendingRetryPlansV1({
|
|
@@ -1383,8 +1486,8 @@ export async function collectStallAlertResult(stallAlert, clock, deps = {}) {
|
|
|
1383
1486
|
}
|
|
1384
1487
|
return {
|
|
1385
1488
|
workflowId: workflow.workflowId,
|
|
1386
|
-
stalledLaneCount:
|
|
1387
|
-
lateLaneCount:
|
|
1489
|
+
stalledLaneCount: scopedStalledCount,
|
|
1490
|
+
lateLaneCount: scopedLateCount,
|
|
1388
1491
|
...(primary?.secondsSinceLastSignal === undefined
|
|
1389
1492
|
? {}
|
|
1390
1493
|
: { secondsSinceLastSignal: primary.secondsSinceLastSignal }),
|
|
@@ -1392,17 +1495,61 @@ export async function collectStallAlertResult(stallAlert, clock, deps = {}) {
|
|
|
1392
1495
|
...(primary?.failureHint === undefined
|
|
1393
1496
|
? {}
|
|
1394
1497
|
: { failureHint: primary.failureHint }),
|
|
1498
|
+
...(workflow.laneProgressAggregate === undefined
|
|
1499
|
+
? {}
|
|
1500
|
+
: { laneProgressAggregate: workflow.laneProgressAggregate }),
|
|
1501
|
+
...(autoNextReady ? { autoNextReady: true } : {}),
|
|
1502
|
+
...(workflow.latestWorkflowSynthesisTasksSummarized === undefined
|
|
1503
|
+
? {}
|
|
1504
|
+
: { synthesisTasksSummarized: workflow.latestWorkflowSynthesisTasksSummarized }),
|
|
1505
|
+
...(workflow.latestWorkflowSynthesisConflictDetected === undefined
|
|
1506
|
+
? {}
|
|
1507
|
+
: { synthesisConflictDetected: workflow.latestWorkflowSynthesisConflictDetected }),
|
|
1508
|
+
...(stallAlert.includeProgressCards === true
|
|
1509
|
+
? {
|
|
1510
|
+
laneCards: scopedLaneCards
|
|
1511
|
+
.slice(0, stallAlert.maxProgressCards ?? 3)
|
|
1512
|
+
.map((lane) => ({
|
|
1513
|
+
laneId: lane.laneId,
|
|
1514
|
+
taskId: lane.taskId,
|
|
1515
|
+
state: lane.state,
|
|
1516
|
+
classification: lane.classification,
|
|
1517
|
+
secondsSinceLastSignal: lane.secondsSinceLastSignal,
|
|
1518
|
+
agentRef: lane.agentRef,
|
|
1519
|
+
providerQualifiedModelId: lane.providerQualifiedModelId,
|
|
1520
|
+
promptPreview: lane.promptPreview,
|
|
1521
|
+
nudgeCount: lane.nudgeCount,
|
|
1522
|
+
progressPhase: lane.progressPhase,
|
|
1523
|
+
progressLabel: lane.progressLabel,
|
|
1524
|
+
verdictLabel: lane.verdictLabel,
|
|
1525
|
+
completionStatus: lane.completionStatus,
|
|
1526
|
+
outputKind: lane.outputKind,
|
|
1527
|
+
usableForSynthesis: lane.usableForSynthesis,
|
|
1528
|
+
failureHint: lane.failureHint,
|
|
1529
|
+
})),
|
|
1530
|
+
}
|
|
1531
|
+
: {}),
|
|
1395
1532
|
};
|
|
1396
1533
|
}));
|
|
1534
|
+
const workflowSummaries = workflowSummariesWithEmpty
|
|
1535
|
+
.filter((summary) => summary !== undefined)
|
|
1536
|
+
.slice(0, 3);
|
|
1397
1537
|
if (workflowSummaries.length === 0) {
|
|
1398
1538
|
return { status: "none" };
|
|
1399
1539
|
}
|
|
1540
|
+
const scopedTotalStalled = workflowSummaries.reduce((sum, workflow) => sum + workflow.stalledLaneCount, 0);
|
|
1541
|
+
const scopedTotalLate = workflowSummaries.reduce((sum, workflow) => sum + workflow.lateLaneCount, 0);
|
|
1542
|
+
const scopedWorstClassification = scopedTotalStalled > 0
|
|
1543
|
+
? "stalled"
|
|
1544
|
+
: scopedTotalLate > 0
|
|
1545
|
+
? "progressing_late"
|
|
1546
|
+
: (result.worstLaneStallClassification ?? "unknown");
|
|
1400
1547
|
return {
|
|
1401
1548
|
status: "ok",
|
|
1402
1549
|
data: {
|
|
1403
|
-
worstClassification:
|
|
1404
|
-
totalStalled:
|
|
1405
|
-
totalLate:
|
|
1550
|
+
worstClassification: scopedWorstClassification,
|
|
1551
|
+
totalStalled: scopedTotalStalled,
|
|
1552
|
+
totalLate: scopedTotalLate,
|
|
1406
1553
|
workflowSummaries,
|
|
1407
1554
|
...(autoAbortSummaries.length === 0 ? {} : { autoAbortSummaries }),
|
|
1408
1555
|
}
|
|
@@ -1424,7 +1571,15 @@ function stallAlertDuplicateKey(request, summary) {
|
|
|
1424
1571
|
const ageMinutes = typeof entry.secondsSinceLastSignal === "number"
|
|
1425
1572
|
? Math.floor(entry.secondsSinceLastSignal / 60)
|
|
1426
1573
|
: -1;
|
|
1427
|
-
|
|
1574
|
+
const lanes = (entry.laneCards ?? [])
|
|
1575
|
+
.map((lane) => {
|
|
1576
|
+
const laneAge = typeof lane.secondsSinceLastSignal === "number"
|
|
1577
|
+
? Math.floor(lane.secondsSinceLastSignal / 60)
|
|
1578
|
+
: -1;
|
|
1579
|
+
return `${lane.laneId}:${lane.state ?? "unknown"}:${lane.classification}:${laneAge}`;
|
|
1580
|
+
})
|
|
1581
|
+
.join(",");
|
|
1582
|
+
return `${entry.workflowId}:${entry.stalledLaneCount}:${ageMinutes}:${lanes}`;
|
|
1428
1583
|
})
|
|
1429
1584
|
.join("|");
|
|
1430
1585
|
return `${safeToken(request.session_ref, "session")}|stall|${wf}|worst:${summary.worstClassification}`;
|
|
@@ -1432,12 +1587,19 @@ function stallAlertDuplicateKey(request, summary) {
|
|
|
1432
1587
|
function stallAlertText(summary) {
|
|
1433
1588
|
const lines = [];
|
|
1434
1589
|
lines.push("FlowDesk");
|
|
1590
|
+
const progressCardCount = summary.workflowSummaries.reduce((sum, workflow) => sum + (workflow.laneCards?.length ?? 0), 0);
|
|
1435
1591
|
if (summary.worstClassification === "stalled") {
|
|
1436
1592
|
lines.push(`Stalled lanes detected: ${summary.totalStalled} stalled, ${summary.totalLate} progressing-late.`);
|
|
1437
1593
|
}
|
|
1438
1594
|
else if (summary.worstClassification === "progressing_late") {
|
|
1439
1595
|
lines.push(`Late-progressing lanes detected: ${summary.totalLate} late, ${summary.totalStalled} stalled.`);
|
|
1440
1596
|
}
|
|
1597
|
+
else if (summary.workflowSummaries.some((workflow) => workflow.autoNextReady === true)) {
|
|
1598
|
+
lines.push("All FlowDesk subtasks completed normally. Auto-next synthesis is ready.");
|
|
1599
|
+
}
|
|
1600
|
+
else if (progressCardCount > 0) {
|
|
1601
|
+
lines.push(`Lane progress: ${progressCardCount} lane(s) visible on the main screen.`);
|
|
1602
|
+
}
|
|
1441
1603
|
else {
|
|
1442
1604
|
lines.push(`Lane progress check: ${summary.totalStalled} stalled, ${summary.totalLate} progressing-late.`);
|
|
1443
1605
|
}
|
|
@@ -1445,12 +1607,58 @@ function stallAlertText(summary) {
|
|
|
1445
1607
|
const secs = workflow.secondsSinceLastSignal ?? 0;
|
|
1446
1608
|
const minutes = Math.floor(secs / 60);
|
|
1447
1609
|
const hint = workflow.failureHint ?? "no recent heartbeat";
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1610
|
+
if (workflow.autoNextReady === true) {
|
|
1611
|
+
const synthesis = workflow.synthesisTasksSummarized === undefined
|
|
1612
|
+
? "synthesis not yet recorded"
|
|
1613
|
+
: `synthesis recorded for ${workflow.synthesisTasksSummarized} task(s)`;
|
|
1614
|
+
const conflict = workflow.synthesisConflictDetected === undefined
|
|
1615
|
+
? " conflict=unknown"
|
|
1616
|
+
: workflow.synthesisConflictDetected === true ? " conflict=true" : " conflict=false";
|
|
1617
|
+
const nextAction = workflow.laneProgressAggregate?.nextActionAvailable === true
|
|
1618
|
+
? ` next_action=${workflow.laneProgressAggregate.nextActionKind ?? "available"}_ready`
|
|
1619
|
+
: "";
|
|
1620
|
+
lines.push(`- workflow ${workflow.workflowId}: auto-next ready (${synthesis},${conflict}${nextAction}).`);
|
|
1621
|
+
}
|
|
1622
|
+
else {
|
|
1623
|
+
const counts = workflow.stalledLaneCount > 0
|
|
1624
|
+
? `${workflow.stalledLaneCount} stalled`
|
|
1625
|
+
: `${workflow.lateLaneCount} progressing-late`;
|
|
1626
|
+
lines.push(`- workflow ${workflow.workflowId}: ${counts} (last signal ~${minutes}m ago, ${hint}).`);
|
|
1627
|
+
}
|
|
1628
|
+
if (workflow.laneProgressAggregate !== undefined) {
|
|
1629
|
+
const aggregate = workflow.laneProgressAggregate;
|
|
1630
|
+
const nextAction = aggregate.nextActionAvailable === true
|
|
1631
|
+
? `, next_action=${aggregate.nextActionKind ?? "available"}_ready`
|
|
1632
|
+
: "";
|
|
1633
|
+
lines.push(` tasks: expected=${aggregate.expected}, terminal=${aggregate.terminal}, completed=${aggregate.normalCompleted}, failed=${aggregate.failed}, awaiting_permission=${aggregate.awaitingPermission}, auto_next=${aggregate.autoNextStepEligible}${nextAction}`);
|
|
1634
|
+
}
|
|
1635
|
+
for (const lane of workflow.laneCards?.slice(0, 3) ?? []) {
|
|
1636
|
+
const age = lane.secondsSinceLastSignal === undefined
|
|
1637
|
+
? "unknown"
|
|
1638
|
+
: `~${Math.floor(lane.secondsSinceLastSignal / 60)}m ago`;
|
|
1639
|
+
const model = lane.providerQualifiedModelId ?? "(unknown)";
|
|
1640
|
+
const agent = lane.agentRef ?? "(unknown)";
|
|
1641
|
+
const task = lane.taskId ?? lane.laneId;
|
|
1642
|
+
const prompt = lane.promptPreview ?? "(hidden)";
|
|
1643
|
+
const nudge = lane.nudgeCount === undefined ? "?" : String(lane.nudgeCount);
|
|
1644
|
+
const progress = lane.progressLabel === undefined
|
|
1645
|
+
? "(none)"
|
|
1646
|
+
: `${lane.progressPhase ?? "progress"}: ${lane.progressLabel}`;
|
|
1647
|
+
const verdict = lane.verdictLabel ?? "(none)";
|
|
1648
|
+
const resultQuality = lane.completionStatus === undefined && lane.outputKind === undefined && lane.usableForSynthesis === undefined
|
|
1649
|
+
? "(none)"
|
|
1650
|
+
: `${lane.completionStatus ?? "unknown"}/${lane.outputKind ?? "unknown"}/synthesis=${lane.usableForSynthesis === undefined ? "unknown" : String(lane.usableForSynthesis)}`;
|
|
1651
|
+
const issue = lane.failureHint === undefined ? "" : ` issue=${lane.failureHint}`;
|
|
1652
|
+
lines.push(` - lane ${lane.laneId}: ${lane.state ?? "unknown"}/${lane.classification}`);
|
|
1653
|
+
lines.push(` task: ${task}`);
|
|
1654
|
+
lines.push(` prompt: ${prompt}`);
|
|
1655
|
+
lines.push(` agent: ${agent}`);
|
|
1656
|
+
lines.push(` model: ${model}`);
|
|
1657
|
+
lines.push(` progress: ${progress}`);
|
|
1658
|
+
lines.push(` result: ${resultQuality}`);
|
|
1659
|
+
lines.push(` last signal: ${age}; nudges=${nudge}; verdict=${verdict}${issue}`);
|
|
1660
|
+
}
|
|
1452
1661
|
}
|
|
1453
|
-
lines.push("FlowDesk does not auto-retry, auto-abort, or auto-fallback on stall.");
|
|
1454
1662
|
if (summary.autoAbortSummaries !== undefined && summary.autoAbortSummaries.length > 0) {
|
|
1455
1663
|
lines.push("Guarded auto-abort diagnostics (evidence-only, opt-in):");
|
|
1456
1664
|
for (const line of summary.autoAbortSummaries.slice(0, 3))
|
|
@@ -1466,6 +1674,9 @@ function stallAlertText(summary) {
|
|
|
1466
1674
|
"/flowdesk-export-debug",
|
|
1467
1675
|
])
|
|
1468
1676
|
lines.push(`- ${action}`);
|
|
1677
|
+
if (progressCardCount > 0) {
|
|
1678
|
+
lines.push("Lane log refs are command-based in this MVP; native clickable task UI is not claimed.");
|
|
1679
|
+
}
|
|
1469
1680
|
return lines.join("\n");
|
|
1470
1681
|
}
|
|
1471
1682
|
function isFds1SchemaConversionProbeEnabled(options) {
|
|
@@ -1973,6 +2184,32 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
|
|
|
1973
2184
|
return undefined;
|
|
1974
2185
|
const client = input.client;
|
|
1975
2186
|
const rootDir = input.durableStateRoot;
|
|
2187
|
+
const promptPreview = (text, max = 120) => {
|
|
2188
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
2189
|
+
return compact.length > max ? `${compact.slice(0, max - 1)}…` : compact;
|
|
2190
|
+
};
|
|
2191
|
+
const taskSummaryForUser = (input) => {
|
|
2192
|
+
const lines = [
|
|
2193
|
+
input.status === "task_completed"
|
|
2194
|
+
? "FlowDesk task completed."
|
|
2195
|
+
: input.status === "task_launched"
|
|
2196
|
+
? "FlowDesk task launched."
|
|
2197
|
+
: "FlowDesk task failed.",
|
|
2198
|
+
`workflow: ${input.workflowId}`,
|
|
2199
|
+
`lane: ${input.laneId}`,
|
|
2200
|
+
`task: ${input.taskId}`,
|
|
2201
|
+
`prompt: ${promptPreview(input.promptText)}`,
|
|
2202
|
+
`agent: ${input.agentName}`,
|
|
2203
|
+
`model: ${input.providerQualifiedModelId}`,
|
|
2204
|
+
];
|
|
2205
|
+
if (input.asyncMode === true)
|
|
2206
|
+
lines.push("progress: use /flowdesk-status or flowdesk_status_live to follow durable lane evidence");
|
|
2207
|
+
if (input.resultText !== undefined)
|
|
2208
|
+
lines.push(`result: ${promptPreview(input.resultText, 200)}`);
|
|
2209
|
+
if (input.failureCategory !== undefined)
|
|
2210
|
+
lines.push(`failure: ${input.failureCategory}${input.redactedReason === undefined ? "" : ` (${input.redactedReason})`}`);
|
|
2211
|
+
return lines.join("\n");
|
|
2212
|
+
};
|
|
1976
2213
|
return {
|
|
1977
2214
|
[flowdeskAgentTaskRunToolName]: tool({
|
|
1978
2215
|
description: [
|
|
@@ -1980,7 +2217,7 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
|
|
|
1980
2217
|
"Use this to delegate a well-defined subtask to a specific model (e.g. Claude Opus for security analysis, GPT for architecture review).",
|
|
1981
2218
|
"Requires developerModeAcknowledged=true and allowProviderCall=true per call.",
|
|
1982
2219
|
"WHEN TO USE: user asks to delegate a specific task to a specific model/agent.",
|
|
1983
|
-
"WHEN NOT TO USE: multi-step workflows (use flowdesk_workflow_dispatch)
|
|
2220
|
+
"WHEN NOT TO USE: multi-step workflows (use flowdesk_workflow_dispatch).",
|
|
1984
2221
|
"After calling, use flowdesk_status_live to check the lane status.",
|
|
1985
2222
|
].join(" "),
|
|
1986
2223
|
args: {
|
|
@@ -1991,6 +2228,8 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
|
|
|
1991
2228
|
parentSessionId: tool.schema.string().optional().describe("Parent session id"),
|
|
1992
2229
|
developerModeAcknowledged: tool.schema.boolean(),
|
|
1993
2230
|
allowProviderCall: tool.schema.boolean(),
|
|
2231
|
+
nudgeQuietPeriodMs: tool.schema.number().optional().describe("Milliseconds of silence before sending a nudge prompt. Default 20000ms (20s). Recommended: always pass 20000. At 20s silence → nudge 1, 40s → nudge 2, 60s+ → lane fails and watchdog retries."),
|
|
2232
|
+
asyncMode: tool.schema.boolean().optional().describe("When true, return laneId immediately after launch. Watchdog polls child session, sends noReply nudges at 20s/40s, and aborts at 60s+. Coordinator uses flowdesk_status_live to detect completion. Recommended for all orchestration calls."),
|
|
1994
2233
|
},
|
|
1995
2234
|
async execute(args, ctx) {
|
|
1996
2235
|
const record = isRecord(args) ? args : {};
|
|
@@ -2010,6 +2249,9 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
|
|
|
2010
2249
|
: typeof ctxRecord.sessionID === "string" && ctxRecord.sessionID.length > 0
|
|
2011
2250
|
? ctxRecord.sessionID
|
|
2012
2251
|
: "";
|
|
2252
|
+
const nudgeQuietPeriodMs = typeof record.nudgeQuietPeriodMs === "number" && record.nudgeQuietPeriodMs > 0
|
|
2253
|
+
? Math.floor(record.nudgeQuietPeriodMs) : undefined;
|
|
2254
|
+
const asyncMode = record.asyncMode === true;
|
|
2013
2255
|
const taskId = `task-${Date.now().toString(36)}`;
|
|
2014
2256
|
const laneId = `lane-task-${Date.now().toString(36)}`;
|
|
2015
2257
|
const result = await executeFlowDeskAgentTaskV1({
|
|
@@ -2022,19 +2264,37 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
|
|
|
2022
2264
|
parentSessionId,
|
|
2023
2265
|
rootDir,
|
|
2024
2266
|
client,
|
|
2267
|
+
asyncMode,
|
|
2268
|
+
_nudgeQuietPeriodMs: nudgeQuietPeriodMs,
|
|
2025
2269
|
});
|
|
2270
|
+
const failureCategory = result.status === "task_failed" ? result.failureCategory : undefined;
|
|
2271
|
+
const redactedReason = result.status === "task_failed" ? result.redactedReason : undefined;
|
|
2026
2272
|
return JSON.stringify({
|
|
2027
2273
|
workflowId,
|
|
2028
2274
|
laneId,
|
|
2029
2275
|
taskId,
|
|
2030
2276
|
status: result.status,
|
|
2277
|
+
taskPreview: promptPreview(taskDescription),
|
|
2278
|
+
agentName,
|
|
2279
|
+
providerQualifiedModelId,
|
|
2280
|
+
...(result.status === "task_launched" ? { childSessionId: result.childSessionId, asyncMode: true, safeNextActions: ["/flowdesk-status"] } : {}),
|
|
2031
2281
|
resultText: result.status === "task_completed" ? result.resultText.slice(0, 4_096) : undefined,
|
|
2032
2282
|
resultTruncated: result.status === "task_completed" && result.resultText.length > 4_096,
|
|
2033
|
-
failureCategory
|
|
2034
|
-
redactedReason
|
|
2035
|
-
summaryForUser:
|
|
2036
|
-
|
|
2037
|
-
|
|
2283
|
+
failureCategory,
|
|
2284
|
+
redactedReason,
|
|
2285
|
+
summaryForUser: taskSummaryForUser({
|
|
2286
|
+
status: result.status,
|
|
2287
|
+
workflowId,
|
|
2288
|
+
laneId,
|
|
2289
|
+
taskId,
|
|
2290
|
+
agentName,
|
|
2291
|
+
providerQualifiedModelId,
|
|
2292
|
+
promptText: taskDescription,
|
|
2293
|
+
...(result.status === "task_completed" ? { resultText: result.resultText } : {}),
|
|
2294
|
+
...(result.status === "task_launched" ? { asyncMode: true } : {}),
|
|
2295
|
+
...(failureCategory === undefined ? {} : { failureCategory }),
|
|
2296
|
+
...(redactedReason === undefined ? {} : { redactedReason }),
|
|
2297
|
+
}),
|
|
2038
2298
|
});
|
|
2039
2299
|
},
|
|
2040
2300
|
}),
|
|
@@ -2157,6 +2417,37 @@ function isProviderUsageLiveEnabled(options) {
|
|
|
2157
2417
|
const value = options?.[flowdeskProviderUsageLiveOption];
|
|
2158
2418
|
return value === true || (isRecord(value) && value.enabled === true);
|
|
2159
2419
|
}
|
|
2420
|
+
/**
|
|
2421
|
+
* Returns true when OpenCode auth store has a google/gemini OAuth record that
|
|
2422
|
+
* FlowDesk's Gemini usage collector can auto-detect (via opencode-gemini-auth login),
|
|
2423
|
+
* without requiring explicit geminiOAuthClientId/Secret config.
|
|
2424
|
+
*/
|
|
2425
|
+
function geminiOAuthAutoDetectAvailable(homeDir) {
|
|
2426
|
+
try {
|
|
2427
|
+
const home = homeDir ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
2428
|
+
if (!home)
|
|
2429
|
+
return false;
|
|
2430
|
+
const xdgData = process.env.XDG_DATA_HOME
|
|
2431
|
+
? join(process.env.XDG_DATA_HOME, "opencode")
|
|
2432
|
+
: join(home, ".local", "share", "opencode");
|
|
2433
|
+
const authPath = join(xdgData, "auth.json");
|
|
2434
|
+
const raw = readFileSync(authPath, "utf8");
|
|
2435
|
+
const parsed = JSON.parse(raw);
|
|
2436
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
2437
|
+
return false;
|
|
2438
|
+
const db = parsed;
|
|
2439
|
+
const entry = db.google ?? db.gemini;
|
|
2440
|
+
if (typeof entry !== "object" || entry === null)
|
|
2441
|
+
return false;
|
|
2442
|
+
const rec = entry;
|
|
2443
|
+
return rec.type === "oauth" &&
|
|
2444
|
+
(typeof rec.access === "string" && rec.access.length > 0 ||
|
|
2445
|
+
typeof rec.refresh === "string" && rec.refresh.length > 0);
|
|
2446
|
+
}
|
|
2447
|
+
catch {
|
|
2448
|
+
return false;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2160
2451
|
function providerUsageLiveConfigFromOptions(options) {
|
|
2161
2452
|
const value = options?.[flowdeskProviderUsageLiveOption];
|
|
2162
2453
|
if (!isRecord(value) || value.enabled !== true)
|
|
@@ -2185,6 +2476,8 @@ function providerUsageLiveConfigFromOptions(options) {
|
|
|
2185
2476
|
config.geminiProjectId = value.geminiProjectId;
|
|
2186
2477
|
if (value.persistSnapshots === true)
|
|
2187
2478
|
config.persistSnapshots = true;
|
|
2479
|
+
if (value.appendToChat === true)
|
|
2480
|
+
config.appendToChat = true;
|
|
2188
2481
|
const explicitRoot = typeof value.durableStateRootDir === "string" &&
|
|
2189
2482
|
value.durableStateRootDir.trim().length > 0
|
|
2190
2483
|
? value.durableStateRootDir
|
|
@@ -2292,6 +2585,254 @@ function isLaneHeartbeatWriterEnabled(options) {
|
|
|
2292
2585
|
const value = options?.[flowdeskLaneHeartbeatWriterOption];
|
|
2293
2586
|
return value === true || (isRecord(value) && value.enabled === true);
|
|
2294
2587
|
}
|
|
2588
|
+
function isWorkflowDispatchPlanToolEnabled(options) {
|
|
2589
|
+
const value = options?.[flowdeskWorkflowDispatchPlanToolOption];
|
|
2590
|
+
return value === true || (isRecord(value) && value.enabled === true);
|
|
2591
|
+
}
|
|
2592
|
+
function workflowDispatchPlanToolConfigFromOptions(options) {
|
|
2593
|
+
const value = options?.[flowdeskWorkflowDispatchPlanToolOption];
|
|
2594
|
+
const enabledFromBool = value === true;
|
|
2595
|
+
const enabledFromRecord = isRecord(value) && value.enabled === true;
|
|
2596
|
+
if (!enabledFromBool && !enabledFromRecord)
|
|
2597
|
+
return undefined;
|
|
2598
|
+
const explicitRoot = isRecord(value) &&
|
|
2599
|
+
typeof value.rootDir === "string" &&
|
|
2600
|
+
value.rootDir.trim().length > 0
|
|
2601
|
+
? value.rootDir
|
|
2602
|
+
: undefined;
|
|
2603
|
+
const rootDir = explicitRoot ?? durableStateRootFromOptions(options);
|
|
2604
|
+
return rootDir === undefined ? undefined : { rootDir };
|
|
2605
|
+
}
|
|
2606
|
+
function workflowDispatchToolConfigFromOptions(input, options) {
|
|
2607
|
+
const value = options?.[flowdeskWorkflowDispatchOption];
|
|
2608
|
+
if (!isRecord(value) || value.enabled !== true || value.devBetaActualLaneLaunch !== true)
|
|
2609
|
+
return undefined;
|
|
2610
|
+
const explicitRoot = typeof value.rootDir === "string" && value.rootDir.trim().length > 0 ? value.rootDir : undefined;
|
|
2611
|
+
const rootDir = explicitRoot ?? durableStateRootFromOptions(options);
|
|
2612
|
+
if (rootDir === undefined)
|
|
2613
|
+
return undefined;
|
|
2614
|
+
const client = isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
|
|
2615
|
+
return client === undefined ? undefined : { rootDir, client };
|
|
2616
|
+
}
|
|
2617
|
+
function controlledWriteApplyConfigFromOptions(input, options) {
|
|
2618
|
+
const value = options?.[flowdeskControlledWriteApplyOption];
|
|
2619
|
+
if (!isRecord(value) || value.enabled !== true || value.devBetaControlledWriteApply !== true)
|
|
2620
|
+
return undefined;
|
|
2621
|
+
const explicitRoot = typeof value.rootDir === "string" && value.rootDir.trim().length > 0
|
|
2622
|
+
? value.rootDir
|
|
2623
|
+
: undefined;
|
|
2624
|
+
const durableStateRoot = explicitRoot ?? durableStateRootFromOptions(options);
|
|
2625
|
+
if (durableStateRoot === undefined)
|
|
2626
|
+
return undefined;
|
|
2627
|
+
const optionWorkspaceRoot = typeof value.workspaceRoot === "string" && value.workspaceRoot.trim().length > 0
|
|
2628
|
+
? value.workspaceRoot
|
|
2629
|
+
: undefined;
|
|
2630
|
+
const inputWorkspaceRoot = isRecord(input) && typeof input.workspace === "string" && input.workspace.trim().length > 0
|
|
2631
|
+
? input.workspace
|
|
2632
|
+
: isRecord(input) && typeof input.directory === "string" && input.directory.trim().length > 0
|
|
2633
|
+
? input.directory
|
|
2634
|
+
: undefined;
|
|
2635
|
+
const workspaceRoot = optionWorkspaceRoot ?? inputWorkspaceRoot ?? process.cwd();
|
|
2636
|
+
return { durableStateRoot, workspaceRoot };
|
|
2637
|
+
}
|
|
2638
|
+
export const flowdeskOrchestrateToolName = "flowdesk_orchestrate";
|
|
2639
|
+
export const flowdeskOrchestrateOption = "workflowOrchestrate";
|
|
2640
|
+
function orchestrateToolConfigFromOptions(input, options) {
|
|
2641
|
+
const value = options?.[flowdeskOrchestrateOption];
|
|
2642
|
+
if (!isRecord(value) || value.enabled !== true || value.devBetaActualLaneLaunch !== true)
|
|
2643
|
+
return undefined;
|
|
2644
|
+
const explicitRoot = typeof value.rootDir === "string" && value.rootDir.trim().length > 0 ? value.rootDir : undefined;
|
|
2645
|
+
const rootDir = explicitRoot ?? durableStateRootFromOptions(options);
|
|
2646
|
+
if (rootDir === undefined)
|
|
2647
|
+
return undefined;
|
|
2648
|
+
const client = isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
|
|
2649
|
+
return client === undefined ? undefined : { rootDir, client };
|
|
2650
|
+
}
|
|
2651
|
+
function createFlowDeskOrchestrateOptInTools(config) {
|
|
2652
|
+
return {
|
|
2653
|
+
[flowdeskOrchestrateToolName]: tool({
|
|
2654
|
+
description: [
|
|
2655
|
+
"Run a full FlowDesk multi-task orchestration: Author → Assign → Schedule → Synthesize.",
|
|
2656
|
+
"WHEN TO USE: the user gives a natural-language goal that requires decomposing into multiple tasks, assigning agents/models based on provider usage, executing them in dependency order, and aggregating results.",
|
|
2657
|
+
"INVOKE WITH: goalSummary (clear bounded goal text), parentSessionId, and optional workflowId.",
|
|
2658
|
+
"AFTER CALLING: surface summaryForUser and report synthesisId, taskCount, conflictDetected. All authority flags remain false.",
|
|
2659
|
+
"WHEN NOT TO USE: simple single-task requests, status checks, usage queries, or review fan-out.",
|
|
2660
|
+
].join(" "),
|
|
2661
|
+
args: {
|
|
2662
|
+
goalSummary: tool.schema.string().describe("Bounded natural-language goal for the workflow. Max 500 chars."),
|
|
2663
|
+
parentSessionId: tool.schema.string().describe("Current OpenCode session id (ses-...)."),
|
|
2664
|
+
workflowId: tool.schema.string().optional().describe("Optional stable workflow id. Auto-generated when omitted."),
|
|
2665
|
+
providerQualifiedModelId: tool.schema.string().optional().describe("Concrete provider/model id for author and synthesis calls. Defaults to openai/gpt-5.5."),
|
|
2666
|
+
agentName: tool.schema.string().optional().describe("Agent name for author and synthesis calls. Defaults to reviewer-gpt-frontier."),
|
|
2667
|
+
developerModeAcknowledged: tool.schema.boolean().describe("Must be true to allow provider calls."),
|
|
2668
|
+
allowProviderCall: tool.schema.boolean().describe("Must be true to allow provider calls."),
|
|
2669
|
+
allowActualLaneLaunch: tool.schema.boolean().describe("Must be true to allow actual lane launch."),
|
|
2670
|
+
},
|
|
2671
|
+
async execute(input) {
|
|
2672
|
+
if (!isRecord(input))
|
|
2673
|
+
return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "invalid input", safeNextActions: ["/flowdesk-doctor"] });
|
|
2674
|
+
if (input.developerModeAcknowledged !== true || input.allowProviderCall !== true || input.allowActualLaneLaunch !== true)
|
|
2675
|
+
return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "developerModeAcknowledged, allowProviderCall, and allowActualLaneLaunch must all be true", safeNextActions: ["/flowdesk-doctor"] });
|
|
2676
|
+
const goalSummary = typeof input.goalSummary === "string" ? input.goalSummary.slice(0, 500) : "";
|
|
2677
|
+
if (!goalSummary.trim())
|
|
2678
|
+
return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "goalSummary is required", safeNextActions: ["/flowdesk-doctor"] });
|
|
2679
|
+
const parentSessionId = typeof input.parentSessionId === "string" ? input.parentSessionId : "";
|
|
2680
|
+
if (!parentSessionId.trim())
|
|
2681
|
+
return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "parentSessionId is required", safeNextActions: ["/flowdesk-doctor"] });
|
|
2682
|
+
const providerQualifiedModelId = typeof input.providerQualifiedModelId === "string" ? input.providerQualifiedModelId : "openai/gpt-5.5";
|
|
2683
|
+
const agentName = typeof input.agentName === "string" ? input.agentName : "reviewer-gpt-frontier";
|
|
2684
|
+
const workflowId = typeof input.workflowId === "string" ? input.workflowId : undefined;
|
|
2685
|
+
const result = await executeFlowDeskWorkflowOrchestratorV1({
|
|
2686
|
+
workflowId,
|
|
2687
|
+
goalSummary,
|
|
2688
|
+
parentSessionId,
|
|
2689
|
+
rootDir: config.rootDir,
|
|
2690
|
+
client: config.client,
|
|
2691
|
+
providerQualifiedModelId,
|
|
2692
|
+
agentName,
|
|
2693
|
+
});
|
|
2694
|
+
return JSON.stringify(result);
|
|
2695
|
+
},
|
|
2696
|
+
}),
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
export function createFlowDeskWorkflowDispatchPlanOptInTools(config) {
|
|
2700
|
+
return {
|
|
2701
|
+
[flowdeskWorkflowDispatchPlanToolName]: tool({
|
|
2702
|
+
description: [
|
|
2703
|
+
"Build and persist a FlowDesk planning-only workflow dispatch plan using flowdesk.workflow_dispatch_plan.v1 evidence. This optional tool never opens dispatch authority, never calls providers, never executes runtime work, never launches lanes, and never performs fallback or reselection.",
|
|
2704
|
+
"WHEN TO USE: the user explicitly asks to plan a multi-role or multi-task FlowDesk workflow and durable planning evidence is useful before any later guarded command-backed step.",
|
|
2705
|
+
"WHEN NOT TO USE: ordinary chat, provider usage questions, status checks, code review fan-out, fallback/retry requests, or any request to actually dispatch, run, launch, execute, switch providers, or call a model.",
|
|
2706
|
+
"INVOKE WITH: optional workflowId, goalSummary, optional selectedAgentRoles, and tasks[] with agentRole plus summary/title labels. The configured server durable state root is used; do not pass user filesystem paths.",
|
|
2707
|
+
"AFTER CALLING: surface summaryForUser and safeNextActions. On blocked_before_workflow_dispatch_plan, report redactedBlockReason. Never claim dispatch/provider/runtime/lane/fallback authority.",
|
|
2708
|
+
].join(" "),
|
|
2709
|
+
args: {
|
|
2710
|
+
workflowId: tool.schema
|
|
2711
|
+
.string()
|
|
2712
|
+
.optional()
|
|
2713
|
+
.describe("Optional workflow id to bind the durable planning evidence to. Auto-generated when omitted."),
|
|
2714
|
+
goalSummary: tool.schema
|
|
2715
|
+
.string()
|
|
2716
|
+
.describe("Bounded redacted summary of the workflow planning goal."),
|
|
2717
|
+
selectedAgentRoles: tool.schema
|
|
2718
|
+
.array(tool.schema.string())
|
|
2719
|
+
.optional()
|
|
2720
|
+
.describe("Optional FlowDesk role categories to include in the planning evidence."),
|
|
2721
|
+
tasks: tool.schema
|
|
2722
|
+
.array(tool.schema.object({
|
|
2723
|
+
agentRole: tool.schema.string(),
|
|
2724
|
+
title: tool.schema.string().optional(),
|
|
2725
|
+
summary: tool.schema.string(),
|
|
2726
|
+
agentRoleRef: tool.schema.string().optional(),
|
|
2727
|
+
dependsOnTaskIds: tool.schema.array(tool.schema.string()).optional(),
|
|
2728
|
+
}))
|
|
2729
|
+
.describe("One or more planning-only task labels with agentRole and summary. No raw prompts or provider payloads."),
|
|
2730
|
+
},
|
|
2731
|
+
async execute(input) {
|
|
2732
|
+
const result = executeFlowDeskWorkflowDispatchPlanToolV1({
|
|
2733
|
+
config,
|
|
2734
|
+
rawInput: input,
|
|
2735
|
+
});
|
|
2736
|
+
return JSON.stringify(result);
|
|
2737
|
+
},
|
|
2738
|
+
}),
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
export function createFlowDeskAutoContinuePreviewOptInTools(config) {
|
|
2742
|
+
return {
|
|
2743
|
+
[flowdeskAutoContinuePreviewToolName]: tool({
|
|
2744
|
+
description: [
|
|
2745
|
+
"Preview the next pending task from durable FlowDesk workflow_dispatch_plan evidence without executing it.",
|
|
2746
|
+
"WHEN TO USE: the user asks whether FlowDesk can continue remaining planned work or asks what would run next from a durable plan.",
|
|
2747
|
+
"WHEN NOT TO USE: requests to actually dispatch, call a provider, launch a lane, fallback/reselect, mutate TUI/chat, or write files. This tool is preview-only and never performs continuation.",
|
|
2748
|
+
"INVOKE WITH: workflowId and optional maxSteps. The configured durable state root is used. Todo source is durable workflow_dispatch_plan evidence only, not transient chat/todowrite state.",
|
|
2749
|
+
"AFTER CALLING: surface summaryForUser, nextTaskId/title/summary, pendingTaskCount, and authority. Never claim automatic execution, dispatch, provider call, fallback, hard chat, or TUI action authority.",
|
|
2750
|
+
].join(" "),
|
|
2751
|
+
args: {
|
|
2752
|
+
workflowId: tool.schema.string().describe("Workflow id with durable workflow_dispatch_plan evidence."),
|
|
2753
|
+
maxSteps: tool.schema.number().optional().describe("Bounded preview cap. Clamped to 1..5; no steps are executed."),
|
|
2754
|
+
},
|
|
2755
|
+
async execute(input) {
|
|
2756
|
+
const result = executeFlowDeskAutoContinuePreviewToolV1({
|
|
2757
|
+
config,
|
|
2758
|
+
request: {
|
|
2759
|
+
workflowId: typeof input.workflowId === "string" ? input.workflowId : undefined,
|
|
2760
|
+
maxSteps: typeof input.maxSteps === "number" ? input.maxSteps : undefined,
|
|
2761
|
+
},
|
|
2762
|
+
});
|
|
2763
|
+
return JSON.stringify(result);
|
|
2764
|
+
},
|
|
2765
|
+
}),
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
export function createFlowDeskWorkflowDispatchOptInTools(config) {
|
|
2769
|
+
return {
|
|
2770
|
+
[flowdeskWorkflowDispatchToolName]: tool({
|
|
2771
|
+
description: [
|
|
2772
|
+
"Run one explicit dev-mode FlowDesk workflow task through the injected OpenCode SDK client. This optional beta tool is disabled by default and requires workflowDispatch.enabled=true plus workflowDispatch.devBetaActualLaneLaunch=true, durableStateRoot, developerModeAcknowledged=true, allowProviderCall=true, and allowActualLaneLaunch=true. It persists non-authorizing workflow_dispatch_plan evidence, launches exactly one lane through executeFlowDeskAgentTaskV1, and verifies terminal task evidence.",
|
|
2773
|
+
"WHEN TO USE: only when the user explicitly asks for dev-mode actual one-task workflow dispatch and understands this makes a provider/runtime call.",
|
|
2774
|
+
"WHEN NOT TO USE: default Release 1 workflows, planning-only requests, fallback/reselection, provider switching, controlled write/apply, ordinary chat, status, usage, or review fan-out.",
|
|
2775
|
+
"INVOKE WITH: optional workflowId, goalSummary, parentSessionId, one task with agentRole, summary, promptText, agentName, providerQualifiedModelId, optional outputContractRef=contract-task-result-v1, and the three explicit allow flags. Do not pass raw transcripts, provider payloads, write/apply instructions, fallback wording, or filesystem paths.",
|
|
2776
|
+
"AFTER CALLING: surface summaryForUser, ids, safeNextActions, and authority. Never claim default dispatch authority, write authority, fallback authority, hard chat cancellation, or default Release 1 dispatch enablement.",
|
|
2777
|
+
].join(" "),
|
|
2778
|
+
args: {
|
|
2779
|
+
workflowId: tool.schema.string().optional().describe("Optional workflow id. Auto-generated when omitted."),
|
|
2780
|
+
goalSummary: tool.schema.string().describe("Bounded redacted summary of the one-task dev-mode workflow goal."),
|
|
2781
|
+
parentSessionId: tool.schema.string().describe("Existing OpenCode parent session id. Required; no unrelated silent parent session is created."),
|
|
2782
|
+
task: tool.schema.object({
|
|
2783
|
+
agentRole: tool.schema.string().describe("FlowDesk role category for the single task."),
|
|
2784
|
+
summary: tool.schema.string().describe("Bounded task summary label for evidence."),
|
|
2785
|
+
promptText: tool.schema.string().describe("Bounded prompt text for the one launched lane."),
|
|
2786
|
+
agentName: tool.schema.string().describe("OpenCode agent name or agent-* ref for the lane."),
|
|
2787
|
+
providerQualifiedModelId: tool.schema.string().describe("Concrete provider/model id such as openai/gpt-5.5."),
|
|
2788
|
+
outputContractRef: tool.schema.string().optional().describe("Optional; only contract-task-result-v1 is supported in this pass."),
|
|
2789
|
+
}),
|
|
2790
|
+
developerModeAcknowledged: tool.schema.boolean().describe("Must be true to acknowledge dev-mode beta lane launch."),
|
|
2791
|
+
allowProviderCall: tool.schema.boolean().describe("Must be true to allow the provider call for this one lane."),
|
|
2792
|
+
allowActualLaneLaunch: tool.schema.boolean().describe("Must be true to allow actual one-lane runtime launch."),
|
|
2793
|
+
},
|
|
2794
|
+
async execute(input) {
|
|
2795
|
+
const result = await executeFlowDeskWorkflowDispatchToolV1({
|
|
2796
|
+
config,
|
|
2797
|
+
rawInput: input,
|
|
2798
|
+
});
|
|
2799
|
+
return JSON.stringify(result);
|
|
2800
|
+
},
|
|
2801
|
+
}),
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
export function createFlowDeskControlledWriteApplyOptInTools(config) {
|
|
2805
|
+
return {
|
|
2806
|
+
[flowdeskControlledWriteApplyToolName]: tool({
|
|
2807
|
+
description: [
|
|
2808
|
+
"Apply one complete-file replacement through the FlowDesk dev/beta controlled write path. This optional tool is disabled by default and requires controlledWriteApply.enabled=true plus controlledWriteApply.devBetaControlledWriteApply=true, durableStateRoot, developerModeAcknowledged=true, userApprovalRef, allowControlledWrite=true, and a workspace-relative target path.",
|
|
2809
|
+
"WHEN TO USE: only when the user explicitly approves a bounded local workspace file replacement in dev mode and the current file hash is known or the caller explicitly sets allowMissingExpectedHashForDevMode=true.",
|
|
2810
|
+
"WHEN NOT TO USE: default Release 1 workflows, model-generated automatic apply, provider/runtime dispatch, fallback/reselection, remote writes, shell execution, absolute paths, path traversal, symlink targets, binary content, or hidden injection.",
|
|
2811
|
+
"INVOKE WITH: workflowId, targetFilePath relative to the workspace root, expectedSha256 or expectedContentSha256 when available, replacementText, reasonSummary, developerModeAcknowledged=true, bounded userApprovalRef, and allowControlledWrite=true. Never pass raw transcripts, prompts, provider payloads, secrets, absolute paths, or shell output.",
|
|
2812
|
+
"AFTER CALLING: surface summaryForUser, targetFilePath, ledgerEntryId, hashes, safeNextActions, and authority. Never claim default Release 1 write authority, dispatch authority, provider calls, runtime execution, fallback authority, or hard chat cancellation.",
|
|
2813
|
+
].join(" "),
|
|
2814
|
+
args: {
|
|
2815
|
+
workflowId: tool.schema.string().describe("Stable FlowDesk workflow id for durable ledger evidence."),
|
|
2816
|
+
targetFilePath: tool.schema.string().describe("Workspace-relative target file path only. Absolute paths and traversal are rejected."),
|
|
2817
|
+
expectedSha256: tool.schema.string().optional().describe("Optional sha256-<hex> hash of the current target file content."),
|
|
2818
|
+
expectedContentSha256: tool.schema.string().optional().describe("Optional sha256-<hex> hash alias for the current target file content."),
|
|
2819
|
+
allowMissingExpectedHashForDevMode: tool.schema.boolean().optional().describe("Must be true to proceed without an expected current-content hash."),
|
|
2820
|
+
replacementText: tool.schema.string().describe("Complete replacement file text, bounded and non-binary."),
|
|
2821
|
+
reasonSummary: tool.schema.string().describe("Bounded redacted reason for the controlled write."),
|
|
2822
|
+
developerModeAcknowledged: tool.schema.boolean().describe("Must be true to acknowledge dev/beta controlled write authority."),
|
|
2823
|
+
userApprovalRef: tool.schema.string().describe("Bounded opaque user approval reference for this write."),
|
|
2824
|
+
allowControlledWrite: tool.schema.boolean().describe("Must be true to permit this one controlled local write."),
|
|
2825
|
+
},
|
|
2826
|
+
async execute(input) {
|
|
2827
|
+
const result = executeFlowDeskControlledWriteApplyToolV1({
|
|
2828
|
+
config,
|
|
2829
|
+
rawInput: input,
|
|
2830
|
+
});
|
|
2831
|
+
return JSON.stringify(result);
|
|
2832
|
+
},
|
|
2833
|
+
}),
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2295
2836
|
function isAgentTaskRunEnabled(options) {
|
|
2296
2837
|
const value = options?.[flowdeskAgentTaskRunOption];
|
|
2297
2838
|
return value === true || (isRecord(value) && value.enabled === true);
|
|
@@ -2477,6 +3018,134 @@ export function createFlowDeskStatusLiveOptInTools(config) {
|
|
|
2477
3018
|
}),
|
|
2478
3019
|
};
|
|
2479
3020
|
}
|
|
3021
|
+
export function createFlowDeskWorkflowSynthesisPreviewTools(config) {
|
|
3022
|
+
return {
|
|
3023
|
+
[flowdeskWorkflowSynthesisPreviewToolName]: tool({
|
|
3024
|
+
description: [
|
|
3025
|
+
"Build and persist a provider-free FlowDesk synthesis preview from existing task_result evidence. This local tool never calls providers, launches lanes, dispatches runtime work, performs fallback/reselection, writes workspace files, or grants execution authority.",
|
|
3026
|
+
"WHEN TO USE: a workflow reports next_action=synthesis_ready and the user asks to summarize, synthesize, continue with the next synthesis step, or see the completed task results without making a model/provider call.",
|
|
3027
|
+
"WHEN NOT TO USE: requests for model-authored synthesis, real dispatch, provider calls, write/apply, fallback/retry, or replacing raw task results.",
|
|
3028
|
+
"INVOKE WITH: workflowId for a workflow that already has task_result evidence under the configured durable root.",
|
|
3029
|
+
"AFTER CALLING: surface summaryForUser, synthesisId, tasksSummarized, conflictDetected, safeNextActions, and authority. State that this is a provider-free preview derived from durable task_result evidence.",
|
|
3030
|
+
].join(" "),
|
|
3031
|
+
args: {
|
|
3032
|
+
workflowId: tool.schema.string().describe("Workflow id with existing task_result evidence to synthesize locally."),
|
|
3033
|
+
},
|
|
3034
|
+
async execute(input) {
|
|
3035
|
+
const workflowId = isRecord(input) && typeof input.workflowId === "string" ? input.workflowId : "";
|
|
3036
|
+
const result = executeFlowDeskWorkflowSynthesisPreviewV1({ workflowId, rootDir: config.rootDir });
|
|
3037
|
+
return JSON.stringify(result);
|
|
3038
|
+
},
|
|
3039
|
+
}),
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
function isFlowDeskUiProbeEnabled(options) {
|
|
3043
|
+
const raw = options?.[flowdeskUiProbeOption];
|
|
3044
|
+
return process.env.FLOWDESK_UI_PROBE === "1" || raw === true || (isRecord(raw) && raw.enabled === true);
|
|
3045
|
+
}
|
|
3046
|
+
function boundedUiProbeString(value, maxLength = 160) {
|
|
3047
|
+
if (typeof value !== "string" || value.length === 0)
|
|
3048
|
+
return undefined;
|
|
3049
|
+
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value;
|
|
3050
|
+
}
|
|
3051
|
+
function summarizeFlowDeskUiProbeEventV1(event, observedAt) {
|
|
3052
|
+
const rec = isRecord(event) ? event : {};
|
|
3053
|
+
const properties = Object.keys(rec).sort().slice(0, 20);
|
|
3054
|
+
const nested = (name) => {
|
|
3055
|
+
const value = rec[name];
|
|
3056
|
+
return isRecord(value) ? value : undefined;
|
|
3057
|
+
};
|
|
3058
|
+
const session = nested("session") ?? nested("sessionID") ?? nested("sessionId");
|
|
3059
|
+
const message = nested("message");
|
|
3060
|
+
const part = nested("part");
|
|
3061
|
+
const sessionId = boundedUiProbeString(rec.sessionID) ??
|
|
3062
|
+
boundedUiProbeString(rec.sessionId) ??
|
|
3063
|
+
boundedUiProbeString(session?.id);
|
|
3064
|
+
return {
|
|
3065
|
+
observedAt,
|
|
3066
|
+
eventType: boundedUiProbeString(rec.type, 120) ?? boundedUiProbeString(rec.event, 120) ?? "unknown",
|
|
3067
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
3068
|
+
...(boundedUiProbeString(message?.id) === undefined ? {} : { messageId: boundedUiProbeString(message?.id) }),
|
|
3069
|
+
...(boundedUiProbeString(part?.id) === undefined ? {} : { partId: boundedUiProbeString(part?.id) }),
|
|
3070
|
+
keys: properties,
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
function createFlowDeskUiProbeTools(observations) {
|
|
3074
|
+
return {
|
|
3075
|
+
[flowdeskUiProbeToolName]: tool({
|
|
3076
|
+
description: [
|
|
3077
|
+
"Developer-only FlowDesk UI renderer/event probe. Enabled only when FLOWDESK_UI_PROBE=1.",
|
|
3078
|
+
"It emits bounded markdown/details/link/code-fence/long-log samples and reports redacted OpenCode event observations captured by the plugin event hook.",
|
|
3079
|
+
"No dispatch, provider call, write authority, fallback authority, or hard chat control is granted.",
|
|
3080
|
+
].join(" "),
|
|
3081
|
+
args: {
|
|
3082
|
+
mode: tool.schema
|
|
3083
|
+
.string()
|
|
3084
|
+
.optional()
|
|
3085
|
+
.describe("Probe mode: render, events, or all. Defaults to all."),
|
|
3086
|
+
limit: tool.schema
|
|
3087
|
+
.number()
|
|
3088
|
+
.optional()
|
|
3089
|
+
.describe("Maximum recent event observations to return; clamped to 1..50."),
|
|
3090
|
+
},
|
|
3091
|
+
async execute(input) {
|
|
3092
|
+
const mode = isRecord(input) && typeof input.mode === "string" ? input.mode : "all";
|
|
3093
|
+
const requestedLimit = isRecord(input) && typeof input.limit === "number" ? Math.floor(input.limit) : 20;
|
|
3094
|
+
const limit = Math.max(1, Math.min(50, requestedLimit));
|
|
3095
|
+
const includeRender = mode === "all" || mode === "render";
|
|
3096
|
+
const includeEvents = mode === "all" || mode === "events";
|
|
3097
|
+
const longLog = Array.from({ length: 40 }, (_, index) => {
|
|
3098
|
+
const n = String(index + 1).padStart(2, "0");
|
|
3099
|
+
return `${n}. ui-probe timeline event ${n}: message/status/log renderer sample`;
|
|
3100
|
+
});
|
|
3101
|
+
return JSON.stringify({
|
|
3102
|
+
schema_version: "flowdesk.ui_probe_result.v1",
|
|
3103
|
+
status: "ui_probe_collected",
|
|
3104
|
+
probeEnabled: true,
|
|
3105
|
+
mode,
|
|
3106
|
+
authority: {
|
|
3107
|
+
realOpenCodeDispatch: false,
|
|
3108
|
+
providerCall: false,
|
|
3109
|
+
runtimeExecution: false,
|
|
3110
|
+
actualLaneLaunch: false,
|
|
3111
|
+
fallbackAuthority: false,
|
|
3112
|
+
hardCancelOrNoReplyAuthority: false,
|
|
3113
|
+
},
|
|
3114
|
+
...(includeRender
|
|
3115
|
+
? {
|
|
3116
|
+
renderSamples: {
|
|
3117
|
+
markdownDetails: [
|
|
3118
|
+
"<details>",
|
|
3119
|
+
"<summary>FlowDesk UI probe: details summary</summary>",
|
|
3120
|
+
"",
|
|
3121
|
+
"[FlowDesk markdown link probe](https://example.com/flowdesk-ui-probe)",
|
|
3122
|
+
"",
|
|
3123
|
+
"```text",
|
|
3124
|
+
...longLog.slice(0, 8),
|
|
3125
|
+
"```",
|
|
3126
|
+
"</details>",
|
|
3127
|
+
].join("\n"),
|
|
3128
|
+
longLogText: ["FlowDesk UI probe long log", ...longLog].join("\n"),
|
|
3129
|
+
observationInstructions: [
|
|
3130
|
+
"1. Check whether markdownDetails renders as collapsible UI or plain text.",
|
|
3131
|
+
"2. Click the markdown link and re-run mode=events to see whether any click/navigation event reached Hooks.event.",
|
|
3132
|
+
"3. Check whether longLogText is folded, scrollable, or fully expanded by OpenCode's tool-output renderer.",
|
|
3133
|
+
].join("\n"),
|
|
3134
|
+
},
|
|
3135
|
+
}
|
|
3136
|
+
: {}),
|
|
3137
|
+
...(includeEvents
|
|
3138
|
+
? {
|
|
3139
|
+
recentEventObservations: observations.slice(-limit),
|
|
3140
|
+
recentEventObservationCount: Math.min(observations.length, limit),
|
|
3141
|
+
totalBufferedEventObservationCount: observations.length,
|
|
3142
|
+
}
|
|
3143
|
+
: {}),
|
|
3144
|
+
});
|
|
3145
|
+
},
|
|
3146
|
+
}),
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
2480
3149
|
export function createFlowDeskProviderUsageLiveOptInTools(config) {
|
|
2481
3150
|
return {
|
|
2482
3151
|
[flowdeskProviderUsageLiveToolName]: tool({
|
|
@@ -2486,7 +3155,7 @@ export function createFlowDeskProviderUsageLiveOptInTools(config) {
|
|
|
2486
3155
|
"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.",
|
|
2487
3156
|
"WHEN NOT TO USE: general chat, status of an in-progress workflow (use status instead), or any non-usage question.",
|
|
2488
3157
|
"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.",
|
|
2489
|
-
"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-
|
|
3158
|
+
"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-daily, 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.",
|
|
2490
3159
|
].join(" "),
|
|
2491
3160
|
args: {
|
|
2492
3161
|
providerFamily: tool.schema
|
|
@@ -2603,6 +3272,9 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
2603
3272
|
const laneHeartbeatWriterConfigForDoctor = isLaneHeartbeatWriterEnabled(options)
|
|
2604
3273
|
? laneHeartbeatWriterConfigFromOptions(options)
|
|
2605
3274
|
: undefined;
|
|
3275
|
+
const workflowDispatchPlanConfigForDoctor = isWorkflowDispatchPlanToolEnabled(options)
|
|
3276
|
+
? workflowDispatchPlanToolConfigFromOptions(options)
|
|
3277
|
+
: undefined;
|
|
2606
3278
|
const quickReviewerRunRegistered = isQuickReviewerRunEnabled(options) &&
|
|
2607
3279
|
quickReviewerRunClientFrom(input, options) !== undefined;
|
|
2608
3280
|
const naturalLanguageTools = {
|
|
@@ -2622,14 +3294,15 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
2622
3294
|
"string",
|
|
2623
3295
|
persistWorkflowId: providerUsageLiveConfigForDoctor?.persistWorkflowId,
|
|
2624
3296
|
geminiOAuthConfigured: providerUsageLiveConfigForDoctor !== undefined &&
|
|
2625
|
-
(
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
typeof process.env.FLOWDESK_GEMINI_OAUTH_CLIENT_SECRET ===
|
|
2632
|
-
|
|
3297
|
+
(
|
|
3298
|
+
// Explicit inline config
|
|
3299
|
+
(providerUsageLiveConfigForDoctor.geminiOAuthClientId !== undefined &&
|
|
3300
|
+
providerUsageLiveConfigForDoctor.geminiOAuthClientSecret !== undefined) ||
|
|
3301
|
+
// Explicit env vars
|
|
3302
|
+
typeof process.env.FLOWDESK_GEMINI_OAUTH_CLIENT_ID === "string" ||
|
|
3303
|
+
typeof process.env.FLOWDESK_GEMINI_OAUTH_CLIENT_SECRET === "string" ||
|
|
3304
|
+
// Auto-detect: OpenCode auth store (opencode-gemini-auth login)
|
|
3305
|
+
geminiOAuthAutoDetectAvailable(providerUsageLiveConfigForDoctor.homeDir)),
|
|
2633
3306
|
hint: isProviderUsageLiveEnabled(options) &&
|
|
2634
3307
|
providerUsageLiveConfigForDoctor === undefined
|
|
2635
3308
|
? "providerUsageLive.enabled=true but no provider family configured; set providers=['claude','openai','gemini']"
|
|
@@ -2665,6 +3338,23 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
2665
3338
|
? "laneHeartbeatWriter.enabled=true but no durable state root resolved; set laneHeartbeatWriter.rootDir or top-level durableStateRoot"
|
|
2666
3339
|
: undefined,
|
|
2667
3340
|
},
|
|
3341
|
+
workflowDispatchPlanTool: {
|
|
3342
|
+
enabled: isWorkflowDispatchPlanToolEnabled(options),
|
|
3343
|
+
registered: workflowDispatchPlanConfigForDoctor !== undefined,
|
|
3344
|
+
rootDir: workflowDispatchPlanConfigForDoctor?.rootDir,
|
|
3345
|
+
persistsWorkflowDispatchPlanEvidence: workflowDispatchPlanConfigForDoctor !== undefined,
|
|
3346
|
+
authority: {
|
|
3347
|
+
realOpenCodeDispatch: false,
|
|
3348
|
+
providerCall: false,
|
|
3349
|
+
runtimeExecution: false,
|
|
3350
|
+
actualLaneLaunch: false,
|
|
3351
|
+
fallbackAuthority: false,
|
|
3352
|
+
},
|
|
3353
|
+
hint: isWorkflowDispatchPlanToolEnabled(options) &&
|
|
3354
|
+
workflowDispatchPlanConfigForDoctor === undefined
|
|
3355
|
+
? "workflowDispatchPlanTool.enabled=true but no durable state root resolved; set workflowDispatchPlanTool.rootDir or top-level durableStateRoot"
|
|
3356
|
+
: undefined,
|
|
3357
|
+
},
|
|
2668
3358
|
chatMessageStallAlert: {
|
|
2669
3359
|
enabled: options?.[flowdeskChatMessageStallAlertOption] === true ||
|
|
2670
3360
|
(isRecord(options?.[flowdeskChatMessageStallAlertOption]) &&
|
|
@@ -2763,8 +3453,10 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
2763
3453
|
const statusLiveConfig = isStatusLiveEnabled(options)
|
|
2764
3454
|
? statusLiveConfigFromOptions(options)
|
|
2765
3455
|
: undefined;
|
|
2766
|
-
if (statusLiveConfig !== undefined)
|
|
3456
|
+
if (statusLiveConfig !== undefined) {
|
|
2767
3457
|
Object.assign(tools, createFlowDeskStatusLiveOptInTools(statusLiveConfig));
|
|
3458
|
+
Object.assign(tools, createFlowDeskWorkflowSynthesisPreviewTools({ rootDir: statusLiveConfig.rootDir }));
|
|
3459
|
+
}
|
|
2768
3460
|
const quickFallbackRunConfig = isQuickFallbackRunEnabled(options)
|
|
2769
3461
|
? quickFallbackRunConfigFromOptions(options)
|
|
2770
3462
|
: undefined;
|
|
@@ -2775,6 +3467,19 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
2775
3467
|
: undefined;
|
|
2776
3468
|
if (laneHeartbeatWriterConfig !== undefined)
|
|
2777
3469
|
Object.assign(tools, createFlowDeskLaneHeartbeatWriterOptInTools(laneHeartbeatWriterConfig));
|
|
3470
|
+
const workflowDispatchPlanConfig = isWorkflowDispatchPlanToolEnabled(options)
|
|
3471
|
+
? workflowDispatchPlanToolConfigFromOptions(options)
|
|
3472
|
+
: undefined;
|
|
3473
|
+
if (workflowDispatchPlanConfig !== undefined)
|
|
3474
|
+
Object.assign(tools, createFlowDeskWorkflowDispatchPlanOptInTools(workflowDispatchPlanConfig));
|
|
3475
|
+
if (workflowDispatchPlanConfig !== undefined)
|
|
3476
|
+
Object.assign(tools, createFlowDeskAutoContinuePreviewOptInTools(workflowDispatchPlanConfig));
|
|
3477
|
+
const workflowDispatchConfig = workflowDispatchToolConfigFromOptions(input, options);
|
|
3478
|
+
if (workflowDispatchConfig !== undefined)
|
|
3479
|
+
Object.assign(tools, createFlowDeskWorkflowDispatchOptInTools(workflowDispatchConfig));
|
|
3480
|
+
const controlledWriteApplyConfig = controlledWriteApplyConfigFromOptions(input, options);
|
|
3481
|
+
if (controlledWriteApplyConfig !== undefined)
|
|
3482
|
+
Object.assign(tools, createFlowDeskControlledWriteApplyOptInTools(controlledWriteApplyConfig));
|
|
2778
3483
|
const agentTaskRunEnabled = isAgentTaskRunEnabled(options);
|
|
2779
3484
|
if (agentTaskRunEnabled) {
|
|
2780
3485
|
const agentTaskRunClient = isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
|
|
@@ -2786,6 +3491,13 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
2786
3491
|
if (agentTaskRunTools !== undefined)
|
|
2787
3492
|
Object.assign(tools, agentTaskRunTools);
|
|
2788
3493
|
}
|
|
3494
|
+
const orchestrateConfig = orchestrateToolConfigFromOptions(input, options);
|
|
3495
|
+
if (orchestrateConfig !== undefined)
|
|
3496
|
+
Object.assign(tools, createFlowDeskOrchestrateOptInTools(orchestrateConfig));
|
|
3497
|
+
const uiProbeEnabled = isFlowDeskUiProbeEnabled(options);
|
|
3498
|
+
const uiProbeEventObservations = [];
|
|
3499
|
+
if (uiProbeEnabled)
|
|
3500
|
+
Object.assign(tools, createFlowDeskUiProbeTools(uiProbeEventObservations));
|
|
2789
3501
|
// P8 Background Watchdog
|
|
2790
3502
|
const watchdogConfig = watchdogConfigFromOptions(options);
|
|
2791
3503
|
const chatStallAlertRaw = options?.[flowdeskChatMessageStallAlertOption];
|
|
@@ -2886,14 +3598,54 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
2886
3598
|
},
|
|
2887
3599
|
});
|
|
2888
3600
|
}
|
|
3601
|
+
const eventRootDir = durableStateRootFromOptions(options);
|
|
3602
|
+
const eventHook = eventRootDir === undefined && !uiProbeEnabled
|
|
3603
|
+
? undefined
|
|
3604
|
+
: async (input) => {
|
|
3605
|
+
if (uiProbeEnabled) {
|
|
3606
|
+
uiProbeEventObservations.push(summarizeFlowDeskUiProbeEventV1(input.event, new Date().toISOString()));
|
|
3607
|
+
if (uiProbeEventObservations.length > 200)
|
|
3608
|
+
uiProbeEventObservations.splice(0, uiProbeEventObservations.length - 200);
|
|
3609
|
+
}
|
|
3610
|
+
if (eventRootDir !== undefined) {
|
|
3611
|
+
await observeFlowDeskOpenCodeEventV1({ rootDir: eventRootDir, event: input.event });
|
|
3612
|
+
}
|
|
3613
|
+
};
|
|
2889
3614
|
if (!naturalLanguageRoutingEnabled)
|
|
2890
|
-
return { tool: tools };
|
|
3615
|
+
return eventHook === undefined ? { tool: tools } : { tool: tools, event: eventHook };
|
|
2891
3616
|
const stallAlertOption = chatMessageStallAlertOptionsFrom(options, statusLiveConfig, isRecord(input) && isManagedDispatchBetaClient(input.client)
|
|
2892
3617
|
? input.client
|
|
2893
3618
|
: undefined);
|
|
3619
|
+
// Background sidebar cache refresh interval — runs every 3 minutes regardless of chat activity
|
|
3620
|
+
if (providerUsageLiveConfig?.durableStateRootDir) {
|
|
3621
|
+
const capturedUsageConfig = providerUsageLiveConfig;
|
|
3622
|
+
const sidebarRefreshIntervalMs = 3 * 60_000;
|
|
3623
|
+
const sidebarRefreshInterval = setInterval(() => {
|
|
3624
|
+
const cachePath = join(capturedUsageConfig.durableStateRootDir, ".flowdesk", "ui", "provider-usage-sidebar.json");
|
|
3625
|
+
let isStale = false;
|
|
3626
|
+
try {
|
|
3627
|
+
const cache = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
3628
|
+
if (typeof cache.observed_at === "string" && Date.now() - Date.parse(cache.observed_at) > sidebarRefreshIntervalMs)
|
|
3629
|
+
isStale = true;
|
|
3630
|
+
}
|
|
3631
|
+
catch {
|
|
3632
|
+
isStale = true;
|
|
3633
|
+
}
|
|
3634
|
+
if (isStale) {
|
|
3635
|
+
executeFlowDeskProviderUsageLiveV1({
|
|
3636
|
+
config: { ...capturedUsageConfig, persistSidebarCache: true },
|
|
3637
|
+
request: { providerFamily: "all" },
|
|
3638
|
+
}).catch(() => { });
|
|
3639
|
+
}
|
|
3640
|
+
}, sidebarRefreshIntervalMs);
|
|
3641
|
+
sidebarRefreshInterval.unref();
|
|
3642
|
+
process.once("exit", () => clearInterval(sidebarRefreshInterval));
|
|
3643
|
+
process.once("SIGTERM", () => clearInterval(sidebarRefreshInterval));
|
|
3644
|
+
}
|
|
2894
3645
|
return {
|
|
2895
3646
|
tool: tools,
|
|
2896
|
-
|
|
3647
|
+
...(eventHook === undefined ? {} : { event: eventHook }),
|
|
3648
|
+
"chat.message": createFlowDeskNaturalLanguageChatMessageHook(() => new Date(), localSession, stallAlertOption, durableStateRootFromOptions(options), providerUsageLiveConfig),
|
|
2897
3649
|
};
|
|
2898
3650
|
};
|
|
2899
3651
|
export const flowdeskChatMessageStallAlertOption = "chatMessageStallAlert";
|
|
@@ -2933,6 +3685,13 @@ function chatMessageStallAlertOptionsFrom(options, statusLiveConfig, sdkClient)
|
|
|
2933
3685
|
if (recordRaw !== undefined &&
|
|
2934
3686
|
typeof recordRaw.includeProgressingLate === "boolean")
|
|
2935
3687
|
config.includeProgressingLate = recordRaw.includeProgressingLate;
|
|
3688
|
+
if (recordRaw !== undefined &&
|
|
3689
|
+
typeof recordRaw.includeProgressCards === "boolean")
|
|
3690
|
+
config.includeProgressCards = recordRaw.includeProgressCards;
|
|
3691
|
+
if (recordRaw !== undefined &&
|
|
3692
|
+
typeof recordRaw.maxProgressCards === "number" &&
|
|
3693
|
+
recordRaw.maxProgressCards > 0)
|
|
3694
|
+
config.maxProgressCards = Math.min(6, Math.max(1, Math.floor(recordRaw.maxProgressCards)));
|
|
2936
3695
|
if (recordRaw !== undefined && isRecord(recordRaw.guardedAutoAbort)) {
|
|
2937
3696
|
const rawGuard = recordRaw.guardedAutoAbort;
|
|
2938
3697
|
const guardedAutoAbort = {
|