@united-workforce/cli 0.3.0 → 0.5.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/README.md +45 -11
- package/dist/.build-fingerprint +1 -0
- package/dist/__tests__/adapter-json-roundtrip.test.js +17 -7
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
- package/dist/__tests__/agent-resolution-llm-free.test.d.ts +2 -0
- package/dist/__tests__/agent-resolution-llm-free.test.d.ts.map +1 -0
- package/dist/__tests__/agent-resolution-llm-free.test.js +30 -0
- package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -0
- package/dist/__tests__/build-step-entry.test.d.ts +2 -0
- package/dist/__tests__/build-step-entry.test.d.ts.map +1 -0
- package/dist/__tests__/build-step-entry.test.js +173 -0
- package/dist/__tests__/build-step-entry.test.js.map +1 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.d.ts +2 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.d.ts.map +1 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.js +93 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.js.map +1 -0
- package/dist/__tests__/concurrency.test.d.ts +2 -0
- package/dist/__tests__/concurrency.test.d.ts.map +1 -0
- package/dist/__tests__/concurrency.test.js +196 -0
- package/dist/__tests__/concurrency.test.js.map +1 -0
- package/dist/__tests__/config.test.js +26 -302
- package/dist/__tests__/config.test.js.map +1 -1
- package/dist/__tests__/current-role.test.js +7 -6
- package/dist/__tests__/current-role.test.js.map +1 -1
- package/dist/__tests__/e2e-mock-agent.test.js +43 -30
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
- package/dist/__tests__/format-text-default.test.d.ts +2 -0
- package/dist/__tests__/format-text-default.test.d.ts.map +1 -0
- package/dist/__tests__/format-text-default.test.js +43 -0
- package/dist/__tests__/format-text-default.test.js.map +1 -0
- package/dist/__tests__/format-text-registry.test.d.ts +2 -0
- package/dist/__tests__/format-text-registry.test.d.ts.map +1 -0
- package/dist/__tests__/format-text-registry.test.js +158 -0
- package/dist/__tests__/format-text-registry.test.js.map +1 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts +2 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts.map +1 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.js +40 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.js.map +1 -0
- package/dist/__tests__/log-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/log-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/log-text-renderer.test.js +265 -0
- package/dist/__tests__/log-text-renderer.test.js.map +1 -0
- package/dist/__tests__/moderator-evaluate.test.js +9 -50
- package/dist/__tests__/moderator-evaluate.test.js.map +1 -1
- package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts +2 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts.map +1 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.js +102 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.js.map +1 -0
- package/dist/__tests__/output-mapper-workflow-add.test.d.ts +2 -0
- package/dist/__tests__/output-mapper-workflow-add.test.d.ts.map +1 -0
- package/dist/__tests__/output-mapper-workflow-add.test.js +22 -0
- package/dist/__tests__/output-mapper-workflow-add.test.js.map +1 -0
- package/dist/__tests__/pid-recycling.test.d.ts +2 -0
- package/dist/__tests__/pid-recycling.test.d.ts.map +1 -0
- package/dist/__tests__/pid-recycling.test.js +273 -0
- package/dist/__tests__/pid-recycling.test.js.map +1 -0
- package/dist/__tests__/prompt.test.js +365 -2
- package/dist/__tests__/prompt.test.js.map +1 -1
- package/dist/__tests__/resolve-head-hash.test.js +12 -4
- package/dist/__tests__/resolve-head-hash.test.js.map +1 -1
- package/dist/__tests__/setup-agent-discovery.test.js +21 -30
- package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
- package/dist/__tests__/setup-complexity.test.js +2 -168
- package/dist/__tests__/setup-complexity.test.js.map +1 -1
- package/dist/__tests__/setup-no-llm.test.d.ts +2 -0
- package/dist/__tests__/setup-no-llm.test.d.ts.map +1 -0
- package/dist/__tests__/setup-no-llm.test.js +52 -0
- package/dist/__tests__/setup-no-llm.test.js.map +1 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.js +27 -28
- package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -1
- package/dist/__tests__/step-ask.test.d.ts +2 -0
- package/dist/__tests__/step-ask.test.d.ts.map +1 -0
- package/dist/__tests__/step-ask.test.js +507 -0
- package/dist/__tests__/step-ask.test.js.map +1 -0
- package/dist/__tests__/step-show-json.test.js +1 -0
- package/dist/__tests__/step-show-json.test.js.map +1 -1
- package/dist/__tests__/step-timing.test.js +2 -0
- package/dist/__tests__/step-timing.test.js.map +1 -1
- package/dist/__tests__/store-global-cas.test.js +2 -2
- package/dist/__tests__/store-global-cas.test.js.map +1 -1
- package/dist/__tests__/store-unified-threads.test.js +28 -26
- package/dist/__tests__/store-unified-threads.test.js.map +1 -1
- package/dist/__tests__/thread-cancel-status.test.js +25 -19
- package/dist/__tests__/thread-cancel-status.test.js.map +1 -1
- package/dist/__tests__/thread-cancel-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.js +110 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.js.map +1 -0
- package/dist/__tests__/thread-list-filters.test.js +354 -17
- package/dist/__tests__/thread-list-filters.test.js.map +1 -1
- package/dist/__tests__/thread-list-template-ms-date.test.d.ts +2 -0
- package/dist/__tests__/thread-list-template-ms-date.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-template-ms-date.test.js +102 -0
- package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts +2 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.js +157 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.js.map +1 -0
- package/dist/__tests__/thread-poke.test.d.ts +2 -0
- package/dist/__tests__/thread-poke.test.d.ts.map +1 -0
- package/dist/__tests__/thread-poke.test.js +422 -0
- package/dist/__tests__/thread-poke.test.js.map +1 -0
- package/dist/__tests__/thread-read-xml-tags.test.js +10 -9
- package/dist/__tests__/thread-read-xml-tags.test.js.map +1 -1
- package/dist/__tests__/thread-resume.test.js +21 -15
- package/dist/__tests__/thread-resume.test.js.map +1 -1
- package/dist/__tests__/thread-show-status.test.js +17 -28
- package/dist/__tests__/thread-show-status.test.js.map +1 -1
- package/dist/__tests__/thread-start-cwd-cli.test.js +15 -3
- package/dist/__tests__/thread-start-cwd-cli.test.js.map +1 -1
- package/dist/__tests__/thread-stop-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/thread-stop-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/thread-stop-text-renderer.test.js +148 -0
- package/dist/__tests__/thread-stop-text-renderer.test.js.map +1 -0
- package/dist/__tests__/thread-suspend-step.test.js +13 -16
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
- package/dist/__tests__/thread-suspended-display.test.js +10 -22
- package/dist/__tests__/thread-suspended-display.test.js.map +1 -1
- package/dist/__tests__/thread-test-helpers.d.ts +7 -0
- package/dist/__tests__/thread-test-helpers.d.ts.map +1 -1
- package/dist/__tests__/thread-test-helpers.js +13 -0
- package/dist/__tests__/thread-test-helpers.js.map +1 -1
- package/dist/__tests__/thread.test.js +15 -13
- package/dist/__tests__/thread.test.js.map +1 -1
- package/dist/__tests__/validate-semantic.test.js +105 -23
- package/dist/__tests__/validate-semantic.test.js.map +1 -1
- package/dist/__tests__/workflow-list-recursive.test.d.ts +2 -0
- package/dist/__tests__/workflow-list-recursive.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-list-recursive.test.js +286 -0
- package/dist/__tests__/workflow-list-recursive.test.js.map +1 -0
- package/dist/__tests__/workflow-resolution.test.js +46 -28
- package/dist/__tests__/workflow-resolution.test.js.map +1 -1
- package/dist/__tests__/workflow-show-resolution.test.d.ts +2 -0
- package/dist/__tests__/workflow-show-resolution.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-show-resolution.test.js +213 -0
- package/dist/__tests__/workflow-show-resolution.test.js.map +1 -0
- package/dist/__tests__/workflow-validate.test.d.ts +2 -0
- package/dist/__tests__/workflow-validate.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-validate.test.js +707 -0
- package/dist/__tests__/workflow-validate.test.js.map +1 -0
- package/dist/__tests__/write-envelope.test.d.ts +2 -0
- package/dist/__tests__/write-envelope.test.d.ts.map +1 -0
- package/dist/__tests__/write-envelope.test.js +201 -0
- package/dist/__tests__/write-envelope.test.js.map +1 -0
- package/dist/background/background.d.ts +22 -1
- package/dist/background/background.d.ts.map +1 -1
- package/dist/background/background.js +83 -6
- package/dist/background/background.js.map +1 -1
- package/dist/background/index.d.ts +1 -1
- package/dist/background/index.d.ts.map +1 -1
- package/dist/background/index.js +1 -1
- package/dist/background/index.js.map +1 -1
- package/dist/background/types.d.ts +1 -0
- package/dist/background/types.d.ts.map +1 -1
- package/dist/cli.js +120 -62
- package/dist/cli.js.map +1 -1
- package/dist/commands/config.d.ts +3 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +17 -31
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +57 -31
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/setup.d.ts +12 -39
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +72 -303
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/step.d.ts +44 -1
- package/dist/commands/step.d.ts.map +1 -1
- package/dist/commands/step.js +255 -11
- package/dist/commands/step.js.map +1 -1
- package/dist/commands/thread.d.ts +16 -3
- package/dist/commands/thread.d.ts.map +1 -1
- package/dist/commands/thread.js +423 -142
- package/dist/commands/thread.js.map +1 -1
- package/dist/commands/workflow.d.ts +9 -1
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +126 -6
- package/dist/commands/workflow.js.map +1 -1
- package/dist/concurrency/concurrency.d.ts +34 -0
- package/dist/concurrency/concurrency.d.ts.map +1 -0
- package/dist/concurrency/concurrency.js +216 -0
- package/dist/concurrency/concurrency.js.map +1 -0
- package/dist/concurrency/index.d.ts +3 -0
- package/dist/concurrency/index.d.ts.map +1 -0
- package/dist/concurrency/index.js +2 -0
- package/dist/concurrency/index.js.map +1 -0
- package/dist/concurrency/types.d.ts +19 -0
- package/dist/concurrency/types.d.ts.map +1 -0
- package/dist/concurrency/types.js +2 -0
- package/dist/concurrency/types.js.map +1 -0
- package/dist/format.d.ts +69 -2
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +198 -1
- package/dist/format.js.map +1 -1
- package/dist/moderator/__tests__/evaluate.test.js +31 -17
- package/dist/moderator/__tests__/evaluate.test.js.map +1 -1
- package/dist/moderator/evaluate.d.ts.map +1 -1
- package/dist/moderator/evaluate.js +4 -16
- package/dist/moderator/evaluate.js.map +1 -1
- package/dist/moderator/index.d.ts +1 -2
- package/dist/moderator/index.d.ts.map +1 -1
- package/dist/moderator/index.js +0 -1
- package/dist/moderator/index.js.map +1 -1
- package/dist/moderator/types.d.ts +6 -10
- package/dist/moderator/types.d.ts.map +1 -1
- package/dist/moderator/types.js +1 -3
- package/dist/moderator/types.js.map +1 -1
- package/dist/output-mappers.d.ts +122 -0
- package/dist/output-mappers.d.ts.map +1 -0
- package/dist/output-mappers.js +134 -0
- package/dist/output-mappers.js.map +1 -0
- package/dist/schemas.d.ts +6 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +34 -5
- package/dist/schemas.js.map +1 -1
- package/dist/store.d.ts +28 -9
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +75 -16
- package/dist/store.js.map +1 -1
- package/dist/text-renderers.d.ts +30 -0
- package/dist/text-renderers.d.ts.map +1 -0
- package/dist/text-renderers.js +251 -0
- package/dist/text-renderers.js.map +1 -0
- package/dist/validate-semantic.d.ts.map +1 -1
- package/dist/validate-semantic.js +95 -61
- package/dist/validate-semantic.js.map +1 -1
- package/dist/validate.d.ts +6 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +24 -0
- package/dist/validate.js.map +1 -1
- package/examples/brainstorm.yaml +130 -0
- package/examples/debate.yaml +169 -0
- package/examples/socratic-questioning.yaml +112 -0
- package/package.json +9 -10
- package/src/__tests__/adapter-json-roundtrip.test.ts +16 -7
- package/src/__tests__/agent-resolution-llm-free.test.ts +39 -0
- package/src/__tests__/build-step-entry.test.ts +203 -0
- package/src/__tests__/clear-thread-failed-attempts.test.ts +122 -0
- package/src/__tests__/concurrency.test.ts +266 -0
- package/src/__tests__/config.test.ts +33 -321
- package/src/__tests__/current-role.test.ts +7 -6
- package/src/__tests__/e2e-mock-agent.test.ts +65 -30
- package/src/__tests__/fixtures/e2e-count.workflow.yaml +1 -0
- package/src/__tests__/fixtures/e2e-linear.workflow.yaml +1 -0
- package/src/__tests__/fixtures/{e2e-mustache.workflow.yaml → e2e-liquid.workflow.yaml} +3 -2
- package/src/__tests__/fixtures/e2e-loop.workflow.yaml +1 -0
- package/src/__tests__/fixtures/e2e-suspend.mock.yaml +2 -2
- package/src/__tests__/fixtures/e2e-suspend.workflow.yaml +6 -10
- package/src/__tests__/format-text-default.test.ts +49 -0
- package/src/__tests__/format-text-registry.test.ts +173 -0
- package/src/__tests__/issue-180-workflow-ref-removed.test.ts +43 -0
- package/src/__tests__/log-text-renderer.test.ts +294 -0
- package/src/__tests__/moderator-evaluate.test.ts +9 -52
- package/src/__tests__/output-mapper-thread-list-startedat.test.ts +124 -0
- package/src/__tests__/output-mapper-workflow-add.test.ts +24 -0
- package/src/__tests__/pid-recycling.test.ts +329 -0
- package/src/__tests__/prompt.test.ts +443 -2
- package/src/__tests__/resolve-head-hash.test.ts +11 -4
- package/src/__tests__/setup-agent-discovery.test.ts +26 -51
- package/src/__tests__/setup-complexity.test.ts +1 -203
- package/src/__tests__/setup-no-llm.test.ts +68 -0
- package/src/__tests__/solve-issue-tea-worktree.test.ts +27 -31
- package/src/__tests__/step-ask.test.ts +677 -0
- package/src/__tests__/step-show-json.test.ts +1 -0
- package/src/__tests__/step-timing.test.ts +2 -0
- package/src/__tests__/store-global-cas.test.ts +2 -2
- package/src/__tests__/store-unified-threads.test.ts +30 -27
- package/src/__tests__/thread-cancel-status.test.ts +27 -20
- package/src/__tests__/thread-cancel-text-renderer.test.ts +125 -0
- package/src/__tests__/thread-list-filters.test.ts +443 -17
- package/src/__tests__/thread-list-template-ms-date.test.ts +110 -0
- package/src/__tests__/thread-list-workflow-corrupt.test.ts +198 -0
- package/src/__tests__/thread-poke.test.ts +554 -0
- package/src/__tests__/thread-read-xml-tags.test.ts +9 -11
- package/src/__tests__/thread-resume.test.ts +20 -15
- package/src/__tests__/thread-show-status.test.ts +17 -29
- package/src/__tests__/thread-start-cwd-cli.test.ts +15 -3
- package/src/__tests__/thread-stop-text-renderer.test.ts +168 -0
- package/src/__tests__/thread-suspend-step.test.ts +13 -16
- package/src/__tests__/thread-suspended-display.test.ts +10 -22
- package/src/__tests__/thread-test-helpers.ts +15 -1
- package/src/__tests__/thread.test.ts +14 -14
- package/src/__tests__/validate-semantic.test.ts +118 -33
- package/src/__tests__/workflow-list-recursive.test.ts +370 -0
- package/src/__tests__/workflow-resolution.test.ts +48 -29
- package/src/__tests__/workflow-show-resolution.test.ts +286 -0
- package/src/__tests__/workflow-validate.test.ts +828 -0
- package/src/__tests__/write-envelope.test.ts +257 -0
- package/src/background/background.ts +88 -6
- package/src/background/index.ts +2 -0
- package/src/background/types.ts +1 -0
- package/src/cli.ts +184 -77
- package/src/commands/config.ts +16 -33
- package/src/commands/prompt.ts +57 -31
- package/src/commands/setup.ts +80 -358
- package/src/commands/step.ts +339 -12
- package/src/commands/thread.ts +511 -171
- package/src/commands/workflow.ts +155 -4
- package/src/concurrency/concurrency.ts +245 -0
- package/src/concurrency/index.ts +10 -0
- package/src/concurrency/types.ts +19 -0
- package/src/format.ts +282 -2
- package/src/moderator/__tests__/evaluate.test.ts +34 -17
- package/src/moderator/evaluate.ts +5 -17
- package/src/moderator/index.ts +1 -6
- package/src/moderator/types.ts +6 -14
- package/src/output-mappers.ts +254 -0
- package/src/schemas.ts +51 -5
- package/src/store.ts +86 -20
- package/src/text-renderers.ts +355 -0
- package/src/validate-semantic.ts +125 -73
- package/src/validate.ts +27 -0
- package/dist/__tests__/setup-validate.test.d.ts +0 -2
- package/dist/__tests__/setup-validate.test.d.ts.map +0 -1
- package/dist/__tests__/setup-validate.test.js +0 -108
- package/dist/__tests__/setup-validate.test.js.map +0 -1
- package/src/__tests__/setup-validate.test.ts +0 -148
- /package/src/__tests__/fixtures/{e2e-mustache.mock.yaml → e2e-liquid.mock.yaml} +0 -0
package/dist/commands/thread.js
CHANGED
|
@@ -1,73 +1,71 @@
|
|
|
1
1
|
import { execFileSync, spawn } from "node:child_process";
|
|
2
2
|
import { access, readFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, isAbsolute, resolve as resolvePath } from "node:path";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
4
4
|
import { validate } from "@ocas/core";
|
|
5
|
-
import { createThreadIndexEntry, markThreadSuspended, updateThreadHead, } from "@united-workforce/protocol";
|
|
5
|
+
import { createThreadIndexEntry, markThreadSuspended, SUSPEND_STATUS, updateThreadHead, } from "@united-workforce/protocol";
|
|
6
6
|
import { createProcessLogger, extractUlidTimestamp, generateUlid, } from "@united-workforce/util";
|
|
7
7
|
import { getEnvPath, loadWorkflowConfig } from "@united-workforce/util-agent";
|
|
8
8
|
import { config as loadDotenv } from "dotenv";
|
|
9
9
|
import { parse } from "yaml";
|
|
10
|
-
import { createMarker, deleteMarker, isThreadRunning } from "../background/index.js";
|
|
10
|
+
import { createMarker, deleteMarker, getProcessStartTime, isMarkerValid, isThreadRunning, readMarker, } from "../background/index.js";
|
|
11
|
+
import { acquireSlot, DEFAULT_MAX_RUNNING, installSlotCleanup } from "../concurrency/index.js";
|
|
11
12
|
import { createIncludeTag } from "../include.js";
|
|
12
|
-
import { evaluate
|
|
13
|
-
import { completeThread, createUwfStore, getThread, loadActiveThreads, loadHistoryThreads, loadWorkflowRegistry, resolveWorkflowHash, setThread, } from "../store.js";
|
|
13
|
+
import { evaluate } from "../moderator/index.js";
|
|
14
|
+
import { completeThread, createUwfStore, findRegistryName, getThread, loadActiveThreads, loadHistoryThreads, loadWorkflowRegistry, resolveWorkflowHash, setThread, } from "../store.js";
|
|
14
15
|
import { checkWorkflowFilenameConsistency, isCasRef, parseWorkflowPayload } from "../validate.js";
|
|
15
16
|
import { validateWorkflow } from "../validate-semantic.js";
|
|
17
|
+
import { getConfigPath, getNestedValue, loadConfig, parseDotPath } from "./config.js";
|
|
16
18
|
import { collectOrderedSteps, expandOutput, fail, walkChain, } from "./shared.js";
|
|
17
19
|
import { materializeWorkflowPayload } from "./workflow.js";
|
|
18
20
|
const END_ROLE = "$END";
|
|
19
21
|
const START_ROLE = "$START";
|
|
20
22
|
export const THREAD_READ_DEFAULT_QUOTA = 4000;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
suspendMessage = evaluation.value.prompt;
|
|
30
|
-
}
|
|
31
|
-
else if (evaluation.value.role !== END_ROLE) {
|
|
32
|
-
currentRole = evaluation.value.role;
|
|
33
|
-
}
|
|
23
|
+
/**
|
|
24
|
+
* Read the suspend reason from an agent output if it is an engine-level suspend
|
|
25
|
+
* (coroutine yield). Returns the reason string when `$status === "$SUSPEND"`,
|
|
26
|
+
* or `null` otherwise. A suspend output with no `reason` yields an empty string.
|
|
27
|
+
*/
|
|
28
|
+
function readSuspendReason(lastOutput) {
|
|
29
|
+
if (lastOutput[STATUS_KEY] !== SUSPEND_STATUS) {
|
|
30
|
+
return null;
|
|
34
31
|
}
|
|
32
|
+
const reason = lastOutput.reason;
|
|
33
|
+
return typeof reason === "string" ? reason : "";
|
|
34
|
+
}
|
|
35
|
+
function buildSuspendStepOutput(workflowHash, threadId, head, suspendedRole, suspendMessage) {
|
|
35
36
|
return {
|
|
36
37
|
workflow: workflowHash,
|
|
37
38
|
thread: threadId,
|
|
38
39
|
head,
|
|
39
|
-
status,
|
|
40
|
-
currentRole,
|
|
40
|
+
status: "suspended",
|
|
41
|
+
currentRole: null,
|
|
41
42
|
suspendedRole,
|
|
42
43
|
suspendMessage,
|
|
43
|
-
done,
|
|
44
|
-
background,
|
|
44
|
+
done: false,
|
|
45
|
+
background: null,
|
|
46
|
+
error: null,
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
|
-
function
|
|
49
|
+
function resolveSuspendFieldsFromOutput(uwf, head) {
|
|
48
50
|
const chain = walkChain(uwf, head);
|
|
49
51
|
const { lastRole, lastOutput } = resolveEvaluateArgs(uwf, chain);
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
suspendedRole: result.value.suspendedRole,
|
|
55
|
-
suspendMessage: result.value.prompt,
|
|
56
|
-
};
|
|
52
|
+
const reason = readSuspendReason(lastOutput);
|
|
53
|
+
if (reason !== null) {
|
|
54
|
+
return { suspendedRole: lastRole, suspendMessage: reason };
|
|
57
55
|
}
|
|
58
56
|
return { suspendedRole: null, suspendMessage: null };
|
|
59
57
|
}
|
|
60
|
-
function resolveSuspendFieldsForShow(entry, status, uwf, head
|
|
58
|
+
function resolveSuspendFieldsForShow(entry, status, uwf, head) {
|
|
61
59
|
if (status !== "suspended") {
|
|
62
60
|
return { suspendedRole: null, suspendMessage: null };
|
|
63
61
|
}
|
|
64
62
|
if (entry.suspendedRole !== null && entry.suspendMessage !== null) {
|
|
65
63
|
return { suspendedRole: entry.suspendedRole, suspendMessage: entry.suspendMessage };
|
|
66
64
|
}
|
|
67
|
-
const
|
|
65
|
+
const fromOutput = resolveSuspendFieldsFromOutput(uwf, head);
|
|
68
66
|
return {
|
|
69
|
-
suspendedRole: entry.suspendedRole ??
|
|
70
|
-
suspendMessage: entry.suspendMessage ??
|
|
67
|
+
suspendedRole: entry.suspendedRole ?? fromOutput.suspendedRole,
|
|
68
|
+
suspendMessage: entry.suspendMessage ?? fromOutput.suspendMessage,
|
|
71
69
|
};
|
|
72
70
|
}
|
|
73
71
|
async function ensureThreadSuspendMetadata(varStore, threadId, entry, suspendedRole, suspendMessage) {
|
|
@@ -78,16 +76,14 @@ async function ensureThreadSuspendMetadata(varStore, threadId, entry, suspendedR
|
|
|
78
76
|
setThread(varStore, threadId, updated);
|
|
79
77
|
return updated;
|
|
80
78
|
}
|
|
81
|
-
async function resolveActiveThreadStatus(storageRoot, threadId, uwf, head
|
|
79
|
+
async function resolveActiveThreadStatus(storageRoot, threadId, uwf, head) {
|
|
82
80
|
const runningMarker = await isThreadRunning(storageRoot, threadId);
|
|
83
81
|
if (runningMarker !== null) {
|
|
84
82
|
return "running";
|
|
85
83
|
}
|
|
86
84
|
const chain = walkChain(uwf, head);
|
|
87
|
-
const {
|
|
88
|
-
|
|
89
|
-
const result = evaluate(workflow.graph, lastRole, lastOutput);
|
|
90
|
-
if (result.ok && isSuspendResult(result.value)) {
|
|
85
|
+
const { lastOutput } = resolveEvaluateArgs(uwf, chain);
|
|
86
|
+
if (readSuspendReason(lastOutput) !== null) {
|
|
91
87
|
return "suspended";
|
|
92
88
|
}
|
|
93
89
|
return "idle";
|
|
@@ -99,12 +95,15 @@ async function resolveActiveThreadStatus(storageRoot, threadId, uwf, head, workf
|
|
|
99
95
|
function resolveCurrentRole(uwf, head, workflowRef) {
|
|
100
96
|
const chain = walkChain(uwf, head);
|
|
101
97
|
const { lastRole, lastOutput } = resolveEvaluateArgs(uwf, chain);
|
|
98
|
+
if (readSuspendReason(lastOutput) !== null) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
102
101
|
const workflow = loadWorkflowPayload(uwf, workflowRef);
|
|
103
102
|
const result = evaluate(workflow.graph, lastRole, lastOutput);
|
|
104
103
|
if (!result.ok) {
|
|
105
104
|
return null;
|
|
106
105
|
}
|
|
107
|
-
if (
|
|
106
|
+
if (result.value.role === END_ROLE) {
|
|
108
107
|
return null;
|
|
109
108
|
}
|
|
110
109
|
return result.value.role;
|
|
@@ -113,10 +112,12 @@ const PL_THREAD_START = "7HNQ4B2X";
|
|
|
113
112
|
const PL_MODERATOR = "M3K8V9T1";
|
|
114
113
|
const PL_AGENT_SPAWN = "R5J2W8N4";
|
|
115
114
|
const PL_AGENT_DONE = "C6P9E3H7";
|
|
115
|
+
const PL_AGENT_ERROR = "Z3F7K8M2";
|
|
116
116
|
const PL_THREAD_ARCHIVED = "F4D8Q2K5";
|
|
117
117
|
const PL_STEP_ERROR = "B8T5N1V6";
|
|
118
118
|
const PL_BACKGROUND_START = "X7Q4W9M2";
|
|
119
119
|
const PL_THREAD_RESUME = "K2R7M4N8";
|
|
120
|
+
const PL_THREAD_POKE = "P4Q9R3X7";
|
|
120
121
|
function buildResumePrompt(graphPrompt, supplement) {
|
|
121
122
|
if (supplement === null || supplement === "") {
|
|
122
123
|
return graphPrompt;
|
|
@@ -147,18 +148,19 @@ async function workflowFileExists(dir, name, ext) {
|
|
|
147
148
|
}
|
|
148
149
|
}
|
|
149
150
|
/**
|
|
150
|
-
* Search for a workflow file in a given directory (checks both .
|
|
151
|
+
* Search for a workflow file in a given directory (checks both .workflows/ and .workflow/).
|
|
152
|
+
* `.workflows/` (primary) takes priority over `.workflow/` (legacy fallback).
|
|
151
153
|
*/
|
|
152
154
|
async function findWorkflowInDir(dir, name) {
|
|
153
|
-
// Check .
|
|
155
|
+
// Check .workflows/ directory first (primary)
|
|
154
156
|
for (const ext of [".yaml", ".yml"]) {
|
|
155
|
-
const result = await workflowFileExists(resolvePath(dir, ".
|
|
157
|
+
const result = await workflowFileExists(resolvePath(dir, ".workflows"), name, ext);
|
|
156
158
|
if (result !== null) {
|
|
157
159
|
return result;
|
|
158
160
|
}
|
|
159
161
|
}
|
|
160
162
|
for (const indexName of ["index.yaml", "index.yml"]) {
|
|
161
|
-
const candidate = resolvePath(dir, ".
|
|
163
|
+
const candidate = resolvePath(dir, ".workflows", name, indexName);
|
|
162
164
|
try {
|
|
163
165
|
await access(candidate);
|
|
164
166
|
return candidate;
|
|
@@ -167,15 +169,15 @@ async function findWorkflowInDir(dir, name) {
|
|
|
167
169
|
/* not found */
|
|
168
170
|
}
|
|
169
171
|
}
|
|
170
|
-
// Check .
|
|
172
|
+
// Check .workflow/ directory as fallback (legacy)
|
|
171
173
|
for (const ext of [".yaml", ".yml"]) {
|
|
172
|
-
const result = await workflowFileExists(resolvePath(dir, ".
|
|
174
|
+
const result = await workflowFileExists(resolvePath(dir, ".workflow"), name, ext);
|
|
173
175
|
if (result !== null) {
|
|
174
176
|
return result;
|
|
175
177
|
}
|
|
176
178
|
}
|
|
177
179
|
for (const indexName of ["index.yaml", "index.yml"]) {
|
|
178
|
-
const candidate = resolvePath(dir, ".
|
|
180
|
+
const candidate = resolvePath(dir, ".workflow", name, indexName);
|
|
179
181
|
try {
|
|
180
182
|
await access(candidate);
|
|
181
183
|
return candidate;
|
|
@@ -186,8 +188,21 @@ async function findWorkflowInDir(dir, name) {
|
|
|
186
188
|
}
|
|
187
189
|
return null;
|
|
188
190
|
}
|
|
191
|
+
/** Check if a directory contains a .git marker (directory or file). */
|
|
192
|
+
async function hasGitMarker(dir) {
|
|
193
|
+
try {
|
|
194
|
+
await access(join(dir, ".git"));
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
189
201
|
/**
|
|
190
|
-
* Traverse parent directories looking for
|
|
202
|
+
* Traverse parent directories looking for a workflow named `name` under
|
|
203
|
+
* `.workflows/` (primary) or `.workflow/` (legacy fallback). Within each
|
|
204
|
+
* directory the lookup checks flat YAML files (`<name>.yaml`/`.yml`) and
|
|
205
|
+
* folder-based layouts (`<name>/index.yaml`/`.yml`).
|
|
191
206
|
* Returns the absolute path if found, otherwise null.
|
|
192
207
|
* Stops at filesystem root or .git directory.
|
|
193
208
|
*/
|
|
@@ -199,6 +214,10 @@ async function findWorkflowInParents(startDir, name) {
|
|
|
199
214
|
if (found !== null) {
|
|
200
215
|
return found;
|
|
201
216
|
}
|
|
217
|
+
// Stop at .git boundary (repo root)
|
|
218
|
+
if (await hasGitMarker(currentDir)) {
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
202
221
|
// Stop at filesystem root
|
|
203
222
|
if (currentDir === root) {
|
|
204
223
|
break;
|
|
@@ -346,8 +365,8 @@ export async function cmdThreadShow(storageRoot, threadId) {
|
|
|
346
365
|
if (workflow === null) {
|
|
347
366
|
fail(`failed to resolve workflow from head: ${activeHead}`);
|
|
348
367
|
}
|
|
349
|
-
// Determine if this is
|
|
350
|
-
if (entry.status === "
|
|
368
|
+
// Determine if this is an ended/cancelled thread
|
|
369
|
+
if (entry.status === "end" || entry.status === "cancelled") {
|
|
351
370
|
const hint = null;
|
|
352
371
|
return {
|
|
353
372
|
workflow,
|
|
@@ -359,13 +378,14 @@ export async function cmdThreadShow(storageRoot, threadId) {
|
|
|
359
378
|
suspendMessage: null,
|
|
360
379
|
done: true,
|
|
361
380
|
background: null,
|
|
381
|
+
error: null,
|
|
362
382
|
hint,
|
|
363
383
|
};
|
|
364
384
|
}
|
|
365
385
|
// Active thread
|
|
366
|
-
const status = await resolveActiveThreadStatus(storageRoot, threadId, uwf, activeHead
|
|
386
|
+
const status = await resolveActiveThreadStatus(storageRoot, threadId, uwf, activeHead);
|
|
367
387
|
const currentRole = resolveCurrentRole(uwf, activeHead, workflow);
|
|
368
|
-
const suspendFields = resolveSuspendFieldsForShow(entry, status, uwf, activeHead
|
|
388
|
+
const suspendFields = resolveSuspendFieldsForShow(entry, status, uwf, activeHead);
|
|
369
389
|
const hint = status === "suspended"
|
|
370
390
|
? `Thread is suspended. Resume with: uwf thread resume ${threadId}`
|
|
371
391
|
: null;
|
|
@@ -379,15 +399,25 @@ export async function cmdThreadShow(storageRoot, threadId) {
|
|
|
379
399
|
suspendMessage: suspendFields.suspendMessage,
|
|
380
400
|
done: false,
|
|
381
401
|
background: null,
|
|
402
|
+
error: null,
|
|
382
403
|
hint,
|
|
383
404
|
};
|
|
384
405
|
}
|
|
385
|
-
async function threadListItemFromActive(storageRoot, uwf, threadId, head) {
|
|
406
|
+
async function threadListItemFromActive(storageRoot, uwf, threadId, head, registry) {
|
|
386
407
|
const workflow = resolveWorkflowFromHead(uwf, head);
|
|
387
408
|
if (workflow === null) {
|
|
388
|
-
|
|
409
|
+
// Head CAS node missing or unrecognized — treat as corrupt rather than silently skipping
|
|
410
|
+
return {
|
|
411
|
+
thread: threadId,
|
|
412
|
+
workflow: "",
|
|
413
|
+
head,
|
|
414
|
+
status: "corrupt",
|
|
415
|
+
currentRole: null,
|
|
416
|
+
statusDisplay: "corrupt",
|
|
417
|
+
workflowName: null,
|
|
418
|
+
};
|
|
389
419
|
}
|
|
390
|
-
const status = await resolveActiveThreadStatus(storageRoot, threadId, uwf, head
|
|
420
|
+
const status = await resolveActiveThreadStatus(storageRoot, threadId, uwf, head);
|
|
391
421
|
const statusDisplay = status === "suspended" ? `${status} [suspended]` : status;
|
|
392
422
|
return {
|
|
393
423
|
thread: threadId,
|
|
@@ -396,35 +426,67 @@ async function threadListItemFromActive(storageRoot, uwf, threadId, head) {
|
|
|
396
426
|
status,
|
|
397
427
|
currentRole: resolveCurrentRole(uwf, head, workflow),
|
|
398
428
|
statusDisplay,
|
|
429
|
+
workflowName: findRegistryName(registry, workflow),
|
|
399
430
|
};
|
|
400
431
|
}
|
|
401
|
-
async function collectActiveThreads(storageRoot, uwf, index) {
|
|
432
|
+
async function collectActiveThreads(storageRoot, uwf, index, registry) {
|
|
402
433
|
const items = [];
|
|
403
434
|
for (const [threadId, entry] of Object.entries(index)) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
435
|
+
try {
|
|
436
|
+
const item = await threadListItemFromActive(storageRoot, uwf, threadId, entry.head, registry);
|
|
437
|
+
if (item !== null) {
|
|
438
|
+
items.push(item);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch (err) {
|
|
442
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
443
|
+
process.stderr.write(`warning: thread ${threadId} is corrupt: ${message}\n`);
|
|
444
|
+
items.push({
|
|
445
|
+
thread: threadId,
|
|
446
|
+
workflow: "",
|
|
447
|
+
head: entry.head,
|
|
448
|
+
status: "corrupt",
|
|
449
|
+
currentRole: null,
|
|
450
|
+
statusDisplay: "corrupt",
|
|
451
|
+
workflowName: null,
|
|
452
|
+
});
|
|
407
453
|
}
|
|
408
454
|
}
|
|
409
455
|
return items;
|
|
410
456
|
}
|
|
411
|
-
function collectCompletedThreads(uwf, activeIds) {
|
|
457
|
+
function collectCompletedThreads(uwf, activeIds, registry) {
|
|
412
458
|
const items = [];
|
|
413
459
|
const history = loadHistoryThreads(uwf.varStore);
|
|
414
460
|
const seen = new Set(); // Deduplication (issue #470)
|
|
415
461
|
for (const [threadId, entry] of Object.entries(history)) {
|
|
416
462
|
if (!activeIds.has(threadId) && !seen.has(threadId)) {
|
|
417
463
|
seen.add(threadId);
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
464
|
+
try {
|
|
465
|
+
const status = entry.status;
|
|
466
|
+
const workflow = resolveWorkflowFromHead(uwf, entry.head);
|
|
467
|
+
items.push({
|
|
468
|
+
thread: threadId,
|
|
469
|
+
workflow: workflow ?? "",
|
|
470
|
+
head: entry.head,
|
|
471
|
+
status,
|
|
472
|
+
currentRole: null,
|
|
473
|
+
statusDisplay: status,
|
|
474
|
+
workflowName: workflow !== null ? findRegistryName(registry, workflow) : null,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
catch (err) {
|
|
478
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
479
|
+
process.stderr.write(`warning: completed thread ${threadId} is corrupt: ${message}\n`);
|
|
480
|
+
items.push({
|
|
481
|
+
thread: threadId,
|
|
482
|
+
workflow: "",
|
|
483
|
+
head: entry.head,
|
|
484
|
+
status: "corrupt",
|
|
485
|
+
currentRole: null,
|
|
486
|
+
statusDisplay: "corrupt",
|
|
487
|
+
workflowName: null,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
428
490
|
}
|
|
429
491
|
}
|
|
430
492
|
return items;
|
|
@@ -455,23 +517,28 @@ function applyPagination(items, skip, take) {
|
|
|
455
517
|
const takeCount = take ?? items.length;
|
|
456
518
|
return items.slice(skipCount, skipCount + takeCount);
|
|
457
519
|
}
|
|
458
|
-
export async function cmdThreadList(storageRoot, statusFilter, afterMs, beforeMs, skip, take) {
|
|
520
|
+
export async function cmdThreadList(storageRoot, statusFilter, afterMs, beforeMs, skip, take, showAll = false) {
|
|
459
521
|
const uwf = await createUwfStore(storageRoot);
|
|
460
522
|
const index = loadActiveThreads(uwf.varStore);
|
|
523
|
+
const registry = loadWorkflowRegistry(uwf.varStore);
|
|
524
|
+
// Resolve the effective filter:
|
|
525
|
+
// - explicit --status wins (showAll has no effect)
|
|
526
|
+
// - otherwise: --all → no filter; default → ["idle", "running"]
|
|
527
|
+
const effectiveFilter = statusFilter !== null ? statusFilter : showAll ? null : ["idle", "running", "corrupt"];
|
|
461
528
|
// Collect active threads
|
|
462
|
-
let items = await collectActiveThreads(storageRoot, uwf, index);
|
|
529
|
+
let items = await collectActiveThreads(storageRoot, uwf, index, registry);
|
|
463
530
|
// Collect completed threads (if relevant for status filter)
|
|
464
|
-
const includeCompleted =
|
|
465
|
-
|
|
466
|
-
|
|
531
|
+
const includeCompleted = effectiveFilter === null ||
|
|
532
|
+
effectiveFilter.includes("end") ||
|
|
533
|
+
effectiveFilter.includes("cancelled");
|
|
467
534
|
if (includeCompleted) {
|
|
468
535
|
const activeIds = new Set(items.map((i) => i.thread));
|
|
469
|
-
const completedItems = collectCompletedThreads(uwf, activeIds);
|
|
536
|
+
const completedItems = collectCompletedThreads(uwf, activeIds, registry);
|
|
470
537
|
items = items.concat(completedItems);
|
|
471
538
|
}
|
|
472
539
|
// Apply status filter
|
|
473
|
-
if (
|
|
474
|
-
items = items.filter((item) =>
|
|
540
|
+
if (effectiveFilter !== null) {
|
|
541
|
+
items = items.filter((item) => effectiveFilter.includes(item.status));
|
|
475
542
|
}
|
|
476
543
|
// Apply time range filters
|
|
477
544
|
items = applyTimeFilters(items, afterMs, beforeMs);
|
|
@@ -636,6 +703,14 @@ function formatThreadReadMarkdown(options) {
|
|
|
636
703
|
return parts.join("\n\n---\n\n");
|
|
637
704
|
}
|
|
638
705
|
const STATUS_KEY = "$status";
|
|
706
|
+
/**
|
|
707
|
+
* Strip YAML frontmatter (---...---) from a raw markdown string,
|
|
708
|
+
* returning only the body portion.
|
|
709
|
+
*/
|
|
710
|
+
function stripFrontmatter(raw) {
|
|
711
|
+
const match = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
712
|
+
return match ? raw.slice(match[0].length).trim() : raw.trim();
|
|
713
|
+
}
|
|
639
714
|
function resolveEvaluateArgs(uwf, chain) {
|
|
640
715
|
if (chain.headIsStart) {
|
|
641
716
|
return { lastRole: START_ROLE, lastOutput: { [STATUS_KEY]: "new" } };
|
|
@@ -648,6 +723,12 @@ function resolveEvaluateArgs(uwf, chain) {
|
|
|
648
723
|
const base = typeof raw === "object" && raw !== null && !Array.isArray(raw)
|
|
649
724
|
? raw
|
|
650
725
|
: {};
|
|
726
|
+
// Inject _body — the markdown body (after frontmatter) from the last step's
|
|
727
|
+
// assistant output. Workflow edge prompts can reference it via {{ _body }}.
|
|
728
|
+
const content = extractLastAssistantContent(uwf, lastStep.detail);
|
|
729
|
+
if (content !== null) {
|
|
730
|
+
base._body = stripFrontmatter(content);
|
|
731
|
+
}
|
|
651
732
|
return {
|
|
652
733
|
lastRole: lastStep.role,
|
|
653
734
|
lastOutput: base,
|
|
@@ -656,10 +737,10 @@ function resolveEvaluateArgs(uwf, chain) {
|
|
|
656
737
|
function loadWorkflowPayload(uwf, workflowRef) {
|
|
657
738
|
const node = uwf.store.cas.get(workflowRef);
|
|
658
739
|
if (node === null) {
|
|
659
|
-
|
|
740
|
+
throw new Error(`workflow CAS node not found: ${workflowRef}`);
|
|
660
741
|
}
|
|
661
742
|
if (node.type !== uwf.schemas.workflow) {
|
|
662
|
-
|
|
743
|
+
throw new Error(`node ${workflowRef} is not a Workflow`);
|
|
663
744
|
}
|
|
664
745
|
return node.payload;
|
|
665
746
|
}
|
|
@@ -697,11 +778,9 @@ function resolveAgentConfig(config, workflow, role, agentOverride) {
|
|
|
697
778
|
}
|
|
698
779
|
return agentConfig;
|
|
699
780
|
}
|
|
700
|
-
function
|
|
701
|
-
const argv = [...agent.args, "--thread", threadId, "--role", role, "--prompt", edgePrompt];
|
|
702
|
-
let stdout;
|
|
781
|
+
function executeAgentCommand(agent, argv, cwd, plog) {
|
|
703
782
|
try {
|
|
704
|
-
|
|
783
|
+
return execFileSync(agent.command, argv, {
|
|
705
784
|
encoding: "utf8",
|
|
706
785
|
stdio: ["ignore", "pipe", "pipe"],
|
|
707
786
|
maxBuffer: 50 * 1024 * 1024, // 50 MB — stream-json output can be large
|
|
@@ -721,14 +800,17 @@ function spawnAgent(plog, agent, threadId, role, edgePrompt, cwd) {
|
|
|
721
800
|
const detail = stderr.trim() !== "" ? `: ${stderr.trim()}` : "";
|
|
722
801
|
failStep(plog, `agent command failed (${agent.command})${detail}`);
|
|
723
802
|
}
|
|
803
|
+
}
|
|
804
|
+
function parseAgentOutput(stdout, plog) {
|
|
724
805
|
const line = stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
725
|
-
let parsed;
|
|
726
806
|
try {
|
|
727
|
-
|
|
807
|
+
return JSON.parse(line);
|
|
728
808
|
}
|
|
729
809
|
catch {
|
|
730
810
|
failStep(plog, `agent stdout last line is not valid JSON: ${line || "(empty)"}`);
|
|
731
811
|
}
|
|
812
|
+
}
|
|
813
|
+
function validateAndNormalizeOutput(parsed, line, plog) {
|
|
732
814
|
const obj = parsed;
|
|
733
815
|
if (typeof obj !== "object" ||
|
|
734
816
|
obj === null ||
|
|
@@ -736,10 +818,33 @@ function spawnAgent(plog, agent, threadId, role, edgePrompt, cwd) {
|
|
|
736
818
|
!isCasRef(obj.stepHash)) {
|
|
737
819
|
failStep(plog, `agent stdout JSON missing valid stepHash: ${line}`);
|
|
738
820
|
}
|
|
821
|
+
// Normalize isError / errorMessage so downstream code can rely on them.
|
|
822
|
+
// Legacy adapters that don't emit these fields default to isError=false.
|
|
823
|
+
if (obj.isError !== undefined && typeof obj.isError !== "boolean") {
|
|
824
|
+
failStep(plog, `agent stdout JSON has non-boolean isError: ${line}`);
|
|
825
|
+
}
|
|
826
|
+
if (obj.isError === undefined) {
|
|
827
|
+
obj.isError = false;
|
|
828
|
+
}
|
|
829
|
+
if (obj.errorMessage !== undefined &&
|
|
830
|
+
obj.errorMessage !== null &&
|
|
831
|
+
typeof obj.errorMessage !== "string") {
|
|
832
|
+
failStep(plog, `agent stdout JSON has non-string errorMessage: ${line}`);
|
|
833
|
+
}
|
|
834
|
+
if (obj.errorMessage === undefined) {
|
|
835
|
+
obj.errorMessage = null;
|
|
836
|
+
}
|
|
739
837
|
return obj;
|
|
740
838
|
}
|
|
839
|
+
function spawnAgent(plog, agent, threadId, role, edgePrompt, cwd) {
|
|
840
|
+
const argv = [...agent.args, "--thread", threadId, "--role", role, "--prompt", edgePrompt];
|
|
841
|
+
const stdout = executeAgentCommand(agent, argv, cwd, plog);
|
|
842
|
+
const line = stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
843
|
+
const parsed = parseAgentOutput(stdout, plog);
|
|
844
|
+
return validateAndNormalizeOutput(parsed, line, plog);
|
|
845
|
+
}
|
|
741
846
|
function archiveThread(uwf, threadId, _workflow, _head) {
|
|
742
|
-
completeThread(uwf.varStore, threadId, "
|
|
847
|
+
completeThread(uwf.varStore, threadId, "end");
|
|
743
848
|
}
|
|
744
849
|
export async function cmdThreadResume(storageRoot, threadId, supplement, agentOverride) {
|
|
745
850
|
const runningMarker = await isThreadRunning(storageRoot, threadId);
|
|
@@ -754,15 +859,15 @@ export async function cmdThreadResume(storageRoot, threadId, supplement, agentOv
|
|
|
754
859
|
const headHash = entry.head;
|
|
755
860
|
const chain = walkChain(uwf, headHash);
|
|
756
861
|
const workflowHash = chain.start.workflow;
|
|
757
|
-
// Check entry.status first for
|
|
862
|
+
// Check entry.status first for end/cancelled (like in cmdThreadShow)
|
|
758
863
|
let status;
|
|
759
|
-
if (entry.status === "
|
|
864
|
+
if (entry.status === "end" || entry.status === "cancelled") {
|
|
760
865
|
status = entry.status;
|
|
761
866
|
}
|
|
762
867
|
else {
|
|
763
|
-
status = await resolveActiveThreadStatus(storageRoot, threadId, uwf, headHash
|
|
868
|
+
status = await resolveActiveThreadStatus(storageRoot, threadId, uwf, headHash);
|
|
764
869
|
}
|
|
765
|
-
if (status !== "suspended" && status !== "
|
|
870
|
+
if (status !== "suspended" && status !== "end") {
|
|
766
871
|
fail(`thread cannot be resumed: ${threadId} (status: ${status})`);
|
|
767
872
|
}
|
|
768
873
|
const plog = createProcessLogger({
|
|
@@ -770,7 +875,7 @@ export async function cmdThreadResume(storageRoot, threadId, supplement, agentOv
|
|
|
770
875
|
context: { thread: threadId, workflow: workflowHash },
|
|
771
876
|
});
|
|
772
877
|
if (status === "suspended") {
|
|
773
|
-
const suspendFields = resolveSuspendFieldsForShow(entry, status, uwf, headHash
|
|
878
|
+
const suspendFields = resolveSuspendFieldsForShow(entry, status, uwf, headHash);
|
|
774
879
|
if (suspendFields.suspendedRole === null) {
|
|
775
880
|
fail(`thread is suspended but suspendedRole is missing: ${threadId}`);
|
|
776
881
|
}
|
|
@@ -784,40 +889,169 @@ export async function cmdThreadResume(storageRoot, threadId, supplement, agentOv
|
|
|
784
889
|
prompt: resumePrompt,
|
|
785
890
|
});
|
|
786
891
|
}
|
|
787
|
-
// status === "
|
|
892
|
+
// status === "end"
|
|
788
893
|
const workflow = loadWorkflowPayload(uwf, workflowHash);
|
|
789
894
|
const startResult = evaluate(workflow.graph, START_ROLE, { [STATUS_KEY]: "resume" });
|
|
790
895
|
if (!startResult.ok) {
|
|
791
896
|
fail(`failed to evaluate $START: ${startResult.error.message}`);
|
|
792
897
|
}
|
|
793
|
-
if (isSuspendResult(startResult.value)) {
|
|
794
|
-
fail("workflow cannot start with $SUSPEND");
|
|
795
|
-
}
|
|
796
898
|
if (startResult.value.role === END_ROLE) {
|
|
797
899
|
fail("workflow cannot start with $END");
|
|
798
900
|
}
|
|
799
901
|
const startRole = startResult.value.role;
|
|
800
|
-
const
|
|
902
|
+
const endResumePrompt = buildResumePrompt(startResult.value.prompt, supplement);
|
|
801
903
|
const updatedEntry = { ...entry, status: "idle", completedAt: null };
|
|
802
904
|
setThread(uwf.varStore, threadId, updatedEntry);
|
|
803
905
|
plog.log(PL_THREAD_RESUME, `resume completed role=${startRole} supplement=${supplement !== null}`, null);
|
|
804
906
|
return cmdThreadStepOnce(storageRoot, threadId, agentOverride, plog, {
|
|
805
907
|
role: startRole,
|
|
806
|
-
prompt:
|
|
908
|
+
prompt: endResumePrompt,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Validate that a thread can be poked. Returns the existing entry and the head StepNode payload.
|
|
913
|
+
* Fails (process exit) when the thread is missing, running, completed, cancelled, or has no
|
|
914
|
+
* StepNode at its head.
|
|
915
|
+
*/
|
|
916
|
+
async function validatePokePreconditions(storageRoot, uwf, threadId) {
|
|
917
|
+
const runningMarker = await isThreadRunning(storageRoot, threadId);
|
|
918
|
+
if (runningMarker !== null) {
|
|
919
|
+
fail(`thread already executing in background (PID: ${runningMarker.pid})`);
|
|
920
|
+
}
|
|
921
|
+
const entry = getThread(uwf.varStore, threadId);
|
|
922
|
+
if (entry === null) {
|
|
923
|
+
fail(`thread not active: ${threadId}`);
|
|
924
|
+
}
|
|
925
|
+
if (entry.status === "end" || entry.status === "cancelled") {
|
|
926
|
+
fail(`thread cannot be poked: ${threadId} (status: ${entry.status})`);
|
|
927
|
+
}
|
|
928
|
+
const oldHead = entry.head;
|
|
929
|
+
const oldHeadNode = uwf.store.cas.get(oldHead);
|
|
930
|
+
if (oldHeadNode === null) {
|
|
931
|
+
fail(`CAS node not found: ${oldHead}`);
|
|
932
|
+
}
|
|
933
|
+
if (oldHeadNode.type !== uwf.schemas.stepNode) {
|
|
934
|
+
fail("thread cannot be poked: no step to replace (head is StartNode)");
|
|
935
|
+
}
|
|
936
|
+
return { entry, oldHead, oldHeadPayload: oldHeadNode.payload };
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Resolve the next role from the post-poke chain state, used for the StepOutput.currentRole field.
|
|
940
|
+
* Returns null when the next role is $END, evaluation fails, or the result is a suspend.
|
|
941
|
+
*/
|
|
942
|
+
function resolveCurrentRoleFromChain(uwfAfter, workflow, replacedHash) {
|
|
943
|
+
const chainAfter = walkChain(uwfAfter, replacedHash);
|
|
944
|
+
const { lastRole, lastOutput } = resolveEvaluateArgs(uwfAfter, chainAfter);
|
|
945
|
+
if (readSuspendReason(lastOutput) !== null) {
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
const afterResult = evaluate(workflow.graph, lastRole, lastOutput);
|
|
949
|
+
if (!afterResult.ok) {
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
if (afterResult.value.role === END_ROLE) {
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
return afterResult.value.role;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Poke a thread: re-run the agent on the head step with a supplementary prompt,
|
|
959
|
+
* replacing the head step's output. The new step's `prev` points to the OLD head's
|
|
960
|
+
* `prev` — semantically replacing (not appending to) the head. The moderator is NOT
|
|
961
|
+
* re-evaluated for routing; the role of the head step is re-used.
|
|
962
|
+
*/
|
|
963
|
+
export async function cmdThreadPoke(storageRoot, threadId, prompt, agentOverride) {
|
|
964
|
+
const uwf = await createUwfStore(storageRoot);
|
|
965
|
+
const { entry, oldHeadPayload } = await validatePokePreconditions(storageRoot, uwf, threadId);
|
|
966
|
+
const chain = walkChain(uwf, entry.head);
|
|
967
|
+
const workflowHash = chain.start.workflow;
|
|
968
|
+
const threadCwd = chain.start.cwd;
|
|
969
|
+
const plog = createProcessLogger({
|
|
970
|
+
storageRoot,
|
|
971
|
+
context: { thread: threadId, workflow: workflowHash },
|
|
807
972
|
});
|
|
973
|
+
// Resolve the agent: --agent override wins; otherwise read from old head step's `agent` field.
|
|
974
|
+
const config = await loadWorkflowConfig(storageRoot);
|
|
975
|
+
const workflow = loadWorkflowPayload(uwf, workflowHash);
|
|
976
|
+
const role = oldHeadPayload.role;
|
|
977
|
+
const agent = agentOverride !== null
|
|
978
|
+
? resolveAgentConfig(config, workflow, role, agentOverride)
|
|
979
|
+
: parseAgentOverride(oldHeadPayload.agent);
|
|
980
|
+
const effectiveCwd = oldHeadPayload.cwd !== "" ? oldHeadPayload.cwd : threadCwd;
|
|
981
|
+
plog.log(PL_THREAD_POKE, `poke role=${role} agent=${agent.command}`, null);
|
|
982
|
+
plog.log(PL_AGENT_SPAWN, `spawning agent command=${agent.command}`, {
|
|
983
|
+
args: [...agent.args, threadId, role].join(" "),
|
|
984
|
+
});
|
|
985
|
+
loadDotenv({ path: getEnvPath(storageRoot) });
|
|
986
|
+
// Spawn the agent. The agent will create a new StepNode with prev=oldHead (it reads
|
|
987
|
+
// the active thread head). After the agent returns, we rewrite that node's prev so
|
|
988
|
+
// that the new head replaces the old head instead of appending after it.
|
|
989
|
+
const agentResult = spawnAgent(plog, agent, threadId, role, prompt, effectiveCwd);
|
|
990
|
+
const agentStepHash = agentResult.stepHash;
|
|
991
|
+
plog.log(PL_AGENT_DONE, `agent returned head=${agentStepHash}`, null);
|
|
992
|
+
const uwfAfter = await createUwfStore(storageRoot);
|
|
993
|
+
const agentNode = uwfAfter.store.cas.get(agentStepHash);
|
|
994
|
+
if (agentNode === null || agentNode.type !== uwfAfter.schemas.stepNode) {
|
|
995
|
+
failStep(plog, `agent returned hash that is not a StepNode: ${agentStepHash}`);
|
|
996
|
+
}
|
|
997
|
+
const agentPayload = agentNode.payload;
|
|
998
|
+
// Rewrite the new step so that its `prev` points to the OLD head's prev (replace semantics).
|
|
999
|
+
const replacedPayload = {
|
|
1000
|
+
...agentPayload,
|
|
1001
|
+
prev: oldHeadPayload.prev,
|
|
1002
|
+
};
|
|
1003
|
+
const replacedHash = await uwfAfter.store.cas.put(uwfAfter.schemas.stepNode, replacedPayload);
|
|
1004
|
+
const replacedNode = uwfAfter.store.cas.get(replacedHash);
|
|
1005
|
+
if (replacedNode === null || !validate(uwfAfter.store, replacedNode)) {
|
|
1006
|
+
failStep(plog, "rewritten StepNode failed schema validation");
|
|
1007
|
+
}
|
|
1008
|
+
// Update thread head to the replaced step. Status becomes idle (no moderator re-route).
|
|
1009
|
+
setThread(uwfAfter.varStore, threadId, updateThreadHead(entry, replacedHash));
|
|
1010
|
+
return {
|
|
1011
|
+
workflow: workflowHash,
|
|
1012
|
+
thread: threadId,
|
|
1013
|
+
head: replacedHash,
|
|
1014
|
+
status: "idle",
|
|
1015
|
+
currentRole: resolveCurrentRoleFromChain(uwfAfter, workflow, replacedHash),
|
|
1016
|
+
suspendedRole: null,
|
|
1017
|
+
suspendMessage: null,
|
|
1018
|
+
done: false,
|
|
1019
|
+
background: null,
|
|
1020
|
+
error: null,
|
|
1021
|
+
};
|
|
808
1022
|
}
|
|
809
1023
|
export function validateCount(count) {
|
|
810
1024
|
if (count < 1 || !Number.isInteger(count)) {
|
|
811
1025
|
throw new Error(`--count must be a positive integer, got: ${count}`);
|
|
812
1026
|
}
|
|
813
1027
|
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Resolve the effective maxRunning limit.
|
|
1030
|
+
* Priority: config file > DEFAULT_MAX_RUNNING (2).
|
|
1031
|
+
*/
|
|
1032
|
+
async function resolveMaxRunning(storageRoot) {
|
|
1033
|
+
try {
|
|
1034
|
+
const configPath = getConfigPath(storageRoot);
|
|
1035
|
+
const config = loadConfig(configPath);
|
|
1036
|
+
const path = parseDotPath("concurrency.maxRunning");
|
|
1037
|
+
const value = getNestedValue(config, path);
|
|
1038
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
1039
|
+
return value;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
catch {
|
|
1043
|
+
// Config file missing or invalid — fall through to default
|
|
1044
|
+
}
|
|
1045
|
+
return DEFAULT_MAX_RUNNING;
|
|
1046
|
+
}
|
|
814
1047
|
export async function cmdThreadExec(storageRoot, threadId, agentOverride, count, background, backgroundWorker) {
|
|
815
1048
|
validateCount(count);
|
|
816
|
-
//
|
|
1049
|
+
// Reject concurrent exec on the same thread (unless we ARE the background worker,
|
|
1050
|
+
// which hasn't created its own marker yet at this point).
|
|
817
1051
|
if (!backgroundWorker) {
|
|
818
1052
|
const runningMarker = await isThreadRunning(storageRoot, threadId);
|
|
819
1053
|
if (runningMarker !== null) {
|
|
820
|
-
fail(`thread already
|
|
1054
|
+
fail(`thread ${threadId} is already being executed by PID ${runningMarker.pid}`);
|
|
821
1055
|
}
|
|
822
1056
|
}
|
|
823
1057
|
const workflowHash = await resolveActiveThreadWorkflowHash(storageRoot, threadId);
|
|
@@ -829,17 +1063,20 @@ export async function cmdThreadExec(storageRoot, threadId, agentOverride, count,
|
|
|
829
1063
|
// Spawn background process
|
|
830
1064
|
return cmdThreadStepBackground(storageRoot, threadId, agentOverride, count, plog, workflowHash);
|
|
831
1065
|
}
|
|
832
|
-
//
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1066
|
+
// Create running marker so `thread list` shows "running" during execution
|
|
1067
|
+
// and concurrent `exec` on the same thread is rejected (see check above).
|
|
1068
|
+
await createMarker(storageRoot, {
|
|
1069
|
+
thread: threadId,
|
|
1070
|
+
workflow: workflowHash,
|
|
1071
|
+
pid: process.pid,
|
|
1072
|
+
startedAt: Date.now(),
|
|
1073
|
+
processStartTime: getProcessStartTime(process.pid),
|
|
1074
|
+
});
|
|
1075
|
+
// Resolve concurrency limit: config > default
|
|
1076
|
+
const effectiveMaxRunning = await resolveMaxRunning(storageRoot);
|
|
1077
|
+
// Acquire concurrency slot (blocks if at capacity)
|
|
1078
|
+
const slotHandle = await acquireSlot(storageRoot, effectiveMaxRunning);
|
|
1079
|
+
const uninstallCleanup = installSlotCleanup(slotHandle);
|
|
843
1080
|
try {
|
|
844
1081
|
const results = [];
|
|
845
1082
|
for (let i = 0; i < count; i++) {
|
|
@@ -852,10 +1089,9 @@ export async function cmdThreadExec(storageRoot, threadId, agentOverride, count,
|
|
|
852
1089
|
return results;
|
|
853
1090
|
}
|
|
854
1091
|
finally {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
}
|
|
1092
|
+
uninstallCleanup();
|
|
1093
|
+
await slotHandle.release();
|
|
1094
|
+
await deleteMarker(storageRoot, threadId);
|
|
859
1095
|
}
|
|
860
1096
|
}
|
|
861
1097
|
async function resolveActiveThreadWorkflowHash(storageRoot, threadId) {
|
|
@@ -903,6 +1139,7 @@ async function cmdThreadStepBackground(storageRoot, threadId, agentOverride, cou
|
|
|
903
1139
|
suspendMessage: null,
|
|
904
1140
|
done: false,
|
|
905
1141
|
background: true,
|
|
1142
|
+
error: null,
|
|
906
1143
|
},
|
|
907
1144
|
];
|
|
908
1145
|
}
|
|
@@ -917,17 +1154,19 @@ function resolveResumeStepTarget(resume, chain, threadCwd, plog) {
|
|
|
917
1154
|
}
|
|
918
1155
|
async function resolveModeratorStepTarget(_storageRoot, threadId, entry, headHash, workflowHash, workflow, uwf, chain, threadCwd, plog) {
|
|
919
1156
|
const { lastRole, lastOutput } = resolveEvaluateArgs(uwf, chain);
|
|
1157
|
+
// Intercept an already-suspended head before the moderator: a thread whose
|
|
1158
|
+
// head step yielded `$status: "$SUSPEND"` stays suspended (idempotent re-exec).
|
|
1159
|
+
const suspendReason = readSuspendReason(lastOutput);
|
|
1160
|
+
if (suspendReason !== null) {
|
|
1161
|
+
await ensureThreadSuspendMetadata(uwf.varStore, threadId, entry, lastRole, suspendReason);
|
|
1162
|
+
plog.log(PL_MODERATOR, `moderator action=suspend suspendedRole=${lastRole}`, null);
|
|
1163
|
+
return buildSuspendStepOutput(workflowHash, threadId, headHash, lastRole, suspendReason);
|
|
1164
|
+
}
|
|
920
1165
|
const nextResult = evaluate(workflow.graph, lastRole, lastOutput);
|
|
921
1166
|
if (!nextResult.ok) {
|
|
922
1167
|
failStep(plog, `moderator evaluate failed: ${nextResult.error.message}`);
|
|
923
1168
|
}
|
|
924
|
-
plog.log(PL_MODERATOR, `moderator
|
|
925
|
-
? `action=suspend suspendedRole=${nextResult.value.suspendedRole}`
|
|
926
|
-
: `role=${nextResult.value.role}`} prompt=${nextResult.value.prompt}`, null);
|
|
927
|
-
if (isSuspendResult(nextResult.value)) {
|
|
928
|
-
await ensureThreadSuspendMetadata(uwf.varStore, threadId, entry, nextResult.value.suspendedRole, nextResult.value.prompt);
|
|
929
|
-
return buildStepOutputFromEvaluation(workflowHash, threadId, headHash, "suspended", nextResult, null);
|
|
930
|
-
}
|
|
1169
|
+
plog.log(PL_MODERATOR, `moderator role=${nextResult.value.role} prompt=${nextResult.value.prompt}`, null);
|
|
931
1170
|
if (nextResult.value.role === END_ROLE) {
|
|
932
1171
|
plog.log(PL_THREAD_ARCHIVED, `thread archived head=${headHash}`, null);
|
|
933
1172
|
archiveThread(uwf, threadId, workflowHash, headHash);
|
|
@@ -935,12 +1174,13 @@ async function resolveModeratorStepTarget(_storageRoot, threadId, entry, headHas
|
|
|
935
1174
|
workflow: workflowHash,
|
|
936
1175
|
thread: threadId,
|
|
937
1176
|
head: headHash,
|
|
938
|
-
status: "
|
|
1177
|
+
status: "end",
|
|
939
1178
|
currentRole: null,
|
|
940
1179
|
suspendedRole: null,
|
|
941
1180
|
suspendMessage: null,
|
|
942
1181
|
done: true,
|
|
943
1182
|
background: null,
|
|
1183
|
+
error: null,
|
|
944
1184
|
};
|
|
945
1185
|
}
|
|
946
1186
|
return {
|
|
@@ -954,20 +1194,24 @@ async function finalizeAgentStep(_storageRoot, threadId, workflowHash, workflow,
|
|
|
954
1194
|
setThread(uwfAfter.varStore, threadId, updateThreadHead(priorEntry, newHead));
|
|
955
1195
|
const chainAfter = walkChain(uwfAfter, newHead);
|
|
956
1196
|
const { lastRole: lastRoleAfter, lastOutput: lastOutputAfter } = resolveEvaluateArgs(uwfAfter, chainAfter);
|
|
1197
|
+
// Intercept `$status: "$SUSPEND"` before the moderator (coroutine yield): the
|
|
1198
|
+
// step is already in CAS and the head has advanced — mark the thread suspended
|
|
1199
|
+
// and return without routing through the graph.
|
|
1200
|
+
const suspendReason = readSuspendReason(lastOutputAfter);
|
|
1201
|
+
if (suspendReason !== null) {
|
|
1202
|
+
setThread(uwfAfter.varStore, threadId, markThreadSuspended(getThread(uwfAfter.varStore, threadId) ?? createThreadIndexEntry(newHead), lastRoleAfter, suspendReason));
|
|
1203
|
+
return buildSuspendStepOutput(workflowHash, threadId, newHead, lastRoleAfter, suspendReason);
|
|
1204
|
+
}
|
|
957
1205
|
const afterResult = evaluate(workflow.graph, lastRoleAfter, lastOutputAfter);
|
|
958
1206
|
if (!afterResult.ok) {
|
|
959
1207
|
failStep(plog, `post-step moderator evaluate failed: ${afterResult.error.message}`);
|
|
960
1208
|
}
|
|
961
|
-
if (isSuspendResult(afterResult.value)) {
|
|
962
|
-
setThread(uwfAfter.varStore, threadId, markThreadSuspended(getThread(uwfAfter.varStore, threadId) ?? createThreadIndexEntry(newHead), afterResult.value.suspendedRole, afterResult.value.prompt));
|
|
963
|
-
return buildStepOutputFromEvaluation(workflowHash, threadId, newHead, "suspended", afterResult, null);
|
|
964
|
-
}
|
|
965
1209
|
const done = afterResult.value.role === END_ROLE;
|
|
966
1210
|
if (done) {
|
|
967
1211
|
plog.log(PL_THREAD_ARCHIVED, `thread archived head=${newHead}`, null);
|
|
968
1212
|
archiveThread(uwfAfter, threadId, workflowHash, newHead);
|
|
969
1213
|
}
|
|
970
|
-
const status = done ? "
|
|
1214
|
+
const status = done ? "end" : "idle";
|
|
971
1215
|
const currentRole = done ? null : afterResult.value.role;
|
|
972
1216
|
return {
|
|
973
1217
|
workflow: workflowHash,
|
|
@@ -979,6 +1223,7 @@ async function finalizeAgentStep(_storageRoot, threadId, workflowHash, workflow,
|
|
|
979
1223
|
suspendMessage: null,
|
|
980
1224
|
done,
|
|
981
1225
|
background: null,
|
|
1226
|
+
error: null,
|
|
982
1227
|
};
|
|
983
1228
|
}
|
|
984
1229
|
async function cmdThreadStepOnce(storageRoot, threadId, agentOverride, plog, resume = null) {
|
|
@@ -1013,6 +1258,26 @@ async function cmdThreadStepOnce(storageRoot, threadId, agentOverride, plog, res
|
|
|
1013
1258
|
if (newNode === null || newNode.type !== uwfAfter.schemas.stepNode) {
|
|
1014
1259
|
failStep(plog, `agent returned hash that is not a StepNode: ${newHead}`);
|
|
1015
1260
|
}
|
|
1261
|
+
// Recoverable failure: agent persisted a failed StepNode (e.g. frontmatter
|
|
1262
|
+
// validation exhausted retries) but the engine MUST NOT advance head. The
|
|
1263
|
+
// moderator graph is also untouched — the same role will be replayed on the
|
|
1264
|
+
// next exec (until eventual success records `previousAttempts` linking the
|
|
1265
|
+
// failed step hashes).
|
|
1266
|
+
if (agentResult.isError === true) {
|
|
1267
|
+
plog.log(PL_AGENT_ERROR, `agent reported recoverable failure stepHash=${newHead} message=${agentResult.errorMessage ?? ""}`, null);
|
|
1268
|
+
return {
|
|
1269
|
+
workflow: workflowHash,
|
|
1270
|
+
thread: threadId,
|
|
1271
|
+
head: headHash,
|
|
1272
|
+
status: "idle",
|
|
1273
|
+
currentRole: role,
|
|
1274
|
+
suspendedRole: null,
|
|
1275
|
+
suspendMessage: null,
|
|
1276
|
+
done: false,
|
|
1277
|
+
background: null,
|
|
1278
|
+
error: { stepHash: newHead, message: agentResult.errorMessage ?? "agent reported error" },
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1016
1281
|
return finalizeAgentStep(storageRoot, threadId, workflowHash, workflow, newHead, uwfAfter, plog);
|
|
1017
1282
|
}
|
|
1018
1283
|
async function resolveHeadHash(storageRoot, threadId) {
|
|
@@ -1043,7 +1308,9 @@ export async function cmdThreadRead(storageRoot, threadId, quota = THREAD_READ_D
|
|
|
1043
1308
|
});
|
|
1044
1309
|
}
|
|
1045
1310
|
/**
|
|
1046
|
-
* Stop background execution of a thread (but keep thread active)
|
|
1311
|
+
* Stop background execution of a thread (but keep thread active).
|
|
1312
|
+
* Validates process identity before sending signals to prevent killing
|
|
1313
|
+
* unrelated processes when PIDs are recycled.
|
|
1047
1314
|
*/
|
|
1048
1315
|
export async function cmdThreadStop(storageRoot, threadId) {
|
|
1049
1316
|
const uwf = await createUwfStore(storageRoot);
|
|
@@ -1051,14 +1318,22 @@ export async function cmdThreadStop(storageRoot, threadId) {
|
|
|
1051
1318
|
if (entry === null) {
|
|
1052
1319
|
fail(`thread not active: ${threadId}`);
|
|
1053
1320
|
}
|
|
1054
|
-
//
|
|
1055
|
-
const
|
|
1056
|
-
if (
|
|
1321
|
+
// Read the raw marker to check process identity
|
|
1322
|
+
const marker = await readMarker(storageRoot, threadId);
|
|
1323
|
+
if (marker === null) {
|
|
1057
1324
|
process.stderr.write(`Warning: thread ${threadId} is not currently running\n`);
|
|
1058
1325
|
return { thread: threadId, stopped: false };
|
|
1059
1326
|
}
|
|
1327
|
+
// Validate that the marker's PID still belongs to the same process
|
|
1328
|
+
if (!isMarkerValid(marker)) {
|
|
1329
|
+
// Stale marker — PID was recycled or process died. Do NOT send a signal.
|
|
1330
|
+
process.stderr.write(`Warning: thread ${threadId} was not actually running (stale marker cleaned up)\n`);
|
|
1331
|
+
await deleteMarker(storageRoot, threadId);
|
|
1332
|
+
return { thread: threadId, stopped: false };
|
|
1333
|
+
}
|
|
1334
|
+
// Process identity confirmed — safe to send SIGTERM
|
|
1060
1335
|
try {
|
|
1061
|
-
process.kill(
|
|
1336
|
+
process.kill(marker.pid, "SIGTERM");
|
|
1062
1337
|
}
|
|
1063
1338
|
catch {
|
|
1064
1339
|
// Process may have already exited, ignore error
|
|
@@ -1067,7 +1342,9 @@ export async function cmdThreadStop(storageRoot, threadId) {
|
|
|
1067
1342
|
return { thread: threadId, stopped: true };
|
|
1068
1343
|
}
|
|
1069
1344
|
/**
|
|
1070
|
-
* Cancel a thread (stop execution + move to history)
|
|
1345
|
+
* Cancel a thread (stop execution + move to history).
|
|
1346
|
+
* Validates process identity before sending signals to prevent killing
|
|
1347
|
+
* unrelated processes when PIDs are recycled.
|
|
1071
1348
|
*/
|
|
1072
1349
|
export async function cmdThreadCancel(storageRoot, threadId) {
|
|
1073
1350
|
const uwf = await createUwfStore(storageRoot);
|
|
@@ -1075,15 +1352,19 @@ export async function cmdThreadCancel(storageRoot, threadId) {
|
|
|
1075
1352
|
if (entry === null) {
|
|
1076
1353
|
fail(`thread not active: ${threadId}`);
|
|
1077
1354
|
}
|
|
1078
|
-
//
|
|
1079
|
-
const
|
|
1080
|
-
if (
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1355
|
+
// Read the raw marker and validate process identity before sending signals
|
|
1356
|
+
const marker = await readMarker(storageRoot, threadId);
|
|
1357
|
+
if (marker !== null) {
|
|
1358
|
+
if (isMarkerValid(marker)) {
|
|
1359
|
+
// Process identity confirmed — safe to send SIGTERM
|
|
1360
|
+
try {
|
|
1361
|
+
process.kill(marker.pid, "SIGTERM");
|
|
1362
|
+
}
|
|
1363
|
+
catch {
|
|
1364
|
+
// Process may have already exited, ignore error
|
|
1365
|
+
}
|
|
1086
1366
|
}
|
|
1367
|
+
// Always delete the marker (stale or not) — cancellation proceeds
|
|
1087
1368
|
await deleteMarker(storageRoot, threadId);
|
|
1088
1369
|
}
|
|
1089
1370
|
completeThread(uwf.varStore, threadId, "cancelled");
|