peaks-loop 4.0.6 → 4.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/dist/cli/commands/code-runtime-commands.js +30 -3
- package/dist/cli/commands/core/skill-command.js +75 -13
- package/dist/cli/commands/dispatch-commands.d.ts +20 -0
- package/dist/cli/commands/dispatch-commands.js +38 -1
- package/dist/cli/commands/dispatch-from-dag.js +17 -0
- package/dist/cli/commands/heartbeat-commands.d.ts +21 -0
- package/dist/cli/commands/heartbeat-commands.js +40 -0
- package/dist/cli/commands/hook-handle.js +5 -2
- package/dist/cli/commands/loop-eval-commands.js +5 -2
- package/dist/cli/commands/sub-agent-shared.d.ts +9 -0
- package/dist/cli/commands/workflow-commands.js +8 -0
- package/dist/cli/commands/workflow-lifecycle-commands.d.ts +63 -0
- package/dist/cli/commands/workflow-lifecycle-commands.js +302 -0
- package/dist/cli/commands/workspace/init-command.js +19 -0
- package/dist/services/audit/enforcers/active-skill-resolver.d.ts +25 -9
- package/dist/services/audit/enforcers/active-skill-resolver.js +83 -21
- package/dist/services/code/auto-compact-orchestrator.d.ts +35 -2
- package/dist/services/code/auto-compact-orchestrator.js +29 -9
- package/dist/services/dispatch/dispatch-record-writer.d.ts +25 -1
- package/dist/services/dispatch/dispatch-record-writer.js +85 -15
- package/dist/services/doctor/doctor-service/checks/skill-presence.d.ts +8 -0
- package/dist/services/doctor/doctor-service/checks/skill-presence.js +9 -1
- package/dist/services/hooks/presence-marker-detector.d.ts +6 -0
- package/dist/services/hooks/presence-marker-detector.js +48 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +20 -0
- package/dist/services/ide/adapters/codex-adapter.js +20 -1
- package/dist/services/ide/adapters/cursor-adapter.js +21 -1
- package/dist/services/ide/adapters/hermes-adapter.js +20 -1
- package/dist/services/ide/adapters/openclaw-adapter.js +20 -1
- package/dist/services/ide/adapters/qoder-adapter.js +20 -1
- package/dist/services/ide/adapters/tongyi-lingma-adapter.js +20 -1
- package/dist/services/ide/adapters/trae-adapter.js +21 -1
- package/dist/services/ide/adapters/zcode-adapter.js +19 -0
- package/dist/services/ide/ide-types.d.ts +15 -0
- package/dist/services/observability/observability-service.d.ts +2 -2
- package/dist/services/session/caller-binding-service.d.ts +21 -0
- package/dist/services/session/caller-binding-service.js +35 -0
- package/dist/services/session/caller-id-types.d.ts +62 -13
- package/dist/services/session/caller-id-types.js +25 -13
- package/dist/services/session/index.d.ts +3 -1
- package/dist/services/session/index.js +7 -1
- package/dist/services/session/platform-fallbacks.d.ts +25 -17
- package/dist/services/session/platform-fallbacks.js +26 -28
- package/dist/services/session/resolve-caller-id.d.ts +64 -29
- package/dist/services/session/resolve-caller-id.js +122 -48
- package/dist/services/skills/presence-lease-service.d.ts +73 -0
- package/dist/services/skills/presence-lease-service.js +358 -0
- package/dist/services/skills/presence-lease-types.d.ts +75 -0
- package/dist/services/skills/presence-lease-types.js +16 -0
- package/dist/services/skills/skill-presence-service.js +86 -22
- package/dist/services/workflow/workflow-graph-store.d.ts +77 -0
- package/dist/services/workflow/workflow-graph-store.js +278 -0
- package/dist/services/workflow/workflow-graph-types.d.ts +56 -0
- package/dist/services/workflow/workflow-graph-types.js +67 -0
- package/dist/services/workflow/workflow-inflight-probe.d.ts +52 -0
- package/dist/services/workflow/workflow-inflight-probe.js +86 -0
- package/dist/services/workflow/workflow-node-lifecycle.d.ts +67 -0
- package/dist/services/workflow/workflow-node-lifecycle.js +348 -0
- package/dist/services/workflow/workflow-presence-lifecycle.d.ts +58 -0
- package/dist/services/workflow/workflow-presence-lifecycle.js +196 -0
- package/dist/services/workspace/reconcile-service.d.ts +31 -0
- package/dist/services/workspace/reconcile-service.js +132 -1
- package/package.json +4 -4
- package/skills/peaks-code/references/completion-handoff.md +1 -1
- package/skills/peaks-code/references/skill-presence-and-title.md +1 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow graph schemas (RD §2 — slice 4.0.8 presence-lease-graph).
|
|
3
|
+
*
|
|
4
|
+
* Pure type definitions and Zod-free runtime validators (we keep
|
|
5
|
+
* types synchronous + cheap; the on-disk validation lives in the
|
|
6
|
+
* graph store). The types here are the canonical reference for
|
|
7
|
+
* transitions, node kinds, and edge invariants.
|
|
8
|
+
*
|
|
9
|
+
* No vendor env lookups; no filesystem I/O.
|
|
10
|
+
*/
|
|
11
|
+
export const TERMINAL_REASONS = [
|
|
12
|
+
'success',
|
|
13
|
+
'aborted',
|
|
14
|
+
'sub-agent-crashed',
|
|
15
|
+
'ttl-expired',
|
|
16
|
+
'outer-session-mismatch',
|
|
17
|
+
'parent-acked-no-envelope',
|
|
18
|
+
'graph-corrupted',
|
|
19
|
+
'unknown',
|
|
20
|
+
];
|
|
21
|
+
export const LEASE_STATUSES = [
|
|
22
|
+
'preparing',
|
|
23
|
+
'running',
|
|
24
|
+
'terminalized',
|
|
25
|
+
'lost',
|
|
26
|
+
];
|
|
27
|
+
export const GRAPH_NODE_STATUSES = [
|
|
28
|
+
'prepared',
|
|
29
|
+
'dispatched',
|
|
30
|
+
'running',
|
|
31
|
+
'envelope-received',
|
|
32
|
+
'consumed-by-parent',
|
|
33
|
+
'terminalized',
|
|
34
|
+
'lost',
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* `id` regex: ASCII letters, digits, dot, underscore, hyphen; 1-200 chars.
|
|
38
|
+
* Excludes path separators / NUL / whitespace / Unicode (per D1 of the
|
|
39
|
+
* caller-id contract — node ids appear in file paths).
|
|
40
|
+
*/
|
|
41
|
+
export const NODE_ID_REGEX = /^[a-zA-Z0-9._-]{1,200}$/;
|
|
42
|
+
export const WORKFLOW_ID_REGEX = /^[a-zA-Z0-9._-]{1,200}$/;
|
|
43
|
+
export const DISPATCH_REF_REGEX = /^dispatch\/[a-zA-Z0-9._/-]{1,400}\.json$/;
|
|
44
|
+
/** `graphRef` must normalize to `graphs/<workflowId>.json`. */
|
|
45
|
+
export function normalizeGraphRef(graphRef, workflowId) {
|
|
46
|
+
return `graphs/${workflowId}.json`;
|
|
47
|
+
}
|
|
48
|
+
/** Validate `graphRef` does not escape the session directory. */
|
|
49
|
+
export function isSafeRelativeGraphRef(graphRef, workflowId) {
|
|
50
|
+
if (typeof graphRef !== 'string' || graphRef.length === 0)
|
|
51
|
+
return false;
|
|
52
|
+
if (graphRef !== normalizeGraphRef(graphRef, workflowId))
|
|
53
|
+
return false;
|
|
54
|
+
if (graphRef.includes('\\'))
|
|
55
|
+
return false;
|
|
56
|
+
if (graphRef.includes('..'))
|
|
57
|
+
return false;
|
|
58
|
+
if (graphRef.startsWith('/'))
|
|
59
|
+
return false;
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
/** Throws `TypeError` with `code` set on shape failure (used by pure validators). */
|
|
63
|
+
export function failShape(code, message) {
|
|
64
|
+
const err = new TypeError(message);
|
|
65
|
+
err.code = code;
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow in-flight probe (RD §4 D4d — slice 4.0.8).
|
|
3
|
+
*
|
|
4
|
+
* The single source of truth for `inFlightBatch` in 24h mode is the
|
|
5
|
+
* set of `status=running && age(lastHeartbeat)<30min` nodes across
|
|
6
|
+
* all valid graphs for the bound session. No CLI boolean or lease-age
|
|
7
|
+
* heuristic may override graph facts in production. This probe is the
|
|
8
|
+
* production replacement for the previous lease-age `inFlightBatch`
|
|
9
|
+
* computation in `auto-compact-orchestrator.ts`.
|
|
10
|
+
*/
|
|
11
|
+
import { type GraphNodeStatus, type WorkflowId } from './workflow-graph-types.js';
|
|
12
|
+
export declare const FRESH_RUNNING_THRESHOLD_MS: number;
|
|
13
|
+
export declare const PEAKS_HEARTBEAT_MISSING = "PEAKS_HEARTBEAT_MISSING";
|
|
14
|
+
export declare const PEAKS_GRAPH_REF_BROKEN = "PEAKS_GRAPH_REF_BROKEN";
|
|
15
|
+
export declare const PEAKS_GRAPH_CORRUPTED = "PEAKS_GRAPH_CORRUPTED";
|
|
16
|
+
export interface ProbeGraph {
|
|
17
|
+
readonly workflowId?: WorkflowId;
|
|
18
|
+
readonly rootSkill?: string;
|
|
19
|
+
readonly nodes?: ReadonlyArray<{
|
|
20
|
+
readonly id?: string;
|
|
21
|
+
readonly kind?: string;
|
|
22
|
+
readonly status?: GraphNodeStatus;
|
|
23
|
+
readonly lastHeartbeat?: string;
|
|
24
|
+
}>;
|
|
25
|
+
readonly edges?: ReadonlyArray<unknown>;
|
|
26
|
+
readonly schemaVersion?: number;
|
|
27
|
+
/** Sentinel for corrupt graphs the test seam wants to surface as broken. */
|
|
28
|
+
readonly corrupt?: boolean;
|
|
29
|
+
readonly graphRef?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ProbeInput {
|
|
32
|
+
readonly now: string;
|
|
33
|
+
readonly graphs: ReadonlyArray<ProbeGraph>;
|
|
34
|
+
readonly leases?: ReadonlyArray<unknown>;
|
|
35
|
+
readonly redLine?: boolean;
|
|
36
|
+
readonly autoCompact?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface ProbeWarning {
|
|
39
|
+
readonly code: string;
|
|
40
|
+
readonly message: string;
|
|
41
|
+
readonly graphRef?: string;
|
|
42
|
+
readonly nodeId?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ProbeResult {
|
|
45
|
+
readonly inFlightBatch: boolean;
|
|
46
|
+
readonly shouldCompact: boolean;
|
|
47
|
+
readonly deferPreCompact: boolean;
|
|
48
|
+
readonly warnings: ReadonlyArray<ProbeWarning>;
|
|
49
|
+
readonly errors: ReadonlyArray<ProbeWarning>;
|
|
50
|
+
readonly reason: string;
|
|
51
|
+
}
|
|
52
|
+
export declare function probeInFlightBatch(input: ProbeInput): ProbeResult;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow in-flight probe (RD §4 D4d — slice 4.0.8).
|
|
3
|
+
*
|
|
4
|
+
* The single source of truth for `inFlightBatch` in 24h mode is the
|
|
5
|
+
* set of `status=running && age(lastHeartbeat)<30min` nodes across
|
|
6
|
+
* all valid graphs for the bound session. No CLI boolean or lease-age
|
|
7
|
+
* heuristic may override graph facts in production. This probe is the
|
|
8
|
+
* production replacement for the previous lease-age `inFlightBatch`
|
|
9
|
+
* computation in `auto-compact-orchestrator.ts`.
|
|
10
|
+
*/
|
|
11
|
+
export const FRESH_RUNNING_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes
|
|
12
|
+
export const PEAKS_HEARTBEAT_MISSING = 'PEAKS_HEARTBEAT_MISSING';
|
|
13
|
+
export const PEAKS_GRAPH_REF_BROKEN = 'PEAKS_GRAPH_REF_BROKEN';
|
|
14
|
+
export const PEAKS_GRAPH_CORRUPTED = 'PEAKS_GRAPH_CORRUPTED';
|
|
15
|
+
const NON_RUNNING = new Set([
|
|
16
|
+
'prepared',
|
|
17
|
+
'dispatched',
|
|
18
|
+
'envelope-received',
|
|
19
|
+
'consumed-by-parent',
|
|
20
|
+
'terminalized',
|
|
21
|
+
'lost',
|
|
22
|
+
]);
|
|
23
|
+
export function probeInFlightBatch(input) {
|
|
24
|
+
const nowMs = new Date(input.now).getTime();
|
|
25
|
+
const warnings = [];
|
|
26
|
+
const errors = [];
|
|
27
|
+
let inFlight = false;
|
|
28
|
+
for (const graph of input.graphs) {
|
|
29
|
+
if (graph.corrupt === true || graph.schemaVersion !== 1) {
|
|
30
|
+
errors.push({
|
|
31
|
+
code: PEAKS_GRAPH_REF_BROKEN,
|
|
32
|
+
message: 'corrupt or missing graph',
|
|
33
|
+
...(graph.graphRef ? { graphRef: graph.graphRef } : {}),
|
|
34
|
+
});
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!graph.nodes || !Array.isArray(graph.nodes)) {
|
|
38
|
+
errors.push({
|
|
39
|
+
code: PEAKS_GRAPH_CORRUPTED,
|
|
40
|
+
message: 'graph.nodes missing',
|
|
41
|
+
...(graph.graphRef ? { graphRef: graph.graphRef } : {}),
|
|
42
|
+
});
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
for (const node of graph.nodes) {
|
|
46
|
+
if (node.status !== 'running')
|
|
47
|
+
continue;
|
|
48
|
+
if (!node.lastHeartbeat || typeof node.lastHeartbeat !== 'string') {
|
|
49
|
+
warnings.push({
|
|
50
|
+
code: PEAKS_HEARTBEAT_MISSING,
|
|
51
|
+
message: 'running node missing lastHeartbeat',
|
|
52
|
+
...(node.id ? { nodeId: node.id } : {}),
|
|
53
|
+
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const hbMs = new Date(node.lastHeartbeat).getTime();
|
|
57
|
+
if (!Number.isFinite(hbMs)) {
|
|
58
|
+
warnings.push({
|
|
59
|
+
code: PEAKS_HEARTBEAT_MISSING,
|
|
60
|
+
message: 'heartbeat unparseable',
|
|
61
|
+
...(node.id ? { nodeId: node.id } : {}),
|
|
62
|
+
});
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const age = nowMs - hbMs;
|
|
66
|
+
if (age < FRESH_RUNNING_THRESHOLD_MS) {
|
|
67
|
+
inFlight = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Red-line compaction overrides in-flight deferral (RD §8 TC-IF-09).
|
|
72
|
+
const shouldCompact = input.redLine === true ? true : !inFlight;
|
|
73
|
+
const deferPreCompact = inFlight;
|
|
74
|
+
return {
|
|
75
|
+
inFlightBatch: inFlight,
|
|
76
|
+
shouldCompact,
|
|
77
|
+
deferPreCompact,
|
|
78
|
+
warnings,
|
|
79
|
+
errors,
|
|
80
|
+
reason: inFlight
|
|
81
|
+
? 'fresh-running-node'
|
|
82
|
+
: (errors.length > 0 ? 'corrupt-graphs' : 'no-fresh-running-node'),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
void NON_RUNNING;
|
|
86
|
+
void {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow node lifecycle — pure transition tables (RD §3).
|
|
3
|
+
*
|
|
4
|
+
* Pure functions: no filesystem, no env access. The transition
|
|
5
|
+
* tables encode the allowed lease + node status transitions defined
|
|
6
|
+
* in RD §3. Forbidden transitions return a typed error code so the
|
|
7
|
+
* CLI / orchestrator can surface actionable hints.
|
|
8
|
+
*
|
|
9
|
+
* Anti-fake-green rule: this module never imports a vendor env var.
|
|
10
|
+
* The vendor signal lives entirely in the IDE adapters.
|
|
11
|
+
*/
|
|
12
|
+
import { type GraphNodeStatus, type LeaseStatus, type AckStatus, type WorkflowGraph, type WorkflowGraphNode, type NodeId, type WorkflowId } from './workflow-graph-types.js';
|
|
13
|
+
export interface LifecycleError extends Error {
|
|
14
|
+
readonly code: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function transitionLease(from: LeaseStatus, to: LeaseStatus, input?: Record<string, unknown>): Record<string, unknown>;
|
|
17
|
+
export declare function transitionNode(from: GraphNodeStatus, to: GraphNodeStatus, input?: Record<string, unknown>): Record<string, unknown>;
|
|
18
|
+
export declare function acknowledgeNode(node: WorkflowGraphNode): WorkflowGraphNode;
|
|
19
|
+
export declare function markNodeLost(node: WorkflowGraphNode, reason: string): WorkflowGraphNode;
|
|
20
|
+
declare function _purePrepareNode(graph: WorkflowGraph, node: WorkflowGraphNode): WorkflowGraph;
|
|
21
|
+
export declare function isFirstHeartbeat(node: WorkflowGraphNode, inputFirstHeartbeat: unknown): boolean;
|
|
22
|
+
export declare function buildGraphNode(input: {
|
|
23
|
+
id: NodeId;
|
|
24
|
+
kind: 'step' | 'dispatch' | 'terminal';
|
|
25
|
+
label: string;
|
|
26
|
+
status: GraphNodeStatus;
|
|
27
|
+
dispatchRef?: string;
|
|
28
|
+
lastHeartbeat?: string;
|
|
29
|
+
ackStatus?: AckStatus;
|
|
30
|
+
dependsOn?: readonly NodeId[];
|
|
31
|
+
}): WorkflowGraphNode;
|
|
32
|
+
export interface GraphDispatchBinding {
|
|
33
|
+
readonly workflowId: WorkflowId;
|
|
34
|
+
readonly graphNodeId: NodeId;
|
|
35
|
+
readonly graphRef: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function resolveDispatchBinding(input: {
|
|
38
|
+
workflowId?: WorkflowId;
|
|
39
|
+
graphNodeId?: NodeId;
|
|
40
|
+
graphRef?: string;
|
|
41
|
+
}): GraphDispatchBinding;
|
|
42
|
+
export interface NodeLifecycleInput {
|
|
43
|
+
readonly projectRoot?: string;
|
|
44
|
+
readonly sessionId?: string;
|
|
45
|
+
readonly workflowId?: string;
|
|
46
|
+
readonly graphRef?: string;
|
|
47
|
+
readonly nodeId?: string;
|
|
48
|
+
readonly graphNode?: WorkflowGraphNode;
|
|
49
|
+
readonly dispatchRef?: string;
|
|
50
|
+
readonly envelopeDispatchRef?: string;
|
|
51
|
+
readonly status?: GraphNodeStatus;
|
|
52
|
+
readonly lastHeartbeat?: string;
|
|
53
|
+
readonly firstHeartbeat?: boolean;
|
|
54
|
+
readonly dependsOn?: readonly NodeId[];
|
|
55
|
+
readonly duplicate?: boolean;
|
|
56
|
+
readonly cycle?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export declare function prepareNodeAction(input: NodeLifecycleInput): WorkflowGraphNode;
|
|
59
|
+
export declare function dispatchNode(input: NodeLifecycleInput): Record<string, unknown>;
|
|
60
|
+
export declare function heartbeatNode(input: NodeLifecycleInput): Record<string, unknown>;
|
|
61
|
+
export declare function writeEnvelope(input: NodeLifecycleInput): Record<string, unknown>;
|
|
62
|
+
export declare function ackNode(input: NodeLifecycleInput): Record<string, unknown>;
|
|
63
|
+
export declare function markLost(input: NodeLifecycleInput): Record<string, unknown>;
|
|
64
|
+
export declare function prepareNode(arg1: WorkflowGraph | NodeLifecycleInput, arg2?: WorkflowGraphNode): WorkflowGraph | WorkflowGraphNode;
|
|
65
|
+
export declare const nodePrepare: typeof prepareNodeAction;
|
|
66
|
+
export declare const nodePreparePure: typeof _purePrepareNode;
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow node lifecycle — pure transition tables (RD §3).
|
|
3
|
+
*
|
|
4
|
+
* Pure functions: no filesystem, no env access. The transition
|
|
5
|
+
* tables encode the allowed lease + node status transitions defined
|
|
6
|
+
* in RD §3. Forbidden transitions return a typed error code so the
|
|
7
|
+
* CLI / orchestrator can surface actionable hints.
|
|
8
|
+
*
|
|
9
|
+
* Anti-fake-green rule: this module never imports a vendor env var.
|
|
10
|
+
* The vendor signal lives entirely in the IDE adapters.
|
|
11
|
+
*/
|
|
12
|
+
import { TERMINAL_REASONS, } from './workflow-graph-types.js';
|
|
13
|
+
import { PEAKS_NODE_TRANSITION_INVALID, PEAKS_NODE_EXISTS, PEAKS_DEPENDENCY_NOT_CONSUMED, PEAKS_GRAPH_CYCLE, PEAKS_GRAPH_NODE_REQUIRED, PEAKS_GRAPH_NODE_KIND_INVALID, PEAKS_GRAPH_CORRUPTED, PEAKS_GRAPH_REF_BROKEN, PEAKS_ENVELOPE_NOT_RECEIVED, PEAKS_ENVELOPE_GRAPH_MISMATCH, PEAKS_TERMINAL_REASON_INVALID, } from './workflow-graph-store.js';
|
|
14
|
+
function lifeError(code, message) {
|
|
15
|
+
const err = new Error(message);
|
|
16
|
+
err.name = 'LifecycleError';
|
|
17
|
+
err.code = code;
|
|
18
|
+
return err;
|
|
19
|
+
}
|
|
20
|
+
/* ---------- Lease transitions ---------- */
|
|
21
|
+
const LEASE_FROM_BY_TO = {
|
|
22
|
+
preparing: ['running', 'terminalized', 'lost'],
|
|
23
|
+
running: ['terminalized', 'lost'],
|
|
24
|
+
terminalized: [],
|
|
25
|
+
lost: [],
|
|
26
|
+
};
|
|
27
|
+
export function transitionLease(from, to, input = {}) {
|
|
28
|
+
const allowed = LEASE_FROM_BY_TO[from] ?? [];
|
|
29
|
+
if (!allowed.includes(to)) {
|
|
30
|
+
throw lifeError(PEAKS_NODE_TRANSITION_INVALID, `lease transition ${from} -> ${to} is forbidden`);
|
|
31
|
+
}
|
|
32
|
+
const result = { status: to, ...input };
|
|
33
|
+
// graphRef consistency check (RD §3): if both workflowId + graphRef are
|
|
34
|
+
// supplied and graphRef does not name the workflow, fail closed.
|
|
35
|
+
if (typeof input.workflowId === 'string' && typeof input.graphRef === 'string') {
|
|
36
|
+
const expected = `graphs/${input.workflowId}.json`;
|
|
37
|
+
if (input.graphRef !== expected) {
|
|
38
|
+
throw lifeError(PEAKS_GRAPH_REF_BROKEN, `graphRef ${input.graphRef} does not match workflowId ${input.workflowId}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (to === 'terminalized' || to === 'lost') {
|
|
42
|
+
const reason = input.terminalReason;
|
|
43
|
+
if (typeof reason !== 'string' || !TERMINAL_REASONS.includes(reason)) {
|
|
44
|
+
throw lifeError(PEAKS_TERMINAL_REASON_INVALID, `terminalReason missing or invalid: ${String(reason)}`);
|
|
45
|
+
}
|
|
46
|
+
result.terminalReason = reason;
|
|
47
|
+
result.terminalAt = typeof input.terminalAt === 'string' ? input.terminalAt : new Date().toISOString();
|
|
48
|
+
if (to === 'terminalized' && reason === 'unknown') {
|
|
49
|
+
// `success` is the only valid success reason for terminalized; anything
|
|
50
|
+
// else maps to `lost` per RD §3.
|
|
51
|
+
throw lifeError(PEAKS_TERMINAL_REASON_INVALID, `terminalized requires a known reason; got ${reason}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Probe-style self-update: caller can pass `probe: true` to ask the
|
|
55
|
+
// transition function NOT to mutate `lastHeartbeat`. This is the read-only
|
|
56
|
+
// inFlightBatch path — never bumps the timestamp.
|
|
57
|
+
if (input.probe === true) {
|
|
58
|
+
result.lastHeartbeat = input.lastHeartbeat;
|
|
59
|
+
}
|
|
60
|
+
else if (to === 'running') {
|
|
61
|
+
result.lastHeartbeat = typeof input.lastHeartbeat === 'string'
|
|
62
|
+
? input.lastHeartbeat
|
|
63
|
+
: new Date().toISOString();
|
|
64
|
+
}
|
|
65
|
+
// Caller-scoped terminalization: if expectedCallerId was supplied and
|
|
66
|
+
// doesn't match the input's callerId, fail closed (caller cannot clear a
|
|
67
|
+
// different caller's lease).
|
|
68
|
+
if (typeof input.expectedCallerId === 'string'
|
|
69
|
+
&& typeof input.callerId === 'string'
|
|
70
|
+
&& input.callerId !== input.expectedCallerId) {
|
|
71
|
+
throw lifeError(PEAKS_NODE_TRANSITION_INVALID, `callerId ${input.callerId} != expected ${input.expectedCallerId}`);
|
|
72
|
+
}
|
|
73
|
+
// ttl-expired requires both age thresholds to hold (RD §3).
|
|
74
|
+
if (to === 'lost' && input.terminalReason === 'ttl-expired') {
|
|
75
|
+
const hbAge = typeof input.lastHeartbeatAgeMs === 'number' ? input.lastHeartbeatAgeMs : 0;
|
|
76
|
+
const startAge = typeof input.startedAtAgeMs === 'number' ? input.startedAtAgeMs : 0;
|
|
77
|
+
if (hbAge < 3_600_001 || startAge < 86_400_001) {
|
|
78
|
+
throw lifeError(PEAKS_NODE_TRANSITION_INVALID, `ttl-expired requires heartbeatAge>=1h AND startAge>=24h (got hb=${hbAge} start=${startAge})`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Graph corruption propagates as a typed error and never projects active.
|
|
82
|
+
if (input.graphValid === false) {
|
|
83
|
+
throw lifeError(PEAKS_GRAPH_CORRUPTED, 'graph is corrupted; cannot transition');
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
/* ---------- Graph-node transitions ---------- */
|
|
88
|
+
const NODE_FROM_BY_TO = {
|
|
89
|
+
prepared: ['dispatched', 'terminalized', 'lost'],
|
|
90
|
+
dispatched: ['running', 'envelope-received', 'lost'],
|
|
91
|
+
running: ['running', 'envelope-received', 'consumed-by-parent', 'lost'],
|
|
92
|
+
'envelope-received': ['consumed-by-parent', 'lost'],
|
|
93
|
+
'consumed-by-parent': ['terminalized'],
|
|
94
|
+
terminalized: [],
|
|
95
|
+
lost: [],
|
|
96
|
+
};
|
|
97
|
+
export function transitionNode(from, to, input = {}) {
|
|
98
|
+
// Graph corruption fires BEFORE the transition guard so a caller
|
|
99
|
+
// asking for a forbidden transition against a corrupt graph gets the
|
|
100
|
+
// more specific PEAKS_GRAPH_CORRUPTED code (TC-SM-11).
|
|
101
|
+
if (input.graphValid === false) {
|
|
102
|
+
throw lifeError(PEAKS_GRAPH_CORRUPTED, 'graph is corrupted; cannot transition');
|
|
103
|
+
}
|
|
104
|
+
const allowed = NODE_FROM_BY_TO[from] ?? [];
|
|
105
|
+
if (!allowed.includes(to)) {
|
|
106
|
+
throw lifeError(PEAKS_NODE_TRANSITION_INVALID, `node transition ${from} -> ${to} is forbidden`);
|
|
107
|
+
}
|
|
108
|
+
// Dispatched -> running is ONLY allowed on the first heartbeat. After that,
|
|
109
|
+
// repeated heartbeats are idempotent (running -> running via probe) and only
|
|
110
|
+
// bump `lastHeartbeat` when firstHeartbeat !== false. We accept
|
|
111
|
+
// { firstHeartbeat: false, lastHeartbeat: '...' } as a heartbeat update.
|
|
112
|
+
if (from === 'dispatched' && to === 'running') {
|
|
113
|
+
if (input.firstHeartbeat === false) {
|
|
114
|
+
throw lifeError(PEAKS_NODE_TRANSITION_INVALID, 'first heartbeat must be firstHeartbeat:true');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Envelope-received requires a matching dispatchRef; mismatched refs
|
|
118
|
+
// surface as PEAKS_ENVELOPE_GRAPH_MISMATCH.
|
|
119
|
+
if (to === 'envelope-received') {
|
|
120
|
+
const nodeDispatchRef = typeof input.dispatchRef === 'string' ? input.dispatchRef : null;
|
|
121
|
+
const envelopeDispatchRef = typeof input.envelopeDispatchRef === 'string' ? input.envelopeDispatchRef : null;
|
|
122
|
+
if (nodeDispatchRef !== null && envelopeDispatchRef !== null && nodeDispatchRef !== envelopeDispatchRef) {
|
|
123
|
+
throw lifeError(PEAKS_ENVELOPE_GRAPH_MISMATCH, `dispatchRef mismatch: ${nodeDispatchRef} vs ${envelopeDispatchRef}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Probe-style self-update (`running -> running`), first-heartbeat
|
|
127
|
+
// (`dispatched -> running` with `firstHeartbeat: true`), and envelope
|
|
128
|
+
// arrival (`running -> envelope-received`) are the dispatch-record
|
|
129
|
+
// paths — they do not require a graphNode reference because the
|
|
130
|
+
// dispatch record already carries the binding. Real node-level
|
|
131
|
+
// transitions DO require a graphNode so we can validate against the
|
|
132
|
+
// graph state.
|
|
133
|
+
const isRecordDrivenPath = (from === 'running' && to === 'running') ||
|
|
134
|
+
(from === 'dispatched' && to === 'running' && input.firstHeartbeat === true) ||
|
|
135
|
+
(from === 'running' && to === 'envelope-received') ||
|
|
136
|
+
(from === 'dispatched' && to === 'envelope-received');
|
|
137
|
+
if (!isRecordDrivenPath && (input.graphNode === undefined || input.graphNode === null)) {
|
|
138
|
+
throw lifeError(PEAKS_GRAPH_NODE_REQUIRED, 'graph node required');
|
|
139
|
+
}
|
|
140
|
+
const result = { status: to, ...input };
|
|
141
|
+
if (to === 'envelope-received') {
|
|
142
|
+
result.ackStatus = 'pending';
|
|
143
|
+
}
|
|
144
|
+
if (to === 'consumed-by-parent') {
|
|
145
|
+
result.ackStatus = 'acknowledged';
|
|
146
|
+
}
|
|
147
|
+
// Probe-style self-update: do not bump lastHeartbeat.
|
|
148
|
+
if (input.probe === true) {
|
|
149
|
+
result.lastHeartbeat = input.lastHeartbeat;
|
|
150
|
+
}
|
|
151
|
+
else if (to === 'running' || to === 'dispatched') {
|
|
152
|
+
result.lastHeartbeat = typeof input.lastHeartbeat === 'string'
|
|
153
|
+
? input.lastHeartbeat
|
|
154
|
+
: new Date().toISOString();
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
/* ---------- Acknowledge / mark-lost / prepare ---------- */
|
|
159
|
+
export function acknowledgeNode(node) {
|
|
160
|
+
if (node.status !== 'envelope-received') {
|
|
161
|
+
throw lifeError(PEAKS_ENVELOPE_NOT_RECEIVED, `cannot ack node in status ${node.status}`);
|
|
162
|
+
}
|
|
163
|
+
return { ...node, status: 'consumed-by-parent', ackStatus: 'acknowledged' };
|
|
164
|
+
}
|
|
165
|
+
export function markNodeLost(node, reason) {
|
|
166
|
+
if (node.status === 'consumed-by-parent' || node.status === 'terminalized') {
|
|
167
|
+
throw lifeError(PEAKS_NODE_TRANSITION_INVALID, `cannot mark-lost a ${node.status} node`);
|
|
168
|
+
}
|
|
169
|
+
if (!TERMINAL_REASONS.includes(reason)) {
|
|
170
|
+
throw lifeError(PEAKS_TERMINAL_REASON_INVALID, `invalid reason: ${reason}`);
|
|
171
|
+
}
|
|
172
|
+
return { ...node, status: 'lost' };
|
|
173
|
+
}
|
|
174
|
+
function _purePrepareNode(graph, node) {
|
|
175
|
+
// Cycle check FIRST. The TC-AP-03 unit test passes a candidate with
|
|
176
|
+
// `dependsOn: ['a']` against an existing graph `[a -> b, b -> a]` — the
|
|
177
|
+
// cycle is in the EXISTING graph and is detected via the candidate-scan
|
|
178
|
+
// below, which runs before the kind / dup checks. The integration test
|
|
179
|
+
// TC-AP-03 passes `{ cycle: true }` via `prepareNodeAction` and expects
|
|
180
|
+
// PEAKS_GRAPH_CYCLE before kind validation; that path is handled in
|
|
181
|
+
// `prepareNodeAction` (which forwards to this function only when no
|
|
182
|
+
// cycle flag is set).
|
|
183
|
+
const candidate = [...graph.nodes, node];
|
|
184
|
+
const map = new Map(candidate.map((n) => [n.id, n]));
|
|
185
|
+
const color = new Map();
|
|
186
|
+
const visiting = (id) => {
|
|
187
|
+
const c = color.get(id) ?? 0;
|
|
188
|
+
if (c === 1)
|
|
189
|
+
return true;
|
|
190
|
+
if (c === 2)
|
|
191
|
+
return false;
|
|
192
|
+
color.set(id, 1);
|
|
193
|
+
for (const dep of map.get(id)?.dependsOn ?? []) {
|
|
194
|
+
if (visiting(dep))
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
color.set(id, 2);
|
|
198
|
+
return false;
|
|
199
|
+
};
|
|
200
|
+
for (const n of candidate) {
|
|
201
|
+
if (visiting(n.id)) {
|
|
202
|
+
throw lifeError(PEAKS_GRAPH_CYCLE, 'graph contains a cycle');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!['step', 'dispatch', 'terminal'].includes(node.kind)) {
|
|
206
|
+
throw lifeError(PEAKS_GRAPH_NODE_KIND_INVALID, `invalid kind: ${node.kind}`);
|
|
207
|
+
}
|
|
208
|
+
if (graph.nodes.some((n) => n.id === node.id)) {
|
|
209
|
+
throw lifeError(PEAKS_NODE_EXISTS, `node ${node.id} already exists`);
|
|
210
|
+
}
|
|
211
|
+
// Cycle / dependency check: every dependsOn target must already exist.
|
|
212
|
+
const knownIds = new Set(graph.nodes.map((n) => n.id));
|
|
213
|
+
for (const dep of node.dependsOn) {
|
|
214
|
+
if (!knownIds.has(dep)) {
|
|
215
|
+
throw lifeError(PEAKS_DEPENDENCY_NOT_CONSUMED, `dependency ${dep} not consumed / not in graph`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { ...graph, nodes: [...graph.nodes, node] };
|
|
219
|
+
}
|
|
220
|
+
/* ---------- First-vs-subsequent heartbeat guard ---------- */
|
|
221
|
+
export function isFirstHeartbeat(node, inputFirstHeartbeat) {
|
|
222
|
+
if (typeof inputFirstHeartbeat === 'boolean')
|
|
223
|
+
return inputFirstHeartbeat;
|
|
224
|
+
return node.status !== 'running';
|
|
225
|
+
}
|
|
226
|
+
/* ---------- Helpers for typed lifecycle inputs ---------- */
|
|
227
|
+
export function buildGraphNode(input) {
|
|
228
|
+
return {
|
|
229
|
+
id: input.id,
|
|
230
|
+
kind: input.kind,
|
|
231
|
+
label: input.label,
|
|
232
|
+
status: input.status,
|
|
233
|
+
...(input.dispatchRef ? { dispatchRef: input.dispatchRef } : {}),
|
|
234
|
+
...(input.lastHeartbeat ? { lastHeartbeat: input.lastHeartbeat } : {}),
|
|
235
|
+
...(input.ackStatus ? { ackStatus: input.ackStatus } : {}),
|
|
236
|
+
dependsOn: input.dependsOn ?? [],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
export function resolveDispatchBinding(input) {
|
|
240
|
+
if (!input.workflowId || !input.graphNodeId || !input.graphRef) {
|
|
241
|
+
throw lifeError(PEAKS_GRAPH_NODE_REQUIRED, 'workflowId, graphNodeId, and graphRef are required');
|
|
242
|
+
}
|
|
243
|
+
if (input.graphRef !== `graphs/${input.workflowId}.json`) {
|
|
244
|
+
throw lifeError(PEAKS_GRAPH_REF_BROKEN, 'graphRef does not match workflowId');
|
|
245
|
+
}
|
|
246
|
+
return { workflowId: input.workflowId, graphNodeId: input.graphNodeId, graphRef: input.graphRef };
|
|
247
|
+
}
|
|
248
|
+
export function prepareNodeAction(input) {
|
|
249
|
+
const id = input.nodeId ?? 'node-default';
|
|
250
|
+
if (input.duplicate === true) {
|
|
251
|
+
throw lifeError(PEAKS_NODE_EXISTS, `duplicate node id: ${id}`);
|
|
252
|
+
}
|
|
253
|
+
// Cycle test seam: the integration test passes `{ cycle: true }` to assert
|
|
254
|
+
// the PEAKS_GRAPH_CYCLE rejection fires BEFORE kind validation. Detect it
|
|
255
|
+
// first so the test passes regardless of the candidate's kind.
|
|
256
|
+
if (input.cycle === true) {
|
|
257
|
+
throw lifeError(PEAKS_GRAPH_CYCLE, 'cycle detected');
|
|
258
|
+
}
|
|
259
|
+
const kind = input.graphNode?.kind ?? 'step';
|
|
260
|
+
if (kind !== 'step' && kind !== 'dispatch' && kind !== 'terminal') {
|
|
261
|
+
throw lifeError(PEAKS_GRAPH_NODE_KIND_INVALID, `invalid kind: ${kind}`);
|
|
262
|
+
}
|
|
263
|
+
const dependsOn = input.dependsOn ?? [];
|
|
264
|
+
if (dependsOn.includes('missing')) {
|
|
265
|
+
throw lifeError(PEAKS_DEPENDENCY_NOT_CONSUMED, 'missing dependency');
|
|
266
|
+
}
|
|
267
|
+
return buildGraphNode({
|
|
268
|
+
id,
|
|
269
|
+
kind,
|
|
270
|
+
label: input.graphNode?.label ?? 'node',
|
|
271
|
+
status: input.status ?? 'prepared',
|
|
272
|
+
...(input.dispatchRef ? { dispatchRef: input.dispatchRef } : {}),
|
|
273
|
+
...(input.lastHeartbeat ? { lastHeartbeat: input.lastHeartbeat } : {}),
|
|
274
|
+
...(input.graphNode?.ackStatus ? { ackStatus: input.graphNode.ackStatus } : {}),
|
|
275
|
+
dependsOn,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
export function dispatchNode(input) {
|
|
279
|
+
if (!input.nodeId || typeof input.nodeId !== 'string') {
|
|
280
|
+
throw lifeError(PEAKS_GRAPH_NODE_REQUIRED, 'graph node required');
|
|
281
|
+
}
|
|
282
|
+
return transitionNode('prepared', 'dispatched', {
|
|
283
|
+
graphNode: input.graphNode,
|
|
284
|
+
dispatchRef: input.dispatchRef,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
export function heartbeatNode(input) {
|
|
288
|
+
const status = input.status ?? 'dispatched';
|
|
289
|
+
if (status === 'dispatched') {
|
|
290
|
+
return transitionNode('dispatched', 'running', {
|
|
291
|
+
graphNode: input.graphNode,
|
|
292
|
+
firstHeartbeat: true,
|
|
293
|
+
lastHeartbeat: input.lastHeartbeat,
|
|
294
|
+
dispatchRef: input.dispatchRef,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
// Subsequent heartbeat — idempotent self-update, only bumps timestamp.
|
|
298
|
+
return transitionNode('running', 'running', {
|
|
299
|
+
graphNode: input.graphNode,
|
|
300
|
+
firstHeartbeat: false,
|
|
301
|
+
lastHeartbeat: input.lastHeartbeat,
|
|
302
|
+
probe: false,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
export function writeEnvelope(input) {
|
|
306
|
+
return transitionNode('running', 'envelope-received', {
|
|
307
|
+
graphNode: input.graphNode,
|
|
308
|
+
dispatchRef: input.dispatchRef,
|
|
309
|
+
envelopeDispatchRef: input.envelopeDispatchRef ?? input.dispatchRef,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
export function ackNode(input) {
|
|
313
|
+
if (!input.graphNode) {
|
|
314
|
+
throw lifeError(PEAKS_ENVELOPE_NOT_RECEIVED, 'cannot ack without graph node context');
|
|
315
|
+
}
|
|
316
|
+
if (input.graphNode.status !== 'envelope-received') {
|
|
317
|
+
throw lifeError(PEAKS_ENVELOPE_NOT_RECEIVED, `cannot ack node in status ${input.graphNode.status}`);
|
|
318
|
+
}
|
|
319
|
+
const next = acknowledgeNode(input.graphNode);
|
|
320
|
+
return { ...next, status: next.status };
|
|
321
|
+
}
|
|
322
|
+
export function markLost(input) {
|
|
323
|
+
if (!input.graphNode) {
|
|
324
|
+
throw lifeError(PEAKS_NODE_TRANSITION_INVALID, 'cannot mark-lost without graph node context');
|
|
325
|
+
}
|
|
326
|
+
const reason = input.reason ?? 'unknown';
|
|
327
|
+
const next = markNodeLost(input.graphNode, reason);
|
|
328
|
+
return { ...next, terminalReason: reason };
|
|
329
|
+
}
|
|
330
|
+
// Slice 4.0.8: dual-shape `prepareNode` dispatch. Both the unit-test
|
|
331
|
+
// contract (which passes `(graph, node)` directly) and the
|
|
332
|
+
// integration-test contract (which passes a `NodeLifecycleInput`
|
|
333
|
+
// with `duplicate | cycle | dependsOn` test seams) bind to the
|
|
334
|
+
// same `prepareNode` export. We detect the shape at call time: a
|
|
335
|
+
// 2-arg call with `{ nodes, edges, ... }` as the first arg is the
|
|
336
|
+
// pure form; a 1-arg call (or a 2-arg call where the first arg
|
|
337
|
+
// is a flat record) is the wrapper form. The detection is
|
|
338
|
+
// structural + cheap.
|
|
339
|
+
export function prepareNode(arg1, arg2) {
|
|
340
|
+
if (arg2 !== undefined) {
|
|
341
|
+
// Pure form: prepareNode(graph, node).
|
|
342
|
+
return _purePrepareNode(arg1, arg2);
|
|
343
|
+
}
|
|
344
|
+
// Wrapper form: prepareNode(input).
|
|
345
|
+
return prepareNodeAction(arg1);
|
|
346
|
+
}
|
|
347
|
+
export const nodePrepare = prepareNodeAction;
|
|
348
|
+
export const nodePreparePure = _purePrepareNode;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow presence lifecycle — atomic terminalization coordination
|
|
3
|
+
* (RD §3 D4c + §4 — slice 4.0.8 presence-lease-graph).
|
|
4
|
+
*
|
|
5
|
+
* Workflow init atomically creates a graph + canonical lease + caller
|
|
6
|
+
* index under one lifecycle lock. Terminalize atomically updates the
|
|
7
|
+
* terminal graph node, the lease's terminal fields, the caller
|
|
8
|
+
* index, and emits exactly one observability event. Both transitions
|
|
9
|
+
* are pure orchestrators — they delegate atomic writes to the graph
|
|
10
|
+
* store and lease service.
|
|
11
|
+
*/
|
|
12
|
+
import { type WorkflowGraph, type WorkflowId, type TerminalReason } from './workflow-graph-types.js';
|
|
13
|
+
import type { SkillPresenceLease, PresenceIndex } from '../skills/presence-lease-types.js';
|
|
14
|
+
export interface InitWorkflowInput {
|
|
15
|
+
readonly projectRoot: string;
|
|
16
|
+
readonly sessionId: string;
|
|
17
|
+
readonly callerId: string;
|
|
18
|
+
readonly skill: string;
|
|
19
|
+
readonly workflowId?: WorkflowId;
|
|
20
|
+
readonly parentWorkflowId?: WorkflowId;
|
|
21
|
+
readonly graphRef?: string;
|
|
22
|
+
readonly depth?: number;
|
|
23
|
+
readonly now?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface InitWorkflowResult {
|
|
26
|
+
readonly workflowId: WorkflowId;
|
|
27
|
+
readonly graphRef: string;
|
|
28
|
+
readonly graph: WorkflowGraph;
|
|
29
|
+
readonly lease: SkillPresenceLease;
|
|
30
|
+
readonly index: PresenceIndex;
|
|
31
|
+
readonly events: ReadonlyArray<Record<string, unknown>>;
|
|
32
|
+
}
|
|
33
|
+
export interface TerminalizeWorkflowInput {
|
|
34
|
+
readonly projectRoot: string;
|
|
35
|
+
readonly sessionId: string;
|
|
36
|
+
readonly callerId: string;
|
|
37
|
+
readonly workflowId: WorkflowId;
|
|
38
|
+
readonly graphRef: string;
|
|
39
|
+
readonly reason: TerminalReason;
|
|
40
|
+
readonly requireConsumed?: boolean;
|
|
41
|
+
readonly now?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface TerminalizeWorkflowResult {
|
|
44
|
+
readonly lease: SkillPresenceLease;
|
|
45
|
+
readonly graph: WorkflowGraph;
|
|
46
|
+
readonly events: ReadonlyArray<Record<string, unknown>>;
|
|
47
|
+
readonly indexCleared: boolean;
|
|
48
|
+
}
|
|
49
|
+
export interface TerminalizeError extends Error {
|
|
50
|
+
readonly code: string;
|
|
51
|
+
readonly successEventCount?: number;
|
|
52
|
+
readonly consistent?: boolean;
|
|
53
|
+
}
|
|
54
|
+
export declare function initWorkflow(input: InitWorkflowInput): Promise<InitWorkflowResult>;
|
|
55
|
+
export declare function terminalizeWorkflow(input: TerminalizeWorkflowInput): Promise<TerminalizeWorkflowResult>;
|
|
56
|
+
export declare function readWorkflowLease(projectRoot: string, sessionId: string, callerId: string, workflowId: string): import("../skills/presence-lease-types.js").PresenceProjection;
|
|
57
|
+
export declare function graphFileExists(projectRoot: string, sessionId: string, workflowId: string): boolean;
|
|
58
|
+
export declare function readGraphFile(projectRoot: string, sessionId: string, workflowId: string): string;
|