taskmeld 0.1.2 → 0.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +176 -176
- package/README.zh-CN.md +176 -176
- package/dist/src/app/app-context-env.js +1 -1
- package/dist/src/app/create-app-context.js +3 -3
- package/dist/src/app/data-dir.js +13 -3
- package/dist/src/app/pipeline-config.js +4 -4
- package/dist/src/app/pipeline-registry.js +11 -11
- package/dist/src/app/pipeline-runtime.js +6 -9
- package/dist/src/app/runtime-store.js +3 -3
- package/dist/src/artifacts/artifact-cleanup.js +17 -17
- package/dist/src/artifacts/artifact-index.js +14 -14
- package/dist/src/artifacts/artifact-rebuilder.js +3 -3
- package/dist/src/artifacts/storage-service.js +18 -18
- package/dist/src/cli/bootstrap.js +7 -7
- package/dist/src/cli/commands/agent.js +12 -11
- package/dist/src/cli/commands/artifact.js +31 -30
- package/dist/src/cli/commands/init.js +49 -47
- package/dist/src/cli/commands/pipeline/result.js +9 -8
- package/dist/src/cli/commands/pipeline/selector.js +1 -1
- package/dist/src/cli/commands/pipeline/watch.js +2 -2
- package/dist/src/cli/commands/pipeline.js +54 -53
- package/dist/src/cli/commands/scheduler.js +9 -8
- package/dist/src/cli/commands/server.js +12 -11
- package/dist/src/cli/commands/system.js +4 -3
- package/dist/src/cli/errors.js +2 -2
- package/dist/src/cli/help.js +18 -17
- package/dist/src/cli/i18n.js +46 -0
- package/dist/src/cli/locales/en.json +244 -0
- package/dist/src/cli/locales/zh.json +244 -0
- package/dist/src/cli/output.js +3 -3
- package/dist/src/cli/renderers/engine/markdown.js +1 -1
- package/dist/src/cli/renderers/specs/index.js +1 -1
- package/dist/src/cli/router.js +1 -1
- package/dist/src/cli/server-runtime-client.js +54 -95
- package/dist/src/cli/ui-prompts.js +96 -0
- package/dist/src/cli/ws-runtime-client.js +51 -0
- package/dist/src/gateway/gateway-client.js +4 -4
- package/dist/src/index.js +28 -2
- package/dist/src/logs/run-log-reader.js +1 -1
- package/dist/src/pipeline/agent-activity.js +2 -2
- package/dist/src/pipeline/artifact-storage.js +11 -11
- package/dist/src/pipeline/diagnostics/dependency-diagnostic.js +11 -11
- package/dist/src/pipeline/dispatch/pipeline-inbound-queue.js +2 -2
- package/dist/src/pipeline/execution/group-item-executor.js +1 -1
- package/dist/src/pipeline/execution/node-item-executor.js +3 -3
- package/dist/src/pipeline/execution/node-runner.js +7 -7
- package/dist/src/pipeline/execution/readiness-state.js +1 -1
- package/dist/src/pipeline/execution/reject-handler.js +5 -5
- package/dist/src/pipeline/execution/rejected-artifact-archiver.js +1 -1
- package/dist/src/pipeline/execution/route-item-manager.js +4 -4
- package/dist/src/pipeline/execution/run-abort-controller.js +5 -5
- package/dist/src/pipeline/execution/run-state-helpers.js +2 -2
- package/dist/src/pipeline/execution/service.js +4 -4
- package/dist/src/pipeline/execution/structured-node-runner.js +24 -24
- package/dist/src/pipeline/execution-timeout.js +3 -3
- package/dist/src/pipeline/identity/index.js +3 -3
- package/dist/src/pipeline/item-batch-controller.js +6 -6
- package/dist/src/pipeline/scheduler/dependency-state.js +5 -5
- package/dist/src/pipeline/scheduler-service.js +24 -24
- package/dist/src/pipeline/state-machine.js +2 -2
- package/dist/src/pipeline/structured-output/contract.js +4 -4
- package/dist/src/pipeline/structured-output/index.js +2 -2
- package/dist/src/pipeline/structured-output/parser.js +5 -5
- package/dist/src/pipeline/structured-output/prompt.js +38 -38
- package/dist/src/pipeline/structured-output/waiter.js +6 -6
- package/dist/src/pipeline/template.js +5 -5
- package/dist/src/pipeline/timeline-log-store.js +5 -5
- package/dist/src/pipeline/tool-activity.js +3 -3
- package/dist/src/pipeline/types/pipeline-output.js +1 -1
- package/dist/src/pipeline/workflow/branch-rules.js +19 -19
- package/dist/src/pipeline/workflow/io.js +1 -1
- package/dist/src/pipeline/workflow/normalize.js +18 -18
- package/dist/src/pipeline/workflow/template-mapper.js +3 -3
- package/dist/src/pipeline/workflow/validate.js +39 -39
- package/dist/src/pipeline/workflow-graph.js +10 -10
- package/dist/src/server/http-handler.js +74 -0
- package/dist/src/services/agent-service.js +2 -2
- package/dist/src/services/gateway-read-helpers.js +1 -1
- package/dist/src/services/pipeline-service.js +19 -19
- package/dist/src/services/pipeline-status.js +4 -4
- package/dist/src/services/read-services.js +1 -1
- package/dist/src/services/session-service.js +6 -6
- package/dist/src/services/system-service.js +1 -1
- package/dist/src/transport/ws-broker.js +12 -1
- package/dist/src/transport/ws-handler.js +60 -0
- package/dist/src/transport/ws-methods/agents.js +144 -0
- package/dist/src/transport/ws-methods/artifacts.js +171 -0
- package/dist/src/transport/ws-methods/gateway.js +16 -0
- package/dist/src/transport/ws-methods/logs.js +43 -0
- package/dist/src/transport/ws-methods/pipeline-batch.js +68 -0
- package/dist/src/transport/ws-methods/pipeline-links.js +100 -0
- package/dist/src/transport/ws-methods/pipeline-queue.js +51 -0
- package/dist/src/transport/ws-methods/pipeline-runtime.js +151 -0
- package/dist/src/transport/ws-methods/pipeline-scheduler.js +48 -0
- package/dist/src/transport/ws-methods/pipeline-workflow.js +127 -0
- package/dist/src/transport/ws-methods/pipelines.js +56 -0
- package/dist/src/transport/ws-methods/register-all.js +32 -0
- package/dist/src/transport/ws-methods/sessions.js +154 -0
- package/dist/src/transport/ws-methods/timeline.js +10 -0
- package/dist/src/{server/routes/pipeline-identity.js → transport/ws-methods/utils.js} +14 -9
- package/dist/src/version.js +1 -1
- package/package.json +15 -7
- package/web/dist/assets/agent-DP6TMcLj.js +1 -0
- package/web/dist/assets/agent-DmJHzLyj.js +1 -0
- package/web/dist/assets/artifact-BqnoZy2M.js +1 -0
- package/web/dist/assets/artifact-DfDkgkno.js +1 -0
- package/web/dist/assets/common-DRMTVwE9.js +1 -0
- package/web/dist/assets/common-DeXccbr2.js +1 -0
- package/web/dist/assets/dispatch-CBskGCQI.js +1 -0
- package/web/dist/assets/dispatch-sk4Wp30e.js +1 -0
- package/web/dist/assets/index-C8wTjZvH.css +1 -0
- package/web/dist/assets/index-DYDQZRLk.js +58 -0
- package/web/dist/assets/log-DN8cjb0w.js +1 -0
- package/web/dist/assets/log-HSeA_dYy.js +1 -0
- package/web/dist/assets/modal-BdNai9jf.js +1 -0
- package/web/dist/assets/modal-D9_KDpFD.js +1 -0
- package/web/dist/assets/nav-BmF7oAKg.js +1 -0
- package/web/dist/assets/nav-IjC2xqXQ.js +1 -0
- package/web/dist/assets/node-detail-CENRXcrh.js +1 -0
- package/web/dist/assets/node-detail-bndPr0IM.js +1 -0
- package/web/dist/assets/overview-B87zWAxq.js +1 -0
- package/web/dist/assets/overview-gQvk-NOK.js +1 -0
- package/web/dist/assets/pipeline-D4dSJRDz.js +1 -0
- package/web/dist/assets/pipeline-DZzyOqQa.js +1 -0
- package/web/dist/assets/session-CUWvU14v.js +5 -0
- package/web/dist/assets/session-DQ6UuCaJ.js +5 -0
- package/web/dist/assets/timeline-8y_2_0Em.js +1 -0
- package/web/dist/assets/timeline-CAPsXUTC.js +1 -0
- package/web/dist/index.html +3 -3
- package/dist/src/app/pipeline-plugin-config.js +0 -2
- package/dist/src/server/api-handler.js +0 -163
- package/dist/src/server/http-utils.js +0 -34
- package/dist/src/server/middleware.js +0 -61
- package/dist/src/server/router.js +0 -105
- package/dist/src/server/routes/agents.js +0 -189
- package/dist/src/server/routes/artifacts.js +0 -163
- package/dist/src/server/routes/gateway.js +0 -18
- package/dist/src/server/routes/health.js +0 -16
- package/dist/src/server/routes/logs.js +0 -73
- package/dist/src/server/routes/pipeline-batch.js +0 -163
- package/dist/src/server/routes/pipeline-diagnostics.js +0 -33
- package/dist/src/server/routes/pipeline-links.js +0 -117
- package/dist/src/server/routes/pipeline-outputs.js +0 -27
- package/dist/src/server/routes/pipeline-queue.js +0 -62
- package/dist/src/server/routes/pipeline-runtime.js +0 -162
- package/dist/src/server/routes/pipeline-scheduler.js +0 -69
- package/dist/src/server/routes/pipeline-workflow.js +0 -180
- package/dist/src/server/routes/pipelines.js +0 -96
- package/dist/src/server/routes/sessions.js +0 -244
- package/dist/src/server/routes/timeline.js +0 -14
- package/dist/src/server/serve-static.js +0 -42
- package/web/dist/assets/index-CWnfhkn-.js +0 -65
- package/web/dist/assets/index-gZ0xOfSO.css +0 -1
- /package/dist/src/{server → transport/ws-methods}/types.js +0 -0
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Pure-functional branch determination rules.
|
|
4
|
+
* No external dependencies, accepts only data parameters. Shared by workflow/validate and execution/dependency-check.
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.getBranchScope = exports.isCrossBranchEdgeByScope = exports.computeNodeScopes = void 0;
|
|
8
|
-
// ====== Phase 2:
|
|
8
|
+
// ====== Phase 2: Branch rules based on explicit branchScopeId ======
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Derive each node's branch scope from the workflow's route edges.
|
|
11
|
+
* Scope identifier = routerNodeId:routeValue, e.g. "router:a".
|
|
12
|
+
* Mainline nodes have null scope.
|
|
13
13
|
*/
|
|
14
14
|
const computeNodeScopes = (nodes, edges, explicitScopes) => {
|
|
15
15
|
const scopes = new Map();
|
|
16
|
-
//
|
|
16
|
+
// Initialize: use explicit scope if provided, otherwise null
|
|
17
17
|
for (const node of nodes) {
|
|
18
18
|
scopes.set(node.id, explicitScopes?.get(node.id) ?? null);
|
|
19
19
|
}
|
|
20
|
-
//
|
|
20
|
+
// Derive scope from route edges: when from's scope is known, to's scope = fromScope != null ? fromScope : "from:when"
|
|
21
21
|
for (const edge of edges) {
|
|
22
22
|
if (edge.when === null)
|
|
23
23
|
continue;
|
|
@@ -28,17 +28,17 @@ const computeNodeScopes = (nodes, edges, explicitScopes) => {
|
|
|
28
28
|
scopes.set(edge.to, targetScope);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
//
|
|
32
|
-
//
|
|
31
|
+
// Propagate scope along unconditional dependency edges: BFS ensures downstream nodes within the same branch inherit the upstream scope.
|
|
32
|
+
// When multiple paths reach a node, merge if scopes agree; on conflict preserve the first-set scope.
|
|
33
33
|
let changed = true;
|
|
34
34
|
while (changed) {
|
|
35
35
|
changed = false;
|
|
36
36
|
for (const edge of edges) {
|
|
37
37
|
if (edge.when !== null)
|
|
38
|
-
continue; //
|
|
38
|
+
continue; // Only propagate unconditional dependency edges
|
|
39
39
|
const fromScope = scopes.get(edge.from);
|
|
40
40
|
if (fromScope == null)
|
|
41
|
-
continue; //
|
|
41
|
+
continue; // Don't propagate if upstream has no scope
|
|
42
42
|
const toScope = scopes.get(edge.to);
|
|
43
43
|
if (toScope == null) {
|
|
44
44
|
scopes.set(edge.to, fromScope);
|
|
@@ -50,25 +50,25 @@ const computeNodeScopes = (nodes, edges, explicitScopes) => {
|
|
|
50
50
|
};
|
|
51
51
|
exports.computeNodeScopes = computeNodeScopes;
|
|
52
52
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
53
|
+
* Determine whether an edge is a cross-branch unconditional edge based on scope.
|
|
54
|
+
* Cross-branch: when is null and from and to are in different scopes.
|
|
55
55
|
*/
|
|
56
56
|
const isCrossBranchEdgeByScope = (edge, nodeScopes) => {
|
|
57
57
|
if (edge.when !== null)
|
|
58
58
|
return false;
|
|
59
59
|
const fromScope = nodeScopes.get(edge.from) ?? null;
|
|
60
60
|
const toScope = nodeScopes.get(edge.to) ?? null;
|
|
61
|
-
//
|
|
61
|
+
// Same scope → within the same branch, allowed
|
|
62
62
|
if (fromScope === toScope)
|
|
63
63
|
return false;
|
|
64
|
-
// from
|
|
64
|
+
// from has scope, to has no scope → branch node propagating to mainline, don't block (scope inherits along dependency edges)
|
|
65
65
|
if (fromScope != null && toScope == null)
|
|
66
66
|
return false;
|
|
67
|
-
// from
|
|
68
|
-
// from
|
|
67
|
+
// from has no scope (mainline), to has scope (branch) → forbidden: mainline cannot unconditionally depend on branch-internal nodes
|
|
68
|
+
// from and to both have scope but different → forbidden: cross-branch dependency between different branches
|
|
69
69
|
return fromScope !== toScope;
|
|
70
70
|
};
|
|
71
71
|
exports.isCrossBranchEdgeByScope = isCrossBranchEdgeByScope;
|
|
72
|
-
/**
|
|
72
|
+
/** Get a node's branch identity based on scope. null scope = mainline. */
|
|
73
73
|
const getBranchScope = (nodeId, nodeScopes) => nodeScopes.get(nodeId) ?? null;
|
|
74
74
|
exports.getBranchScope = getBranchScope;
|
|
@@ -33,7 +33,7 @@ const loadWorkflowDefinitionWithStorage = (options) => {
|
|
|
33
33
|
throw error;
|
|
34
34
|
}
|
|
35
35
|
const wrapped = new Error("invalid_persisted_workflow_definition");
|
|
36
|
-
wrapped.detail = "workflow
|
|
36
|
+
wrapped.detail = "Failed to parse workflow file, please check JSON format";
|
|
37
37
|
throw wrapped;
|
|
38
38
|
}
|
|
39
39
|
};
|
|
@@ -57,7 +57,7 @@ const normalizeRemoteBatchPlugin = (value) => {
|
|
|
57
57
|
enabled: record.enabled === true,
|
|
58
58
|
url: normalizeNonEmptyString(record.url) ?? DEFAULT_REMOTE_BATCH_URL,
|
|
59
59
|
startBatch: normalizeIntegerInRange(record.startBatch, 1, 1, 1_000_000),
|
|
60
|
-
//
|
|
60
|
+
// Batch size and data source are low-frequency configuration; consolidate into plugin config to avoid too many params piling up on the main panel.
|
|
61
61
|
batchSize: normalizeIntegerInRange(record.batchSize, 5, 1, 1_000),
|
|
62
62
|
sourceField: normalizeNonEmptyString(record.sourceField) ?? "list30",
|
|
63
63
|
};
|
|
@@ -65,14 +65,14 @@ const normalizeRemoteBatchPlugin = (value) => {
|
|
|
65
65
|
const normalizeSchedulerPlugin = (value) => {
|
|
66
66
|
const record = (0, guards_1.isRecord)(value) ? value : {};
|
|
67
67
|
return {
|
|
68
|
-
//
|
|
68
|
+
// The scheduler was historically enabled for all pipelines by default; after plugin-ification, keep it enabled by default so the UI and behavior don't suddenly disappear after upgrade.
|
|
69
69
|
enabled: record.enabled !== false,
|
|
70
70
|
};
|
|
71
71
|
};
|
|
72
72
|
const normalizeWorkflowPlugins = (value) => {
|
|
73
73
|
const record = (0, guards_1.isRecord)(value) ? value : {};
|
|
74
74
|
return {
|
|
75
|
-
//
|
|
75
|
+
// Plugin capabilities are consistent across all pipelines; only record each pipeline's own enabled + parameter configuration here.
|
|
76
76
|
remoteBatch: normalizeRemoteBatchPlugin(record.remoteBatch),
|
|
77
77
|
scheduler: normalizeSchedulerPlugin(record.scheduler),
|
|
78
78
|
};
|
|
@@ -193,7 +193,7 @@ const normalizeWorkflowNode = (value) => {
|
|
|
193
193
|
allowReject: value.allowReject === true,
|
|
194
194
|
maxRejectCount: normalizeIntegerInRange(value.maxRejectCount, 3, 0, 10),
|
|
195
195
|
};
|
|
196
|
-
//
|
|
196
|
+
// Only write branch scope fields when explicitly provided to avoid null values polluting JSON output
|
|
197
197
|
if (branchScopeId)
|
|
198
198
|
node.branchScopeId = branchScopeId;
|
|
199
199
|
if (routeSourceNodeId)
|
|
@@ -224,8 +224,8 @@ const normalizeWorkflowEdgeV3 = (value) => {
|
|
|
224
224
|
const to = normalizeNonEmptyString(value.to);
|
|
225
225
|
if (!from || !to)
|
|
226
226
|
return null;
|
|
227
|
-
// v3 API
|
|
228
|
-
//
|
|
227
|
+
// v3 API closed-loop requirement: when the read API returns runtime when shapes, writing back to /workflow must also be directly accepted.
|
|
228
|
+
// Here, only do "same-version dual-shape" normalization under the version=3.0 contract; do not allow version=2.0 bypass writes.
|
|
229
229
|
if ("when" in value) {
|
|
230
230
|
return {
|
|
231
231
|
from,
|
|
@@ -258,10 +258,10 @@ const normalizeWorkflowGroup = (value) => {
|
|
|
258
258
|
if (!type)
|
|
259
259
|
return null;
|
|
260
260
|
const joinPolicy = "all";
|
|
261
|
-
//
|
|
262
|
-
//
|
|
261
|
+
// Historical any/quorum downgrade: runtime only supports all; silently downgrade to all when reading historical data.
|
|
262
|
+
// When saving a new workflow, validate will explicitly reject any/quorum.
|
|
263
263
|
if (value.joinPolicy === "any" || value.joinPolicy === "quorum") {
|
|
264
|
-
//
|
|
264
|
+
// Silently downgrade — historical data compatibility; new saves will be intercepted by validate
|
|
265
265
|
}
|
|
266
266
|
return {
|
|
267
267
|
id,
|
|
@@ -286,48 +286,48 @@ const readWorkflowDefinitionFromRaw = (value) => {
|
|
|
286
286
|
exports.readWorkflowDefinitionFromRaw = readWorkflowDefinitionFromRaw;
|
|
287
287
|
const readWorkflowDefinitionFromRawDetailed = (value) => {
|
|
288
288
|
if (!(0, guards_1.isRecord)(value)) {
|
|
289
|
-
return { ok: false, error: "invalid_workflow_definition", detail: "
|
|
289
|
+
return { ok: false, error: "invalid_workflow_definition", detail: "Workflow root object has an invalid format" };
|
|
290
290
|
}
|
|
291
291
|
if (value.version === "2.0") {
|
|
292
292
|
return {
|
|
293
293
|
ok: false,
|
|
294
294
|
error: "workflow_migration_required",
|
|
295
|
-
detail: "
|
|
295
|
+
detail: "Workflow v2.0 detected, please run the migration script before writing",
|
|
296
296
|
};
|
|
297
297
|
}
|
|
298
298
|
if (value.version !== "3.0") {
|
|
299
|
-
return { ok: false, error: "invalid_workflow_definition", detail: `workflow.version
|
|
299
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Invalid workflow.version: ${String(value.version ?? "")}` };
|
|
300
300
|
}
|
|
301
301
|
if (!Array.isArray(value.nodes) || !Array.isArray(value.edges) || !Array.isArray(value.groups)) {
|
|
302
|
-
return { ok: false, error: "invalid_workflow_definition", detail: "workflow.nodes/edges/groups
|
|
302
|
+
return { ok: false, error: "invalid_workflow_definition", detail: "workflow.nodes/edges/groups must be arrays" };
|
|
303
303
|
}
|
|
304
304
|
const nodes = [];
|
|
305
305
|
for (const item of value.nodes) {
|
|
306
306
|
const normalized = (0, exports.normalizeWorkflowNode)(item);
|
|
307
307
|
if (!normalized)
|
|
308
|
-
return { ok: false, error: "invalid_workflow_definition", detail: "workflow.nodes
|
|
308
|
+
return { ok: false, error: "invalid_workflow_definition", detail: "workflow.nodes contains an invalid node structure" };
|
|
309
309
|
nodes.push(normalized);
|
|
310
310
|
}
|
|
311
311
|
const edges = [];
|
|
312
312
|
for (const item of value.edges) {
|
|
313
313
|
const normalized = normalizeWorkflowEdgeV3(item);
|
|
314
314
|
if (!normalized)
|
|
315
|
-
return { ok: false, error: "invalid_workflow_definition", detail: "workflow.edges
|
|
315
|
+
return { ok: false, error: "invalid_workflow_definition", detail: "workflow.edges contains an invalid edge structure" };
|
|
316
316
|
edges.push(normalized);
|
|
317
317
|
}
|
|
318
318
|
const groups = [];
|
|
319
319
|
for (const item of value.groups) {
|
|
320
|
-
//
|
|
320
|
+
// Pre-check: explicitly reject unsupported joinPolicy before normalize silently downgrades it
|
|
321
321
|
if ((0, guards_1.isRecord)(item) && (item.joinPolicy === "any" || item.joinPolicy === "quorum")) {
|
|
322
322
|
return {
|
|
323
323
|
ok: false,
|
|
324
324
|
error: "join_policy_not_supported",
|
|
325
|
-
detail:
|
|
325
|
+
detail: `Parallel group "${String(item.id ?? "?")}" has unsupported joinPolicy "${String(item.joinPolicy)}", only "all" is currently supported`,
|
|
326
326
|
};
|
|
327
327
|
}
|
|
328
328
|
const normalized = (0, exports.normalizeWorkflowGroup)(item);
|
|
329
329
|
if (!normalized)
|
|
330
|
-
return { ok: false, error: "invalid_workflow_definition", detail: "workflow.groups
|
|
330
|
+
return { ok: false, error: "invalid_workflow_definition", detail: "workflow.groups contains an invalid parallel group structure" };
|
|
331
331
|
groups.push(normalized);
|
|
332
332
|
}
|
|
333
333
|
const workflow = {
|
|
@@ -5,13 +5,13 @@ const validate_1 = require("./validate");
|
|
|
5
5
|
const toUniqueList = (items) => [...new Set(items)];
|
|
6
6
|
// ====== Workflow → Template nodes (unified, with dedup) ======
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Extract template nodes from WorkflowDefinitionRuntime (dependency-type outgoing edges only).
|
|
9
|
+
* This is the single authoritative implementation of the workflow → template mapping.
|
|
10
10
|
*/
|
|
11
11
|
const workflowToTemplateNodes = (workflow) => {
|
|
12
12
|
const incomingByNodeId = new Map();
|
|
13
13
|
for (const edge of workflow.edges) {
|
|
14
|
-
// template.dependsOn
|
|
14
|
+
// template.dependsOn only expresses dependency edges; route edges belong to routing semantics and cannot be written back as ordinary dependencies.
|
|
15
15
|
if (edge.when !== null)
|
|
16
16
|
continue;
|
|
17
17
|
const prev = incomingByNodeId.get(edge.to) ?? [];
|
|
@@ -8,11 +8,11 @@ const validateWorkflowGraph = (workflow) => {
|
|
|
8
8
|
if (workflow.nodes.length === 0) {
|
|
9
9
|
return workflow.edges.length === 0 && workflow.groups.length === 0
|
|
10
10
|
? { ok: true }
|
|
11
|
-
: { ok: false, error: "invalid_workflow_definition", detail: "
|
|
11
|
+
: { ok: false, error: "invalid_workflow_definition", detail: "Empty workflow cannot contain edges or groups" };
|
|
12
12
|
}
|
|
13
13
|
const nodeIds = new Set(workflow.nodes.map((node) => node.id));
|
|
14
14
|
if (nodeIds.size !== workflow.nodes.length) {
|
|
15
|
-
return { ok: false, error: "invalid_workflow_definition", detail: "workflow.nodes
|
|
15
|
+
return { ok: false, error: "invalid_workflow_definition", detail: "workflow.nodes contains duplicate IDs" };
|
|
16
16
|
}
|
|
17
17
|
const groupIds = new Set(workflow.groups.map((group) => group.id));
|
|
18
18
|
const entityIds = new Set([...nodeIds, ...groupIds]);
|
|
@@ -29,14 +29,14 @@ const validateWorkflowGraph = (workflow) => {
|
|
|
29
29
|
const edgeDedupe = new Set();
|
|
30
30
|
for (const edge of workflow.edges) {
|
|
31
31
|
if (!entityIds.has(edge.from) || !entityIds.has(edge.to)) {
|
|
32
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
32
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Edge references non-existent entity: ${edge.from} -> ${edge.to}` };
|
|
33
33
|
}
|
|
34
34
|
if (edge.from === edge.to) {
|
|
35
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
35
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Self-loop edge detected: ${edge.from} -> ${edge.to}` };
|
|
36
36
|
}
|
|
37
37
|
const key = `${edge.from}|${edge.when ?? ""}|${edge.to}`;
|
|
38
38
|
if (edgeDedupe.has(key)) {
|
|
39
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
39
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Duplicate edge detected: ${edge.from} -> ${edge.to}` };
|
|
40
40
|
}
|
|
41
41
|
edgeDedupe.add(key);
|
|
42
42
|
outgoing.set(edge.from, [...(outgoing.get(edge.from) ?? []), edge.to]);
|
|
@@ -57,15 +57,15 @@ const validateWorkflowGraph = (workflow) => {
|
|
|
57
57
|
const sourceNode = workflow.nodes.find((node) => node.id === sourceId);
|
|
58
58
|
if (sourceNode?.routePolicy)
|
|
59
59
|
continue;
|
|
60
|
-
//
|
|
60
|
+
// Non-routing nodes are still forbidden from mixing dependency and route edges on the same node, to prevent unconditional passthrough leading to double execution.
|
|
61
61
|
return {
|
|
62
62
|
ok: false,
|
|
63
63
|
error: "mixed_outgoing_edge_kinds_forbidden",
|
|
64
|
-
detail:
|
|
64
|
+
detail: `Node ${sourceId} has both dependency and route outgoing edges, which is not allowed`,
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
-
// Phase 2:
|
|
68
|
-
// computeNodeScopes + isCrossBranchEdgeByScope
|
|
67
|
+
// Phase 2: Cross-branch edge detection based on explicit scope.
|
|
68
|
+
// computeNodeScopes + isCrossBranchEdgeByScope use explicit branchScopeId (derived from route edges when missing).
|
|
69
69
|
{
|
|
70
70
|
const explicitScopes = new Map();
|
|
71
71
|
const mergeNodeIds = new Set();
|
|
@@ -73,13 +73,13 @@ const validateWorkflowGraph = (workflow) => {
|
|
|
73
73
|
if (node.branchScopeId != null) {
|
|
74
74
|
explicitScopes.set(node.id, node.branchScopeId);
|
|
75
75
|
}
|
|
76
|
-
// merge
|
|
76
|
+
// merge nodes (dependencyPolicy !== "all") are explicit branch convergence points, accepting dependency edges from different scopes
|
|
77
77
|
if (node.dependencyPolicy && node.dependencyPolicy !== "all") {
|
|
78
78
|
mergeNodeIds.add(node.id);
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
const nodeScopes = (0, branch_rules_1.computeNodeScopes)(workflow.nodes, workflow.edges, explicitScopes);
|
|
82
|
-
//
|
|
82
|
+
// Clear scope for merge nodes to avoid them being misjudged as cross-branch (consistent with buildIndices in workflow-graph.ts)
|
|
83
83
|
for (const nodeId of mergeNodeIds) {
|
|
84
84
|
nodeScopes.set(nodeId, null);
|
|
85
85
|
}
|
|
@@ -88,7 +88,7 @@ const validateWorkflowGraph = (workflow) => {
|
|
|
88
88
|
return {
|
|
89
89
|
ok: false,
|
|
90
90
|
error: "cross_branch_edge_forbidden",
|
|
91
|
-
detail:
|
|
91
|
+
detail: `Cross-branch unconditional edge is not allowed: ${scopeCrossEdges[0].from} -> ${scopeCrossEdges[0].to} (from branch ${nodeScopes.get(scopeCrossEdges[0].from) ?? "main"} -> to branch ${nodeScopes.get(scopeCrossEdges[0].to) ?? "main"}, cross-branch dependency edges require an explicit merge node)`,
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -96,12 +96,12 @@ const validateWorkflowGraph = (workflow) => {
|
|
|
96
96
|
const explicitGroupById = new Map(workflow.groups.map((group) => [group.id, group]));
|
|
97
97
|
for (const group of workflow.groups) {
|
|
98
98
|
if (uniqueGroupIds.has(group.id)) {
|
|
99
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
99
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Duplicate parallel group ID: ${group.id}` };
|
|
100
100
|
}
|
|
101
101
|
uniqueGroupIds.add(group.id);
|
|
102
102
|
for (const member of group.members) {
|
|
103
103
|
if (!nodeIds.has(member)) {
|
|
104
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
104
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Parallel group ${group.id} references non-existent member ${member}` };
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
}
|
|
@@ -111,9 +111,9 @@ const validateWorkflowGraph = (workflow) => {
|
|
|
111
111
|
continue;
|
|
112
112
|
const group = explicitGroupById.get(groupId);
|
|
113
113
|
if (!group)
|
|
114
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
114
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Node ${node.id} references non-existent parallel group ${groupId}` };
|
|
115
115
|
if (!group.members.includes(node.id)) {
|
|
116
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
116
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Node ${node.id} is not a member of its declared parallel group ${groupId}` };
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
for (const group of workflow.groups) {
|
|
@@ -129,20 +129,20 @@ const validateWorkflowGraph = (workflow) => {
|
|
|
129
129
|
if (edge.to === group.id)
|
|
130
130
|
continue;
|
|
131
131
|
if (edge.from === group.id)
|
|
132
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
132
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Parallel group ${group.id} cannot directly connect to member nodes` };
|
|
133
133
|
if (memberSet.has(edge.from))
|
|
134
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
134
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Direct dependencies between members of parallel group ${group.id} are not allowed` };
|
|
135
135
|
if (groupIncoming.has(edge.from))
|
|
136
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
136
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `The entry node of parallel group ${group.id} cannot directly connect to members` };
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
for (const group of workflow.groups) {
|
|
140
|
-
// joinPolicy
|
|
140
|
+
// joinPolicy only supports "all"; any/quorum are not implemented at runtime, explicitly reject on save
|
|
141
141
|
if (group.joinPolicy !== "all") {
|
|
142
142
|
return {
|
|
143
143
|
ok: false,
|
|
144
144
|
error: "join_policy_not_supported",
|
|
145
|
-
detail:
|
|
145
|
+
detail: `Parallel group ${group.id} has unsupported joinPolicy "${group.joinPolicy}", only "all" is currently supported`,
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
}
|
|
@@ -150,24 +150,24 @@ const validateWorkflowGraph = (workflow) => {
|
|
|
150
150
|
if (node.routePolicy) {
|
|
151
151
|
const { allowed } = node.routePolicy;
|
|
152
152
|
if (allowed.length < 2 || allowed.length > 5) {
|
|
153
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
153
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Node ${node.id} has an invalid route set size` };
|
|
154
154
|
}
|
|
155
155
|
if (!allowed.includes(routes_1.MAINLINE_ROUTE_VALUE) || !allowed.includes(routes_1.DEFAULT_BRANCH_ROUTE_VALUE)) {
|
|
156
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
156
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Node ${node.id} must include "yes" and "no" routes when routing is enabled` };
|
|
157
157
|
}
|
|
158
158
|
const outgoingEdges = edgesBySource.get(node.id) ?? [];
|
|
159
159
|
const dependencyEdges = outgoingEdges.filter((edge) => edge.when === null);
|
|
160
160
|
if (dependencyEdges.length > 1) {
|
|
161
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
161
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Node ${node.id} can have at most 1 "yes" mainline dependency edge` };
|
|
162
162
|
}
|
|
163
163
|
const routeEdgeCounts = new Map();
|
|
164
164
|
for (const edge of outgoingEdges.filter((item) => item.when !== null)) {
|
|
165
165
|
routeEdgeCounts.set(edge.when ?? "", (routeEdgeCounts.get(edge.when ?? "") ?? 0) + 1);
|
|
166
166
|
if (edge.when === routes_1.MAINLINE_ROUTE_VALUE) {
|
|
167
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
167
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Node ${node.id} cannot save "yes" as a route edge` };
|
|
168
168
|
}
|
|
169
169
|
if (!allowed.includes(edge.when ?? "")) {
|
|
170
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
170
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Node ${node.id} has an undeclared route edge: ${edge.when}` };
|
|
171
171
|
}
|
|
172
172
|
const targetNode = workflow.nodes.find((candidate) => candidate.id === edge.to);
|
|
173
173
|
const targetGroup = workflow.groups.find((group) => group.id === edge.to);
|
|
@@ -176,17 +176,17 @@ const validateWorkflowGraph = (workflow) => {
|
|
|
176
176
|
: [];
|
|
177
177
|
const isBranchTarget = targetNode?.lane === "branch" || (targetGroupMembers.length > 0 && targetGroupMembers.every((member) => member?.lane === "branch"));
|
|
178
178
|
if (!isBranchTarget) {
|
|
179
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
179
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Node ${node.id} route "${edge.when}" can only target branch nodes or branch parallel groups` };
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
for (const route of allowed.filter((item) => item !== routes_1.MAINLINE_ROUTE_VALUE)) {
|
|
183
183
|
if ((routeEdgeCounts.get(route) ?? 0) !== 1) {
|
|
184
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
184
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Node ${node.id} route "${route}" must have exactly 1 branch target` };
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
if (node.dependencyPolicy !== undefined && node.dependencyPolicy !== "all" && node.dependencyPolicy !== "any") {
|
|
189
|
-
return { ok: false, error: "invalid_workflow_definition", detail:
|
|
189
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `Node ${node.id} has an invalid dependencyPolicy` };
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
const queue = [...[...entityIds].filter((id) => (indegree.get(id) ?? 0) === 0)];
|
|
@@ -203,7 +203,7 @@ const validateWorkflowGraph = (workflow) => {
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
if (visited !== entityIds.size) {
|
|
206
|
-
return { ok: false, error: "invalid_workflow_definition", detail: "
|
|
206
|
+
return { ok: false, error: "invalid_workflow_definition", detail: "Workflow contains a cycle, cannot perform topological sort" };
|
|
207
207
|
}
|
|
208
208
|
return { ok: true };
|
|
209
209
|
};
|
|
@@ -212,25 +212,25 @@ const validateWorkflowOutputConfig = (workflow) => {
|
|
|
212
212
|
const output = workflow.output ?? { mode: "mainline_last", nodeId: null };
|
|
213
213
|
if (workflow.nodes.length === 0) {
|
|
214
214
|
return output.mode === "explicit" && output.nodeId
|
|
215
|
-
? { ok: false, error: "invalid_workflow_output_config", detail: "
|
|
215
|
+
? { ok: false, error: "invalid_workflow_output_config", detail: "Empty workflow cannot specify an output node" }
|
|
216
216
|
: { ok: true };
|
|
217
217
|
}
|
|
218
218
|
if (output.mode === "explicit") {
|
|
219
219
|
if (!output.nodeId) {
|
|
220
|
-
return { ok: false, error: "invalid_workflow_output_config", detail: "mode=explicit
|
|
220
|
+
return { ok: false, error: "invalid_workflow_output_config", detail: "nodeId is required when mode=explicit" };
|
|
221
221
|
}
|
|
222
222
|
const node = workflow.nodes.find((n) => n.id === output.nodeId);
|
|
223
223
|
if (!node) {
|
|
224
|
-
return { ok: false, error: "invalid_workflow_output_config", detail:
|
|
224
|
+
return { ok: false, error: "invalid_workflow_output_config", detail: `Output node ${output.nodeId} does not exist` };
|
|
225
225
|
}
|
|
226
226
|
if (!node.enabled) {
|
|
227
|
-
return { ok: false, error: "invalid_workflow_output_config", detail:
|
|
227
|
+
return { ok: false, error: "invalid_workflow_output_config", detail: `Output node ${output.nodeId} must be enabled` };
|
|
228
228
|
}
|
|
229
229
|
if (node.lane !== "main") {
|
|
230
|
-
return { ok: false, error: "invalid_workflow_output_config", detail:
|
|
230
|
+
return { ok: false, error: "invalid_workflow_output_config", detail: `Output node ${output.nodeId} must be a mainline node` };
|
|
231
231
|
}
|
|
232
232
|
if (node.branchScopeId) {
|
|
233
|
-
return { ok: false, error: "invalid_workflow_output_config", detail:
|
|
233
|
+
return { ok: false, error: "invalid_workflow_output_config", detail: `Output node ${output.nodeId} cannot belong to a branch scope` };
|
|
234
234
|
}
|
|
235
235
|
return { ok: true };
|
|
236
236
|
}
|
|
@@ -239,7 +239,7 @@ const validateWorkflowOutputConfig = (workflow) => {
|
|
|
239
239
|
.filter((n) => n.enabled && n.lane === "main" && !n.branchScopeId && !n.routeSourceNodeId && !n.routeValue)
|
|
240
240
|
.map((n) => n.id));
|
|
241
241
|
if (mainlineNodeIds.size === 0) {
|
|
242
|
-
return { ok: false, error: "invalid_workflow_output_config", detail: "
|
|
242
|
+
return { ok: false, error: "invalid_workflow_output_config", detail: "No available mainline nodes" };
|
|
243
243
|
}
|
|
244
244
|
// Build full adjacency (all nodes, all edges) for reachability DFS
|
|
245
245
|
const allNodeIds = new Set(workflow.nodes.map((n) => n.id));
|
|
@@ -296,13 +296,13 @@ const validateWorkflowOutputConfig = (workflow) => {
|
|
|
296
296
|
? sinkNodes // all candidates are orphans, keep as-is
|
|
297
297
|
: sinkNodes.filter((id) => !orphanIds.has(id)); // exclude orphans
|
|
298
298
|
if (effectiveSinks.length === 0) {
|
|
299
|
-
return { ok: false, error: "invalid_workflow_output_config", detail: "
|
|
299
|
+
return { ok: false, error: "invalid_workflow_output_config", detail: "Cannot derive a unique mainline sink node" };
|
|
300
300
|
}
|
|
301
301
|
if (effectiveSinks.length > 1) {
|
|
302
302
|
return {
|
|
303
303
|
ok: false,
|
|
304
304
|
error: "invalid_workflow_output_config",
|
|
305
|
-
detail:
|
|
305
|
+
detail: `Multiple mainline sink nodes found: ${effectiveSinks.join(", ")}, switch to mode=explicit and specify nodeId`,
|
|
306
306
|
};
|
|
307
307
|
}
|
|
308
308
|
return { ok: true };
|
|
@@ -21,8 +21,8 @@ const buildIndices = (workflow) => {
|
|
|
21
21
|
outgoing.push(edge);
|
|
22
22
|
outgoingEdgesBySource.set(edge.from, outgoing);
|
|
23
23
|
}
|
|
24
|
-
//
|
|
25
|
-
//
|
|
24
|
+
// Compute node branch scope: prefer explicit branchScopeId, derive from route edges when missing.
|
|
25
|
+
// Include both nodes and groups in scope computation so group scopes are correctly propagated.
|
|
26
26
|
const explicitScopes = new Map();
|
|
27
27
|
for (const node of workflow.nodes) {
|
|
28
28
|
if (node.branchScopeId != null) {
|
|
@@ -34,9 +34,9 @@ const buildIndices = (workflow) => {
|
|
|
34
34
|
...workflow.groups.map((g) => ({ id: g.id })),
|
|
35
35
|
];
|
|
36
36
|
const nodeScopes = (0, branch_rules_1.computeNodeScopes)(allEntities, workflow.edges, explicitScopes);
|
|
37
|
-
// explicitScopeIds:
|
|
38
|
-
//
|
|
39
|
-
//
|
|
37
|
+
// explicitScopeIds: nodes that explicitly declare a non-default merge strategy (dependencyPolicy != "all"),
|
|
38
|
+
// they are explicit convergence points for multiple branches and should accept dependency edges from different scopes.
|
|
39
|
+
// Reset their scope to null to avoid cross-branch false positives blocking valid branch merges.
|
|
40
40
|
for (const node of workflow.nodes) {
|
|
41
41
|
if (node.dependencyPolicy && node.dependencyPolicy !== "all") {
|
|
42
42
|
nodeScopes.set(node.id, null);
|
|
@@ -103,14 +103,14 @@ const createWorkflowGraph = (initialWorkflow, initialTemplateNodes) => {
|
|
|
103
103
|
const getParallelGroupByMemberNodeId = (nodeId) => indices.parallelGroupByMemberNodeId.get(nodeId) ?? null;
|
|
104
104
|
const isWorkflowNodeEnabled = (nodeId) => getWorkflowNodeById(nodeId)?.enabled !== false;
|
|
105
105
|
const isGroupId = (id) => indices.groupById.has(id);
|
|
106
|
-
// Phase 2:
|
|
107
|
-
// scope
|
|
106
|
+
// Phase 2: determine branch identity based on scope, replacing incoming-edge shape inference.
|
|
107
|
+
// scope is null → mainline node; scope is non-null → branch node.
|
|
108
108
|
const isBranchNode = (nodeId) => {
|
|
109
109
|
const scope = indices.nodeScopes.get(nodeId);
|
|
110
110
|
return scope != null;
|
|
111
111
|
};
|
|
112
|
-
// Phase 2:
|
|
113
|
-
//
|
|
112
|
+
// Phase 2: determine cross-branch edges based on scope, replacing incoming-edge shape inference.
|
|
113
|
+
// The old version would miss detection when B1→B2 unconditional edges exist because B2 was no longer "purely branch"; the new version is unaffected.
|
|
114
114
|
const isCrossBranchEdge = (edge) => (0, branch_rules_1.isCrossBranchEdgeByScope)(edge, indices.nodeScopes);
|
|
115
115
|
const getNodeScope = (nodeId) => indices.nodeScopes.get(nodeId) ?? null;
|
|
116
116
|
const getNodesWithWorkflowMeta = (nodes) => nodes.map((node) => {
|
|
@@ -126,7 +126,7 @@ const createWorkflowGraph = (initialWorkflow, initialTemplateNodes) => {
|
|
|
126
126
|
const current = new Map((run.groups ?? []).map((group) => [group.id, group]));
|
|
127
127
|
run.groups = indices.groups.map((group) => ({
|
|
128
128
|
id: group.id,
|
|
129
|
-
title:
|
|
129
|
+
title: `Group ${group.id}`,
|
|
130
130
|
status: current.get(group.id)?.status ?? "blocked",
|
|
131
131
|
members: group.members,
|
|
132
132
|
joinPolicy: group.joinPolicy,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createApiHandler = void 0;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
// Static file MIME type map
|
|
7
|
+
const MIME = {
|
|
8
|
+
".html": "text/html; charset=utf-8",
|
|
9
|
+
".css": "text/css; charset=utf-8",
|
|
10
|
+
".js": "application/javascript; charset=utf-8",
|
|
11
|
+
".json": "application/json; charset=utf-8",
|
|
12
|
+
".svg": "image/svg+xml",
|
|
13
|
+
".png": "image/png",
|
|
14
|
+
".ico": "image/x-icon",
|
|
15
|
+
".woff": "font/woff",
|
|
16
|
+
".woff2": "font/woff2",
|
|
17
|
+
};
|
|
18
|
+
// Resolve the web static assets directory
|
|
19
|
+
const resolveWebDist = () => (0, node_path_1.join)(__dirname, "..", "..", "..", "web", "dist");
|
|
20
|
+
const webDistRoot = resolveWebDist();
|
|
21
|
+
// Static file serving (SPA falls back to index.html)
|
|
22
|
+
const serveStatic = (req, res) => {
|
|
23
|
+
if (!(0, node_fs_1.existsSync)(webDistRoot))
|
|
24
|
+
return false;
|
|
25
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
26
|
+
let filePath = (0, node_path_1.join)(webDistRoot, url.pathname === "/" ? "index.html" : url.pathname);
|
|
27
|
+
if (!(0, node_fs_1.existsSync)(filePath) || !filePath.startsWith(webDistRoot)) {
|
|
28
|
+
filePath = (0, node_path_1.join)(webDistRoot, "index.html");
|
|
29
|
+
}
|
|
30
|
+
if (!(0, node_fs_1.existsSync)(filePath))
|
|
31
|
+
return false;
|
|
32
|
+
const ext = (0, node_path_1.extname)(filePath);
|
|
33
|
+
const contentType = MIME[ext] ?? "application/octet-stream";
|
|
34
|
+
res.writeHead(200, {
|
|
35
|
+
"Content-Type": contentType,
|
|
36
|
+
"Cache-Control": ext === ".html" ? "no-cache" : "public, max-age=3600",
|
|
37
|
+
});
|
|
38
|
+
(0, node_fs_1.createReadStream)(filePath).pipe(res);
|
|
39
|
+
return true;
|
|
40
|
+
};
|
|
41
|
+
// Create HTTP request handler (health check + CORS + static files)
|
|
42
|
+
const createApiHandler = (options) => {
|
|
43
|
+
return (req, res) => {
|
|
44
|
+
const url = req.url ?? "/";
|
|
45
|
+
// /api/health — CLI server lifecycle check
|
|
46
|
+
if (req.method === "GET" && url === "/api/health") {
|
|
47
|
+
res.writeHead(200, {
|
|
48
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
49
|
+
"Access-Control-Allow-Origin": options.webOrigin,
|
|
50
|
+
});
|
|
51
|
+
res.end(JSON.stringify({ ok: true, ...options.serverRuntimeIdentity }));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// OPTIONS — CORS preflight
|
|
55
|
+
if (req.method === "OPTIONS") {
|
|
56
|
+
res.writeHead(204, {
|
|
57
|
+
"Access-Control-Allow-Origin": options.webOrigin,
|
|
58
|
+
"Access-Control-Allow-Methods": "GET,POST,PATCH,DELETE,OPTIONS",
|
|
59
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
60
|
+
});
|
|
61
|
+
res.end();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Static SPA files (falls back to index.html)
|
|
65
|
+
if (serveStatic(req, res))
|
|
66
|
+
return;
|
|
67
|
+
res.writeHead(404, {
|
|
68
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
69
|
+
"Access-Control-Allow-Origin": options.webOrigin,
|
|
70
|
+
});
|
|
71
|
+
res.end(JSON.stringify({ error: "not_found" }));
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
exports.createApiHandler = createApiHandler;
|
|
@@ -38,14 +38,14 @@ const createAgentService = (app) => {
|
|
|
38
38
|
await (0, gateway_read_helpers_1.ensureGatewayReadyForReadonly)(app);
|
|
39
39
|
const payload = await app.gateway.client.sendReq("agents.list");
|
|
40
40
|
const rawItems = app.gateway.pickArray(payload);
|
|
41
|
-
//
|
|
41
|
+
// Try refreshing session activity first; fall back to cache on failure to keep read-only queries available.
|
|
42
42
|
let sessionItems = app.gateway.getSessionCache();
|
|
43
43
|
try {
|
|
44
44
|
const refreshed = await app.gateway.refreshSessionsFromGateway();
|
|
45
45
|
sessionItems = refreshed.items;
|
|
46
46
|
}
|
|
47
47
|
catch {
|
|
48
|
-
//
|
|
48
|
+
// Read-only query resilience: session refresh failure must not block agents.list output.
|
|
49
49
|
}
|
|
50
50
|
const lastActiveByAgentId = new Map();
|
|
51
51
|
for (const session of sessionItems) {
|