@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/src/commands/thread.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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 type { VarStore } from "@ocas/core";
|
|
5
5
|
import { validate } from "@ocas/core";
|
|
6
6
|
import type {
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
import {
|
|
23
23
|
createThreadIndexEntry,
|
|
24
24
|
markThreadSuspended,
|
|
25
|
+
SUSPEND_STATUS,
|
|
25
26
|
updateThreadHead,
|
|
26
27
|
} from "@united-workforce/protocol";
|
|
27
28
|
import {
|
|
@@ -34,12 +35,21 @@ import type { AdapterOutput } from "@united-workforce/util-agent";
|
|
|
34
35
|
import { getEnvPath, loadWorkflowConfig } from "@united-workforce/util-agent";
|
|
35
36
|
import { config as loadDotenv } from "dotenv";
|
|
36
37
|
import { parse } from "yaml";
|
|
37
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
createMarker,
|
|
40
|
+
deleteMarker,
|
|
41
|
+
getProcessStartTime,
|
|
42
|
+
isMarkerValid,
|
|
43
|
+
isThreadRunning,
|
|
44
|
+
readMarker,
|
|
45
|
+
} from "../background/index.js";
|
|
46
|
+
import { acquireSlot, DEFAULT_MAX_RUNNING, installSlotCleanup } from "../concurrency/index.js";
|
|
38
47
|
import { createIncludeTag } from "../include.js";
|
|
39
|
-
import { evaluate
|
|
48
|
+
import { evaluate } from "../moderator/index.js";
|
|
40
49
|
import {
|
|
41
50
|
completeThread,
|
|
42
51
|
createUwfStore,
|
|
52
|
+
findRegistryName,
|
|
43
53
|
getThread,
|
|
44
54
|
loadActiveThreads,
|
|
45
55
|
loadHistoryThreads,
|
|
@@ -47,9 +57,11 @@ import {
|
|
|
47
57
|
resolveWorkflowHash,
|
|
48
58
|
setThread,
|
|
49
59
|
type UwfStore,
|
|
60
|
+
type WorkflowRegistry,
|
|
50
61
|
} from "../store.js";
|
|
51
62
|
import { checkWorkflowFilenameConsistency, isCasRef, parseWorkflowPayload } from "../validate.js";
|
|
52
63
|
import { validateWorkflow } from "../validate-semantic.js";
|
|
64
|
+
import { getConfigPath, getNestedValue, loadConfig, parseDotPath } from "./config.js";
|
|
53
65
|
import {
|
|
54
66
|
type ChainState,
|
|
55
67
|
collectOrderedSteps,
|
|
@@ -64,53 +76,49 @@ const END_ROLE = "$END";
|
|
|
64
76
|
const START_ROLE = "$START";
|
|
65
77
|
export const THREAD_READ_DEFAULT_QUOTA = 4000;
|
|
66
78
|
|
|
67
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Read the suspend reason from an agent output if it is an engine-level suspend
|
|
81
|
+
* (coroutine yield). Returns the reason string when `$status === "$SUSPEND"`,
|
|
82
|
+
* or `null` otherwise. A suspend output with no `reason` yields an empty string.
|
|
83
|
+
*/
|
|
84
|
+
function readSuspendReason(lastOutput: Record<string, unknown>): string | null {
|
|
85
|
+
if (lastOutput[STATUS_KEY] !== SUSPEND_STATUS) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const reason = lastOutput.reason;
|
|
89
|
+
return typeof reason === "string" ? reason : "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildSuspendStepOutput(
|
|
68
93
|
workflowHash: CasRef,
|
|
69
94
|
threadId: ThreadId,
|
|
70
95
|
head: CasRef,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
background: boolean | null,
|
|
96
|
+
suspendedRole: string,
|
|
97
|
+
suspendMessage: string,
|
|
74
98
|
): StepOutput {
|
|
75
|
-
const done = status === "completed";
|
|
76
|
-
let currentRole: string | null = null;
|
|
77
|
-
let suspendedRole: string | null = null;
|
|
78
|
-
let suspendMessage: string | null = null;
|
|
79
|
-
if (evaluation.ok) {
|
|
80
|
-
if (isSuspendResult(evaluation.value)) {
|
|
81
|
-
suspendedRole = evaluation.value.suspendedRole;
|
|
82
|
-
suspendMessage = evaluation.value.prompt;
|
|
83
|
-
} else if (evaluation.value.role !== END_ROLE) {
|
|
84
|
-
currentRole = evaluation.value.role;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
99
|
return {
|
|
88
100
|
workflow: workflowHash,
|
|
89
101
|
thread: threadId,
|
|
90
102
|
head,
|
|
91
|
-
status,
|
|
92
|
-
currentRole,
|
|
103
|
+
status: "suspended",
|
|
104
|
+
currentRole: null,
|
|
93
105
|
suspendedRole,
|
|
94
106
|
suspendMessage,
|
|
95
|
-
done,
|
|
96
|
-
background,
|
|
107
|
+
done: false,
|
|
108
|
+
background: null,
|
|
109
|
+
error: null,
|
|
97
110
|
};
|
|
98
111
|
}
|
|
99
112
|
|
|
100
|
-
function
|
|
113
|
+
function resolveSuspendFieldsFromOutput(
|
|
101
114
|
uwf: UwfStore,
|
|
102
115
|
head: CasRef,
|
|
103
|
-
workflowRef: CasRef,
|
|
104
116
|
): { suspendedRole: string | null; suspendMessage: string | null } {
|
|
105
117
|
const chain = walkChain(uwf, head);
|
|
106
118
|
const { lastRole, lastOutput } = resolveEvaluateArgs(uwf, chain);
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
suspendedRole: result.value.suspendedRole,
|
|
112
|
-
suspendMessage: result.value.prompt,
|
|
113
|
-
};
|
|
119
|
+
const reason = readSuspendReason(lastOutput);
|
|
120
|
+
if (reason !== null) {
|
|
121
|
+
return { suspendedRole: lastRole, suspendMessage: reason };
|
|
114
122
|
}
|
|
115
123
|
return { suspendedRole: null, suspendMessage: null };
|
|
116
124
|
}
|
|
@@ -120,7 +128,6 @@ function resolveSuspendFieldsForShow(
|
|
|
120
128
|
status: ThreadStatus,
|
|
121
129
|
uwf: UwfStore,
|
|
122
130
|
head: CasRef,
|
|
123
|
-
workflowRef: CasRef,
|
|
124
131
|
): { suspendedRole: string | null; suspendMessage: string | null } {
|
|
125
132
|
if (status !== "suspended") {
|
|
126
133
|
return { suspendedRole: null, suspendMessage: null };
|
|
@@ -128,10 +135,10 @@ function resolveSuspendFieldsForShow(
|
|
|
128
135
|
if (entry.suspendedRole !== null && entry.suspendMessage !== null) {
|
|
129
136
|
return { suspendedRole: entry.suspendedRole, suspendMessage: entry.suspendMessage };
|
|
130
137
|
}
|
|
131
|
-
const
|
|
138
|
+
const fromOutput = resolveSuspendFieldsFromOutput(uwf, head);
|
|
132
139
|
return {
|
|
133
|
-
suspendedRole: entry.suspendedRole ??
|
|
134
|
-
suspendMessage: entry.suspendMessage ??
|
|
140
|
+
suspendedRole: entry.suspendedRole ?? fromOutput.suspendedRole,
|
|
141
|
+
suspendMessage: entry.suspendMessage ?? fromOutput.suspendMessage,
|
|
135
142
|
};
|
|
136
143
|
}
|
|
137
144
|
|
|
@@ -155,7 +162,6 @@ async function resolveActiveThreadStatus(
|
|
|
155
162
|
threadId: ThreadId,
|
|
156
163
|
uwf: UwfStore,
|
|
157
164
|
head: CasRef,
|
|
158
|
-
workflowRef: CasRef,
|
|
159
165
|
): Promise<ThreadStatus> {
|
|
160
166
|
const runningMarker = await isThreadRunning(storageRoot, threadId);
|
|
161
167
|
if (runningMarker !== null) {
|
|
@@ -163,10 +169,8 @@ async function resolveActiveThreadStatus(
|
|
|
163
169
|
}
|
|
164
170
|
|
|
165
171
|
const chain = walkChain(uwf, head);
|
|
166
|
-
const {
|
|
167
|
-
|
|
168
|
-
const result = evaluate(workflow.graph, lastRole, lastOutput);
|
|
169
|
-
if (result.ok && isSuspendResult(result.value)) {
|
|
172
|
+
const { lastOutput } = resolveEvaluateArgs(uwf, chain);
|
|
173
|
+
if (readSuspendReason(lastOutput) !== null) {
|
|
170
174
|
return "suspended";
|
|
171
175
|
}
|
|
172
176
|
|
|
@@ -180,12 +184,15 @@ async function resolveActiveThreadStatus(
|
|
|
180
184
|
function resolveCurrentRole(uwf: UwfStore, head: CasRef, workflowRef: CasRef): string | null {
|
|
181
185
|
const chain = walkChain(uwf, head);
|
|
182
186
|
const { lastRole, lastOutput } = resolveEvaluateArgs(uwf, chain);
|
|
187
|
+
if (readSuspendReason(lastOutput) !== null) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
183
190
|
const workflow = loadWorkflowPayload(uwf, workflowRef);
|
|
184
191
|
const result = evaluate(workflow.graph, lastRole, lastOutput);
|
|
185
192
|
if (!result.ok) {
|
|
186
193
|
return null;
|
|
187
194
|
}
|
|
188
|
-
if (
|
|
195
|
+
if (result.value.role === END_ROLE) {
|
|
189
196
|
return null;
|
|
190
197
|
}
|
|
191
198
|
return result.value.role;
|
|
@@ -195,10 +202,12 @@ const PL_THREAD_START = "7HNQ4B2X";
|
|
|
195
202
|
const PL_MODERATOR = "M3K8V9T1";
|
|
196
203
|
const PL_AGENT_SPAWN = "R5J2W8N4";
|
|
197
204
|
const PL_AGENT_DONE = "C6P9E3H7";
|
|
205
|
+
const PL_AGENT_ERROR = "Z3F7K8M2";
|
|
198
206
|
const PL_THREAD_ARCHIVED = "F4D8Q2K5";
|
|
199
207
|
const PL_STEP_ERROR = "B8T5N1V6";
|
|
200
208
|
const PL_BACKGROUND_START = "X7Q4W9M2";
|
|
201
209
|
const PL_THREAD_RESUME = "K2R7M4N8";
|
|
210
|
+
const PL_THREAD_POKE = "P4Q9R3X7";
|
|
202
211
|
|
|
203
212
|
type ResumeStepConfig = {
|
|
204
213
|
role: string;
|
|
@@ -246,18 +255,19 @@ async function workflowFileExists(dir: string, name: string, ext: string): Promi
|
|
|
246
255
|
}
|
|
247
256
|
|
|
248
257
|
/**
|
|
249
|
-
* Search for a workflow file in a given directory (checks both .
|
|
258
|
+
* Search for a workflow file in a given directory (checks both .workflows/ and .workflow/).
|
|
259
|
+
* `.workflows/` (primary) takes priority over `.workflow/` (legacy fallback).
|
|
250
260
|
*/
|
|
251
261
|
async function findWorkflowInDir(dir: string, name: string): Promise<string | null> {
|
|
252
|
-
// Check .
|
|
262
|
+
// Check .workflows/ directory first (primary)
|
|
253
263
|
for (const ext of [".yaml", ".yml"]) {
|
|
254
|
-
const result = await workflowFileExists(resolvePath(dir, ".
|
|
264
|
+
const result = await workflowFileExists(resolvePath(dir, ".workflows"), name, ext);
|
|
255
265
|
if (result !== null) {
|
|
256
266
|
return result;
|
|
257
267
|
}
|
|
258
268
|
}
|
|
259
269
|
for (const indexName of ["index.yaml", "index.yml"]) {
|
|
260
|
-
const candidate = resolvePath(dir, ".
|
|
270
|
+
const candidate = resolvePath(dir, ".workflows", name, indexName);
|
|
261
271
|
try {
|
|
262
272
|
await access(candidate);
|
|
263
273
|
return candidate;
|
|
@@ -266,15 +276,15 @@ async function findWorkflowInDir(dir: string, name: string): Promise<string | nu
|
|
|
266
276
|
}
|
|
267
277
|
}
|
|
268
278
|
|
|
269
|
-
// Check .
|
|
279
|
+
// Check .workflow/ directory as fallback (legacy)
|
|
270
280
|
for (const ext of [".yaml", ".yml"]) {
|
|
271
|
-
const result = await workflowFileExists(resolvePath(dir, ".
|
|
281
|
+
const result = await workflowFileExists(resolvePath(dir, ".workflow"), name, ext);
|
|
272
282
|
if (result !== null) {
|
|
273
283
|
return result;
|
|
274
284
|
}
|
|
275
285
|
}
|
|
276
286
|
for (const indexName of ["index.yaml", "index.yml"]) {
|
|
277
|
-
const candidate = resolvePath(dir, ".
|
|
287
|
+
const candidate = resolvePath(dir, ".workflow", name, indexName);
|
|
278
288
|
try {
|
|
279
289
|
await access(candidate);
|
|
280
290
|
return candidate;
|
|
@@ -286,8 +296,21 @@ async function findWorkflowInDir(dir: string, name: string): Promise<string | nu
|
|
|
286
296
|
return null;
|
|
287
297
|
}
|
|
288
298
|
|
|
299
|
+
/** Check if a directory contains a .git marker (directory or file). */
|
|
300
|
+
async function hasGitMarker(dir: string): Promise<boolean> {
|
|
301
|
+
try {
|
|
302
|
+
await access(join(dir, ".git"));
|
|
303
|
+
return true;
|
|
304
|
+
} catch {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
289
309
|
/**
|
|
290
|
-
* Traverse parent directories looking for
|
|
310
|
+
* Traverse parent directories looking for a workflow named `name` under
|
|
311
|
+
* `.workflows/` (primary) or `.workflow/` (legacy fallback). Within each
|
|
312
|
+
* directory the lookup checks flat YAML files (`<name>.yaml`/`.yml`) and
|
|
313
|
+
* folder-based layouts (`<name>/index.yaml`/`.yml`).
|
|
291
314
|
* Returns the absolute path if found, otherwise null.
|
|
292
315
|
* Stops at filesystem root or .git directory.
|
|
293
316
|
*/
|
|
@@ -301,6 +324,11 @@ async function findWorkflowInParents(startDir: string, name: string): Promise<st
|
|
|
301
324
|
return found;
|
|
302
325
|
}
|
|
303
326
|
|
|
327
|
+
// Stop at .git boundary (repo root)
|
|
328
|
+
if (await hasGitMarker(currentDir)) {
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
|
|
304
332
|
// Stop at filesystem root
|
|
305
333
|
if (currentDir === root) {
|
|
306
334
|
break;
|
|
@@ -492,8 +520,8 @@ export async function cmdThreadShow(
|
|
|
492
520
|
fail(`failed to resolve workflow from head: ${activeHead}`);
|
|
493
521
|
}
|
|
494
522
|
|
|
495
|
-
// Determine if this is
|
|
496
|
-
if (entry.status === "
|
|
523
|
+
// Determine if this is an ended/cancelled thread
|
|
524
|
+
if (entry.status === "end" || entry.status === "cancelled") {
|
|
497
525
|
const hint = null;
|
|
498
526
|
return {
|
|
499
527
|
workflow,
|
|
@@ -505,14 +533,15 @@ export async function cmdThreadShow(
|
|
|
505
533
|
suspendMessage: null,
|
|
506
534
|
done: true,
|
|
507
535
|
background: null,
|
|
536
|
+
error: null,
|
|
508
537
|
hint,
|
|
509
538
|
};
|
|
510
539
|
}
|
|
511
540
|
|
|
512
541
|
// Active thread
|
|
513
|
-
const status = await resolveActiveThreadStatus(storageRoot, threadId, uwf, activeHead
|
|
542
|
+
const status = await resolveActiveThreadStatus(storageRoot, threadId, uwf, activeHead);
|
|
514
543
|
const currentRole = resolveCurrentRole(uwf, activeHead, workflow);
|
|
515
|
-
const suspendFields = resolveSuspendFieldsForShow(entry, status, uwf, activeHead
|
|
544
|
+
const suspendFields = resolveSuspendFieldsForShow(entry, status, uwf, activeHead);
|
|
516
545
|
|
|
517
546
|
const hint =
|
|
518
547
|
status === "suspended"
|
|
@@ -529,6 +558,7 @@ export async function cmdThreadShow(
|
|
|
529
558
|
suspendMessage: suspendFields.suspendMessage,
|
|
530
559
|
done: false,
|
|
531
560
|
background: null,
|
|
561
|
+
error: null,
|
|
532
562
|
hint,
|
|
533
563
|
};
|
|
534
564
|
}
|
|
@@ -538,6 +568,8 @@ export type ThreadListItemWithStatus = ThreadListItem & {
|
|
|
538
568
|
currentRole: string | null;
|
|
539
569
|
/** Display label with status marker for suspended threads */
|
|
540
570
|
statusDisplay: string;
|
|
571
|
+
/** Resolved workflow name from registry, or null if orphaned (hash not in registry) */
|
|
572
|
+
workflowName: string | null;
|
|
541
573
|
};
|
|
542
574
|
|
|
543
575
|
export type ThreadShowOutput = StepOutput & {
|
|
@@ -550,13 +582,23 @@ async function threadListItemFromActive(
|
|
|
550
582
|
uwf: UwfStore,
|
|
551
583
|
threadId: ThreadId,
|
|
552
584
|
head: CasRef,
|
|
585
|
+
registry: WorkflowRegistry,
|
|
553
586
|
): Promise<ThreadListItemWithStatus | null> {
|
|
554
587
|
const workflow = resolveWorkflowFromHead(uwf, head);
|
|
555
588
|
if (workflow === null) {
|
|
556
|
-
|
|
589
|
+
// Head CAS node missing or unrecognized — treat as corrupt rather than silently skipping
|
|
590
|
+
return {
|
|
591
|
+
thread: threadId,
|
|
592
|
+
workflow: "" as CasRef,
|
|
593
|
+
head,
|
|
594
|
+
status: "corrupt",
|
|
595
|
+
currentRole: null,
|
|
596
|
+
statusDisplay: "corrupt",
|
|
597
|
+
workflowName: null,
|
|
598
|
+
};
|
|
557
599
|
}
|
|
558
600
|
|
|
559
|
-
const status = await resolveActiveThreadStatus(storageRoot, threadId, uwf, head
|
|
601
|
+
const status = await resolveActiveThreadStatus(storageRoot, threadId, uwf, head);
|
|
560
602
|
const statusDisplay = status === "suspended" ? `${status} [suspended]` : status;
|
|
561
603
|
|
|
562
604
|
return {
|
|
@@ -566,6 +608,7 @@ async function threadListItemFromActive(
|
|
|
566
608
|
status,
|
|
567
609
|
currentRole: resolveCurrentRole(uwf, head, workflow),
|
|
568
610
|
statusDisplay,
|
|
611
|
+
workflowName: findRegistryName(registry, workflow),
|
|
569
612
|
};
|
|
570
613
|
}
|
|
571
614
|
|
|
@@ -573,12 +616,33 @@ async function collectActiveThreads(
|
|
|
573
616
|
storageRoot: string,
|
|
574
617
|
uwf: UwfStore,
|
|
575
618
|
index: ThreadsIndex,
|
|
619
|
+
registry: WorkflowRegistry,
|
|
576
620
|
): Promise<ThreadListItemWithStatus[]> {
|
|
577
621
|
const items: ThreadListItemWithStatus[] = [];
|
|
578
622
|
for (const [threadId, entry] of Object.entries(index)) {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
623
|
+
try {
|
|
624
|
+
const item = await threadListItemFromActive(
|
|
625
|
+
storageRoot,
|
|
626
|
+
uwf,
|
|
627
|
+
threadId as ThreadId,
|
|
628
|
+
entry.head,
|
|
629
|
+
registry,
|
|
630
|
+
);
|
|
631
|
+
if (item !== null) {
|
|
632
|
+
items.push(item);
|
|
633
|
+
}
|
|
634
|
+
} catch (err) {
|
|
635
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
636
|
+
process.stderr.write(`warning: thread ${threadId} is corrupt: ${message}\n`);
|
|
637
|
+
items.push({
|
|
638
|
+
thread: threadId as ThreadId,
|
|
639
|
+
workflow: "" as CasRef,
|
|
640
|
+
head: entry.head,
|
|
641
|
+
status: "corrupt",
|
|
642
|
+
currentRole: null,
|
|
643
|
+
statusDisplay: "corrupt",
|
|
644
|
+
workflowName: null,
|
|
645
|
+
});
|
|
582
646
|
}
|
|
583
647
|
}
|
|
584
648
|
return items;
|
|
@@ -587,6 +651,7 @@ async function collectActiveThreads(
|
|
|
587
651
|
function collectCompletedThreads(
|
|
588
652
|
uwf: UwfStore,
|
|
589
653
|
activeIds: Set<ThreadId>,
|
|
654
|
+
registry: WorkflowRegistry,
|
|
590
655
|
): ThreadListItemWithStatus[] {
|
|
591
656
|
const items: ThreadListItemWithStatus[] = [];
|
|
592
657
|
const history = loadHistoryThreads(uwf.varStore);
|
|
@@ -594,16 +659,31 @@ function collectCompletedThreads(
|
|
|
594
659
|
for (const [threadId, entry] of Object.entries(history)) {
|
|
595
660
|
if (!activeIds.has(threadId as ThreadId) && !seen.has(threadId as ThreadId)) {
|
|
596
661
|
seen.add(threadId as ThreadId);
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
662
|
+
try {
|
|
663
|
+
const status = entry.status;
|
|
664
|
+
const workflow = resolveWorkflowFromHead(uwf, entry.head);
|
|
665
|
+
items.push({
|
|
666
|
+
thread: threadId as ThreadId,
|
|
667
|
+
workflow: workflow ?? "",
|
|
668
|
+
head: entry.head,
|
|
669
|
+
status,
|
|
670
|
+
currentRole: null,
|
|
671
|
+
statusDisplay: status,
|
|
672
|
+
workflowName: workflow !== null ? findRegistryName(registry, workflow) : null,
|
|
673
|
+
});
|
|
674
|
+
} catch (err) {
|
|
675
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
676
|
+
process.stderr.write(`warning: completed thread ${threadId} is corrupt: ${message}\n`);
|
|
677
|
+
items.push({
|
|
678
|
+
thread: threadId as ThreadId,
|
|
679
|
+
workflow: "" as CasRef,
|
|
680
|
+
head: entry.head,
|
|
681
|
+
status: "corrupt",
|
|
682
|
+
currentRole: null,
|
|
683
|
+
statusDisplay: "corrupt",
|
|
684
|
+
workflowName: null,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
607
687
|
}
|
|
608
688
|
}
|
|
609
689
|
return items;
|
|
@@ -649,27 +729,35 @@ export async function cmdThreadList(
|
|
|
649
729
|
beforeMs: number | null,
|
|
650
730
|
skip: number | null,
|
|
651
731
|
take: number | null,
|
|
732
|
+
showAll: boolean = false,
|
|
652
733
|
): Promise<ThreadListItemWithStatus[]> {
|
|
653
734
|
const uwf = await createUwfStore(storageRoot);
|
|
654
735
|
const index = loadActiveThreads(uwf.varStore);
|
|
736
|
+
const registry = loadWorkflowRegistry(uwf.varStore);
|
|
737
|
+
|
|
738
|
+
// Resolve the effective filter:
|
|
739
|
+
// - explicit --status wins (showAll has no effect)
|
|
740
|
+
// - otherwise: --all → no filter; default → ["idle", "running"]
|
|
741
|
+
const effectiveFilter: ThreadStatus[] | null =
|
|
742
|
+
statusFilter !== null ? statusFilter : showAll ? null : ["idle", "running", "corrupt"];
|
|
655
743
|
|
|
656
744
|
// Collect active threads
|
|
657
|
-
let items = await collectActiveThreads(storageRoot, uwf, index);
|
|
745
|
+
let items = await collectActiveThreads(storageRoot, uwf, index, registry);
|
|
658
746
|
|
|
659
747
|
// Collect completed threads (if relevant for status filter)
|
|
660
748
|
const includeCompleted =
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
749
|
+
effectiveFilter === null ||
|
|
750
|
+
effectiveFilter.includes("end") ||
|
|
751
|
+
effectiveFilter.includes("cancelled");
|
|
664
752
|
if (includeCompleted) {
|
|
665
753
|
const activeIds = new Set(items.map((i) => i.thread));
|
|
666
|
-
const completedItems = collectCompletedThreads(uwf, activeIds);
|
|
754
|
+
const completedItems = collectCompletedThreads(uwf, activeIds, registry);
|
|
667
755
|
items = items.concat(completedItems);
|
|
668
756
|
}
|
|
669
757
|
|
|
670
758
|
// Apply status filter
|
|
671
|
-
if (
|
|
672
|
-
items = items.filter((item) =>
|
|
759
|
+
if (effectiveFilter !== null) {
|
|
760
|
+
items = items.filter((item) => effectiveFilter.includes(item.status));
|
|
673
761
|
}
|
|
674
762
|
|
|
675
763
|
// Apply time range filters
|
|
@@ -906,6 +994,15 @@ type EvaluateLastOutput = Record<string, unknown>;
|
|
|
906
994
|
|
|
907
995
|
const STATUS_KEY = "$status";
|
|
908
996
|
|
|
997
|
+
/**
|
|
998
|
+
* Strip YAML frontmatter (---...---) from a raw markdown string,
|
|
999
|
+
* returning only the body portion.
|
|
1000
|
+
*/
|
|
1001
|
+
function stripFrontmatter(raw: string): string {
|
|
1002
|
+
const match = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
1003
|
+
return match ? raw.slice(match[0].length).trim() : raw.trim();
|
|
1004
|
+
}
|
|
1005
|
+
|
|
909
1006
|
function resolveEvaluateArgs(
|
|
910
1007
|
uwf: UwfStore,
|
|
911
1008
|
chain: ChainState,
|
|
@@ -925,6 +1022,13 @@ function resolveEvaluateArgs(
|
|
|
925
1022
|
? (raw as Record<string, unknown>)
|
|
926
1023
|
: {};
|
|
927
1024
|
|
|
1025
|
+
// Inject _body — the markdown body (after frontmatter) from the last step's
|
|
1026
|
+
// assistant output. Workflow edge prompts can reference it via {{ _body }}.
|
|
1027
|
+
const content = extractLastAssistantContent(uwf, lastStep.detail);
|
|
1028
|
+
if (content !== null) {
|
|
1029
|
+
base._body = stripFrontmatter(content);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
928
1032
|
return {
|
|
929
1033
|
lastRole: lastStep.role,
|
|
930
1034
|
lastOutput: base,
|
|
@@ -934,10 +1038,10 @@ function resolveEvaluateArgs(
|
|
|
934
1038
|
function loadWorkflowPayload(uwf: UwfStore, workflowRef: CasRef): WorkflowPayload {
|
|
935
1039
|
const node = uwf.store.cas.get(workflowRef);
|
|
936
1040
|
if (node === null) {
|
|
937
|
-
|
|
1041
|
+
throw new Error(`workflow CAS node not found: ${workflowRef}`);
|
|
938
1042
|
}
|
|
939
1043
|
if (node.type !== uwf.schemas.workflow) {
|
|
940
|
-
|
|
1044
|
+
throw new Error(`node ${workflowRef} is not a Workflow`);
|
|
941
1045
|
}
|
|
942
1046
|
return node.payload as WorkflowPayload;
|
|
943
1047
|
}
|
|
@@ -985,18 +1089,14 @@ function resolveAgentConfig(
|
|
|
985
1089
|
return agentConfig;
|
|
986
1090
|
}
|
|
987
1091
|
|
|
988
|
-
function
|
|
989
|
-
plog: ProcessLogger,
|
|
1092
|
+
function executeAgentCommand(
|
|
990
1093
|
agent: AgentConfig,
|
|
991
|
-
|
|
992
|
-
role: string,
|
|
993
|
-
edgePrompt: string,
|
|
1094
|
+
argv: readonly string[],
|
|
994
1095
|
cwd: string,
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
let stdout: string;
|
|
1096
|
+
plog: ProcessLogger,
|
|
1097
|
+
): string {
|
|
998
1098
|
try {
|
|
999
|
-
|
|
1099
|
+
return execFileSync(agent.command, argv, {
|
|
1000
1100
|
encoding: "utf8",
|
|
1001
1101
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1002
1102
|
maxBuffer: 50 * 1024 * 1024, // 50 MB — stream-json output can be large
|
|
@@ -1019,14 +1119,22 @@ function spawnAgent(
|
|
|
1019
1119
|
const detail = stderr.trim() !== "" ? `: ${stderr.trim()}` : "";
|
|
1020
1120
|
failStep(plog, `agent command failed (${agent.command})${detail}`);
|
|
1021
1121
|
}
|
|
1122
|
+
}
|
|
1022
1123
|
|
|
1124
|
+
function parseAgentOutput(stdout: string, plog: ProcessLogger): unknown {
|
|
1023
1125
|
const line = stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
1024
|
-
let parsed: unknown;
|
|
1025
1126
|
try {
|
|
1026
|
-
|
|
1127
|
+
return JSON.parse(line);
|
|
1027
1128
|
} catch {
|
|
1028
1129
|
failStep(plog, `agent stdout last line is not valid JSON: ${line || "(empty)"}`);
|
|
1029
1130
|
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function validateAndNormalizeOutput(
|
|
1134
|
+
parsed: unknown,
|
|
1135
|
+
line: string,
|
|
1136
|
+
plog: ProcessLogger,
|
|
1137
|
+
): AdapterOutput {
|
|
1030
1138
|
const obj = parsed as Record<string, unknown>;
|
|
1031
1139
|
if (
|
|
1032
1140
|
typeof obj !== "object" ||
|
|
@@ -1036,11 +1144,44 @@ function spawnAgent(
|
|
|
1036
1144
|
) {
|
|
1037
1145
|
failStep(plog, `agent stdout JSON missing valid stepHash: ${line}`);
|
|
1038
1146
|
}
|
|
1147
|
+
// Normalize isError / errorMessage so downstream code can rely on them.
|
|
1148
|
+
// Legacy adapters that don't emit these fields default to isError=false.
|
|
1149
|
+
if (obj.isError !== undefined && typeof obj.isError !== "boolean") {
|
|
1150
|
+
failStep(plog, `agent stdout JSON has non-boolean isError: ${line}`);
|
|
1151
|
+
}
|
|
1152
|
+
if (obj.isError === undefined) {
|
|
1153
|
+
obj.isError = false;
|
|
1154
|
+
}
|
|
1155
|
+
if (
|
|
1156
|
+
obj.errorMessage !== undefined &&
|
|
1157
|
+
obj.errorMessage !== null &&
|
|
1158
|
+
typeof obj.errorMessage !== "string"
|
|
1159
|
+
) {
|
|
1160
|
+
failStep(plog, `agent stdout JSON has non-string errorMessage: ${line}`);
|
|
1161
|
+
}
|
|
1162
|
+
if (obj.errorMessage === undefined) {
|
|
1163
|
+
obj.errorMessage = null;
|
|
1164
|
+
}
|
|
1039
1165
|
return obj as unknown as AdapterOutput;
|
|
1040
1166
|
}
|
|
1041
1167
|
|
|
1168
|
+
function spawnAgent(
|
|
1169
|
+
plog: ProcessLogger,
|
|
1170
|
+
agent: AgentConfig,
|
|
1171
|
+
threadId: ThreadId,
|
|
1172
|
+
role: string,
|
|
1173
|
+
edgePrompt: string,
|
|
1174
|
+
cwd: string,
|
|
1175
|
+
): AdapterOutput {
|
|
1176
|
+
const argv = [...agent.args, "--thread", threadId, "--role", role, "--prompt", edgePrompt];
|
|
1177
|
+
const stdout = executeAgentCommand(agent, argv, cwd, plog);
|
|
1178
|
+
const line = stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
1179
|
+
const parsed = parseAgentOutput(stdout, plog);
|
|
1180
|
+
return validateAndNormalizeOutput(parsed, line, plog);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1042
1183
|
function archiveThread(uwf: UwfStore, threadId: ThreadId, _workflow: CasRef, _head: CasRef): void {
|
|
1043
|
-
completeThread(uwf.varStore, threadId, "
|
|
1184
|
+
completeThread(uwf.varStore, threadId, "end");
|
|
1044
1185
|
}
|
|
1045
1186
|
|
|
1046
1187
|
export async function cmdThreadResume(
|
|
@@ -1064,15 +1205,15 @@ export async function cmdThreadResume(
|
|
|
1064
1205
|
const chain = walkChain(uwf, headHash);
|
|
1065
1206
|
const workflowHash = chain.start.workflow;
|
|
1066
1207
|
|
|
1067
|
-
// Check entry.status first for
|
|
1208
|
+
// Check entry.status first for end/cancelled (like in cmdThreadShow)
|
|
1068
1209
|
let status: ThreadStatus;
|
|
1069
|
-
if (entry.status === "
|
|
1210
|
+
if (entry.status === "end" || entry.status === "cancelled") {
|
|
1070
1211
|
status = entry.status;
|
|
1071
1212
|
} else {
|
|
1072
|
-
status = await resolveActiveThreadStatus(storageRoot, threadId, uwf, headHash
|
|
1213
|
+
status = await resolveActiveThreadStatus(storageRoot, threadId, uwf, headHash);
|
|
1073
1214
|
}
|
|
1074
1215
|
|
|
1075
|
-
if (status !== "suspended" && status !== "
|
|
1216
|
+
if (status !== "suspended" && status !== "end") {
|
|
1076
1217
|
fail(`thread cannot be resumed: ${threadId} (status: ${status})`);
|
|
1077
1218
|
}
|
|
1078
1219
|
|
|
@@ -1082,7 +1223,7 @@ export async function cmdThreadResume(
|
|
|
1082
1223
|
});
|
|
1083
1224
|
|
|
1084
1225
|
if (status === "suspended") {
|
|
1085
|
-
const suspendFields = resolveSuspendFieldsForShow(entry, status, uwf, headHash
|
|
1226
|
+
const suspendFields = resolveSuspendFieldsForShow(entry, status, uwf, headHash);
|
|
1086
1227
|
if (suspendFields.suspendedRole === null) {
|
|
1087
1228
|
fail(`thread is suspended but suspendedRole is missing: ${threadId}`);
|
|
1088
1229
|
}
|
|
@@ -1104,21 +1245,18 @@ export async function cmdThreadResume(
|
|
|
1104
1245
|
});
|
|
1105
1246
|
}
|
|
1106
1247
|
|
|
1107
|
-
// status === "
|
|
1248
|
+
// status === "end"
|
|
1108
1249
|
const workflow = loadWorkflowPayload(uwf, workflowHash);
|
|
1109
1250
|
const startResult = evaluate(workflow.graph, START_ROLE, { [STATUS_KEY]: "resume" });
|
|
1110
1251
|
if (!startResult.ok) {
|
|
1111
1252
|
fail(`failed to evaluate $START: ${startResult.error.message}`);
|
|
1112
1253
|
}
|
|
1113
|
-
if (isSuspendResult(startResult.value)) {
|
|
1114
|
-
fail("workflow cannot start with $SUSPEND");
|
|
1115
|
-
}
|
|
1116
1254
|
if (startResult.value.role === END_ROLE) {
|
|
1117
1255
|
fail("workflow cannot start with $END");
|
|
1118
1256
|
}
|
|
1119
1257
|
|
|
1120
1258
|
const startRole = startResult.value.role;
|
|
1121
|
-
const
|
|
1259
|
+
const endResumePrompt = buildResumePrompt(startResult.value.prompt, supplement);
|
|
1122
1260
|
|
|
1123
1261
|
const updatedEntry = { ...entry, status: "idle" as const, completedAt: null };
|
|
1124
1262
|
setThread(uwf.varStore, threadId, updatedEntry);
|
|
@@ -1131,16 +1269,180 @@ export async function cmdThreadResume(
|
|
|
1131
1269
|
|
|
1132
1270
|
return cmdThreadStepOnce(storageRoot, threadId, agentOverride, plog, {
|
|
1133
1271
|
role: startRole,
|
|
1134
|
-
prompt:
|
|
1272
|
+
prompt: endResumePrompt,
|
|
1135
1273
|
});
|
|
1136
1274
|
}
|
|
1137
1275
|
|
|
1276
|
+
/**
|
|
1277
|
+
* Validate that a thread can be poked. Returns the existing entry and the head StepNode payload.
|
|
1278
|
+
* Fails (process exit) when the thread is missing, running, completed, cancelled, or has no
|
|
1279
|
+
* StepNode at its head.
|
|
1280
|
+
*/
|
|
1281
|
+
async function validatePokePreconditions(
|
|
1282
|
+
storageRoot: string,
|
|
1283
|
+
uwf: UwfStore,
|
|
1284
|
+
threadId: ThreadId,
|
|
1285
|
+
): Promise<{ entry: ThreadIndexEntry; oldHead: CasRef; oldHeadPayload: StepNodePayload }> {
|
|
1286
|
+
const runningMarker = await isThreadRunning(storageRoot, threadId);
|
|
1287
|
+
if (runningMarker !== null) {
|
|
1288
|
+
fail(`thread already executing in background (PID: ${runningMarker.pid})`);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
const entry = getThread(uwf.varStore, threadId);
|
|
1292
|
+
if (entry === null) {
|
|
1293
|
+
fail(`thread not active: ${threadId}`);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
if (entry.status === "end" || entry.status === "cancelled") {
|
|
1297
|
+
fail(`thread cannot be poked: ${threadId} (status: ${entry.status})`);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
const oldHead = entry.head;
|
|
1301
|
+
const oldHeadNode = uwf.store.cas.get(oldHead);
|
|
1302
|
+
if (oldHeadNode === null) {
|
|
1303
|
+
fail(`CAS node not found: ${oldHead}`);
|
|
1304
|
+
}
|
|
1305
|
+
if (oldHeadNode.type !== uwf.schemas.stepNode) {
|
|
1306
|
+
fail("thread cannot be poked: no step to replace (head is StartNode)");
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
return { entry, oldHead, oldHeadPayload: oldHeadNode.payload as StepNodePayload };
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Resolve the next role from the post-poke chain state, used for the StepOutput.currentRole field.
|
|
1314
|
+
* Returns null when the next role is $END, evaluation fails, or the result is a suspend.
|
|
1315
|
+
*/
|
|
1316
|
+
function resolveCurrentRoleFromChain(
|
|
1317
|
+
uwfAfter: UwfStore,
|
|
1318
|
+
workflow: WorkflowPayload,
|
|
1319
|
+
replacedHash: CasRef,
|
|
1320
|
+
): string | null {
|
|
1321
|
+
const chainAfter = walkChain(uwfAfter, replacedHash);
|
|
1322
|
+
const { lastRole, lastOutput } = resolveEvaluateArgs(uwfAfter, chainAfter);
|
|
1323
|
+
if (readSuspendReason(lastOutput) !== null) {
|
|
1324
|
+
return null;
|
|
1325
|
+
}
|
|
1326
|
+
const afterResult = evaluate(workflow.graph, lastRole, lastOutput);
|
|
1327
|
+
if (!afterResult.ok) {
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
if (afterResult.value.role === END_ROLE) {
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
return afterResult.value.role;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* Poke a thread: re-run the agent on the head step with a supplementary prompt,
|
|
1338
|
+
* replacing the head step's output. The new step's `prev` points to the OLD head's
|
|
1339
|
+
* `prev` — semantically replacing (not appending to) the head. The moderator is NOT
|
|
1340
|
+
* re-evaluated for routing; the role of the head step is re-used.
|
|
1341
|
+
*/
|
|
1342
|
+
export async function cmdThreadPoke(
|
|
1343
|
+
storageRoot: string,
|
|
1344
|
+
threadId: ThreadId,
|
|
1345
|
+
prompt: string,
|
|
1346
|
+
agentOverride: string | null,
|
|
1347
|
+
): Promise<StepOutput> {
|
|
1348
|
+
const uwf = await createUwfStore(storageRoot);
|
|
1349
|
+
const { entry, oldHeadPayload } = await validatePokePreconditions(storageRoot, uwf, threadId);
|
|
1350
|
+
|
|
1351
|
+
const chain = walkChain(uwf, entry.head);
|
|
1352
|
+
const workflowHash = chain.start.workflow;
|
|
1353
|
+
const threadCwd = chain.start.cwd;
|
|
1354
|
+
|
|
1355
|
+
const plog = createProcessLogger({
|
|
1356
|
+
storageRoot,
|
|
1357
|
+
context: { thread: threadId, workflow: workflowHash },
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
// Resolve the agent: --agent override wins; otherwise read from old head step's `agent` field.
|
|
1361
|
+
const config = await loadWorkflowConfig(storageRoot);
|
|
1362
|
+
const workflow = loadWorkflowPayload(uwf, workflowHash);
|
|
1363
|
+
const role = oldHeadPayload.role;
|
|
1364
|
+
const agent =
|
|
1365
|
+
agentOverride !== null
|
|
1366
|
+
? resolveAgentConfig(config, workflow, role, agentOverride)
|
|
1367
|
+
: parseAgentOverride(oldHeadPayload.agent);
|
|
1368
|
+
|
|
1369
|
+
const effectiveCwd = oldHeadPayload.cwd !== "" ? oldHeadPayload.cwd : threadCwd;
|
|
1370
|
+
|
|
1371
|
+
plog.log(PL_THREAD_POKE, `poke role=${role} agent=${agent.command}`, null);
|
|
1372
|
+
plog.log(PL_AGENT_SPAWN, `spawning agent command=${agent.command}`, {
|
|
1373
|
+
args: [...agent.args, threadId, role].join(" "),
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
loadDotenv({ path: getEnvPath(storageRoot) });
|
|
1377
|
+
|
|
1378
|
+
// Spawn the agent. The agent will create a new StepNode with prev=oldHead (it reads
|
|
1379
|
+
// the active thread head). After the agent returns, we rewrite that node's prev so
|
|
1380
|
+
// that the new head replaces the old head instead of appending after it.
|
|
1381
|
+
const agentResult = spawnAgent(plog, agent, threadId, role, prompt, effectiveCwd);
|
|
1382
|
+
const agentStepHash = agentResult.stepHash as CasRef;
|
|
1383
|
+
|
|
1384
|
+
plog.log(PL_AGENT_DONE, `agent returned head=${agentStepHash}`, null);
|
|
1385
|
+
|
|
1386
|
+
const uwfAfter = await createUwfStore(storageRoot);
|
|
1387
|
+
const agentNode = uwfAfter.store.cas.get(agentStepHash);
|
|
1388
|
+
if (agentNode === null || agentNode.type !== uwfAfter.schemas.stepNode) {
|
|
1389
|
+
failStep(plog, `agent returned hash that is not a StepNode: ${agentStepHash}`);
|
|
1390
|
+
}
|
|
1391
|
+
const agentPayload = agentNode.payload as StepNodePayload;
|
|
1392
|
+
|
|
1393
|
+
// Rewrite the new step so that its `prev` points to the OLD head's prev (replace semantics).
|
|
1394
|
+
const replacedPayload: StepNodePayload = {
|
|
1395
|
+
...agentPayload,
|
|
1396
|
+
prev: oldHeadPayload.prev,
|
|
1397
|
+
};
|
|
1398
|
+
const replacedHash = await uwfAfter.store.cas.put(uwfAfter.schemas.stepNode, replacedPayload);
|
|
1399
|
+
const replacedNode = uwfAfter.store.cas.get(replacedHash);
|
|
1400
|
+
if (replacedNode === null || !validate(uwfAfter.store, replacedNode)) {
|
|
1401
|
+
failStep(plog, "rewritten StepNode failed schema validation");
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// Update thread head to the replaced step. Status becomes idle (no moderator re-route).
|
|
1405
|
+
setThread(uwfAfter.varStore, threadId, updateThreadHead(entry, replacedHash));
|
|
1406
|
+
|
|
1407
|
+
return {
|
|
1408
|
+
workflow: workflowHash,
|
|
1409
|
+
thread: threadId,
|
|
1410
|
+
head: replacedHash,
|
|
1411
|
+
status: "idle",
|
|
1412
|
+
currentRole: resolveCurrentRoleFromChain(uwfAfter, workflow, replacedHash),
|
|
1413
|
+
suspendedRole: null,
|
|
1414
|
+
suspendMessage: null,
|
|
1415
|
+
done: false,
|
|
1416
|
+
background: null,
|
|
1417
|
+
error: null,
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1138
1421
|
export function validateCount(count: number): void {
|
|
1139
1422
|
if (count < 1 || !Number.isInteger(count)) {
|
|
1140
1423
|
throw new Error(`--count must be a positive integer, got: ${count}`);
|
|
1141
1424
|
}
|
|
1142
1425
|
}
|
|
1143
1426
|
|
|
1427
|
+
/**
|
|
1428
|
+
* Resolve the effective maxRunning limit.
|
|
1429
|
+
* Priority: config file > DEFAULT_MAX_RUNNING (2).
|
|
1430
|
+
*/
|
|
1431
|
+
async function resolveMaxRunning(storageRoot: string): Promise<number> {
|
|
1432
|
+
try {
|
|
1433
|
+
const configPath = getConfigPath(storageRoot);
|
|
1434
|
+
const config = loadConfig(configPath);
|
|
1435
|
+
const path = parseDotPath("concurrency.maxRunning");
|
|
1436
|
+
const value = getNestedValue(config, path);
|
|
1437
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
1438
|
+
return value;
|
|
1439
|
+
}
|
|
1440
|
+
} catch {
|
|
1441
|
+
// Config file missing or invalid — fall through to default
|
|
1442
|
+
}
|
|
1443
|
+
return DEFAULT_MAX_RUNNING;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1144
1446
|
export async function cmdThreadExec(
|
|
1145
1447
|
storageRoot: string,
|
|
1146
1448
|
threadId: ThreadId,
|
|
@@ -1151,11 +1453,12 @@ export async function cmdThreadExec(
|
|
|
1151
1453
|
): Promise<StepOutput[]> {
|
|
1152
1454
|
validateCount(count);
|
|
1153
1455
|
|
|
1154
|
-
//
|
|
1456
|
+
// Reject concurrent exec on the same thread (unless we ARE the background worker,
|
|
1457
|
+
// which hasn't created its own marker yet at this point).
|
|
1155
1458
|
if (!backgroundWorker) {
|
|
1156
1459
|
const runningMarker = await isThreadRunning(storageRoot, threadId);
|
|
1157
1460
|
if (runningMarker !== null) {
|
|
1158
|
-
fail(`thread already
|
|
1461
|
+
fail(`thread ${threadId} is already being executed by PID ${runningMarker.pid}`);
|
|
1159
1462
|
}
|
|
1160
1463
|
}
|
|
1161
1464
|
|
|
@@ -1170,17 +1473,22 @@ export async function cmdThreadExec(
|
|
|
1170
1473
|
return cmdThreadStepBackground(storageRoot, threadId, agentOverride, count, plog, workflowHash);
|
|
1171
1474
|
}
|
|
1172
1475
|
|
|
1173
|
-
//
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1476
|
+
// Create running marker so `thread list` shows "running" during execution
|
|
1477
|
+
// and concurrent `exec` on the same thread is rejected (see check above).
|
|
1478
|
+
await createMarker(storageRoot, {
|
|
1479
|
+
thread: threadId,
|
|
1480
|
+
workflow: workflowHash,
|
|
1481
|
+
pid: process.pid,
|
|
1482
|
+
startedAt: Date.now(),
|
|
1483
|
+
processStartTime: getProcessStartTime(process.pid),
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
// Resolve concurrency limit: config > default
|
|
1487
|
+
const effectiveMaxRunning = await resolveMaxRunning(storageRoot);
|
|
1488
|
+
|
|
1489
|
+
// Acquire concurrency slot (blocks if at capacity)
|
|
1490
|
+
const slotHandle = await acquireSlot(storageRoot, effectiveMaxRunning);
|
|
1491
|
+
const uninstallCleanup = installSlotCleanup(slotHandle);
|
|
1184
1492
|
|
|
1185
1493
|
try {
|
|
1186
1494
|
const results: StepOutput[] = [];
|
|
@@ -1193,10 +1501,9 @@ export async function cmdThreadExec(
|
|
|
1193
1501
|
}
|
|
1194
1502
|
return results;
|
|
1195
1503
|
} finally {
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
}
|
|
1504
|
+
uninstallCleanup();
|
|
1505
|
+
await slotHandle.release();
|
|
1506
|
+
await deleteMarker(storageRoot, threadId);
|
|
1200
1507
|
}
|
|
1201
1508
|
}
|
|
1202
1509
|
|
|
@@ -1264,6 +1571,7 @@ async function cmdThreadStepBackground(
|
|
|
1264
1571
|
suspendMessage: null,
|
|
1265
1572
|
done: false,
|
|
1266
1573
|
background: true,
|
|
1574
|
+
error: null,
|
|
1267
1575
|
},
|
|
1268
1576
|
];
|
|
1269
1577
|
}
|
|
@@ -1296,6 +1604,16 @@ async function resolveModeratorStepTarget(
|
|
|
1296
1604
|
plog: ProcessLogger,
|
|
1297
1605
|
): Promise<StepOutput | AgentStepTarget> {
|
|
1298
1606
|
const { lastRole, lastOutput } = resolveEvaluateArgs(uwf, chain);
|
|
1607
|
+
|
|
1608
|
+
// Intercept an already-suspended head before the moderator: a thread whose
|
|
1609
|
+
// head step yielded `$status: "$SUSPEND"` stays suspended (idempotent re-exec).
|
|
1610
|
+
const suspendReason = readSuspendReason(lastOutput);
|
|
1611
|
+
if (suspendReason !== null) {
|
|
1612
|
+
await ensureThreadSuspendMetadata(uwf.varStore, threadId, entry, lastRole, suspendReason);
|
|
1613
|
+
plog.log(PL_MODERATOR, `moderator action=suspend suspendedRole=${lastRole}`, null);
|
|
1614
|
+
return buildSuspendStepOutput(workflowHash, threadId, headHash, lastRole, suspendReason);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1299
1617
|
const nextResult = evaluate(workflow.graph, lastRole, lastOutput);
|
|
1300
1618
|
if (!nextResult.ok) {
|
|
1301
1619
|
failStep(plog, `moderator evaluate failed: ${nextResult.error.message}`);
|
|
@@ -1303,32 +1621,10 @@ async function resolveModeratorStepTarget(
|
|
|
1303
1621
|
|
|
1304
1622
|
plog.log(
|
|
1305
1623
|
PL_MODERATOR,
|
|
1306
|
-
`moderator
|
|
1307
|
-
isSuspendResult(nextResult.value)
|
|
1308
|
-
? `action=suspend suspendedRole=${nextResult.value.suspendedRole}`
|
|
1309
|
-
: `role=${nextResult.value.role}`
|
|
1310
|
-
} prompt=${nextResult.value.prompt}`,
|
|
1624
|
+
`moderator role=${nextResult.value.role} prompt=${nextResult.value.prompt}`,
|
|
1311
1625
|
null,
|
|
1312
1626
|
);
|
|
1313
1627
|
|
|
1314
|
-
if (isSuspendResult(nextResult.value)) {
|
|
1315
|
-
await ensureThreadSuspendMetadata(
|
|
1316
|
-
uwf.varStore,
|
|
1317
|
-
threadId,
|
|
1318
|
-
entry,
|
|
1319
|
-
nextResult.value.suspendedRole,
|
|
1320
|
-
nextResult.value.prompt,
|
|
1321
|
-
);
|
|
1322
|
-
return buildStepOutputFromEvaluation(
|
|
1323
|
-
workflowHash,
|
|
1324
|
-
threadId,
|
|
1325
|
-
headHash,
|
|
1326
|
-
"suspended",
|
|
1327
|
-
nextResult,
|
|
1328
|
-
null,
|
|
1329
|
-
);
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
1628
|
if (nextResult.value.role === END_ROLE) {
|
|
1333
1629
|
plog.log(PL_THREAD_ARCHIVED, `thread archived head=${headHash}`, null);
|
|
1334
1630
|
archiveThread(uwf, threadId, workflowHash, headHash);
|
|
@@ -1336,12 +1632,13 @@ async function resolveModeratorStepTarget(
|
|
|
1336
1632
|
workflow: workflowHash,
|
|
1337
1633
|
thread: threadId,
|
|
1338
1634
|
head: headHash,
|
|
1339
|
-
status: "
|
|
1635
|
+
status: "end",
|
|
1340
1636
|
currentRole: null,
|
|
1341
1637
|
suspendedRole: null,
|
|
1342
1638
|
suspendMessage: null,
|
|
1343
1639
|
done: true,
|
|
1344
1640
|
background: null,
|
|
1641
|
+
error: null,
|
|
1345
1642
|
};
|
|
1346
1643
|
}
|
|
1347
1644
|
|
|
@@ -1369,29 +1666,27 @@ async function finalizeAgentStep(
|
|
|
1369
1666
|
uwfAfter,
|
|
1370
1667
|
chainAfter,
|
|
1371
1668
|
);
|
|
1372
|
-
const afterResult = evaluate(workflow.graph, lastRoleAfter, lastOutputAfter);
|
|
1373
|
-
if (!afterResult.ok) {
|
|
1374
|
-
failStep(plog, `post-step moderator evaluate failed: ${afterResult.error.message}`);
|
|
1375
|
-
}
|
|
1376
1669
|
|
|
1377
|
-
|
|
1670
|
+
// Intercept `$status: "$SUSPEND"` before the moderator (coroutine yield): the
|
|
1671
|
+
// step is already in CAS and the head has advanced — mark the thread suspended
|
|
1672
|
+
// and return without routing through the graph.
|
|
1673
|
+
const suspendReason = readSuspendReason(lastOutputAfter);
|
|
1674
|
+
if (suspendReason !== null) {
|
|
1378
1675
|
setThread(
|
|
1379
1676
|
uwfAfter.varStore,
|
|
1380
1677
|
threadId,
|
|
1381
1678
|
markThreadSuspended(
|
|
1382
1679
|
getThread(uwfAfter.varStore, threadId) ?? createThreadIndexEntry(newHead),
|
|
1383
|
-
|
|
1384
|
-
|
|
1680
|
+
lastRoleAfter,
|
|
1681
|
+
suspendReason,
|
|
1385
1682
|
),
|
|
1386
1683
|
);
|
|
1387
|
-
return
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
null,
|
|
1394
|
-
);
|
|
1684
|
+
return buildSuspendStepOutput(workflowHash, threadId, newHead, lastRoleAfter, suspendReason);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
const afterResult = evaluate(workflow.graph, lastRoleAfter, lastOutputAfter);
|
|
1688
|
+
if (!afterResult.ok) {
|
|
1689
|
+
failStep(plog, `post-step moderator evaluate failed: ${afterResult.error.message}`);
|
|
1395
1690
|
}
|
|
1396
1691
|
|
|
1397
1692
|
const done = afterResult.value.role === END_ROLE;
|
|
@@ -1400,7 +1695,7 @@ async function finalizeAgentStep(
|
|
|
1400
1695
|
archiveThread(uwfAfter, threadId, workflowHash, newHead);
|
|
1401
1696
|
}
|
|
1402
1697
|
|
|
1403
|
-
const status: ThreadStatus = done ? "
|
|
1698
|
+
const status: ThreadStatus = done ? "end" : "idle";
|
|
1404
1699
|
const currentRole = done ? null : afterResult.value.role;
|
|
1405
1700
|
|
|
1406
1701
|
return {
|
|
@@ -1413,6 +1708,7 @@ async function finalizeAgentStep(
|
|
|
1413
1708
|
suspendMessage: null,
|
|
1414
1709
|
done,
|
|
1415
1710
|
background: null,
|
|
1711
|
+
error: null,
|
|
1416
1712
|
};
|
|
1417
1713
|
}
|
|
1418
1714
|
|
|
@@ -1476,6 +1772,31 @@ async function cmdThreadStepOnce(
|
|
|
1476
1772
|
failStep(plog, `agent returned hash that is not a StepNode: ${newHead}`);
|
|
1477
1773
|
}
|
|
1478
1774
|
|
|
1775
|
+
// Recoverable failure: agent persisted a failed StepNode (e.g. frontmatter
|
|
1776
|
+
// validation exhausted retries) but the engine MUST NOT advance head. The
|
|
1777
|
+
// moderator graph is also untouched — the same role will be replayed on the
|
|
1778
|
+
// next exec (until eventual success records `previousAttempts` linking the
|
|
1779
|
+
// failed step hashes).
|
|
1780
|
+
if (agentResult.isError === true) {
|
|
1781
|
+
plog.log(
|
|
1782
|
+
PL_AGENT_ERROR,
|
|
1783
|
+
`agent reported recoverable failure stepHash=${newHead} message=${agentResult.errorMessage ?? ""}`,
|
|
1784
|
+
null,
|
|
1785
|
+
);
|
|
1786
|
+
return {
|
|
1787
|
+
workflow: workflowHash,
|
|
1788
|
+
thread: threadId,
|
|
1789
|
+
head: headHash,
|
|
1790
|
+
status: "idle",
|
|
1791
|
+
currentRole: role,
|
|
1792
|
+
suspendedRole: null,
|
|
1793
|
+
suspendMessage: null,
|
|
1794
|
+
done: false,
|
|
1795
|
+
background: null,
|
|
1796
|
+
error: { stepHash: newHead, message: agentResult.errorMessage ?? "agent reported error" },
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1479
1800
|
return finalizeAgentStep(storageRoot, threadId, workflowHash, workflow, newHead, uwfAfter, plog);
|
|
1480
1801
|
}
|
|
1481
1802
|
|
|
@@ -1526,7 +1847,9 @@ export type CancelOutput = {
|
|
|
1526
1847
|
};
|
|
1527
1848
|
|
|
1528
1849
|
/**
|
|
1529
|
-
* Stop background execution of a thread (but keep thread active)
|
|
1850
|
+
* Stop background execution of a thread (but keep thread active).
|
|
1851
|
+
* Validates process identity before sending signals to prevent killing
|
|
1852
|
+
* unrelated processes when PIDs are recycled.
|
|
1530
1853
|
*/
|
|
1531
1854
|
export async function cmdThreadStop(storageRoot: string, threadId: ThreadId): Promise<StopOutput> {
|
|
1532
1855
|
const uwf = await createUwfStore(storageRoot);
|
|
@@ -1535,15 +1858,26 @@ export async function cmdThreadStop(storageRoot: string, threadId: ThreadId): Pr
|
|
|
1535
1858
|
fail(`thread not active: ${threadId}`);
|
|
1536
1859
|
}
|
|
1537
1860
|
|
|
1538
|
-
//
|
|
1539
|
-
const
|
|
1540
|
-
if (
|
|
1861
|
+
// Read the raw marker to check process identity
|
|
1862
|
+
const marker = await readMarker(storageRoot, threadId);
|
|
1863
|
+
if (marker === null) {
|
|
1541
1864
|
process.stderr.write(`Warning: thread ${threadId} is not currently running\n`);
|
|
1542
1865
|
return { thread: threadId, stopped: false };
|
|
1543
1866
|
}
|
|
1544
1867
|
|
|
1868
|
+
// Validate that the marker's PID still belongs to the same process
|
|
1869
|
+
if (!isMarkerValid(marker)) {
|
|
1870
|
+
// Stale marker — PID was recycled or process died. Do NOT send a signal.
|
|
1871
|
+
process.stderr.write(
|
|
1872
|
+
`Warning: thread ${threadId} was not actually running (stale marker cleaned up)\n`,
|
|
1873
|
+
);
|
|
1874
|
+
await deleteMarker(storageRoot, threadId);
|
|
1875
|
+
return { thread: threadId, stopped: false };
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// Process identity confirmed — safe to send SIGTERM
|
|
1545
1879
|
try {
|
|
1546
|
-
process.kill(
|
|
1880
|
+
process.kill(marker.pid, "SIGTERM");
|
|
1547
1881
|
} catch {
|
|
1548
1882
|
// Process may have already exited, ignore error
|
|
1549
1883
|
}
|
|
@@ -1553,7 +1887,9 @@ export async function cmdThreadStop(storageRoot: string, threadId: ThreadId): Pr
|
|
|
1553
1887
|
}
|
|
1554
1888
|
|
|
1555
1889
|
/**
|
|
1556
|
-
* Cancel a thread (stop execution + move to history)
|
|
1890
|
+
* Cancel a thread (stop execution + move to history).
|
|
1891
|
+
* Validates process identity before sending signals to prevent killing
|
|
1892
|
+
* unrelated processes when PIDs are recycled.
|
|
1557
1893
|
*/
|
|
1558
1894
|
export async function cmdThreadCancel(
|
|
1559
1895
|
storageRoot: string,
|
|
@@ -1565,14 +1901,18 @@ export async function cmdThreadCancel(
|
|
|
1565
1901
|
fail(`thread not active: ${threadId}`);
|
|
1566
1902
|
}
|
|
1567
1903
|
|
|
1568
|
-
//
|
|
1569
|
-
const
|
|
1570
|
-
if (
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1904
|
+
// Read the raw marker and validate process identity before sending signals
|
|
1905
|
+
const marker = await readMarker(storageRoot, threadId);
|
|
1906
|
+
if (marker !== null) {
|
|
1907
|
+
if (isMarkerValid(marker)) {
|
|
1908
|
+
// Process identity confirmed — safe to send SIGTERM
|
|
1909
|
+
try {
|
|
1910
|
+
process.kill(marker.pid, "SIGTERM");
|
|
1911
|
+
} catch {
|
|
1912
|
+
// Process may have already exited, ignore error
|
|
1913
|
+
}
|
|
1575
1914
|
}
|
|
1915
|
+
// Always delete the marker (stale or not) — cancellation proceeds
|
|
1576
1916
|
await deleteMarker(storageRoot, threadId);
|
|
1577
1917
|
}
|
|
1578
1918
|
|