principles-disciple 1.72.0 → 1.73.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/openclaw.plugin.json +10 -5
- package/package.json +17 -19
- package/scripts/acceptance-test.mjs +16 -73
- package/scripts/sync-plugin.mjs +382 -77
- package/src/commands/archive-impl.ts +2 -1
- package/src/commands/capabilities.ts +2 -2
- package/src/commands/context.ts +2 -2
- package/src/commands/disable-impl.ts +2 -1
- package/src/commands/evolution-status.ts +16 -16
- package/src/commands/export.ts +12 -67
- package/src/commands/pain.ts +91 -1
- package/src/commands/principle-rollback.ts +2 -1
- package/src/commands/promote-impl.ts +7 -43
- package/src/commands/rollback-impl.ts +2 -1
- package/src/commands/rollback.ts +2 -1
- package/src/commands/samples.ts +2 -1
- package/src/commands/thinking-os.ts +2 -1
- package/src/config/errors.ts +18 -2
- package/src/constants/diagnostician.ts +2 -2
- package/src/constants/tools.ts +2 -1
- package/src/core/__tests__/focus-history.test.ts +210 -0
- package/src/core/config.ts +1 -1
- package/src/core/confirm-first-gate.ts +255 -0
- package/src/core/correction-cue-learner.ts +2 -136
- package/src/core/correction-types.ts +16 -88
- package/src/core/dictionary.ts +19 -20
- package/src/core/empathy-keyword-matcher.ts +17 -289
- package/src/core/empathy-types.ts +18 -229
- package/src/core/event-log.ts +38 -132
- package/src/core/evolution-reducer.ts +21 -2
- package/src/core/evolution-types.ts +76 -464
- package/src/core/file-store.ts +80 -0
- package/src/core/focus-history.ts +228 -955
- package/src/core/local-worker-routing.ts +34 -314
- package/src/core/merge-gate-audit.ts +0 -195
- package/src/core/pain-diagnostic-gate.ts +154 -0
- package/src/core/pain-signal.ts +21 -138
- package/src/core/pain.ts +15 -88
- package/src/core/pd-task-reconciler.ts +26 -115
- package/src/core/pd-task-service.ts +9 -9
- package/src/core/pd-task-types.ts +23 -127
- package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
- package/src/core/principle-compiler/code-validator.ts +15 -42
- package/src/core/principle-compiler/compiler.ts +100 -15
- package/src/core/principle-compiler/index.ts +5 -2
- package/src/core/principle-compiler/template-generator.ts +4 -104
- package/src/core/principle-injection.ts +10 -202
- package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
- package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
- package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
- package/src/core/principle-tree-ledger-adapter.ts +145 -0
- package/src/core/principle-tree-ledger.ts +8 -6
- package/src/core/reflection/reflection-context.ts +14 -109
- package/src/core/replay-engine.ts +8 -500
- package/src/core/rule-host-helpers.ts +5 -35
- package/src/core/rule-host-types.ts +10 -82
- package/src/core/rule-host.ts +6 -63
- package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
- package/src/core/session-tracker.ts +87 -101
- package/src/core/shadow-observation-registry.ts +19 -48
- package/src/core/trajectory.ts +3 -1
- package/src/core/workflow-funnel-loader.ts +62 -68
- package/src/core/workspace-context.ts +46 -0
- package/src/core/workspace-dir-service.ts +1 -1
- package/src/core/workspace-dir-validation.ts +18 -9
- package/src/hooks/AGENTS.md +1 -1
- package/src/hooks/gate-block-helper.ts +46 -44
- package/src/hooks/gate.ts +207 -7
- package/src/hooks/lifecycle.ts +30 -32
- package/src/hooks/llm.ts +60 -32
- package/src/hooks/pain.ts +297 -103
- package/src/hooks/prompt.ts +459 -439
- package/src/hooks/subagent.ts +2 -29
- package/src/i18n/commands.ts +2 -10
- package/src/index.ts +95 -85
- package/src/openclaw-sdk.ts +311 -0
- package/src/service/central-database.ts +8 -4
- package/src/service/evolution-queue-migration.ts +2 -1
- package/src/service/evolution-worker.ts +163 -1786
- package/src/service/internalization-trigger-adapter.ts +302 -0
- package/src/service/keyword-optimization-service.ts +4 -4
- package/src/service/monitoring-query-service.ts +1 -215
- package/src/service/queue-io.ts +60 -331
- package/src/service/runtime-summary-service.ts +59 -16
- package/src/service/subagent-workflow/index.ts +0 -41
- package/src/service/subagent-workflow/types.ts +9 -120
- package/src/service/subagent-workflow/workflow-store.ts +2 -119
- package/src/service/workflow-watchdog.ts +0 -43
- package/src/types/event-payload.ts +16 -74
- package/src/types/event-types.ts +39 -547
- package/src/types/hygiene-types.ts +7 -30
- package/src/types/principle-tree-schema.ts +20 -222
- package/src/types/queue.ts +15 -70
- package/src/types/runtime-summary.ts +5 -49
- package/src/utils/io.ts +10 -0
- package/src/utils/retry.ts +1 -1
- package/src/utils/shadow-fingerprint.ts +2 -2
- package/src/utils/workspace-resolver.ts +50 -0
- package/templates/langs/en/core/AGENTS.md +2 -2
- package/templates/langs/en/core/BOOT.md +1 -1
- package/templates/langs/en/core/HEARTBEAT.md +2 -2
- package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
- package/templates/langs/en/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
- package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
- package/templates/langs/en/skills/evolve-task/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
- package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-mentor/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
- package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
- package/templates/langs/zh/core/AGENTS.md +2 -2
- package/templates/langs/zh/core/BOOT.md +1 -1
- package/templates/langs/zh/core/HEARTBEAT.md +2 -2
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
- package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
- package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
- package/templates/langs/zh/skills/evolve-task/SKILL.md +2 -2
- package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-mentor/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
- package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
- package/tests/build-artifacts.test.ts +1 -3
- package/tests/commands/evolution-status.test.ts +0 -118
- package/tests/core/bootstrap-rules.test.ts +1 -1
- package/tests/core/config.test.ts +1 -1
- package/tests/core/event-log.test.ts +35 -0
- package/tests/core/evolution-engine.test.ts +610 -0
- package/tests/core/file-store.test.ts +102 -0
- package/tests/core/focus-history.test.ts +203 -11
- package/tests/core/merge-gate-audit.test.ts +2 -169
- package/tests/core/model-deployment-registry.test.ts +7 -1
- package/tests/core/model-training-registry.test.ts +19 -0
- package/tests/core/observability.test.ts +0 -1
- package/tests/core/pain-diagnostic-gate.test.ts +498 -0
- package/tests/core/pain.test.ts +0 -1
- package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
- package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
- package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
- package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
- package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
- package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
- package/tests/core/reflection-context.test.ts +0 -14
- package/tests/core/replay-engine.test.ts +127 -215
- package/tests/core/rule-host-helpers.test.ts +2 -2
- package/tests/core/rule-implementation-runtime.test.ts +0 -27
- package/tests/core/workflow-funnel-loader.test.ts +162 -0
- package/tests/core/workspace-dir-validation.test.ts +8 -1
- package/tests/core-anti-growth.test.ts +192 -0
- package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
- package/tests/hooks/confirm-first-gate.test.ts +333 -0
- package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
- package/tests/hooks/gate-auto-correct.test.ts +665 -0
- package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
- package/tests/hooks/pain.test.ts +269 -12
- package/tests/hooks/prompt-characterization.test.ts +500 -0
- package/tests/hooks/prompt-size-guard.test.ts +32 -17
- package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
- package/tests/index.test.ts +94 -1
- package/tests/integration/auto-entry-gate.test.ts +248 -0
- package/tests/integration/internalization-trigger-guard.test.ts +69 -0
- package/tests/integration/m8-legacy-paths.test.ts +63 -0
- package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
- package/tests/plugin-config-resolution-cutover.test.ts +359 -0
- package/tests/runtime-v2-discovery-guard.test.ts +154 -0
- package/tests/service/central-database.test.ts +457 -0
- package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
- package/tests/service/evolution-worker.timeout.test.ts +11 -129
- package/tests/service/internalization-trigger-adapter.test.ts +251 -0
- package/tests/service/monitoring-query-service.test.ts +1 -47
- package/tests/service/queue-io.test.ts +1 -62
- package/tests/service/runtime-summary-service.test.ts +3 -1
- package/tests/service/workflow-watchdog.test.ts +0 -91
- package/tests/utils/file-lock.test.ts +5 -3
- package/tests/utils/session-key.test.ts +52 -0
- package/tests/utils/subagent-probe.test.ts +48 -1
- package/vitest.config.ts +4 -11
- package/.planning/codebase/ARCHITECTURE.md +0 -157
- package/.planning/codebase/CONCERNS.md +0 -145
- package/.planning/codebase/CONVENTIONS.md +0 -148
- package/.planning/codebase/INTEGRATIONS.md +0 -81
- package/.planning/codebase/STACK.md +0 -87
- package/.planning/codebase/STRUCTURE.md +0 -193
- package/.planning/codebase/TESTING.md +0 -243
- package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
- package/docs/COMMAND_REFERENCE.md +0 -76
- package/docs/COMMAND_REFERENCE_EN.md +0 -79
- package/scripts/build-web.mjs +0 -46
- package/scripts/diagnose-nocturnal.mjs +0 -537
- package/scripts/seed-nocturnal-scenarios.mjs +0 -384
- package/src/commands/nocturnal-review.ts +0 -322
- package/src/commands/nocturnal-rollout.ts +0 -790
- package/src/commands/nocturnal-train.ts +0 -986
- package/src/commands/pd-reflect.ts +0 -88
- package/src/core/adaptive-thresholds.ts +0 -478
- package/src/core/diagnostician-task-store.ts +0 -192
- package/src/core/nocturnal-arbiter.ts +0 -715
- package/src/core/nocturnal-artifact-lineage.ts +0 -116
- package/src/core/nocturnal-artificer.ts +0 -257
- package/src/core/nocturnal-candidate-scoring.ts +0 -530
- package/src/core/nocturnal-compliance.ts +0 -1146
- package/src/core/nocturnal-dataset.ts +0 -763
- package/src/core/nocturnal-executability.ts +0 -428
- package/src/core/nocturnal-export.ts +0 -499
- package/src/core/nocturnal-paths.ts +0 -240
- package/src/core/nocturnal-reasoning-deriver.ts +0 -343
- package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
- package/src/core/nocturnal-snapshot-contract.ts +0 -99
- package/src/core/nocturnal-trajectory-extractor.ts +0 -512
- package/src/core/nocturnal-trinity-types.ts +0 -218
- package/src/core/nocturnal-trinity.ts +0 -2680
- package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
- package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
- package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
- package/src/http/principles-console-route.ts +0 -709
- package/src/service/central-health-service.ts +0 -49
- package/src/service/central-overview-service.ts +0 -138
- package/src/service/control-ui-query-service.ts +0 -900
- package/src/service/cooldown-strategy.ts +0 -97
- package/src/service/evolution-pain-context.ts +0 -79
- package/src/service/evolution-query-service.ts +0 -407
- package/src/service/health-query-service.ts +0 -1038
- package/src/service/nocturnal-config.ts +0 -214
- package/src/service/nocturnal-runtime.ts +0 -734
- package/src/service/nocturnal-service.ts +0 -1605
- package/src/service/nocturnal-target-selector.ts +0 -545
- package/src/service/sleep-cycle.ts +0 -157
- package/src/service/startup-reconciler.ts +0 -112
- package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
- package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
- package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
- package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
- package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
- package/src/tools/write-pain-flag.ts +0 -215
- package/tests/commands/nocturnal-review.test.ts +0 -448
- package/tests/commands/nocturnal-train.test.ts +0 -97
- package/tests/commands/pd-reflect.test.ts +0 -49
- package/tests/core/adaptive-thresholds.test.ts +0 -261
- package/tests/core/nocturnal-arbiter.test.ts +0 -559
- package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
- package/tests/core/nocturnal-artificer.test.ts +0 -241
- package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
- package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
- package/tests/core/nocturnal-compliance.test.ts +0 -646
- package/tests/core/nocturnal-dataset.test.ts +0 -892
- package/tests/core/nocturnal-e2e.test.ts +0 -234
- package/tests/core/nocturnal-executability.test.ts +0 -357
- package/tests/core/nocturnal-export.test.ts +0 -517
- package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
- package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
- package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
- package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
- package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
- package/tests/core/nocturnal-trinity.test.ts +0 -2053
- package/tests/core/pain-auto-repair.test.ts +0 -96
- package/tests/core/pain-integration.test.ts +0 -510
- package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
- package/tests/http/principles-console-route.test.ts +0 -162
- package/tests/integration/chaos-resilience.test.ts +0 -348
- package/tests/integration/empathy-workflow-integration.test.ts +0 -626
- package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
- package/tests/service/control-ui-query-service.test.ts +0 -121
- package/tests/service/cooldown-strategy.test.ts +0 -164
- package/tests/service/data-endpoints-regression.test.ts +0 -834
- package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
- package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
- package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
- package/tests/service/nocturnal-runtime.test.ts +0 -473
- package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
- package/tests/service/nocturnal-target-selector.test.ts +0 -615
- package/tests/service/startup-reconciler.test.ts +0 -148
- package/tests/tools/write-pain-flag.test.ts +0 -358
- package/ui/src/App.tsx +0 -45
- package/ui/src/api.ts +0 -220
- package/ui/src/charts.tsx +0 -955
- package/ui/src/components/ErrorState.tsx +0 -6
- package/ui/src/components/Loading.tsx +0 -13
- package/ui/src/components/ProtectedRoute.tsx +0 -12
- package/ui/src/components/Shell.tsx +0 -91
- package/ui/src/components/WorkspaceConfig.tsx +0 -178
- package/ui/src/components/index.ts +0 -5
- package/ui/src/context/auth.tsx +0 -80
- package/ui/src/context/theme.tsx +0 -66
- package/ui/src/hooks/useAutoRefresh.ts +0 -39
- package/ui/src/i18n/ui.ts +0 -473
- package/ui/src/main.tsx +0 -16
- package/ui/src/pages/EvolutionPage.tsx +0 -333
- package/ui/src/pages/FeedbackPage.tsx +0 -138
- package/ui/src/pages/GateMonitorPage.tsx +0 -136
- package/ui/src/pages/LoginPage.tsx +0 -89
- package/ui/src/pages/OverviewPage.tsx +0 -599
- package/ui/src/pages/SamplesPage.tsx +0 -174
- package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
- package/ui/src/styles.css +0 -2020
- package/ui/src/types.ts +0 -384
- package/ui/src/utils/format.ts +0 -15
|
@@ -1,580 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WorkflowManagerBase - Shared base class for polling-based workflow managers
|
|
3
|
-
*
|
|
4
|
-
* Extracts common lifecycle, state transitions, and store operations from
|
|
5
|
-
* EmpathyObserverWorkflowManager.
|
|
6
|
-
*
|
|
7
|
-
* Both managers implement the same polling-based workflow lifecycle:
|
|
8
|
-
* startWorkflow → driver.run() → store.createWorkflow → scheduleWaitPollWithRetry
|
|
9
|
-
* → driver.wait() → finalizeOnce → driver.cleanup()
|
|
10
|
-
*
|
|
11
|
-
* The base class handles all shared workflow management.
|
|
12
|
-
* Subclasses provide only:
|
|
13
|
-
* - Their specific constants (WORKFLOW_SESSION_PREFIX, DEFAULT_TIMEOUT_MS, DEFAULT_TTL_MS)
|
|
14
|
-
* - Surface-degrade checks (boot session skip, subagent availability)
|
|
15
|
-
* - The createWorkflowRecord hook for type-specific metadata
|
|
16
|
-
*
|
|
17
|
-
* @module subagent-workflow/workflow-manager-base
|
|
18
|
-
*/
|
|
19
|
-
/* global NodeJS */
|
|
20
|
-
import type { PluginLogger } from '../../openclaw-sdk.js';
|
|
21
|
-
import type {
|
|
22
|
-
WorkflowManager,
|
|
23
|
-
WorkflowHandle,
|
|
24
|
-
SubagentWorkflowSpec,
|
|
25
|
-
WorkflowMetadata,
|
|
26
|
-
WorkflowDebugSummary,
|
|
27
|
-
} from './types.js';
|
|
28
|
-
import { RuntimeDirectDriver, type RunParams } from './runtime-direct-driver.js';
|
|
29
|
-
import { WorkflowStore } from './workflow-store.js';
|
|
30
|
-
import { isSubagentRuntimeAvailable } from '../../utils/subagent-probe.js';
|
|
31
|
-
import { computeDynamicTimeout, computeRetrySchedule, MAX_TIMEOUT_RETRIES } from './dynamic-timeout.js';
|
|
32
|
-
|
|
33
|
-
// ── Constructor Options ────────────────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
export interface WorkflowManagerBaseOptions {
|
|
36
|
-
workspaceDir: string;
|
|
37
|
-
logger: PluginLogger;
|
|
38
|
-
subagent: RuntimeDirectDriver['subagent'];
|
|
39
|
-
/** Pass api.runtime.agent.session to enable heartbeat-safe cleanup (#188) */
|
|
40
|
-
agentSession?: RuntimeDirectDriver['agentSession'];
|
|
41
|
-
/** Workflow type identifier for logging and dynamic timeout lookups (e.g. 'empathy-observer') */
|
|
42
|
-
workflowType: string;
|
|
43
|
-
/** Session key prefix (e.g. 'agent:main:subagent:workflow-') */
|
|
44
|
-
sessionPrefix: string;
|
|
45
|
-
/** Default polling timeout in milliseconds */
|
|
46
|
-
defaultTimeoutMs: number;
|
|
47
|
-
/** Default TTL for orphan cleanup in milliseconds */
|
|
48
|
-
defaultTtlMs: number;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ── Base Class ─────────────────────────────────────────────────────────────────
|
|
52
|
-
|
|
53
|
-
export abstract class WorkflowManagerBase implements WorkflowManager {
|
|
54
|
-
// Protected: accessible to subclasses
|
|
55
|
-
protected readonly store: WorkflowStore;
|
|
56
|
-
protected readonly driver: RuntimeDirectDriver;
|
|
57
|
-
protected readonly logger: PluginLogger;
|
|
58
|
-
protected readonly workspaceDir: string;
|
|
59
|
-
protected readonly workflowType: string;
|
|
60
|
-
|
|
61
|
-
// Protected: shared state
|
|
62
|
-
protected activeWorkflows = new Map<string, NodeJS.Timeout>();
|
|
63
|
-
protected completedWorkflows = new Map<string, number>();
|
|
64
|
-
protected workflowSpecs = new Map<string, SubagentWorkflowSpec<unknown>>();
|
|
65
|
-
|
|
66
|
-
// Private: subclass-specific constants used by base class methods
|
|
67
|
-
private readonly sessionPrefix: string;
|
|
68
|
-
private readonly defaultTimeoutMs: number;
|
|
69
|
-
private readonly defaultTtlMs: number;
|
|
70
|
-
|
|
71
|
-
constructor(opts: WorkflowManagerBaseOptions) {
|
|
72
|
-
this.workspaceDir = opts.workspaceDir;
|
|
73
|
-
this.logger = opts.logger;
|
|
74
|
-
this.store = new WorkflowStore({ workspaceDir: opts.workspaceDir });
|
|
75
|
-
this.driver = new RuntimeDirectDriver({
|
|
76
|
-
subagent: opts.subagent,
|
|
77
|
-
logger: opts.logger,
|
|
78
|
-
agentSession: opts.agentSession,
|
|
79
|
-
});
|
|
80
|
-
this.workflowType = opts.workflowType;
|
|
81
|
-
this.sessionPrefix = opts.sessionPrefix;
|
|
82
|
-
this.defaultTimeoutMs = opts.defaultTimeoutMs;
|
|
83
|
-
this.defaultTtlMs = opts.defaultTtlMs;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ── startWorkflow ─────────────────────────────────────────────────────────
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Start a new workflow.
|
|
90
|
-
*
|
|
91
|
-
* Common flow:
|
|
92
|
-
* 1. Validate surface checks (boot session, subagent availability)
|
|
93
|
-
* 2. Generate workflowId + childSessionKey
|
|
94
|
-
* 3. Build metadata (subclass provides via createWorkflowMetadata hook)
|
|
95
|
-
* 4. Call driver.run()
|
|
96
|
-
* 5. Create workflow record via createWorkflowRecord hook
|
|
97
|
-
* 6. Register spec
|
|
98
|
-
* 7. Schedule wait polling
|
|
99
|
-
*
|
|
100
|
-
* Subclasses override surfaceCheck or startWorkflow to add type-specific
|
|
101
|
-
* surface-degrade checks before calling super.startWorkflow().
|
|
102
|
-
*/
|
|
103
|
-
async startWorkflow<TResult>(
|
|
104
|
-
spec: SubagentWorkflowSpec<TResult>,
|
|
105
|
-
options: {
|
|
106
|
-
parentSessionId: string;
|
|
107
|
-
workspaceDir?: string;
|
|
108
|
-
taskInput: unknown;
|
|
109
|
-
metadata?: Record<string, unknown>;
|
|
110
|
-
}
|
|
111
|
-
): Promise<WorkflowHandle> {
|
|
112
|
-
const workflowId = this.generateWorkflowId();
|
|
113
|
-
const childSessionKey = this.buildChildSessionKey(options.parentSessionId);
|
|
114
|
-
const now = Date.now();
|
|
115
|
-
|
|
116
|
-
// Surface degrade: skip boot sessions
|
|
117
|
-
if (options.parentSessionId.startsWith('boot-')) {
|
|
118
|
-
this.logger.info(`[PD:${this.workflowType}] Skipping workflow: boot session`);
|
|
119
|
-
throw new Error(`${this.constructor.name}: cannot start workflow for boot session`);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Surface degrade: check subagent runtime availability
|
|
123
|
-
if (!isSubagentRuntimeAvailable(this.driver.getSubagent())) {
|
|
124
|
-
this.logger.info(`[PD:${this.workflowType}] Skipping workflow: subagent runtime unavailable`);
|
|
125
|
-
throw new Error(`${this.constructor.name}: subagent runtime unavailable`);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (spec.transport !== 'runtime_direct') {
|
|
129
|
-
throw new Error(`${this.constructor.name} only supports runtime_direct transport`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const runParams = this.buildRunParams(spec, options, childSessionKey);
|
|
133
|
-
const runResult = await this.driver.run(runParams);
|
|
134
|
-
|
|
135
|
-
const metadata = this.createWorkflowMetadata(spec, options, now);
|
|
136
|
-
|
|
137
|
-
await this.createWorkflowRecord(workflowId, childSessionKey, spec, options, now, metadata, runResult.runId);
|
|
138
|
-
|
|
139
|
-
this.store.recordEvent(workflowId, 'spawned', null, 'active', 'subagent spawned', { runId: runResult.runId });
|
|
140
|
-
this.workflowSpecs.set(workflowId, spec as SubagentWorkflowSpec<unknown>);
|
|
141
|
-
|
|
142
|
-
this.scheduleWaitPollWithRetry(workflowId, runResult.runId);
|
|
143
|
-
|
|
144
|
-
return {
|
|
145
|
-
workflowId,
|
|
146
|
-
childSessionKey,
|
|
147
|
-
runId: runResult.runId,
|
|
148
|
-
state: 'active',
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ── Protected Hooks (subclasses override) ─────────────────────────────────
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Create workflow metadata for store.createWorkflow().
|
|
156
|
-
* Subclasses override to add type-specific fields.
|
|
157
|
-
*/
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
protected createWorkflowMetadata<TResult>(
|
|
161
|
-
spec: SubagentWorkflowSpec<TResult>,
|
|
162
|
-
options: {
|
|
163
|
-
parentSessionId: string;
|
|
164
|
-
workspaceDir?: string;
|
|
165
|
-
taskInput: unknown;
|
|
166
|
-
metadata?: Record<string, unknown>;
|
|
167
|
-
},
|
|
168
|
-
now: number
|
|
169
|
-
): WorkflowMetadata {
|
|
170
|
-
return {
|
|
171
|
-
parentSessionId: options.parentSessionId,
|
|
172
|
-
workspaceDir: options.workspaceDir,
|
|
173
|
-
taskInput: options.taskInput,
|
|
174
|
-
startedAt: now,
|
|
175
|
-
workflowType: spec.workflowType,
|
|
176
|
-
...options.metadata,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Create a workflow record in the store.
|
|
182
|
-
* Called after driver.run() succeeds.
|
|
183
|
-
* Subclasses override to call store.createWorkflow() with type-specific metadata.
|
|
184
|
-
*/
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
protected async createWorkflowRecord<TResult>(
|
|
188
|
-
workflowId: string,
|
|
189
|
-
childSessionKey: string,
|
|
190
|
-
spec: SubagentWorkflowSpec<TResult>,
|
|
191
|
-
options: {
|
|
192
|
-
parentSessionId: string;
|
|
193
|
-
workspaceDir?: string;
|
|
194
|
-
taskInput: unknown;
|
|
195
|
-
metadata?: Record<string, unknown>;
|
|
196
|
-
},
|
|
197
|
-
now: number,
|
|
198
|
-
metadata: WorkflowMetadata,
|
|
199
|
-
runId: string
|
|
200
|
-
): Promise<void> {
|
|
201
|
-
this.store.createWorkflow({
|
|
202
|
-
workflow_id: workflowId,
|
|
203
|
-
workflow_type: spec.workflowType,
|
|
204
|
-
transport: spec.transport,
|
|
205
|
-
parent_session_id: options.parentSessionId,
|
|
206
|
-
child_session_key: childSessionKey,
|
|
207
|
-
run_id: runId,
|
|
208
|
-
state: 'active',
|
|
209
|
-
created_at: now,
|
|
210
|
-
updated_at: now,
|
|
211
|
-
duration_ms: null,
|
|
212
|
-
metadata_json: JSON.stringify(metadata),
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ── Protected Helpers ────────────────────────────────────────────────────
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
protected buildRunParams<TResult>(
|
|
221
|
-
spec: SubagentWorkflowSpec<TResult>,
|
|
222
|
-
options: {
|
|
223
|
-
parentSessionId: string;
|
|
224
|
-
workspaceDir?: string;
|
|
225
|
-
taskInput: unknown;
|
|
226
|
-
metadata?: Record<string, unknown>;
|
|
227
|
-
},
|
|
228
|
-
childSessionKey: string
|
|
229
|
-
): RunParams {
|
|
230
|
-
const message = spec.buildPrompt(options.taskInput, {
|
|
231
|
-
parentSessionId: options.parentSessionId,
|
|
232
|
-
workspaceDir: options.workspaceDir,
|
|
233
|
-
taskInput: options.taskInput,
|
|
234
|
-
startedAt: Date.now(),
|
|
235
|
-
workflowType: spec.workflowType,
|
|
236
|
-
...(options.metadata ?? {}),
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
sessionKey: childSessionKey,
|
|
241
|
-
message,
|
|
242
|
-
lane: 'subagent',
|
|
243
|
-
deliver: false,
|
|
244
|
-
idempotencyKey: options.parentSessionId
|
|
245
|
-
? `${options.parentSessionId}:${Date.now()}`
|
|
246
|
-
: `pd:${childSessionKey}:${Date.now()}`,
|
|
247
|
-
expectsCompletionMessage: true,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Schedule wait polling with dynamic timeout and automatic retry.
|
|
253
|
-
*
|
|
254
|
-
* Learns from historical completion times for this workflow type.
|
|
255
|
-
* On timeout, retries with exponential backoff (1x → 2x → 4x base timeout).
|
|
256
|
-
*/
|
|
257
|
-
protected scheduleWaitPollWithRetry(
|
|
258
|
-
workflowId: string,
|
|
259
|
-
runId: string,
|
|
260
|
-
attempt = 0,
|
|
261
|
-
): void {
|
|
262
|
-
const spec = this.workflowSpecs.get(workflowId);
|
|
263
|
-
const staticTimeout = spec?.timeoutMs ?? this.defaultTimeoutMs;
|
|
264
|
-
|
|
265
|
-
// Compute dynamic timeout from historical data using this.workflowType
|
|
266
|
-
const baseTimeout = computeDynamicTimeout(this.store, this.workflowType, staticTimeout);
|
|
267
|
-
const schedule = computeRetrySchedule(baseTimeout, MAX_TIMEOUT_RETRIES);
|
|
268
|
-
const timeoutMs = schedule[Math.min(attempt, schedule.length - 1)];
|
|
269
|
-
const historyCount = this.store.getCompletionDurations(this.workflowType, 50).length;
|
|
270
|
-
|
|
271
|
-
this.logger.info(`[PD:${this.workflowType}] Wait attempt ${attempt + 1}/${schedule.length}: timeout=${timeoutMs}ms (base=${baseTimeout}ms, static=${staticTimeout}ms, samples=${historyCount}) for ${workflowId}`);
|
|
272
|
-
|
|
273
|
-
const timeout = setTimeout(async () => {
|
|
274
|
-
try {
|
|
275
|
-
const result = await this.driver.wait({ runId, timeoutMs });
|
|
276
|
-
if (result.status === 'timeout' && attempt < MAX_TIMEOUT_RETRIES) {
|
|
277
|
-
this.logger.info(`[PD:${this.workflowType}] Timeout on attempt ${attempt + 1}, retrying for ${workflowId}`);
|
|
278
|
-
this.store.recordEvent(workflowId, 'wait_timeout_retry', 'active', 'active', `timeout on attempt ${attempt + 1}, scheduling retry ${attempt + 2}`, { attempt });
|
|
279
|
-
this.activeWorkflows.delete(workflowId);
|
|
280
|
-
this.scheduleWaitPollWithRetry(workflowId, runId, attempt + 1);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
if (result.status === 'ok' && attempt > 0) {
|
|
284
|
-
this.logger.info(`[PD:${this.workflowType}] Retry succeeded on attempt ${attempt + 1} after timeout for ${workflowId}`);
|
|
285
|
-
}
|
|
286
|
-
await this.notifyWaitResult(workflowId, result.status, result.error);
|
|
287
|
-
} catch (error) {
|
|
288
|
-
const errMsg = String(error);
|
|
289
|
-
// Don't retry on database errors — the connection was closed
|
|
290
|
-
// (typically by lifecycle notification dispose). The subagent
|
|
291
|
-
// may have completed successfully.
|
|
292
|
-
if (errMsg.includes('not open') || errMsg.includes('database')) {
|
|
293
|
-
this.logger.warn(`[PD:${this.workflowType}] Database error during wait poll for ${workflowId}: ${errMsg} — not retrying`);
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
this.logger.error(`[PD:${this.workflowType}] Wait poll failed: ${errMsg}`);
|
|
297
|
-
if (attempt < MAX_TIMEOUT_RETRIES) {
|
|
298
|
-
this.logger.info(`[PD:${this.workflowType}] Error on attempt ${attempt + 1}, retrying for ${workflowId}`);
|
|
299
|
-
this.activeWorkflows.delete(workflowId);
|
|
300
|
-
this.scheduleWaitPollWithRetry(workflowId, runId, attempt + 1);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
await this.notifyWaitResult(workflowId, 'error', errMsg);
|
|
304
|
-
}
|
|
305
|
-
}, 100);
|
|
306
|
-
timeout.unref(); // Don't keep process alive for wait poll
|
|
307
|
-
|
|
308
|
-
this.activeWorkflows.set(workflowId, timeout);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// ── WorkflowManager Interface ─────────────────────────────────────────────
|
|
312
|
-
|
|
313
|
-
async notifyWaitResult(
|
|
314
|
-
workflowId: string,
|
|
315
|
-
status: 'ok' | 'error' | 'timeout',
|
|
316
|
-
error?: string
|
|
317
|
-
): Promise<void> {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
let workflow;
|
|
321
|
-
try {
|
|
322
|
-
workflow = this.store.getWorkflow(workflowId);
|
|
323
|
-
/* eslint-disable @typescript-eslint/no-unused-vars -- Reason: Error is handled via early returns based on status, not error value */
|
|
324
|
-
} catch (_dbError) {
|
|
325
|
-
// Database connection closed (e.g., by lifecycle notification dispose).
|
|
326
|
-
// If subagent succeeded, this is a known race condition — the workflow
|
|
327
|
-
// will be handled by the original manager's finalizeOnce path.
|
|
328
|
-
if (status === 'ok') {
|
|
329
|
-
this.logger.info(`[PD:${this.workflowType}] notifyWaitResult: database unavailable for ${workflowId}, status=ok — skipping (original manager will finalize)`);
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
this.logger.warn(`[PD:${this.workflowType}] notifyWaitResult: database unavailable for ${workflowId}, status=${status}`);
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
if (!workflow) {
|
|
336
|
-
this.logger.warn(`[PD:${this.workflowType}] notifyWaitResult: workflow not found: ${workflowId}`);
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (workflow.state === 'completed' || workflow.state === 'terminal_error' || workflow.state === 'expired') {
|
|
341
|
-
this.logger.info(`[PD:${this.workflowType}] notifyWaitResult: ignoring terminal workflow: ${workflowId}, state=${workflow.state}`);
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
this.logger.info(`[PD:${this.workflowType}] notifyWaitResult: workflowId=${workflowId}, status=${status}`);
|
|
346
|
-
|
|
347
|
-
const previousState = workflow.state;
|
|
348
|
-
this.store.updateWorkflowState(workflowId, 'wait_result');
|
|
349
|
-
this.store.recordEvent(workflowId, 'wait_result', previousState, 'wait_result', `wait completed: ${status}`, { error });
|
|
350
|
-
|
|
351
|
-
const spec = this.workflowSpecs.get(workflowId);
|
|
352
|
-
if (!spec) {
|
|
353
|
-
// Spec not registered — lifecycle event notification path (subagent.ts).
|
|
354
|
-
// Original manager's wait poll will handle finalization.
|
|
355
|
-
this.logger.info(`[PD:${this.workflowType}] notifyWaitResult: spec not registered for ${workflowId} — skipping finalization (primary path will handle)`);
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const shouldFinalize = spec.shouldFinalizeOnWaitStatus(status);
|
|
360
|
-
|
|
361
|
-
if (shouldFinalize) {
|
|
362
|
-
await this.finalizeOnce(workflowId);
|
|
363
|
-
} else {
|
|
364
|
-
this.store.updateWorkflowState(workflowId, 'terminal_error');
|
|
365
|
-
this.store.recordEvent(workflowId, 'finalize_skipped', 'wait_result', 'terminal_error', `wait status: ${status}`, { error });
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
async notifyLifecycleEvent(
|
|
370
|
-
workflowId: string,
|
|
371
|
-
event: 'subagent_spawned' | 'subagent_ended',
|
|
372
|
-
data?: { outcome?: 'ok' | 'error' | 'timeout' | 'killed' | 'reset' | 'deleted'; error?: string }
|
|
373
|
-
): Promise<void> {
|
|
374
|
-
this.logger.info(`[PD:${this.workflowType}] notifyLifecycleEvent: workflowId=${workflowId}, event=${event}`);
|
|
375
|
-
|
|
376
|
-
if (event === 'subagent_ended' && data?.outcome) {
|
|
377
|
-
await this.notifyWaitResult(workflowId, data.outcome === 'ok' ? 'ok' : data.outcome === 'error' ? 'error' : 'timeout', data.error);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
async finalizeOnce(workflowId: string): Promise<void> {
|
|
382
|
-
const workflow = this.store.getWorkflow(workflowId);
|
|
383
|
-
if (!workflow) {
|
|
384
|
-
this.logger.warn(`[PD:${this.workflowType}] finalizeOnce: workflow not found: ${workflowId}`);
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const spec = this.workflowSpecs.get(workflowId);
|
|
389
|
-
if (!spec) {
|
|
390
|
-
throw new Error(`Workflow spec not registered for ${workflowId}`);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (this.isCompleted(workflowId)) {
|
|
394
|
-
this.logger.info(`[PD:${this.workflowType}] finalizeOnce: already completed: ${workflowId}`);
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
this.logger.info(`[PD:${this.workflowType}] Finalizing workflow: ${workflowId}`);
|
|
399
|
-
|
|
400
|
-
this.store.updateWorkflowState(workflowId, 'finalizing');
|
|
401
|
-
|
|
402
|
-
try {
|
|
403
|
-
const result = await this.driver.getResult({ sessionKey: workflow.child_session_key, limit: 20 });
|
|
404
|
-
|
|
405
|
-
const metadata = JSON.parse(workflow.metadata_json) as WorkflowMetadata;
|
|
406
|
-
const parsed = await spec.parseResult({
|
|
407
|
-
messages: result.messages,
|
|
408
|
-
assistantTexts: result.assistantTexts,
|
|
409
|
-
metadata,
|
|
410
|
-
waitStatus: 'ok',
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
if (!parsed) {
|
|
414
|
-
this.store.updateWorkflowState(workflowId, 'terminal_error');
|
|
415
|
-
this.store.recordEvent(workflowId, 'parse_failed', 'finalizing', 'terminal_error', 'spec.parseResult returned null', {});
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
await spec.persistResult({
|
|
420
|
-
result: parsed,
|
|
421
|
-
metadata,
|
|
422
|
-
workspaceDir: this.workspaceDir,
|
|
423
|
-
});
|
|
424
|
-
this.store.recordEvent(workflowId, 'persisted', 'finalizing', 'finalizing', 'result persisted', {});
|
|
425
|
-
|
|
426
|
-
if (spec.shouldDeleteSessionAfterFinalize && workflow.run_id) {
|
|
427
|
-
try {
|
|
428
|
-
await this.driver.cleanup({ sessionKey: workflow.child_session_key });
|
|
429
|
-
this.store.updateCleanupState(workflowId, 'completed');
|
|
430
|
-
} catch (cleanupError) {
|
|
431
|
-
this.logger.error(`[PD:${this.workflowType}] cleanup failed after persistence: ${String(cleanupError)}`);
|
|
432
|
-
this.store.updateCleanupState(workflowId, 'failed');
|
|
433
|
-
this.store.updateWorkflowState(workflowId, 'cleanup_pending');
|
|
434
|
-
this.store.recordEvent(workflowId, 'cleanup_failed', 'finalizing', 'cleanup_pending', String(cleanupError), {});
|
|
435
|
-
this.markCompleted(workflowId);
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Record actual completion duration for adaptive timeout learning
|
|
441
|
-
const durationMs = Date.now() - workflow.created_at;
|
|
442
|
-
this.store.recordDuration(workflowId, durationMs);
|
|
443
|
-
this.logger.info(`[PD:${this.workflowType}] Duration recorded: workflowId=${workflowId}, durationMs=${durationMs}ms (${(durationMs / 1000).toFixed(1)}s), type=${spec.workflowType}`);
|
|
444
|
-
|
|
445
|
-
this.store.updateWorkflowState(workflowId, 'completed');
|
|
446
|
-
this.store.recordEvent(workflowId, 'finalized', 'finalizing', 'completed', 'success', { durationMs });
|
|
447
|
-
this.markCompleted(workflowId);
|
|
448
|
-
|
|
449
|
-
} catch (error) {
|
|
450
|
-
this.logger.error(`[PD:${this.workflowType}] finalizeOnce failed: ${String(error)}`);
|
|
451
|
-
this.store.updateWorkflowState(workflowId, 'terminal_error');
|
|
452
|
-
this.store.recordEvent(workflowId, 'finalize_error', 'finalizing', 'terminal_error', String(error), {});
|
|
453
|
-
throw error;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
async sweepExpiredWorkflows(maxAgeMs?: number): Promise<number> {
|
|
459
|
-
const ttl = maxAgeMs ?? this.defaultTtlMs;
|
|
460
|
-
const expired = this.store.getExpiredWorkflows(ttl);
|
|
461
|
-
|
|
462
|
-
this.logger.info(`[PD:${this.workflowType}] sweepExpiredWorkflows: found ${expired.length} expired`);
|
|
463
|
-
|
|
464
|
-
for (const workflow of expired) {
|
|
465
|
-
try {
|
|
466
|
-
this.logger.info(`[PD:${this.workflowType}] Sweeping expired workflow: ${workflow.workflow_id}`);
|
|
467
|
-
|
|
468
|
-
await this.driver.cleanup({ sessionKey: workflow.child_session_key });
|
|
469
|
-
this.store.updateCleanupState(workflow.workflow_id, 'completed');
|
|
470
|
-
this.store.updateWorkflowState(workflow.workflow_id, 'expired');
|
|
471
|
-
this.store.recordEvent(workflow.workflow_id, 'swept', workflow.state, 'expired', 'TTL expired', {});
|
|
472
|
-
|
|
473
|
-
} catch (error) {
|
|
474
|
-
this.logger.error(`[PD:${this.workflowType}] Sweep cleanup failed for ${workflow.workflow_id}: ${String(error)}`);
|
|
475
|
-
this.store.updateCleanupState(workflow.workflow_id, 'failed');
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Clean up memory Maps to prevent leaks
|
|
480
|
-
const cutoff = Date.now() - 60_000;
|
|
481
|
-
for (const [id, timestamp] of this.completedWorkflows) {
|
|
482
|
-
if (timestamp < cutoff) {
|
|
483
|
-
this.completedWorkflows.delete(id);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
for (const [id, timeout] of this.activeWorkflows) {
|
|
487
|
-
const wf = this.store.getWorkflow(id);
|
|
488
|
-
if (!wf || wf.state === 'expired' || wf.state === 'completed' || wf.state === 'terminal_error') {
|
|
489
|
-
clearTimeout(timeout);
|
|
490
|
-
this.activeWorkflows.delete(id);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
return expired.length;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
async getWorkflowDebugSummary(workflowId: string, eventLimit = 10): Promise<WorkflowDebugSummary | null> {
|
|
498
|
-
const workflow = this.store.getWorkflow(workflowId);
|
|
499
|
-
if (!workflow) return null;
|
|
500
|
-
|
|
501
|
-
const metadata = JSON.parse(workflow.metadata_json) as WorkflowMetadata;
|
|
502
|
-
const recentEvents = this.store
|
|
503
|
-
.getEvents(workflowId)
|
|
504
|
-
.slice(-eventLimit)
|
|
505
|
-
.map((event) => ({
|
|
506
|
-
eventType: event.event_type,
|
|
507
|
-
fromState: event.from_state,
|
|
508
|
-
toState: event.to_state,
|
|
509
|
-
reason: event.reason,
|
|
510
|
-
createdAt: event.created_at,
|
|
511
|
-
payload: JSON.parse(event.payload_json || '{}') as Record<string, unknown>,
|
|
512
|
-
}));
|
|
513
|
-
|
|
514
|
-
return {
|
|
515
|
-
workflowId: workflow.workflow_id,
|
|
516
|
-
workflowType: workflow.workflow_type,
|
|
517
|
-
transport: workflow.transport,
|
|
518
|
-
parentSessionId: workflow.parent_session_id,
|
|
519
|
-
childSessionKey: workflow.child_session_key,
|
|
520
|
-
runId: workflow.run_id,
|
|
521
|
-
state: workflow.state,
|
|
522
|
-
cleanupState: workflow.cleanup_state,
|
|
523
|
-
lastObservedAt: workflow.last_observed_at ?? null,
|
|
524
|
-
metadata,
|
|
525
|
-
recentEvents,
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// ── Private Helpers ───────────────────────────────────────────────────────
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
protected generateWorkflowId(): string {
|
|
533
|
-
// Subclasses override the prefix part via wf_ prefix pattern
|
|
534
|
-
return `wf_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
protected buildChildSessionKey(parentSessionId: string): string {
|
|
538
|
-
const safeParentSessionId = parentSessionId
|
|
539
|
-
.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
540
|
-
.substring(0, 64);
|
|
541
|
-
const timestamp = Date.now();
|
|
542
|
-
return `${this.sessionPrefix}${safeParentSessionId}-${timestamp}`;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
protected isCompleted(workflowId: string): boolean {
|
|
546
|
-
const timestamp = this.completedWorkflows.get(workflowId);
|
|
547
|
-
if (!timestamp) return false;
|
|
548
|
-
return Date.now() - timestamp < 60_000; // 1 minute dedup window
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* Get the current state of a workflow by ID.
|
|
553
|
-
* Returns null if the workflow doesn't exist.
|
|
554
|
-
*/
|
|
555
|
-
getWorkflowState(workflowId: string): string | null {
|
|
556
|
-
const workflow = this.store.getWorkflow(workflowId);
|
|
557
|
-
return workflow?.state ?? null;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
protected markCompleted(workflowId: string): void {
|
|
561
|
-
const timeout = this.activeWorkflows.get(workflowId);
|
|
562
|
-
if (timeout) {
|
|
563
|
-
clearTimeout(timeout);
|
|
564
|
-
this.activeWorkflows.delete(workflowId);
|
|
565
|
-
}
|
|
566
|
-
this.completedWorkflows.set(workflowId, Date.now());
|
|
567
|
-
this.workflowSpecs.delete(workflowId);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
dispose(): void {
|
|
571
|
-
for (const [workflowId, timeout] of this.activeWorkflows) {
|
|
572
|
-
clearTimeout(timeout);
|
|
573
|
-
this.logger.info(`[PD:${this.workflowType}] Disposed active workflow: ${workflowId}`);
|
|
574
|
-
}
|
|
575
|
-
this.activeWorkflows.clear();
|
|
576
|
-
this.completedWorkflows.clear();
|
|
577
|
-
this.workflowSpecs.clear();
|
|
578
|
-
this.store.dispose();
|
|
579
|
-
}
|
|
580
|
-
}
|