clinkx 1.1.2 → 1.2.0
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/CHANGELOG.md +31 -0
- package/README.md +63 -12
- package/clinkx-workflows/dist/allowed-roots.js +2 -0
- package/clinkx-workflows/dist/allowed-roots.js.map +1 -1
- package/clinkx-workflows/dist/child-env.d.ts +3 -1
- package/clinkx-workflows/dist/child-env.js +6 -2
- package/clinkx-workflows/dist/child-env.js.map +1 -1
- package/clinkx-workflows/dist/config.d.ts +28 -5
- package/clinkx-workflows/dist/config.js +137 -20
- package/clinkx-workflows/dist/config.js.map +1 -1
- package/clinkx-workflows/dist/contained-file.js +9 -0
- package/clinkx-workflows/dist/contained-file.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/session-log-identity.js +2 -1
- package/clinkx-workflows/dist/dashboard/session-log-identity.js.map +1 -1
- package/clinkx-workflows/dist/root-path-validation.d.ts +12 -0
- package/clinkx-workflows/dist/root-path-validation.js +63 -0
- package/clinkx-workflows/dist/root-path-validation.js.map +1 -0
- package/clinkx-workflows/dist/rosters.d.ts +1 -0
- package/clinkx-workflows/dist/rosters.js +16 -6
- package/clinkx-workflows/dist/rosters.js.map +1 -1
- package/clinkx-workflows/dist/server.js +5 -2
- package/clinkx-workflows/dist/server.js.map +1 -1
- package/clinkx-workflows/dist/state-schema.d.ts +13 -0
- package/clinkx-workflows/dist/state.d.ts +7 -1
- package/clinkx-workflows/dist/state.js +74 -13
- package/clinkx-workflows/dist/state.js.map +1 -1
- package/clinkx-workflows/dist/workflow-completion.d.ts +83 -0
- package/clinkx-workflows/dist/workflow-completion.js +250 -0
- package/clinkx-workflows/dist/workflow-completion.js.map +1 -0
- package/clinkx-workflows/dist/workflow-tools.d.ts +4 -3
- package/clinkx-workflows/dist/workflow-tools.js +341 -34
- package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
- package/dist/allowed-roots.d.ts +1 -1
- package/dist/allowed-roots.js +124 -14
- package/dist/allowed-roots.js.map +1 -1
- package/dist/context-builder/config.d.ts +1 -0
- package/dist/context-builder/config.js +21 -9
- package/dist/context-builder/config.js.map +1 -1
- package/dist/context-builder/mutation-lock.d.ts +4 -0
- package/dist/context-builder/mutation-lock.js +7 -1
- package/dist/context-builder/mutation-lock.js.map +1 -1
- package/dist/context-builder/storage.d.ts +10 -2
- package/dist/context-builder/storage.js +213 -16
- package/dist/context-builder/storage.js.map +1 -1
- package/dist/dashboard-cli.d.ts +6 -0
- package/dist/dashboard-cli.js +8 -29
- package/dist/dashboard-cli.js.map +1 -1
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +5 -1
- package/dist/registry.js.map +1 -1
- package/dist/server.js +12 -5
- package/dist/server.js.map +1 -1
- package/docs/configuration.md +157 -12
- package/docs/security.md +3 -0
- package/package.json +1 -1
- package/scripts/provider-doctor-core.mjs +21 -5
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* - Standalone STDIO server (ClinkSession child process backend)
|
|
6
6
|
* - In-process integration (LocalClinkBackend via root ClinkX)
|
|
7
7
|
*
|
|
8
|
-
* The `registerWorkflowTools()` function registers
|
|
8
|
+
* The `registerWorkflowTools()` function registers six tools on a Server:
|
|
9
9
|
* - run_workflow: start a new workflow execution
|
|
10
10
|
* - resume_workflow: resume a failed/cancelled run
|
|
11
11
|
* - list_workflows: list available workflow definitions
|
|
12
12
|
* - workflow_run_status: read workflow run progress/result
|
|
13
|
+
* - workflow_run_wait: observe local execution settlement
|
|
13
14
|
* - cancel_workflow: cancel a live or orphaned workflow run
|
|
14
15
|
*
|
|
15
16
|
* The caller provides a `createBackend` factory that determines how clink
|
|
@@ -51,7 +52,7 @@ import { restorePlanReviewRun, } from "./plan-review/resume.js";
|
|
|
51
52
|
import { PlanReviewStageError } from "./plan-review/stage-error.js";
|
|
52
53
|
import { CouncilPlanReviewErrorSchema, createPlanReviewPublicationError, inspectPlanReviewPublication, } from "./plan-review/publication.js";
|
|
53
54
|
import { validatePlanReviewCallBindings, validatePlanReviewPreparedInputPolicy, } from "./plan-review/runtime-policy.js";
|
|
54
|
-
import { ContainedFilePolicyError, readContainedRelativeFile, } from "./contained-file.js";
|
|
55
|
+
import { ContainedFileChangedError, ContainedFilePolicyError, readContainedRelativeFile, } from "./contained-file.js";
|
|
55
56
|
import { createDashboardRunReader } from "./dashboard/read-model.js";
|
|
56
57
|
import { ROSTER_NAME_PATTERN, ResolvedRosterSchema, } from "./roster-contract.js";
|
|
57
58
|
import { materializeArchitectureRoutes } from "./architecture-comparison/admission.js";
|
|
@@ -62,6 +63,8 @@ import { materializeVerifiedRepairRoutes, repairRoutesFromVariables } from "./ve
|
|
|
62
63
|
import { VerifiedRepairRuntime } from "./verified-repair/runtime.js";
|
|
63
64
|
import { inspectVerifiedRepairRecord } from "./verified-repair/result.js";
|
|
64
65
|
import { readWorkflowVerification } from "./workflow-verification.js";
|
|
66
|
+
import { readSessionLogGeneration } from "./dashboard/session-log-identity.js";
|
|
67
|
+
import { WorkflowCompletionRegistry, inspectPublication, persistedIdentityFailure, WORKFLOW_WAIT_BUDGET_MS, } from "./workflow-completion.js";
|
|
65
68
|
async function inspectLegacyWorkflowSessionLogGeneration(runDir, runId) {
|
|
66
69
|
try {
|
|
67
70
|
const result = await createDashboardRunReader().inspectGeneration?.(dirname(runDir), runId);
|
|
@@ -104,6 +107,7 @@ function createPreflightOnlyBackend(advertisedCliNames) {
|
|
|
104
107
|
// Admission control
|
|
105
108
|
// ---------------------------------------------------------------------------
|
|
106
109
|
const activeRuns = new Map();
|
|
110
|
+
const completions = new WorkflowCompletionRegistry();
|
|
107
111
|
/**
|
|
108
112
|
* Number of workflow tool handlers currently executing in the standalone
|
|
109
113
|
* registration path. This includes non-run tools such as status/list/cancel;
|
|
@@ -173,6 +177,9 @@ function releaseMutex(runId) {
|
|
|
173
177
|
/** Own setup before its first await, and keep that ownership through cleanup. */
|
|
174
178
|
function acquireWorkflowRun(runId, options, requestSignal) {
|
|
175
179
|
acquireMutex(runId, options);
|
|
180
|
+
// Bound lazily: a request rejected before it owns state never becomes an
|
|
181
|
+
// execution, so it cannot displace a settled one or consume a retained slot.
|
|
182
|
+
const owner = completions.owner(runId, getRunStateDir(runId));
|
|
176
183
|
const controller = new AbortController();
|
|
177
184
|
const signal = requestSignal === undefined
|
|
178
185
|
? controller.signal
|
|
@@ -201,6 +208,15 @@ function acquireWorkflowRun(runId, options, requestSignal) {
|
|
|
201
208
|
if (requestSignal === undefined)
|
|
202
209
|
cancelRegistry.set(runId, controller);
|
|
203
210
|
return {
|
|
211
|
+
get executionId() { return owner.entry?.executionId; },
|
|
212
|
+
bindStore: async (store) => {
|
|
213
|
+
// Register first, so a pin on the previous execution already fails as
|
|
214
|
+
// identity_changed while this execution's id is being persisted.
|
|
215
|
+
const entry = owner.bind(store);
|
|
216
|
+
await store.bindExecution(entry.executionId);
|
|
217
|
+
completions.markStamped(entry);
|
|
218
|
+
return entry.executionId;
|
|
219
|
+
},
|
|
204
220
|
signal,
|
|
205
221
|
throwIfCancelled: () => {
|
|
206
222
|
if (signal.aborted)
|
|
@@ -215,6 +231,14 @@ function acquireWorkflowRun(runId, options, requestSignal) {
|
|
|
215
231
|
catch (err) {
|
|
216
232
|
logger.warn({ err }, "backend.terminate() failed during cleanup");
|
|
217
233
|
}
|
|
234
|
+
try {
|
|
235
|
+
await store?.flush?.();
|
|
236
|
+
if (owner.entry !== undefined)
|
|
237
|
+
await completions.recordPublication(owner.entry);
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
logger.warn({ err }, "workflow publication identity unavailable during cleanup");
|
|
241
|
+
}
|
|
218
242
|
try {
|
|
219
243
|
await store?.close();
|
|
220
244
|
}
|
|
@@ -226,6 +250,8 @@ function acquireWorkflowRun(runId, options, requestSignal) {
|
|
|
226
250
|
cancelRegistry.delete(runId);
|
|
227
251
|
releaseMutex(runId);
|
|
228
252
|
shutdownCoordinator.unregister(handle);
|
|
253
|
+
if (owner.entry !== undefined)
|
|
254
|
+
completions.settle(owner.entry);
|
|
229
255
|
complete();
|
|
230
256
|
}
|
|
231
257
|
},
|
|
@@ -1124,18 +1150,22 @@ function appendDuplicateReplayBlock(content, store) {
|
|
|
1124
1150
|
});
|
|
1125
1151
|
}
|
|
1126
1152
|
/**
|
|
1127
|
-
* Trailing additive
|
|
1128
|
-
* roster
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1153
|
+
* Trailing additive identity block, always last: the admission-time
|
|
1154
|
+
* requested-value roster (omitted for non-council, plan-review, or pre-roster
|
|
1155
|
+
* state) and the execution_id bound to this request (omitted when the request
|
|
1156
|
+
* was rejected before it owned state). No block when neither is present.
|
|
1157
|
+
* Defensive optional access: partial store mocks may omit runState.
|
|
1131
1158
|
*/
|
|
1132
|
-
function
|
|
1159
|
+
function appendIdentityBlock(content, store, executionId) {
|
|
1133
1160
|
const resolvedRoster = store?.runState?.resolvedRoster;
|
|
1134
|
-
if (resolvedRoster === undefined)
|
|
1161
|
+
if (resolvedRoster === undefined && executionId === undefined)
|
|
1135
1162
|
return;
|
|
1136
1163
|
content.push({
|
|
1137
1164
|
type: "text",
|
|
1138
|
-
text: JSON.stringify({
|
|
1165
|
+
text: JSON.stringify({
|
|
1166
|
+
...(resolvedRoster !== undefined ? { resolved_roster: resolvedRoster } : {}),
|
|
1167
|
+
...(executionId !== undefined ? { execution_id: executionId } : {}),
|
|
1168
|
+
}),
|
|
1139
1169
|
});
|
|
1140
1170
|
}
|
|
1141
1171
|
// ---------------------------------------------------------------------------
|
|
@@ -1527,6 +1557,7 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
|
|
|
1527
1557
|
let effectiveDefinition;
|
|
1528
1558
|
let initializedPlanReview;
|
|
1529
1559
|
let preparedSnapshot;
|
|
1560
|
+
let executionId;
|
|
1530
1561
|
try {
|
|
1531
1562
|
ownership.throwIfCancelled();
|
|
1532
1563
|
const runDir = getRunStateDir(runId);
|
|
@@ -1559,6 +1590,7 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
|
|
|
1559
1590
|
catch (err) {
|
|
1560
1591
|
throw new McpError(ErrorCode.InternalError, `Failed to initialize async run ${runId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1561
1592
|
}
|
|
1593
|
+
executionId = await ownership.bindStore(store);
|
|
1562
1594
|
ownership.throwIfCancelled();
|
|
1563
1595
|
// A-10: the async acknowledgement is a durability boundary. A caller
|
|
1564
1596
|
// cannot observe `running` until every plan-review snapshot and the
|
|
@@ -1574,7 +1606,9 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
|
|
|
1574
1606
|
await markWorkflowCancelled(store);
|
|
1575
1607
|
throw new WorkflowCancellationError();
|
|
1576
1608
|
}
|
|
1577
|
-
|
|
1609
|
+
// Once bound (plan-review init, or a failed id stamp), the run owns a
|
|
1610
|
+
// persisted record: terminalize it and disclose the bound identity.
|
|
1611
|
+
if (store !== undefined && ownership.executionId !== undefined) {
|
|
1578
1612
|
try {
|
|
1579
1613
|
await markWorkflowFailed(store, runId, err);
|
|
1580
1614
|
}
|
|
@@ -1601,6 +1635,8 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
|
|
|
1601
1635
|
summary: failureSummary,
|
|
1602
1636
|
debugBlock: debug ? buildErrorDebugBlock(runId, store) : undefined,
|
|
1603
1637
|
});
|
|
1638
|
+
// Bound even if persisting the id then failed: the entry exists.
|
|
1639
|
+
appendIdentityBlock(content, store, ownership.executionId);
|
|
1604
1640
|
return { content, isError: true };
|
|
1605
1641
|
}
|
|
1606
1642
|
throw err;
|
|
@@ -1619,7 +1655,7 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
|
|
|
1619
1655
|
}
|
|
1620
1656
|
}
|
|
1621
1657
|
}
|
|
1622
|
-
if (store === undefined || effectiveDefinition === undefined) {
|
|
1658
|
+
if (store === undefined || effectiveDefinition === undefined || executionId === undefined) {
|
|
1623
1659
|
await ownership.finish(store);
|
|
1624
1660
|
throw new McpError(ErrorCode.InternalError, `Failed to initialize async run ${runId}`);
|
|
1625
1661
|
}
|
|
@@ -1640,6 +1676,7 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
|
|
|
1640
1676
|
type: "text",
|
|
1641
1677
|
text: JSON.stringify({
|
|
1642
1678
|
run_id: runId,
|
|
1679
|
+
execution_id: executionId,
|
|
1643
1680
|
status: "running",
|
|
1644
1681
|
...(duplicateOfFailedRun !== undefined
|
|
1645
1682
|
? { duplicate_of_failed_run: duplicateOfFailedRun }
|
|
@@ -1877,6 +1914,7 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
|
|
|
1877
1914
|
...(rosterName !== undefined ? { rosterName } : {}),
|
|
1878
1915
|
});
|
|
1879
1916
|
}
|
|
1917
|
+
await ownership.bindStore(store);
|
|
1880
1918
|
ownership.throwIfCancelled();
|
|
1881
1919
|
releaseLifecycleBinding = options.contextBuilderLifecycleRouter?.bind(runId, store);
|
|
1882
1920
|
// A-5/A-6: snapshot + contract + typed subtree land here — after state
|
|
@@ -1986,7 +2024,7 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
|
|
|
1986
2024
|
debugBlock: debug ? buildDebugBlock(runId, result, store) : undefined,
|
|
1987
2025
|
});
|
|
1988
2026
|
appendDuplicateReplayBlock(content, store);
|
|
1989
|
-
|
|
2027
|
+
appendIdentityBlock(content, store, ownership.executionId);
|
|
1990
2028
|
return { content };
|
|
1991
2029
|
}
|
|
1992
2030
|
catch (error) {
|
|
@@ -2039,7 +2077,7 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
|
|
|
2039
2077
|
debugBlock: debug && store != null ? buildErrorDebugBlock(runId, store) : undefined,
|
|
2040
2078
|
});
|
|
2041
2079
|
appendDuplicateReplayBlock(content, store);
|
|
2042
|
-
|
|
2080
|
+
appendIdentityBlock(content, store, ownership.executionId);
|
|
2043
2081
|
return { content, isError: true };
|
|
2044
2082
|
}
|
|
2045
2083
|
finally {
|
|
@@ -2053,18 +2091,9 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
|
|
|
2053
2091
|
}
|
|
2054
2092
|
}
|
|
2055
2093
|
async function executeResumeWorkflow(server, options, args, signal, progressToken) {
|
|
2056
|
-
const runId = args
|
|
2094
|
+
const runId = runIdArg(args);
|
|
2057
2095
|
const debug = args["debug"] === true;
|
|
2058
2096
|
const receipt = args["receipt"] === true;
|
|
2059
|
-
if (typeof runId !== "string" || runId === "") {
|
|
2060
|
-
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid "run_id" parameter');
|
|
2061
|
-
}
|
|
2062
|
-
try {
|
|
2063
|
-
validateRunId(runId);
|
|
2064
|
-
}
|
|
2065
|
-
catch {
|
|
2066
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid run_id "${runId}". Must match [A-Za-z0-9_-]{1,64}.`);
|
|
2067
|
-
}
|
|
2068
2097
|
// Plan-review terminal state is authoritative even while the detached run's
|
|
2069
2098
|
// finally still owns the in-process mutex. Read it before duplicate-run
|
|
2070
2099
|
// arbitration so terminal-before-cleanup never leaks as "already running".
|
|
@@ -2117,6 +2146,7 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
|
|
|
2117
2146
|
// Open this attempt before classifying an abort during reconstruction;
|
|
2118
2147
|
// otherwise a prior terminal record would suppress markCancelled.
|
|
2119
2148
|
await store.beginResumeAttempt();
|
|
2149
|
+
await ownership.bindStore(store);
|
|
2120
2150
|
ownership.throwIfCancelled();
|
|
2121
2151
|
releaseLifecycleBinding = options.contextBuilderLifecycleRouter?.bind(runId, store);
|
|
2122
2152
|
const validatorRegistry = createValidatorRegistry();
|
|
@@ -2293,16 +2323,27 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
|
|
|
2293
2323
|
summary,
|
|
2294
2324
|
debugBlock: debug ? buildDebugBlock(runId, result, store) : undefined,
|
|
2295
2325
|
});
|
|
2326
|
+
appendIdentityBlock(content, null, ownership.executionId);
|
|
2296
2327
|
return { content };
|
|
2297
2328
|
}
|
|
2298
2329
|
catch (error) {
|
|
2330
|
+
// Only a bound attempt owns the persisted record. Before bind, a failed
|
|
2331
|
+
// beginResumeAttempt leaves in-memory state "pending" while disk still
|
|
2332
|
+
// holds the settled run's terminal bytes; persisting it would displace
|
|
2333
|
+
// that settled execution.
|
|
2334
|
+
const owned = ownership.executionId !== undefined;
|
|
2299
2335
|
if (ownedSignal.aborted || error instanceof WorkflowCancellationError) {
|
|
2300
|
-
|
|
2336
|
+
if (owned)
|
|
2337
|
+
await markWorkflowCancelled(store);
|
|
2301
2338
|
throw new WorkflowCancellationError();
|
|
2302
2339
|
}
|
|
2340
|
+
// A verified-repair store is never bound: this is the pre-mutex rejection
|
|
2341
|
+
// reached when that read failed, so report it the same way (-32602).
|
|
2342
|
+
if (!owned && store?.runState.workflowName === VERIFIED_REPAIR_WORKFLOW)
|
|
2343
|
+
throw error;
|
|
2303
2344
|
// Persist a terminal "failed" record for post-admission setup failures.
|
|
2304
2345
|
// Idempotent: won't clobber an engine-emitted terminal status.
|
|
2305
|
-
if (store != null) {
|
|
2346
|
+
if (owned && store != null) {
|
|
2306
2347
|
try {
|
|
2307
2348
|
await markWorkflowFailed(store, runId, error);
|
|
2308
2349
|
}
|
|
@@ -2328,6 +2369,7 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
|
|
|
2328
2369
|
summary: failureSummary,
|
|
2329
2370
|
debugBlock: debug && store != null ? buildErrorDebugBlock(runId, store) : undefined,
|
|
2330
2371
|
});
|
|
2372
|
+
appendIdentityBlock(content, null, ownership.executionId);
|
|
2331
2373
|
return { content, isError: true };
|
|
2332
2374
|
}
|
|
2333
2375
|
finally {
|
|
@@ -2428,9 +2470,18 @@ function parseLifecycleForStatus(value) {
|
|
|
2428
2470
|
return undefined;
|
|
2429
2471
|
}
|
|
2430
2472
|
}
|
|
2431
|
-
|
|
2473
|
+
function expectedExecutionId(args) {
|
|
2474
|
+
const expected = args["expected_execution_id"];
|
|
2475
|
+
if (expected === undefined)
|
|
2476
|
+
return undefined;
|
|
2477
|
+
if (typeof expected !== "string" || expected.length === 0 || expected.length > 128) {
|
|
2478
|
+
throw new McpError(ErrorCode.InvalidParams, "expected_execution_id must be a nonempty string of at most 128 characters");
|
|
2479
|
+
}
|
|
2480
|
+
return expected;
|
|
2481
|
+
}
|
|
2482
|
+
/** Shared by resume and pinned/unpinned status/wait, preserving their existing messages. */
|
|
2483
|
+
function runIdArg(args) {
|
|
2432
2484
|
const runId = args["run_id"];
|
|
2433
|
-
const includeResult = args["include_result"] === true;
|
|
2434
2485
|
if (typeof runId !== "string" || runId === "") {
|
|
2435
2486
|
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid "run_id" parameter');
|
|
2436
2487
|
}
|
|
@@ -2440,12 +2491,242 @@ async function executeWorkflowRunStatus(options, args) {
|
|
|
2440
2491
|
catch {
|
|
2441
2492
|
throw new McpError(ErrorCode.InvalidParams, `Invalid run_id "${runId}". Must match [A-Za-z0-9_-]{1,64}.`);
|
|
2442
2493
|
}
|
|
2443
|
-
|
|
2494
|
+
return runId;
|
|
2495
|
+
}
|
|
2496
|
+
function identityError(runId, reason) {
|
|
2497
|
+
return {
|
|
2498
|
+
isError: true,
|
|
2499
|
+
content: [{ type: "text", text: JSON.stringify({ run_id: runId, error: reason }) }],
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
/**
|
|
2503
|
+
* Project status for one bound execution and fence it. A settled execution is
|
|
2504
|
+
* fenced on the digest of the exact state bytes the projection parsed (one
|
|
2505
|
+
* state.json read on the success path); an active one only on provenance and
|
|
2506
|
+
* directory incarnation, since it legitimately republishes state.
|
|
2507
|
+
*/
|
|
2508
|
+
async function projectGuardedStatus(options, args, entry) {
|
|
2509
|
+
const wasSettled = entry.settled;
|
|
2510
|
+
const wasStamped = entry.stamped;
|
|
2511
|
+
const guard = {};
|
|
2512
|
+
let result;
|
|
2513
|
+
try {
|
|
2514
|
+
result = await projectWorkflowRunStatus(options, args, guard);
|
|
2515
|
+
}
|
|
2516
|
+
catch (error) {
|
|
2517
|
+
const reread = wasSettled ? (await inspectPublication(entry.runDir))?.fingerprint ?? null : undefined;
|
|
2518
|
+
const failure = await completions.check(entry, reread);
|
|
2519
|
+
if (failure !== undefined)
|
|
2520
|
+
return { failure };
|
|
2521
|
+
if (error instanceof ContainedFileChangedError) {
|
|
2522
|
+
return { failure: wasSettled ? "identity_unavailable" : "state_read_unstable" };
|
|
2523
|
+
}
|
|
2524
|
+
throw error;
|
|
2525
|
+
}
|
|
2526
|
+
const failure = await completions.check(entry, wasSettled ? guard.stateDigest ?? null : undefined);
|
|
2527
|
+
if (failure !== undefined)
|
|
2528
|
+
return { failure };
|
|
2529
|
+
// Another process sharing the state dir can resume the run in place, which
|
|
2530
|
+
// keeps the directory incarnation; only the persisted id shows the change.
|
|
2531
|
+
if (wasStamped && guard.executionId !== entry.executionId) {
|
|
2532
|
+
return { failure: typeof guard.executionId === "string" ? "identity_changed" : "identity_unavailable" };
|
|
2533
|
+
}
|
|
2534
|
+
return { result };
|
|
2535
|
+
}
|
|
2536
|
+
async function executeWorkflowRunStatus(options, args) {
|
|
2537
|
+
const expected = expectedExecutionId(args);
|
|
2538
|
+
if (expected === undefined)
|
|
2539
|
+
return projectWorkflowRunStatus(options, args);
|
|
2540
|
+
const runId = runIdArg(args);
|
|
2541
|
+
const selected = completions.select(getRunStateDir(runId), expected);
|
|
2542
|
+
if (selected === "identity_unavailable") {
|
|
2543
|
+
const persisted = await projectPersistedSettledStatus(options, args, runId, expected);
|
|
2544
|
+
return "failure" in persisted ? identityError(runId, persisted.failure) : persisted.result;
|
|
2545
|
+
}
|
|
2546
|
+
if (typeof selected === "string")
|
|
2547
|
+
return identityError(runId, selected);
|
|
2548
|
+
if (selected === undefined)
|
|
2549
|
+
return identityError(runId, "identity_unavailable");
|
|
2550
|
+
const release = completions.retain(selected);
|
|
2551
|
+
try {
|
|
2552
|
+
const before = await completions.check(selected);
|
|
2553
|
+
if (before !== undefined)
|
|
2554
|
+
return identityError(runId, before);
|
|
2555
|
+
const fenced = await projectGuardedStatus(options, args, selected);
|
|
2556
|
+
return "failure" in fenced ? identityError(runId, fenced.failure) : fenced.result;
|
|
2557
|
+
}
|
|
2558
|
+
finally {
|
|
2559
|
+
release();
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
async function executeWorkflowRunWait(options, args, signal) {
|
|
2563
|
+
const started = Date.now();
|
|
2564
|
+
const runId = runIdArg(args);
|
|
2565
|
+
const expected = expectedExecutionId(args);
|
|
2566
|
+
const selected = completions.select(getRunStateDir(runId), expected);
|
|
2567
|
+
let outcome;
|
|
2568
|
+
let entry;
|
|
2569
|
+
let persistedExecutionId;
|
|
2570
|
+
const release = typeof selected === "object" ? completions.retain(selected) : undefined;
|
|
2571
|
+
const respond = (projection = { run_id: runId }) => {
|
|
2572
|
+
if (signal.aborted)
|
|
2573
|
+
throw new WorkflowCancellationError();
|
|
2574
|
+
return {
|
|
2575
|
+
content: [{
|
|
2576
|
+
type: "text",
|
|
2577
|
+
text: JSON.stringify({
|
|
2578
|
+
...projection,
|
|
2579
|
+
wait: {
|
|
2580
|
+
outcome,
|
|
2581
|
+
...(entry !== undefined ? { execution_id: entry.executionId } : {}),
|
|
2582
|
+
...(persistedExecutionId !== undefined ? { execution_id: persistedExecutionId } : {}),
|
|
2583
|
+
budget_ms: WORKFLOW_WAIT_BUDGET_MS,
|
|
2584
|
+
elapsed_ms: Math.max(0, Date.now() - started),
|
|
2585
|
+
},
|
|
2586
|
+
}),
|
|
2587
|
+
}],
|
|
2588
|
+
};
|
|
2589
|
+
};
|
|
2590
|
+
try {
|
|
2591
|
+
if (selected === "identity_unavailable" && expected !== undefined) {
|
|
2592
|
+
// No local record: settled only if state.json proves this execution ended.
|
|
2593
|
+
const persisted = await projectPersistedSettledStatus(options, { run_id: runId }, runId, expected);
|
|
2594
|
+
if (signal.aborted)
|
|
2595
|
+
throw new WorkflowCancellationError();
|
|
2596
|
+
if ("failure" in persisted) {
|
|
2597
|
+
outcome = persisted.failure;
|
|
2598
|
+
return respond();
|
|
2599
|
+
}
|
|
2600
|
+
outcome = "settled";
|
|
2601
|
+
persistedExecutionId = expected;
|
|
2602
|
+
return respond(JSON.parse(persisted.result.content[0].text));
|
|
2603
|
+
}
|
|
2604
|
+
if (typeof selected === "string")
|
|
2605
|
+
outcome = selected;
|
|
2606
|
+
else if (selected === undefined)
|
|
2607
|
+
outcome = "not_owned";
|
|
2608
|
+
else {
|
|
2609
|
+
entry = selected;
|
|
2610
|
+
outcome = await completions.wait(entry, signal);
|
|
2611
|
+
const failure = await completions.check(entry);
|
|
2612
|
+
if (failure !== undefined)
|
|
2613
|
+
outcome = failure;
|
|
2614
|
+
}
|
|
2615
|
+
if (signal.aborted)
|
|
2616
|
+
throw new WorkflowCancellationError();
|
|
2617
|
+
// Never inspect a replacement or unreadable state after provenance fails.
|
|
2618
|
+
if (outcome === "identity_changed" || outcome === "identity_unavailable")
|
|
2619
|
+
return respond();
|
|
2620
|
+
// Metadata only: the result body is fetched separately through the guarded status tool.
|
|
2621
|
+
if (entry !== undefined) {
|
|
2622
|
+
const fenced = await projectGuardedStatus(options, { run_id: runId }, entry);
|
|
2623
|
+
if (signal.aborted)
|
|
2624
|
+
throw new WorkflowCancellationError();
|
|
2625
|
+
if ("failure" in fenced) {
|
|
2626
|
+
// An unstable active read is a normal re-arm, never a hard error.
|
|
2627
|
+
outcome = fenced.failure === "state_read_unstable" ? "budget_elapsed" : fenced.failure;
|
|
2628
|
+
return respond();
|
|
2629
|
+
}
|
|
2630
|
+
return respond(JSON.parse(fenced.result.content[0].text));
|
|
2631
|
+
}
|
|
2632
|
+
const status = await projectWorkflowRunStatus(options, { run_id: runId });
|
|
2633
|
+
const projection = JSON.parse(status.content[0].text);
|
|
2634
|
+
if (projection["status"] === "not_found")
|
|
2635
|
+
outcome = "not_found";
|
|
2636
|
+
else if (projection["status"] === "orphaned")
|
|
2637
|
+
outcome = "orphaned";
|
|
2638
|
+
if (signal.aborted)
|
|
2639
|
+
throw new WorkflowCancellationError();
|
|
2640
|
+
return respond(projection);
|
|
2641
|
+
}
|
|
2642
|
+
finally {
|
|
2643
|
+
release?.();
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* A pin with no local record (bound by an earlier process, or evicted here) is
|
|
2648
|
+
* resolved from the executionId persisted in state.json. The id, terminal
|
|
2649
|
+
* status and released run.lock are checked on the exact bytes the projection
|
|
2650
|
+
* then renders. After the projection a second contained read must return the
|
|
2651
|
+
* same bytes with the lock still released, the directory incarnation must be
|
|
2652
|
+
* unchanged, and no local execution may have bound the run dir; otherwise the
|
|
2653
|
+
* read fails closed, since a foreign resume keeps the generation.
|
|
2654
|
+
*/
|
|
2655
|
+
async function projectPersistedSettledStatus(options, args, runId, expected) {
|
|
2656
|
+
const runDir = getRunStateDir(runId);
|
|
2657
|
+
const latest = completions.select(runDir);
|
|
2658
|
+
if (latest !== undefined && !latest.settled)
|
|
2659
|
+
return { failure: "identity_changed" };
|
|
2660
|
+
const before = await readSessionLogGeneration(runDir);
|
|
2661
|
+
if (before.kind !== "available")
|
|
2662
|
+
return { failure: "identity_unavailable" };
|
|
2663
|
+
const guard = { persistedExecutionId: expected };
|
|
2664
|
+
let result;
|
|
2665
|
+
let projectionError;
|
|
2666
|
+
try {
|
|
2667
|
+
result = await projectWorkflowRunStatus(options, args, guard);
|
|
2668
|
+
}
|
|
2669
|
+
catch (error) {
|
|
2670
|
+
// Unreadable, oversized, malformed or persistently changing state.
|
|
2671
|
+
if (guard.persistedIdentity === undefined)
|
|
2672
|
+
return { failure: "identity_unavailable" };
|
|
2673
|
+
projectionError = error;
|
|
2674
|
+
}
|
|
2675
|
+
if (guard.persistedIdentity !== "matched")
|
|
2676
|
+
return { failure: guard.persistedIdentity ?? "identity_unavailable" };
|
|
2677
|
+
let reread;
|
|
2678
|
+
try {
|
|
2679
|
+
reread = await readRunStateForStatus(runId, true);
|
|
2680
|
+
}
|
|
2681
|
+
catch {
|
|
2682
|
+
return { failure: "identity_unavailable" };
|
|
2683
|
+
}
|
|
2684
|
+
if (reread == null)
|
|
2685
|
+
return { failure: "identity_unavailable" };
|
|
2686
|
+
// Same rule as the first check: only a different stamped id is a change.
|
|
2687
|
+
const rereadId = reread.state.executionId;
|
|
2688
|
+
if (typeof rereadId === "string" && rereadId !== expected)
|
|
2689
|
+
return { failure: "identity_changed" };
|
|
2690
|
+
if (reread.stateDigest !== guard.stateDigest || reread.lock !== "stale_or_absent") {
|
|
2691
|
+
return { failure: "identity_unavailable" };
|
|
2692
|
+
}
|
|
2693
|
+
const after = await readSessionLogGeneration(runDir);
|
|
2694
|
+
// A new local bind supersedes the pin; mere eviction of `latest` does not.
|
|
2695
|
+
const current = completions.select(runDir);
|
|
2696
|
+
if (current !== undefined && current !== latest)
|
|
2697
|
+
return { failure: "identity_changed" };
|
|
2698
|
+
if (after.kind !== "available")
|
|
2699
|
+
return { failure: "identity_unavailable" };
|
|
2700
|
+
if (after.sessionLogGeneration !== before.sessionLogGeneration)
|
|
2701
|
+
return { failure: "identity_changed" };
|
|
2702
|
+
if (result !== undefined)
|
|
2703
|
+
return { result };
|
|
2704
|
+
// Same mapping as the in-memory settled path: a racing contained read fails closed.
|
|
2705
|
+
if (projectionError instanceof ContainedFileChangedError)
|
|
2706
|
+
return { failure: "identity_unavailable" };
|
|
2707
|
+
throw projectionError;
|
|
2708
|
+
}
|
|
2709
|
+
async function projectWorkflowRunStatus(options, args, guard) {
|
|
2710
|
+
const runId = runIdArg(args);
|
|
2711
|
+
const includeResult = args["include_result"] === true;
|
|
2712
|
+
const read = await readRunStateForStatus(runId, guard !== undefined);
|
|
2444
2713
|
if (read == null) {
|
|
2714
|
+
if (guard?.persistedExecutionId !== undefined)
|
|
2715
|
+
guard.persistedIdentity = "identity_unavailable";
|
|
2445
2716
|
return {
|
|
2446
2717
|
content: [{ type: "text", text: JSON.stringify({ status: "not_found", run_id: runId }) }],
|
|
2447
2718
|
};
|
|
2448
2719
|
}
|
|
2720
|
+
if (guard !== undefined) {
|
|
2721
|
+
guard.stateDigest = read.stateDigest;
|
|
2722
|
+
guard.executionId = read.state.executionId;
|
|
2723
|
+
}
|
|
2724
|
+
if (guard?.persistedExecutionId !== undefined) {
|
|
2725
|
+
// Checked before any further I/O, on the bytes this projection renders.
|
|
2726
|
+
guard.persistedIdentity = persistedIdentityFailure(read.state, read.lock, runId, guard.persistedExecutionId) ?? "matched";
|
|
2727
|
+
if (guard.persistedIdentity !== "matched")
|
|
2728
|
+
return { content: [] };
|
|
2729
|
+
}
|
|
2449
2730
|
const { state, lock } = read;
|
|
2450
2731
|
const status = (state.status === "running" || state.status === "pending") && lock === "stale_or_absent"
|
|
2451
2732
|
? "orphaned"
|
|
@@ -3028,7 +3309,7 @@ const RUN_WORKFLOW_SCHEMA = {
|
|
|
3028
3309
|
async: {
|
|
3029
3310
|
type: "boolean",
|
|
3030
3311
|
default: false,
|
|
3031
|
-
description: "When true, start
|
|
3312
|
+
description: "When true, start detached and return {run_id, execution_id, status:'running'} immediately. Prefer workflow_run_wait for local completion, then workflow_run_status with expected_execution_id and include_result:true; status polling remains available for recovery. When false (blocking), the final content block carries execution_id once the run owned state.",
|
|
3032
3313
|
},
|
|
3033
3314
|
roster: {
|
|
3034
3315
|
type: "string",
|
|
@@ -3121,6 +3402,12 @@ const LIST_WORKFLOWS_SCHEMA = {
|
|
|
3121
3402
|
properties: {},
|
|
3122
3403
|
additionalProperties: false,
|
|
3123
3404
|
};
|
|
3405
|
+
const EXECUTION_ID_SCHEMA = {
|
|
3406
|
+
type: "string",
|
|
3407
|
+
minLength: 1,
|
|
3408
|
+
maxLength: 128,
|
|
3409
|
+
description: "Opaque execution identity (also persisted in run state) returned by run_workflow (async ack or trailing sync block), resume_workflow, or a bare wait. A fence, not a capability: it grants nothing and only makes reads fail closed (identity_changed/identity_unavailable) if they would observe a different execution.",
|
|
3410
|
+
};
|
|
3124
3411
|
const WORKFLOW_RUN_STATUS_SCHEMA = {
|
|
3125
3412
|
type: "object",
|
|
3126
3413
|
properties: {
|
|
@@ -3128,6 +3415,7 @@ const WORKFLOW_RUN_STATUS_SCHEMA = {
|
|
|
3128
3415
|
type: "string",
|
|
3129
3416
|
pattern: "^[A-Za-z0-9_-]{1,64}$",
|
|
3130
3417
|
},
|
|
3418
|
+
expected_execution_id: EXECUTION_ID_SCHEMA,
|
|
3131
3419
|
include_result: {
|
|
3132
3420
|
type: "boolean",
|
|
3133
3421
|
default: false,
|
|
@@ -3137,6 +3425,15 @@ const WORKFLOW_RUN_STATUS_SCHEMA = {
|
|
|
3137
3425
|
required: ["run_id"],
|
|
3138
3426
|
additionalProperties: false,
|
|
3139
3427
|
};
|
|
3428
|
+
const WORKFLOW_RUN_WAIT_SCHEMA = {
|
|
3429
|
+
type: "object",
|
|
3430
|
+
properties: {
|
|
3431
|
+
run_id: { type: "string", pattern: "^[A-Za-z0-9_-]{1,64}$" },
|
|
3432
|
+
expected_execution_id: EXECUTION_ID_SCHEMA,
|
|
3433
|
+
},
|
|
3434
|
+
required: ["run_id"],
|
|
3435
|
+
additionalProperties: false,
|
|
3436
|
+
};
|
|
3140
3437
|
const CANCEL_WORKFLOW_SCHEMA = {
|
|
3141
3438
|
type: "object",
|
|
3142
3439
|
properties: {
|
|
@@ -3248,13 +3545,14 @@ export function getWorkflowToolDefinitions(workflowsPath, availableCliNames) {
|
|
|
3248
3545
|
return [
|
|
3249
3546
|
{
|
|
3250
3547
|
name: "run_workflow",
|
|
3251
|
-
description: `Start a new multi-stage workflow execution.${workflowSuffix}\nAccepts optional \`members\` and \`chairman\` parameters for council workflows (council-default, council-answer, council-code-review, council-debug, council-discover, council-plan-review) to override member/reviewer composition and chairman CLI adapter at runtime. Council runs include a \`resolved_roster\` of requested seat and delegate values (no provenance) on the async ack, a trailing sync content block, and workflow_run_status.`,
|
|
3548
|
+
description: `Start a new multi-stage workflow execution.${workflowSuffix}\nAccepts optional \`members\` and \`chairman\` parameters for council workflows (council-default, council-answer, council-code-review, council-debug, council-discover, council-plan-review) to override member/reviewer composition and chairman CLI adapter at runtime. Council runs include a \`resolved_roster\` of requested seat and delegate values (no provenance) on the async ack, a trailing sync content block, and workflow_run_status. Blocking runs end with one trailing identity block {resolved_roster?, execution_id?}; execution_id is present once the run owned state, absent when rejected before that.`,
|
|
3252
3549
|
inputSchema: runSchema,
|
|
3253
3550
|
},
|
|
3254
3551
|
{
|
|
3255
3552
|
name: "resume_workflow",
|
|
3256
3553
|
description: "Resume a previously failed or cancelled workflow from its last checkpoint. " +
|
|
3257
|
-
"Requires the original run_id and matching child config."
|
|
3554
|
+
"Requires the original run_id and matching child config. " +
|
|
3555
|
+
"Once the resume owns the run, the final content block is {execution_id} for this attempt; pin workflow_run_wait/workflow_run_status to it. A resume rejected before then returns no execution_id and leaves the previous execution observable.",
|
|
3258
3556
|
inputSchema: RESUME_WORKFLOW_SCHEMA,
|
|
3259
3557
|
},
|
|
3260
3558
|
{
|
|
@@ -3265,10 +3563,16 @@ export function getWorkflowToolDefinitions(workflowsPath, availableCliNames) {
|
|
|
3265
3563
|
},
|
|
3266
3564
|
{
|
|
3267
3565
|
name: "workflow_run_status",
|
|
3268
|
-
description: "Read-only status/progress of a workflow run. The full result is included only with include_result:true. Generic workflows preserve succeeded-final-call behavior; plan-review-receipts reports marker-driven availability and bytes independently of terminal status, with body-blind default polling. Every found run also carries a top-level `health` block {classification: \"ok\"|\"degraded\", reasons: string[]} computed read-time from persisted data — a run can be status:\"succeeded\" yet degraded when on_failure:continue swallowed failed calls or context preflight was partial/degraded (reasons: failed_calls_in_succeeded_run, context_partial, context_lifecycle_failed, coverage_degraded, delegate_fallback_used, context_external_gaps — the finalizer reported evidence gaps outside the delegate packet under external_gap_policy:\"degrade\", so conclusions depending on them are conditional). When a context-builder lifecycle snapshot is persisted, the `context` block additionally includes `lifecycle_status` and `coverage` {policy, status, required, selected, missing}. A parsed snapshot also projects `context.delegate.finalizer` {effective_enabled, execution_status, score?}; `score` is present only for a succeeded finalizer that recorded one, and the finalizer key is omitted when the lifecycle is missing, unparseable, or those fields are absent. While context preflight is still running (non-terminal lifecycle, no committed context yet) the `context` block is absent and a top-level `context_progress` {phase, status, updated_at, counts} reports scan progress instead. Found council runs also include a top-level `resolved_roster` of requested seat and delegate values (no provenance) when the run persisted one; older runs omit the key. Architecture comparisons additionally project comparison completeness and retained record availability independently of run execution status. Every found run has a separately versioned verification projection of historical inspected-at evidence; it establishes neither current source/runtime applicability nor owner acceptance.",
|
|
3566
|
+
description: "Read-only status/progress of a workflow run. The full result is included only with include_result:true. Generic workflows preserve succeeded-final-call behavior; plan-review-receipts reports marker-driven availability and bytes independently of terminal status, with body-blind default polling. Every found run also carries a top-level `health` block {classification: \"ok\"|\"degraded\", reasons: string[]} computed read-time from persisted data — a run can be status:\"succeeded\" yet degraded when on_failure:continue swallowed failed calls or context preflight was partial/degraded (reasons: failed_calls_in_succeeded_run, context_partial, context_lifecycle_failed, coverage_degraded, delegate_fallback_used, context_external_gaps — the finalizer reported evidence gaps outside the delegate packet under external_gap_policy:\"degrade\", so conclusions depending on them are conditional). When a context-builder lifecycle snapshot is persisted, the `context` block additionally includes `lifecycle_status` and `coverage` {policy, status, required, selected, missing}. A parsed snapshot also projects `context.delegate.finalizer` {effective_enabled, execution_status, score?}; `score` is present only for a succeeded finalizer that recorded one, and the finalizer key is omitted when the lifecycle is missing, unparseable, or those fields are absent. While context preflight is still running (non-terminal lifecycle, no committed context yet) the `context` block is absent and a top-level `context_progress` {phase, status, updated_at, counts} reports scan progress instead. Found council runs also include a top-level `resolved_roster` of requested seat and delegate values (no provenance) when the run persisted one; older runs omit the key. Architecture comparisons additionally project comparison completeness and retained record availability independently of run execution status. Every found run has a separately versioned verification projection of historical inspected-at evidence; it establishes neither current source/runtime applicability nor owner acceptance. With expected_execution_id the response is fenced to that execution: a mismatch is isError {run_id, error:\"identity_changed\"|\"identity_unavailable\"} with no result, and an active execution whose state kept changing through every bounded reread is isError {run_id, error:\"state_read_unstable\"} — retryable; re-issue the same pinned call after a short backoff.",
|
|
3269
3567
|
inputSchema: WORKFLOW_RUN_STATUS_SCHEMA,
|
|
3270
3568
|
annotations: { readOnlyHint: true },
|
|
3271
3569
|
},
|
|
3570
|
+
{
|
|
3571
|
+
name: "workflow_run_wait",
|
|
3572
|
+
description: "Observe one local workflow execution through its cleanup fence for at most 30 seconds. Returns metadata status plus wait.outcome and execution_id; wait.budget_ms is the configured budget, not the time waited (see elapsed_ms). Re-arm budget_elapsed with the same identity; a persistently unstable state read on an active execution also re-arms as budget_elapsed, without projection. not_owned, orphaned and not_found return immediately: no local execution is witnessable here yet or any longer (a request still admitting has not bound one). expected_execution_id is a fence, not a capability. The settled fingerprint covers state.json bytes only, not result files. Cancellation detaches observation only. Unpinned foreign/historical state is not witnessed settlement; a pin whose execution reached terminal state settles from persisted state (e.g. after a restart). Fetch final results via workflow_run_status with expected_execution_id (it may return the retryable error state_read_unstable for an active execution; re-issue the same pinned call).",
|
|
3573
|
+
inputSchema: WORKFLOW_RUN_WAIT_SCHEMA,
|
|
3574
|
+
annotations: { readOnlyHint: true },
|
|
3575
|
+
},
|
|
3272
3576
|
{
|
|
3273
3577
|
name: "cancel_workflow",
|
|
3274
3578
|
description: "Cancel a workflow run by run_id. Aborts a live in-process async run (returns status:'cancelling'; poll workflow_run_status until it reaches terminal 'cancelled'). Reaps an orphaned/no-live-worker run to terminal 'cancelled' (reaped:true). Refuses a lock-held run owned by another live worker or a blocking run (cancellable:false). Idempotent; not_found and already_terminal are soft results.",
|
|
@@ -3283,6 +3587,7 @@ export const WORKFLOW_TOOL_NAMES = new Set([
|
|
|
3283
3587
|
"resume_workflow",
|
|
3284
3588
|
"list_workflows",
|
|
3285
3589
|
"workflow_run_status",
|
|
3590
|
+
"workflow_run_wait",
|
|
3286
3591
|
"cancel_workflow",
|
|
3287
3592
|
]);
|
|
3288
3593
|
/**
|
|
@@ -3301,6 +3606,8 @@ export async function handleWorkflowToolCall(server, options, name, args, signal
|
|
|
3301
3606
|
return executeListWorkflows(options);
|
|
3302
3607
|
case "workflow_run_status":
|
|
3303
3608
|
return await executeWorkflowRunStatus(options, args);
|
|
3609
|
+
case "workflow_run_wait":
|
|
3610
|
+
return await executeWorkflowRunWait(options, args, signal);
|
|
3304
3611
|
case "cancel_workflow":
|
|
3305
3612
|
return await executeCancelWorkflow(options, args);
|
|
3306
3613
|
default:
|
|
@@ -3314,8 +3621,8 @@ export async function handleWorkflowToolCall(server, options, name, args, signal
|
|
|
3314
3621
|
* Register workflow tools on an MCP Server instance.
|
|
3315
3622
|
*
|
|
3316
3623
|
* This is the reusable core extracted from createWorkflowServer().
|
|
3317
|
-
* It registers
|
|
3318
|
-
* workflow_run_status, cancel_workflow.
|
|
3624
|
+
* It registers six tools: run_workflow, resume_workflow, list_workflows,
|
|
3625
|
+
* workflow_run_status, workflow_run_wait, cancel_workflow.
|
|
3319
3626
|
*
|
|
3320
3627
|
* The `options.createBackend` factory determines how clink calls are
|
|
3321
3628
|
* dispatched — callers provide either a child-process or in-process backend.
|