@flowdesk/opencode-plugin 0.1.11 → 0.1.12
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/dist/agent-task-runner.d.ts +26 -0
- package/dist/agent-task-runner.d.ts.map +1 -0
- package/dist/agent-task-runner.js +244 -0
- package/dist/agent-task-runner.js.map +1 -0
- package/dist/bootstrap-installer.d.ts +3 -0
- package/dist/bootstrap-installer.d.ts.map +1 -1
- package/dist/bootstrap-installer.js +153 -7
- package/dist/bootstrap-installer.js.map +1 -1
- package/dist/command-handlers.d.ts +3 -0
- package/dist/command-handlers.d.ts.map +1 -1
- package/dist/command-handlers.js +38 -4
- package/dist/command-handlers.js.map +1 -1
- package/dist/local-adapter.d.ts.map +1 -1
- package/dist/local-adapter.js +19 -0
- package/dist/local-adapter.js.map +1 -1
- package/dist/managed-dispatch-adapter.d.ts +2 -0
- package/dist/managed-dispatch-adapter.d.ts.map +1 -1
- package/dist/managed-dispatch-adapter.js +86 -1
- package/dist/managed-dispatch-adapter.js.map +1 -1
- package/dist/quick-reviewer-run.d.ts +13 -2
- package/dist/quick-reviewer-run.d.ts.map +1 -1
- package/dist/quick-reviewer-run.js +213 -69
- package/dist/quick-reviewer-run.js.map +1 -1
- package/dist/runtime-reviewer-execution-bridge.d.ts.map +1 -1
- package/dist/runtime-reviewer-execution-bridge.js +46 -1
- package/dist/runtime-reviewer-execution-bridge.js.map +1 -1
- package/dist/server.d.ts +47 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +467 -59
- package/dist/server.js.map +1 -1
- package/dist/shared/with-timeout.d.ts +12 -0
- package/dist/shared/with-timeout.d.ts.map +1 -0
- package/dist/shared/with-timeout.js +31 -0
- package/dist/shared/with-timeout.js.map +1 -0
- package/dist/stall-recovery.d.ts +187 -0
- package/dist/stall-recovery.d.ts.map +1 -0
- package/dist/stall-recovery.js +962 -0
- package/dist/stall-recovery.js.map +1 -0
- package/dist/status-live-tool.d.ts +1 -0
- package/dist/status-live-tool.d.ts.map +1 -1
- package/dist/status-live-tool.js +128 -1
- package/dist/status-live-tool.js.map +1 -1
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { join, resolve, sep } from "node:path";
|
|
3
4
|
import { applyFlowDeskSessionEvidenceWriteIntentsV1, createFlowDeskChatHookAuthorityProbeV1, evaluateFlowDeskChatIntakeV1, getFlowDeskPortableCommandToolName, getRelease1SchemaArtifact, materializeFlowDeskExactModelCacheEvidenceFromProviderAcquisitionEvidenceV1, materializeFlowDeskRuntimeLaneLaunchPlansFromReviewerFanoutEvidenceV1, planFlowDeskReviewerFanoutFromReloadedCacheEvidenceV1, prepareFlowDeskSessionEvidenceWriteIntentV1, reloadFlowDeskSessionEvidenceV1, validateFlowDeskDefaultManagedDispatchAuthorizationV1, validateProjectConfigV1, validateRunRequestV1, } from "@flowdesk/core";
|
|
@@ -10,7 +11,10 @@ import { executeFlowDeskProviderUsageLiveV1, } from "./provider-usage-live-tool.
|
|
|
10
11
|
import { executeFlowDeskQuickFallbackRunV1, } from "./quick-fallback-run.js";
|
|
11
12
|
import { executeFlowDeskQuickReviewerRunV1, } from "./quick-reviewer-run.js";
|
|
12
13
|
import { executeFlowDeskRuntimeReviewerExecutionBridgeV1, redactedRuntimeReviewerExecutionBlocked, runtimeReviewerExecutionExpectationsFromValue, } from "./runtime-reviewer-execution-bridge.js";
|
|
14
|
+
import { executeFlowDeskAgentTaskV1 } from "./agent-task-runner.js";
|
|
13
15
|
import { executeFlowDeskStatusLiveV1, } from "./status-live-tool.js";
|
|
16
|
+
import { evaluateGuardedAutoAbortHookV1, evaluateGuardedAutoRetryHookV1, reconcileStalePendingRetryPlansV1, checkSdkSessionApiHealthV1, runFlowDeskWatchdogCycleV1, } from "./stall-recovery.js";
|
|
17
|
+
import { withTimeout, FlowDeskTimeoutError } from "./shared/with-timeout.js";
|
|
14
18
|
import { FLOWDESK_PRE_SPIKE_PLUGIN_TOOL_STUBS, getFlowDeskRelease1HandlerReadinessSummary, getFlowDeskRelease1ProductionReadinessSummary, hasPassingFds1SchemaConversionSpike, runFlowDeskPreSpikePluginToolStub, } from "./tool-stubs.js";
|
|
15
19
|
export const flowdeskPreSpikeDoctorToolName = "flowdesk_pre_spike_doctor";
|
|
16
20
|
export const flowdeskChatIntakeToolName = "flowdesk_chat_intake";
|
|
@@ -31,6 +35,8 @@ export const flowdeskStatusLiveOption = "statusLive";
|
|
|
31
35
|
export const flowdeskQuickFallbackRunOption = "quickFallbackRun";
|
|
32
36
|
export const flowdeskLaneHeartbeatWriterOption = "laneHeartbeatWriter";
|
|
33
37
|
export const flowdeskDefaultManagedDispatchAuthorizationOption = "defaultManagedDispatchAuthorization";
|
|
38
|
+
export const flowdeskWatchdogOption = "watchdog";
|
|
39
|
+
export const flowdeskWatchdogTriggerToolName = "flowdesk_watchdog_trigger";
|
|
34
40
|
export const flowdeskManagedDispatchBetaToolName = "flowdesk_managed_dispatch_beta";
|
|
35
41
|
export const flowdeskExactModelProviderAcquisitionLiveTestToolName = "flowdesk_exact_model_provider_acquisition_live_test";
|
|
36
42
|
export const flowdeskRuntimeReviewerExecutionToolName = "flowdesk_runtime_reviewer_execution";
|
|
@@ -40,6 +46,8 @@ export const flowdeskProviderUsageLiveToolName = "flowdesk_provider_usage_live";
|
|
|
40
46
|
export const flowdeskStatusLiveToolName = "flowdesk_status_live";
|
|
41
47
|
export const flowdeskQuickFallbackRunToolName = "flowdesk_quick_fallback_run";
|
|
42
48
|
export const flowdeskLaneHeartbeatWriterToolName = "flowdesk_lane_heartbeat_record";
|
|
49
|
+
export const flowdeskAgentTaskRunOption = "agentTaskRun";
|
|
50
|
+
export const flowdeskAgentTaskRunToolName = "flowdesk_agent_task_run";
|
|
43
51
|
const flowdeskChatSuggestionDuplicateWindowMs = 10_000;
|
|
44
52
|
const disabledAuthority = {
|
|
45
53
|
productionRegistrationEligible: false,
|
|
@@ -1163,6 +1171,15 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
|
|
|
1163
1171
|
const recentStallAlerts = new Map();
|
|
1164
1172
|
return async function message(input, output) {
|
|
1165
1173
|
const inputRecord = isRecord(input) ? input : {};
|
|
1174
|
+
const partSessionID = typeof inputRecord.sessionID === "string" ? inputRecord.sessionID : "";
|
|
1175
|
+
const partMessageID = typeof inputRecord.messageID === "string" ? inputRecord.messageID : "";
|
|
1176
|
+
const buildTextPart = (text) => ({
|
|
1177
|
+
id: `prt_${randomUUID().replaceAll("-", "")}`,
|
|
1178
|
+
sessionID: partSessionID,
|
|
1179
|
+
messageID: partMessageID,
|
|
1180
|
+
type: "text",
|
|
1181
|
+
text,
|
|
1182
|
+
});
|
|
1166
1183
|
const request = intakeRequestFromChatMessage({ ...inputRecord, ...output });
|
|
1167
1184
|
const preview = previewNaturalLanguageRouting(request, session);
|
|
1168
1185
|
const nowMs = clockMs(now);
|
|
@@ -1176,32 +1193,47 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
|
|
|
1176
1193
|
nowMs < recordedAtMs)
|
|
1177
1194
|
recentStallAlerts.delete(key);
|
|
1178
1195
|
}
|
|
1179
|
-
const
|
|
1180
|
-
? await
|
|
1181
|
-
:
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1196
|
+
const stallResult = stallAlert
|
|
1197
|
+
? await collectStallAlertResult(stallAlert, now)
|
|
1198
|
+
: { status: "none" };
|
|
1199
|
+
let stallTextToAppend = undefined;
|
|
1200
|
+
let stallDedupKey = undefined;
|
|
1201
|
+
if (stallResult.status === "unavailable") {
|
|
1202
|
+
stallDedupKey = `${safeToken(request.session_ref, "session")}|stall-unavailable`;
|
|
1203
|
+
stallTextToAppend =
|
|
1204
|
+
"FlowDesk\nStall detection temporarily unavailable (status check timed out).\nSafe next actions:\n- /flowdesk-status\n- /flowdesk-doctor";
|
|
1205
|
+
}
|
|
1206
|
+
else if (stallResult.status === "error") {
|
|
1207
|
+
stallDedupKey = `${safeToken(request.session_ref, "session")}|stall-error`;
|
|
1208
|
+
stallTextToAppend =
|
|
1209
|
+
"FlowDesk\nStall detection encountered an error.\nRun /flowdesk-doctor to diagnose.";
|
|
1210
|
+
}
|
|
1211
|
+
else if (stallResult.status === "ok") {
|
|
1212
|
+
const summary = stallResult.data;
|
|
1213
|
+
const stalledAlertReady = summary.worstClassification === "stalled" &&
|
|
1214
|
+
summary.totalStalled > 0;
|
|
1215
|
+
const lateAlertReady = stallAlert?.includeProgressingLate === true &&
|
|
1216
|
+
summary.worstClassification === "progressing_late" &&
|
|
1217
|
+
summary.totalLate > 0;
|
|
1218
|
+
if (stalledAlertReady || lateAlertReady) {
|
|
1219
|
+
stallDedupKey = stallAlertDuplicateKey(request, summary);
|
|
1220
|
+
stallTextToAppend = stallAlertText(summary);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
const appendStallCard = () => {
|
|
1224
|
+
if (stallDedupKey && stallTextToAppend) {
|
|
1225
|
+
const previous = recentStallAlerts.get(stallDedupKey);
|
|
1226
|
+
recentStallAlerts.set(stallDedupKey, nowMs);
|
|
1195
1227
|
if (previous === undefined ||
|
|
1196
1228
|
nowMs - previous > flowdeskChatSuggestionDuplicateWindowMs) {
|
|
1197
1229
|
if (!Array.isArray(output.parts))
|
|
1198
1230
|
output.parts = [];
|
|
1199
|
-
output.parts.push(
|
|
1200
|
-
type: "text",
|
|
1201
|
-
text: stallAlertText(stallSummary),
|
|
1202
|
-
});
|
|
1231
|
+
output.parts.push(buildTextPart(stallTextToAppend));
|
|
1203
1232
|
}
|
|
1204
1233
|
}
|
|
1234
|
+
};
|
|
1235
|
+
if (preview.evaluation.response.route_decision === "continue_chat") {
|
|
1236
|
+
appendStallCard();
|
|
1205
1237
|
return;
|
|
1206
1238
|
}
|
|
1207
1239
|
if (!mayCreatePendingConfirmation(preview)) {
|
|
@@ -1217,21 +1249,42 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
|
|
|
1217
1249
|
const result = evaluateNaturalLanguageRouting(request, session);
|
|
1218
1250
|
if (!Array.isArray(output.parts))
|
|
1219
1251
|
output.parts = [];
|
|
1220
|
-
output.parts.push(
|
|
1221
|
-
|
|
1222
|
-
const key = stallAlertDuplicateKey(request, stallSummary);
|
|
1223
|
-
const previous = recentStallAlerts.get(key);
|
|
1224
|
-
recentStallAlerts.set(key, nowMs);
|
|
1225
|
-
if (previous === undefined ||
|
|
1226
|
-
nowMs - previous > flowdeskChatSuggestionDuplicateWindowMs)
|
|
1227
|
-
output.parts.push({
|
|
1228
|
-
type: "text",
|
|
1229
|
-
text: stallAlertText(stallSummary),
|
|
1230
|
-
});
|
|
1231
|
-
}
|
|
1252
|
+
output.parts.push(buildTextPart(steeringText(result)));
|
|
1253
|
+
appendStallCard();
|
|
1232
1254
|
};
|
|
1233
1255
|
}
|
|
1234
|
-
|
|
1256
|
+
const ALLOWED_ERROR_NAMES = new Set(["FlowDeskDiskError", "FlowDeskStateError"]);
|
|
1257
|
+
export function assertNever(x) {
|
|
1258
|
+
throw new Error("Unexpected object: " + x);
|
|
1259
|
+
}
|
|
1260
|
+
function latestParentSessionRefForLane(rootDir, workflowId, laneId) {
|
|
1261
|
+
const reload = reloadFlowDeskSessionEvidenceV1({ rootDir, workflowId });
|
|
1262
|
+
if (!reload.ok)
|
|
1263
|
+
return undefined;
|
|
1264
|
+
let latest;
|
|
1265
|
+
for (const entry of reload.entries) {
|
|
1266
|
+
if (entry.evidenceClass !== "lane_lifecycle")
|
|
1267
|
+
continue;
|
|
1268
|
+
const record = entry.record;
|
|
1269
|
+
if (!isRecord(record))
|
|
1270
|
+
continue;
|
|
1271
|
+
if (record.lane_id !== laneId)
|
|
1272
|
+
continue;
|
|
1273
|
+
if (typeof record.parent_session_ref !== "string")
|
|
1274
|
+
continue;
|
|
1275
|
+
const updatedAt = typeof record.updated_at === "string"
|
|
1276
|
+
? Date.parse(record.updated_at)
|
|
1277
|
+
: Number.NaN;
|
|
1278
|
+
if (!Number.isFinite(updatedAt))
|
|
1279
|
+
continue;
|
|
1280
|
+
if (latest === undefined || updatedAt > latest.updatedAtMs) {
|
|
1281
|
+
latest = { parentSessionRef: record.parent_session_ref, updatedAtMs: updatedAt };
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return latest?.parentSessionRef;
|
|
1285
|
+
}
|
|
1286
|
+
export async function collectStallAlertResult(stallAlert, clock, deps = {}) {
|
|
1287
|
+
const statusLiveImpl = deps.statusLiveImpl ?? executeFlowDeskStatusLiveV1;
|
|
1235
1288
|
try {
|
|
1236
1289
|
const observedAt = (typeof clock === "function" ? clock() : clock).toISOString();
|
|
1237
1290
|
const config = {
|
|
@@ -1250,21 +1303,84 @@ async function collectStallAlertSummary(stallAlert, clock) {
|
|
|
1250
1303
|
laneHeartbeatStallThresholdMs: stallAlert.laneHeartbeatStallThresholdMs,
|
|
1251
1304
|
}),
|
|
1252
1305
|
};
|
|
1253
|
-
const result = await
|
|
1306
|
+
const result = await withTimeout(statusLiveImpl({
|
|
1254
1307
|
config,
|
|
1255
1308
|
now: () => new Date(observedAt),
|
|
1256
|
-
});
|
|
1309
|
+
}), stallAlert.statusLiveTimeoutMs ?? 8_000, "executeFlowDeskStatusLiveV1");
|
|
1257
1310
|
if (result.status !== "status_live_collected")
|
|
1258
|
-
return
|
|
1259
|
-
const
|
|
1311
|
+
return { status: "none" };
|
|
1312
|
+
const autoAbortSummaries = [];
|
|
1313
|
+
const workflowSummaries = await Promise.all(result.workflows
|
|
1260
1314
|
.filter((workflow) => (workflow.stalledLaneCount ?? 0) > 0 ||
|
|
1261
1315
|
(stallAlert.includeProgressingLate === true &&
|
|
1262
1316
|
(workflow.progressingLateLaneCount ?? 0) > 0))
|
|
1263
1317
|
.slice(0, 3)
|
|
1264
|
-
.map((workflow) => {
|
|
1318
|
+
.map(async (workflow) => {
|
|
1265
1319
|
const stalledEntry = workflow.laneStallProjection?.entries.find((entry) => entry.classification === "stalled");
|
|
1266
1320
|
const lateEntry = workflow.laneStallProjection?.entries.find((entry) => entry.classification === "progressing_late");
|
|
1267
1321
|
const primary = stalledEntry ?? lateEntry;
|
|
1322
|
+
if (stallAlert.guardedAutoAbort !== undefined &&
|
|
1323
|
+
stalledEntry !== undefined &&
|
|
1324
|
+
(workflow.stalledLaneCount ?? 0) > 0) {
|
|
1325
|
+
// Reconcile stale pending retry plans on each stall check
|
|
1326
|
+
try {
|
|
1327
|
+
reconcileStalePendingRetryPlansV1({
|
|
1328
|
+
rootDir: stallAlert.rootDir,
|
|
1329
|
+
workflowId: workflow.workflowId,
|
|
1330
|
+
now: new Date(observedAt),
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
catch {
|
|
1334
|
+
// Reconciliation is best-effort
|
|
1335
|
+
}
|
|
1336
|
+
let sdkSessionHealth = stallAlert.guardedAutoAbort.sdkSessionHealth;
|
|
1337
|
+
if (sdkSessionHealth === undefined &&
|
|
1338
|
+
stallAlert.guardedAutoAbort.useLiveSdkSessionHealth === true &&
|
|
1339
|
+
stallAlert.guardedAutoAbort.sdkClient !== undefined) {
|
|
1340
|
+
const parentSessionRef = latestParentSessionRefForLane(stallAlert.rootDir, workflow.workflowId, stalledEntry.laneId);
|
|
1341
|
+
sdkSessionHealth = parentSessionRef === undefined
|
|
1342
|
+
? { status: "unknown", reason: "parent_session_ref_missing" }
|
|
1343
|
+
: await checkSdkSessionApiHealthV1(stallAlert.guardedAutoAbort.sdkClient, parentSessionRef);
|
|
1344
|
+
}
|
|
1345
|
+
const autoAbort = evaluateGuardedAutoAbortHookV1({
|
|
1346
|
+
rootDir: stallAlert.rootDir,
|
|
1347
|
+
workflow_id: workflow.workflowId,
|
|
1348
|
+
lane_id: stalledEntry.laneId,
|
|
1349
|
+
config: stallAlert.guardedAutoAbort,
|
|
1350
|
+
stallConfirmed: true,
|
|
1351
|
+
sdkSessionHealth: sdkSessionHealth ?? {
|
|
1352
|
+
status: "unknown",
|
|
1353
|
+
reason: "sdk_session_health_not_supplied_to_chat_hook",
|
|
1354
|
+
},
|
|
1355
|
+
now: () => new Date(observedAt),
|
|
1356
|
+
});
|
|
1357
|
+
autoAbortSummaries.push(`workflow ${workflow.workflowId} lane ${stalledEntry.laneId}: guarded auto-abort ${autoAbort.status}`);
|
|
1358
|
+
// After auto-abort, evaluate guarded auto-retry if configured
|
|
1359
|
+
if (autoAbort.status === "auto_abort_executed" &&
|
|
1360
|
+
stallAlert.guardedAutoAbort.autoRetryAfterAbort === true &&
|
|
1361
|
+
stallAlert.guardedAutoAbort.sdkClient !== undefined) {
|
|
1362
|
+
let retryResult;
|
|
1363
|
+
try {
|
|
1364
|
+
retryResult = await evaluateGuardedAutoRetryHookV1({
|
|
1365
|
+
config: stallAlert.guardedAutoAbort,
|
|
1366
|
+
rootDir: stallAlert.rootDir,
|
|
1367
|
+
workflowId: workflow.workflowId,
|
|
1368
|
+
laneId: stalledEntry.laneId,
|
|
1369
|
+
abortEvidenceId: autoAbort.lifecycle_evidence_id,
|
|
1370
|
+
client: stallAlert.guardedAutoAbort.sdkClient,
|
|
1371
|
+
parentSessionId: stalledEntry.laneId,
|
|
1372
|
+
timeoutMs: 30_000,
|
|
1373
|
+
now: new Date(observedAt),
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
catch {
|
|
1377
|
+
// Retry evaluation is best-effort
|
|
1378
|
+
}
|
|
1379
|
+
if (retryResult !== undefined) {
|
|
1380
|
+
autoAbortSummaries.push(`workflow ${workflow.workflowId} lane ${stalledEntry.laneId}: guarded auto-retry ${retryResult.status}`);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1268
1384
|
return {
|
|
1269
1385
|
workflowId: workflow.workflowId,
|
|
1270
1386
|
stalledLaneCount: workflow.stalledLaneCount ?? 0,
|
|
@@ -1277,16 +1393,29 @@ async function collectStallAlertSummary(stallAlert, clock) {
|
|
|
1277
1393
|
? {}
|
|
1278
1394
|
: { failureHint: primary.failureHint }),
|
|
1279
1395
|
};
|
|
1280
|
-
});
|
|
1396
|
+
}));
|
|
1397
|
+
if (workflowSummaries.length === 0) {
|
|
1398
|
+
return { status: "none" };
|
|
1399
|
+
}
|
|
1281
1400
|
return {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1401
|
+
status: "ok",
|
|
1402
|
+
data: {
|
|
1403
|
+
worstClassification: result.worstLaneStallClassification ?? "unknown",
|
|
1404
|
+
totalStalled: result.totalStalledLaneCount ?? 0,
|
|
1405
|
+
totalLate: result.totalProgressingLateLaneCount ?? 0,
|
|
1406
|
+
workflowSummaries,
|
|
1407
|
+
...(autoAbortSummaries.length === 0 ? {} : { autoAbortSummaries }),
|
|
1408
|
+
}
|
|
1286
1409
|
};
|
|
1287
1410
|
}
|
|
1288
|
-
catch {
|
|
1289
|
-
|
|
1411
|
+
catch (error) {
|
|
1412
|
+
if (error instanceof FlowDeskTimeoutError) {
|
|
1413
|
+
return { status: "unavailable" };
|
|
1414
|
+
}
|
|
1415
|
+
const errorName = error instanceof Error ? error.name : "UnknownError";
|
|
1416
|
+
const safeName = ALLOWED_ERROR_NAMES.has(errorName) ? errorName : "UnknownError";
|
|
1417
|
+
process.stderr.write(`[flowdesk] collectStallAlertResult error: ${safeName}\n`);
|
|
1418
|
+
return { status: "error" };
|
|
1290
1419
|
}
|
|
1291
1420
|
}
|
|
1292
1421
|
function stallAlertDuplicateKey(request, summary) {
|
|
@@ -1322,6 +1451,11 @@ function stallAlertText(summary) {
|
|
|
1322
1451
|
lines.push(`- workflow ${workflow.workflowId}: ${counts} (last signal ~${minutes}m ago, ${hint}).`);
|
|
1323
1452
|
}
|
|
1324
1453
|
lines.push("FlowDesk does not auto-retry, auto-abort, or auto-fallback on stall.");
|
|
1454
|
+
if (summary.autoAbortSummaries !== undefined && summary.autoAbortSummaries.length > 0) {
|
|
1455
|
+
lines.push("Guarded auto-abort diagnostics (evidence-only, opt-in):");
|
|
1456
|
+
for (const line of summary.autoAbortSummaries.slice(0, 3))
|
|
1457
|
+
lines.push(`- ${line}`);
|
|
1458
|
+
}
|
|
1325
1459
|
lines.push("Safe next actions:");
|
|
1326
1460
|
for (const action of [
|
|
1327
1461
|
"/flowdesk-status",
|
|
@@ -1686,6 +1820,7 @@ function redactedQuickReviewerRunBlocked(reason) {
|
|
|
1686
1820
|
laneCount: 0,
|
|
1687
1821
|
lanes: [],
|
|
1688
1822
|
redactedBlockReason: reason,
|
|
1823
|
+
summaryForUser: `FlowDesk quick reviewer blocked before launch: ${reason}. Safe next actions: /flowdesk-status.`,
|
|
1689
1824
|
safeNextActions: ["/flowdesk-status"],
|
|
1690
1825
|
authority: {
|
|
1691
1826
|
realOpenCodeDispatch: false,
|
|
@@ -1719,6 +1854,7 @@ function redactedQuickReviewerRunToolResult(result) {
|
|
|
1719
1854
|
linkedLifecycleCount: result.linkedLifecycleCount,
|
|
1720
1855
|
acceptedPerspectives: result.acceptedPerspectives,
|
|
1721
1856
|
redactedBlockReason: result.redactedBlockReason,
|
|
1857
|
+
summaryForUser: result.summaryForUser,
|
|
1722
1858
|
safeNextActions: result.safeNextActions,
|
|
1723
1859
|
authority: result.authority,
|
|
1724
1860
|
};
|
|
@@ -1727,12 +1863,13 @@ export function createFlowDeskQuickReviewerRunOptInTools(client, defaults) {
|
|
|
1727
1863
|
return {
|
|
1728
1864
|
[flowdeskQuickReviewerRunToolName]: tool({
|
|
1729
1865
|
description: [
|
|
1730
|
-
"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.",
|
|
1866
|
+
"Run a 3-perspective FlowDesk reviewer fan-out (policy_security, architecture, verification_implementation) on a user-supplied prompt, pasted code/content, or the current conversation context, returning typed reviewer verdicts plus a concise summaryForUser string. Supports optional per-perspective bindings[] for multi-model fan-out when exact provider/model ids and reviewer agents are configured.",
|
|
1731
1867
|
"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 '점검'.",
|
|
1868
|
+
"ALSO PROACTIVELY USE: when the user supplies a code snippet, file reference, or design idea and asks for opinion/critique without explicitly typing 'review' (English: 'what do you think', 'how does this look', 'spot issues', 'find problems'; Korean: '어때', '괜찮아', '문제 있어 보여', '이슈 찾아줘', '봐줘'). When the user gives no snippet, derive a concise review target from the immediate prior turn and call this tool instead of asking for one more clarification.",
|
|
1732
1869
|
"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.",
|
|
1733
|
-
"WHEN NOT TO USE: general questions, plain chat, or any request that does not explicitly ask for a review
|
|
1870
|
+
"WHEN NOT TO USE: general questions, plain chat, status/usage/recovery questions (use the other FlowDesk natural-language tools), or any request that does not explicitly ask for a review-style judgement.",
|
|
1734
1871
|
"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.",
|
|
1735
|
-
"AFTER CALLING:
|
|
1872
|
+
"AFTER CALLING: read the result.summaryForUser string and surface it verbatim or compressed in your reply to the user. acceptanceStatus=verdicts_accepted and durableLinkageStatus=durable_verdicts_accepted mean all requested perspectives passed. status=quick_reviewer_run_incomplete means at least one lane did not produce a matching typed verdict; lanes[].redactedObservationErrors and lanes[].observationStatus indicate why. status=blocked_before_quick_reviewer_run means the helper refused before launching providers; report result.redactedBlockReason as-is. After surfacing summaryForUser, recommend the safeNextActions in result.safeNextActions for follow-up.",
|
|
1736
1873
|
"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.",
|
|
1737
1874
|
].join(" "),
|
|
1738
1875
|
args: {
|
|
@@ -1757,6 +1894,15 @@ export function createFlowDeskQuickReviewerRunOptInTools(client, defaults) {
|
|
|
1757
1894
|
.array(tool.schema.string())
|
|
1758
1895
|
.optional()
|
|
1759
1896
|
.describe("Optional subset of reviewer perspectives (policy_security, architecture, verification_implementation). Defaults to all three."),
|
|
1897
|
+
bindings: tool.schema
|
|
1898
|
+
.array(tool.schema.object({
|
|
1899
|
+
perspective: tool.schema.string(),
|
|
1900
|
+
providerQualifiedModelId: tool.schema.string(),
|
|
1901
|
+
runtimeAgent: tool.schema.string(),
|
|
1902
|
+
sourceLabel: tool.schema.string().optional(),
|
|
1903
|
+
}))
|
|
1904
|
+
.optional()
|
|
1905
|
+
.describe("Optional per-perspective reviewer bindings for multi-model fan-out. Each entry must include perspective, concrete providerQualifiedModelId, and runtimeAgent. When omitted, all perspectives use providerQualifiedModelId/runtimeAgent."),
|
|
1760
1906
|
parentSessionId: tool.schema
|
|
1761
1907
|
.string()
|
|
1762
1908
|
.optional()
|
|
@@ -1775,12 +1921,28 @@ export function createFlowDeskQuickReviewerRunOptInTools(client, defaults) {
|
|
|
1775
1921
|
record.runtimeAgent.trim().length > 0
|
|
1776
1922
|
? record.runtimeAgent
|
|
1777
1923
|
: defaults.runtimeAgent;
|
|
1778
|
-
if (typeof providerQualifiedModelId !== "string" ||
|
|
1779
|
-
typeof runtimeAgent !== "string")
|
|
1780
|
-
return JSON.stringify(redactedQuickReviewerRunBlocked("Quick reviewer run requires providerQualifiedModelId and runtimeAgent (either as args or plugin defaults)."));
|
|
1781
1924
|
const perspectives = Array.isArray(record.perspectives)
|
|
1782
1925
|
? record.perspectives.filter((value) => typeof value === "string")
|
|
1783
1926
|
: undefined;
|
|
1927
|
+
const bindings = Array.isArray(record.bindings)
|
|
1928
|
+
? record.bindings
|
|
1929
|
+
.filter((value) => isRecord(value))
|
|
1930
|
+
.map((value) => ({
|
|
1931
|
+
perspective: value.perspective,
|
|
1932
|
+
providerQualifiedModelId: value.providerQualifiedModelId,
|
|
1933
|
+
runtimeAgent: value.runtimeAgent,
|
|
1934
|
+
...(typeof value.sourceLabel === "string"
|
|
1935
|
+
? { sourceLabel: value.sourceLabel }
|
|
1936
|
+
: {}),
|
|
1937
|
+
}))
|
|
1938
|
+
.filter((value) => typeof value.perspective === "string" &&
|
|
1939
|
+
typeof value.providerQualifiedModelId === "string" &&
|
|
1940
|
+
typeof value.runtimeAgent === "string")
|
|
1941
|
+
: undefined;
|
|
1942
|
+
if (bindings === undefined &&
|
|
1943
|
+
(typeof providerQualifiedModelId !== "string" ||
|
|
1944
|
+
typeof runtimeAgent !== "string"))
|
|
1945
|
+
return JSON.stringify(redactedQuickReviewerRunBlocked("Quick reviewer run requires providerQualifiedModelId and runtimeAgent (either as args or plugin defaults) unless per-perspective bindings are supplied."));
|
|
1784
1946
|
const result = await executeFlowDeskQuickReviewerRunV1({
|
|
1785
1947
|
client,
|
|
1786
1948
|
prompt,
|
|
@@ -1789,6 +1951,7 @@ export function createFlowDeskQuickReviewerRunOptInTools(client, defaults) {
|
|
|
1789
1951
|
allowProviderCall: record.allowProviderCall === true,
|
|
1790
1952
|
developerModeAcknowledged: record.developerModeAcknowledged === true,
|
|
1791
1953
|
...(perspectives === undefined ? {} : { perspectives }),
|
|
1954
|
+
...(bindings === undefined ? {} : { bindings }),
|
|
1792
1955
|
...(typeof record.parentSessionId === "string" &&
|
|
1793
1956
|
record.parentSessionId.length > 0
|
|
1794
1957
|
? { parentSessionId: record.parentSessionId }
|
|
@@ -1805,6 +1968,78 @@ export function createFlowDeskQuickReviewerRunOptInTools(client, defaults) {
|
|
|
1805
1968
|
}),
|
|
1806
1969
|
};
|
|
1807
1970
|
}
|
|
1971
|
+
export function createFlowDeskAgentTaskRunOptInTools(input) {
|
|
1972
|
+
if (!input.client || !input.durableStateRoot)
|
|
1973
|
+
return undefined;
|
|
1974
|
+
const client = input.client;
|
|
1975
|
+
const rootDir = input.durableStateRoot;
|
|
1976
|
+
return {
|
|
1977
|
+
[flowdeskAgentTaskRunToolName]: tool({
|
|
1978
|
+
description: [
|
|
1979
|
+
"Run a single task on a specific agent and model, returning the result text.",
|
|
1980
|
+
"Use this to delegate a well-defined subtask to a specific model (e.g. Claude Opus for security analysis, GPT for architecture review).",
|
|
1981
|
+
"Requires developerModeAcknowledged=true and allowProviderCall=true per call.",
|
|
1982
|
+
"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), code review (use flowdesk_quick_reviewer_run).",
|
|
1984
|
+
"After calling, use flowdesk_status_live to check the lane status.",
|
|
1985
|
+
].join(" "),
|
|
1986
|
+
args: {
|
|
1987
|
+
workflowId: tool.schema.string().describe("Workflow id (e.g. workflow-xxx)"),
|
|
1988
|
+
taskDescription: tool.schema.string().max(20_000).describe("The task prompt to send to the agent"),
|
|
1989
|
+
agentName: tool.schema.string().describe("Agent name (e.g. reviewer-claude-opus, reviewer-gpt-frontier)"),
|
|
1990
|
+
providerQualifiedModelId: tool.schema.string().describe("Concrete model id (e.g. anthropic/claude-opus-4-7)"),
|
|
1991
|
+
parentSessionId: tool.schema.string().optional().describe("Parent session id"),
|
|
1992
|
+
developerModeAcknowledged: tool.schema.boolean(),
|
|
1993
|
+
allowProviderCall: tool.schema.boolean(),
|
|
1994
|
+
},
|
|
1995
|
+
async execute(args, ctx) {
|
|
1996
|
+
const record = isRecord(args) ? args : {};
|
|
1997
|
+
if (record.developerModeAcknowledged !== true)
|
|
1998
|
+
return JSON.stringify({ status: "blocked", reason: "developerModeAcknowledged must be true" });
|
|
1999
|
+
if (record.allowProviderCall !== true)
|
|
2000
|
+
return JSON.stringify({ status: "blocked", reason: "allowProviderCall must be true" });
|
|
2001
|
+
const workflowId = typeof record.workflowId === "string" ? record.workflowId : undefined;
|
|
2002
|
+
const taskDescription = typeof record.taskDescription === "string" ? record.taskDescription : undefined;
|
|
2003
|
+
const agentName = typeof record.agentName === "string" ? record.agentName : undefined;
|
|
2004
|
+
const providerQualifiedModelId = typeof record.providerQualifiedModelId === "string" ? record.providerQualifiedModelId : undefined;
|
|
2005
|
+
if (!workflowId || !taskDescription || !agentName || !providerQualifiedModelId)
|
|
2006
|
+
return JSON.stringify({ status: "blocked", reason: "workflowId, taskDescription, agentName, and providerQualifiedModelId are required" });
|
|
2007
|
+
const ctxRecord = isRecord(ctx) ? ctx : {};
|
|
2008
|
+
const parentSessionId = typeof record.parentSessionId === "string" && record.parentSessionId.length > 0
|
|
2009
|
+
? record.parentSessionId
|
|
2010
|
+
: typeof ctxRecord.sessionID === "string" && ctxRecord.sessionID.length > 0
|
|
2011
|
+
? ctxRecord.sessionID
|
|
2012
|
+
: "";
|
|
2013
|
+
const taskId = `task-${Date.now().toString(36)}`;
|
|
2014
|
+
const laneId = `lane-task-${Date.now().toString(36)}`;
|
|
2015
|
+
const result = await executeFlowDeskAgentTaskV1({
|
|
2016
|
+
workflowId,
|
|
2017
|
+
taskId,
|
|
2018
|
+
laneId,
|
|
2019
|
+
agentRef: `agent-${agentName}`,
|
|
2020
|
+
providerQualifiedModelId,
|
|
2021
|
+
promptText: taskDescription,
|
|
2022
|
+
parentSessionId,
|
|
2023
|
+
rootDir,
|
|
2024
|
+
client,
|
|
2025
|
+
});
|
|
2026
|
+
return JSON.stringify({
|
|
2027
|
+
workflowId,
|
|
2028
|
+
laneId,
|
|
2029
|
+
taskId,
|
|
2030
|
+
status: result.status,
|
|
2031
|
+
resultText: result.status === "task_completed" ? result.resultText.slice(0, 4_096) : undefined,
|
|
2032
|
+
resultTruncated: result.status === "task_completed" && result.resultText.length > 4_096,
|
|
2033
|
+
failureCategory: result.status === "task_failed" ? result.failureCategory : undefined,
|
|
2034
|
+
redactedReason: result.status === "task_failed" ? result.redactedReason : undefined,
|
|
2035
|
+
summaryForUser: result.status === "task_completed"
|
|
2036
|
+
? `Task completed on ${agentName} (${providerQualifiedModelId}). Result: ${result.resultText.slice(0, 200)}${result.resultText.length > 200 ? "..." : ""}`
|
|
2037
|
+
: `Task failed on ${agentName}: ${result.failureCategory}`,
|
|
2038
|
+
});
|
|
2039
|
+
},
|
|
2040
|
+
}),
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
1808
2043
|
function redactedManagedFallbackRegateBlocked(reason) {
|
|
1809
2044
|
return {
|
|
1810
2045
|
adapterProfile: "managed_fallback_regate_orchestrator",
|
|
@@ -2057,6 +2292,10 @@ function isLaneHeartbeatWriterEnabled(options) {
|
|
|
2057
2292
|
const value = options?.[flowdeskLaneHeartbeatWriterOption];
|
|
2058
2293
|
return value === true || (isRecord(value) && value.enabled === true);
|
|
2059
2294
|
}
|
|
2295
|
+
function isAgentTaskRunEnabled(options) {
|
|
2296
|
+
const value = options?.[flowdeskAgentTaskRunOption];
|
|
2297
|
+
return value === true || (isRecord(value) && value.enabled === true);
|
|
2298
|
+
}
|
|
2060
2299
|
function laneHeartbeatWriterConfigFromOptions(options) {
|
|
2061
2300
|
const value = options?.[flowdeskLaneHeartbeatWriterOption];
|
|
2062
2301
|
const enabledFromBool = value === true;
|
|
@@ -2209,10 +2448,11 @@ export function createFlowDeskStatusLiveOptInTools(config) {
|
|
|
2209
2448
|
[flowdeskStatusLiveToolName]: tool({
|
|
2210
2449
|
description: [
|
|
2211
2450
|
"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.",
|
|
2212
|
-
"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 '상태', '어디까지', '진행 상황', '진행됐', '오늘 작업', '오늘 뭐했', '최근 활동', '최근 리뷰', '지금 어디', '상태 요약', '워크플로우 상태', '멈춘 것 같아', '멈췄어', '응답이 없어', '아무 로그도 없', '오래 걸리는', '진행이 안돼'.",
|
|
2451
|
+
"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', 'last review', 'just now', 'earlier result', and Korean phrases such as '상태', '어디까지', '진행 상황', '진행됐', '오늘 작업', '오늘 뭐했', '최근 활동', '최근 리뷰', '지금 어디', '상태 요약', '워크플로우 상태', '멈춘 것 같아', '멈췄어', '응답이 없어', '아무 로그도 없', '오래 걸리는', '진행이 안돼', '방금', '직전', '조금 전', '이전', '최근에 한 거', '아까 한 거', '결과 보여줘'.",
|
|
2452
|
+
"ALSO PROACTIVELY USE: as a follow-up after invoking a real-work FlowDesk tool (quick_reviewer_run, quick_fallback_run, managed_dispatch). If the user asks a vague follow-up about the just-completed work ('잘 됐어?', 'how did it go?', '결과는?'), call this tool with the just-created workflowId to pull durable evidence instead of guessing from memory.",
|
|
2213
2453
|
"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.",
|
|
2214
2454
|
"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.",
|
|
2215
|
-
"AFTER CALLING:
|
|
2455
|
+
"AFTER CALLING: read result.summaryForUser and surface that as the headline reply. If the user needs more detail, 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.",
|
|
2216
2456
|
].join(" "),
|
|
2217
2457
|
args: {
|
|
2218
2458
|
workflowId: tool.schema
|
|
@@ -2283,6 +2523,11 @@ function quickReviewerRunDefaultsFromOptions(options) {
|
|
|
2283
2523
|
const option = options?.[flowdeskQuickReviewerRunOption];
|
|
2284
2524
|
if (!isRecord(option))
|
|
2285
2525
|
return {};
|
|
2526
|
+
const optionRoot = typeof option.durableStateRoot === "string" &&
|
|
2527
|
+
option.durableStateRoot.trim().length > 0
|
|
2528
|
+
? option.durableStateRoot
|
|
2529
|
+
: undefined;
|
|
2530
|
+
const rootDir = optionRoot ?? durableStateRootFromOptions(options);
|
|
2286
2531
|
return {
|
|
2287
2532
|
...(typeof option.providerQualifiedModelId === "string" &&
|
|
2288
2533
|
option.providerQualifiedModelId.trim().length > 0
|
|
@@ -2296,10 +2541,18 @@ function quickReviewerRunDefaultsFromOptions(options) {
|
|
|
2296
2541
|
option.sourceLabel.trim().length > 0
|
|
2297
2542
|
? { sourceLabel: option.sourceLabel }
|
|
2298
2543
|
: {}),
|
|
2299
|
-
...(
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2544
|
+
...(rootDir === undefined ? {} : { rootDir }),
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
function watchdogConfigFromOptions(options) {
|
|
2548
|
+
const w = options?.[flowdeskWatchdogOption];
|
|
2549
|
+
if (!isRecord(w) || w.enabled !== true)
|
|
2550
|
+
return undefined;
|
|
2551
|
+
return {
|
|
2552
|
+
enabled: true,
|
|
2553
|
+
intervalMs: Math.max(10_000, typeof w.intervalMs === "number" ? w.intervalMs : 30_000),
|
|
2554
|
+
stallThresholdMs: typeof w.stallThresholdMs === "number" ? w.stallThresholdMs : 3 * 60_000,
|
|
2555
|
+
mcpTriggerEnabled: w.mcpTriggerEnabled === true,
|
|
2303
2556
|
};
|
|
2304
2557
|
}
|
|
2305
2558
|
const flowdeskServerPlugin = async (input, options) => {
|
|
@@ -2522,16 +2775,129 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
2522
2775
|
: undefined;
|
|
2523
2776
|
if (laneHeartbeatWriterConfig !== undefined)
|
|
2524
2777
|
Object.assign(tools, createFlowDeskLaneHeartbeatWriterOptInTools(laneHeartbeatWriterConfig));
|
|
2778
|
+
const agentTaskRunEnabled = isAgentTaskRunEnabled(options);
|
|
2779
|
+
if (agentTaskRunEnabled) {
|
|
2780
|
+
const agentTaskRunClient = isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
|
|
2781
|
+
const agentTaskRunRoot = durableStateRootFromOptions(options);
|
|
2782
|
+
const agentTaskRunTools = createFlowDeskAgentTaskRunOptInTools({
|
|
2783
|
+
client: agentTaskRunClient,
|
|
2784
|
+
durableStateRoot: agentTaskRunRoot,
|
|
2785
|
+
});
|
|
2786
|
+
if (agentTaskRunTools !== undefined)
|
|
2787
|
+
Object.assign(tools, agentTaskRunTools);
|
|
2788
|
+
}
|
|
2789
|
+
// P8 Background Watchdog
|
|
2790
|
+
const watchdogConfig = watchdogConfigFromOptions(options);
|
|
2791
|
+
const chatStallAlertRaw = options?.[flowdeskChatMessageStallAlertOption];
|
|
2792
|
+
const guardedAutoAbortForWatchdog = isRecord(chatStallAlertRaw) && isRecord(chatStallAlertRaw.guardedAutoAbort)
|
|
2793
|
+
? (() => {
|
|
2794
|
+
const raw = chatStallAlertRaw.guardedAutoAbort;
|
|
2795
|
+
const cfg = {
|
|
2796
|
+
autoAbortOnStall: raw.autoAbortOnStall === true,
|
|
2797
|
+
};
|
|
2798
|
+
if (typeof raw.preAbortWarningMs === "number")
|
|
2799
|
+
cfg.preAbortWarningMs = Math.floor(raw.preAbortWarningMs);
|
|
2800
|
+
if (typeof raw.guardSignOffPath === "string")
|
|
2801
|
+
cfg.guardSignOffPath = raw.guardSignOffPath;
|
|
2802
|
+
if (typeof raw.guardHmacKey === "string")
|
|
2803
|
+
cfg.guardHmacKey = raw.guardHmacKey;
|
|
2804
|
+
if (typeof raw.productionMode === "boolean")
|
|
2805
|
+
cfg.productionMode = raw.productionMode;
|
|
2806
|
+
if (raw.autoRetryAfterAbort === true)
|
|
2807
|
+
cfg.autoRetryAfterAbort = true;
|
|
2808
|
+
if (typeof raw.maxAutoRetries === "number")
|
|
2809
|
+
cfg.maxAutoRetries = Math.min(2, Math.max(1, Math.floor(raw.maxAutoRetries)));
|
|
2810
|
+
return cfg;
|
|
2811
|
+
})()
|
|
2812
|
+
: undefined;
|
|
2813
|
+
const durableStateRoot = durableStateRootFromOptions(options);
|
|
2814
|
+
if (watchdogConfig?.enabled === true && guardedAutoAbortForWatchdog !== undefined && durableStateRoot !== undefined) {
|
|
2815
|
+
const capturedClient = isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
|
|
2816
|
+
const capturedParentSessionId = "";
|
|
2817
|
+
const capturedRootDir = durableStateRoot;
|
|
2818
|
+
const capturedConfig = guardedAutoAbortForWatchdog;
|
|
2819
|
+
const watchdogInterval = setInterval(() => {
|
|
2820
|
+
runFlowDeskWatchdogCycleV1({
|
|
2821
|
+
config: capturedConfig,
|
|
2822
|
+
rootDir: capturedRootDir,
|
|
2823
|
+
client: capturedClient,
|
|
2824
|
+
parentSessionId: capturedParentSessionId,
|
|
2825
|
+
now: new Date(),
|
|
2826
|
+
}).catch(() => {
|
|
2827
|
+
// errors are swallowed — watchdog must not crash the plugin
|
|
2828
|
+
});
|
|
2829
|
+
}, watchdogConfig.intervalMs ?? 30_000);
|
|
2830
|
+
// Allow the process to exit even if the interval is still active
|
|
2831
|
+
watchdogInterval.unref();
|
|
2832
|
+
process.once("exit", () => clearInterval(watchdogInterval));
|
|
2833
|
+
process.once("SIGTERM", () => clearInterval(watchdogInterval));
|
|
2834
|
+
if (watchdogConfig.mcpTriggerEnabled === true) {
|
|
2835
|
+
tools[flowdeskWatchdogTriggerToolName] = tool({
|
|
2836
|
+
description: "Trigger one watchdog cycle manually. Called by external flowdesk-watchdog process (Option A). Requires guardedAutoAbort config.",
|
|
2837
|
+
args: { parentSessionId: tool.schema.string().optional() },
|
|
2838
|
+
async execute(args) {
|
|
2839
|
+
const psi = isRecord(args) && typeof args.parentSessionId === "string" ? args.parentSessionId : "";
|
|
2840
|
+
const result = await runFlowDeskWatchdogCycleV1({
|
|
2841
|
+
config: capturedConfig,
|
|
2842
|
+
rootDir: capturedRootDir,
|
|
2843
|
+
client: capturedClient,
|
|
2844
|
+
parentSessionId: psi,
|
|
2845
|
+
now: new Date(),
|
|
2846
|
+
});
|
|
2847
|
+
return JSON.stringify({
|
|
2848
|
+
cycleAt: result.cycleAt,
|
|
2849
|
+
guardValid: result.guardValid,
|
|
2850
|
+
lanesChecked: result.lanesChecked,
|
|
2851
|
+
lanesAborted: result.lanesAborted,
|
|
2852
|
+
lanesRetried: result.lanesRetried,
|
|
2853
|
+
lanesFailed: result.lanesFailed,
|
|
2854
|
+
skippedReason: result.skippedReason,
|
|
2855
|
+
});
|
|
2856
|
+
},
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
else if (watchdogConfig?.mcpTriggerEnabled === true && guardedAutoAbortForWatchdog !== undefined && durableStateRoot !== undefined) {
|
|
2861
|
+
// mcpTriggerEnabled without setInterval (Option A standalone)
|
|
2862
|
+
const capturedClient = isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
|
|
2863
|
+
const capturedRootDir = durableStateRoot;
|
|
2864
|
+
const capturedConfig = guardedAutoAbortForWatchdog;
|
|
2865
|
+
tools[flowdeskWatchdogTriggerToolName] = tool({
|
|
2866
|
+
description: "Trigger one watchdog cycle manually. Called by external flowdesk-watchdog process (Option A). Requires guardedAutoAbort config.",
|
|
2867
|
+
args: { parentSessionId: tool.schema.string().optional() },
|
|
2868
|
+
async execute(args) {
|
|
2869
|
+
const psi = isRecord(args) && typeof args.parentSessionId === "string" ? args.parentSessionId : "";
|
|
2870
|
+
const result = await runFlowDeskWatchdogCycleV1({
|
|
2871
|
+
config: capturedConfig,
|
|
2872
|
+
rootDir: capturedRootDir,
|
|
2873
|
+
client: capturedClient,
|
|
2874
|
+
parentSessionId: psi,
|
|
2875
|
+
now: new Date(),
|
|
2876
|
+
});
|
|
2877
|
+
return JSON.stringify({
|
|
2878
|
+
cycleAt: result.cycleAt,
|
|
2879
|
+
guardValid: result.guardValid,
|
|
2880
|
+
lanesChecked: result.lanesChecked,
|
|
2881
|
+
lanesAborted: result.lanesAborted,
|
|
2882
|
+
lanesRetried: result.lanesRetried,
|
|
2883
|
+
lanesFailed: result.lanesFailed,
|
|
2884
|
+
skippedReason: result.skippedReason,
|
|
2885
|
+
});
|
|
2886
|
+
},
|
|
2887
|
+
});
|
|
2888
|
+
}
|
|
2525
2889
|
if (!naturalLanguageRoutingEnabled)
|
|
2526
2890
|
return { tool: tools };
|
|
2527
|
-
const stallAlertOption = chatMessageStallAlertOptionsFrom(options, statusLiveConfig)
|
|
2891
|
+
const stallAlertOption = chatMessageStallAlertOptionsFrom(options, statusLiveConfig, isRecord(input) && isManagedDispatchBetaClient(input.client)
|
|
2892
|
+
? input.client
|
|
2893
|
+
: undefined);
|
|
2528
2894
|
return {
|
|
2529
2895
|
tool: tools,
|
|
2530
2896
|
"chat.message": createFlowDeskNaturalLanguageChatMessageHook(() => new Date(), localSession, stallAlertOption, durableStateRootFromOptions(options)),
|
|
2531
2897
|
};
|
|
2532
2898
|
};
|
|
2533
2899
|
export const flowdeskChatMessageStallAlertOption = "chatMessageStallAlert";
|
|
2534
|
-
function chatMessageStallAlertOptionsFrom(options, statusLiveConfig) {
|
|
2900
|
+
function chatMessageStallAlertOptionsFrom(options, statusLiveConfig, sdkClient) {
|
|
2535
2901
|
const raw = options?.[flowdeskChatMessageStallAlertOption];
|
|
2536
2902
|
if (raw === false)
|
|
2537
2903
|
return undefined;
|
|
@@ -2567,6 +2933,48 @@ function chatMessageStallAlertOptionsFrom(options, statusLiveConfig) {
|
|
|
2567
2933
|
if (recordRaw !== undefined &&
|
|
2568
2934
|
typeof recordRaw.includeProgressingLate === "boolean")
|
|
2569
2935
|
config.includeProgressingLate = recordRaw.includeProgressingLate;
|
|
2936
|
+
if (recordRaw !== undefined && isRecord(recordRaw.guardedAutoAbort)) {
|
|
2937
|
+
const rawGuard = recordRaw.guardedAutoAbort;
|
|
2938
|
+
const guardedAutoAbort = {
|
|
2939
|
+
autoAbortOnStall: rawGuard.autoAbortOnStall === true,
|
|
2940
|
+
};
|
|
2941
|
+
if (typeof rawGuard.preAbortWarningMs === "number" && rawGuard.preAbortWarningMs > 0)
|
|
2942
|
+
guardedAutoAbort.preAbortWarningMs = Math.floor(rawGuard.preAbortWarningMs);
|
|
2943
|
+
if (typeof rawGuard.guardSignOffPath === "string" && rawGuard.guardSignOffPath.trim().length > 0)
|
|
2944
|
+
guardedAutoAbort.guardSignOffPath = rawGuard.guardSignOffPath;
|
|
2945
|
+
if (typeof rawGuard.guardHmacKey === "string" && rawGuard.guardHmacKey.length > 0)
|
|
2946
|
+
guardedAutoAbort.guardHmacKey = rawGuard.guardHmacKey;
|
|
2947
|
+
if (typeof rawGuard.productionMode === "boolean")
|
|
2948
|
+
guardedAutoAbort.productionMode = rawGuard.productionMode;
|
|
2949
|
+
if (rawGuard.autoRetryAfterAbort === true)
|
|
2950
|
+
guardedAutoAbort.autoRetryAfterAbort = true;
|
|
2951
|
+
if (typeof rawGuard.maxAutoRetries === "number")
|
|
2952
|
+
guardedAutoAbort.maxAutoRetries = Math.min(2, Math.max(1, Math.floor(rawGuard.maxAutoRetries)));
|
|
2953
|
+
if (typeof rawGuard.useLiveSdkSessionHealth === "boolean")
|
|
2954
|
+
guardedAutoAbort.useLiveSdkSessionHealth = rawGuard.useLiveSdkSessionHealth;
|
|
2955
|
+
if (guardedAutoAbort.useLiveSdkSessionHealth === true && sdkClient !== undefined)
|
|
2956
|
+
guardedAutoAbort.sdkClient = sdkClient;
|
|
2957
|
+
if (isRecord(rawGuard.sdkSessionHealth)) {
|
|
2958
|
+
const status = rawGuard.sdkSessionHealth.status;
|
|
2959
|
+
if (status === "api_responsive")
|
|
2960
|
+
guardedAutoAbort.sdkSessionHealth = { status };
|
|
2961
|
+
if (status === "api_timeout")
|
|
2962
|
+
guardedAutoAbort.sdkSessionHealth = {
|
|
2963
|
+
status,
|
|
2964
|
+
reason: typeof rawGuard.sdkSessionHealth.reason === "string"
|
|
2965
|
+
? rawGuard.sdkSessionHealth.reason
|
|
2966
|
+
: "configured_api_timeout",
|
|
2967
|
+
};
|
|
2968
|
+
if (status === "unknown")
|
|
2969
|
+
guardedAutoAbort.sdkSessionHealth = {
|
|
2970
|
+
status,
|
|
2971
|
+
reason: typeof rawGuard.sdkSessionHealth.reason === "string"
|
|
2972
|
+
? rawGuard.sdkSessionHealth.reason
|
|
2973
|
+
: "configured_unknown",
|
|
2974
|
+
};
|
|
2975
|
+
}
|
|
2976
|
+
config.guardedAutoAbort = guardedAutoAbort;
|
|
2977
|
+
}
|
|
2570
2978
|
return config;
|
|
2571
2979
|
}
|
|
2572
2980
|
export const flowdeskOpenCodeServerPlugin = {
|