footprintjs 9.0.0 → 9.2.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 +2 -2
- package/dist/advanced.js +1 -1
- package/dist/esm/advanced.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/lib/decide/decide.js +12 -1
- package/dist/esm/lib/decide/evaluator.js +28 -2
- package/dist/esm/lib/engine/handlers/SubflowExecutor.js +14 -1
- package/dist/esm/lib/engine/narrative/FlowRecorderDispatcher.js +4 -4
- package/dist/esm/lib/engine/narrative/types.js +1 -1
- package/dist/esm/lib/memory/StageContext.js +201 -15
- package/dist/esm/lib/memory/TransactionBuffer.js +17 -7
- package/dist/esm/lib/memory/index.js +2 -1
- package/dist/esm/lib/memory/types.js +3 -2
- package/dist/esm/lib/recorder/CombinedRecorder.js +20 -10
- package/dist/esm/lib/runner/ExecutionRuntime.js +12 -1
- package/dist/esm/lib/runner/FlowChartExecutor.js +27 -4
- package/dist/esm/lib/scope/ScopeFacade.js +28 -1
- package/dist/esm/lib/scope/types.js +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/decide/decide.js +12 -1
- package/dist/lib/decide/evaluator.js +28 -2
- package/dist/lib/engine/handlers/SubflowExecutor.js +14 -1
- package/dist/lib/engine/narrative/FlowRecorderDispatcher.js +4 -4
- package/dist/lib/engine/narrative/types.js +1 -1
- package/dist/lib/memory/StageContext.js +201 -15
- package/dist/lib/memory/TransactionBuffer.js +17 -7
- package/dist/lib/memory/index.js +4 -2
- package/dist/lib/memory/types.js +4 -1
- package/dist/lib/recorder/CombinedRecorder.js +20 -10
- package/dist/lib/runner/ExecutionRuntime.js +12 -1
- package/dist/lib/runner/FlowChartExecutor.js +27 -4
- package/dist/lib/scope/ScopeFacade.js +28 -1
- package/dist/lib/scope/types.js +1 -1
- package/dist/types/advanced.d.ts +1 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/types/lib/decide/decide.d.ts +11 -0
- package/dist/types/lib/decide/evaluator.d.ts +17 -0
- package/dist/types/lib/engine/narrative/types.d.ts +11 -0
- package/dist/types/lib/memory/StageContext.d.ts +113 -5
- package/dist/types/lib/memory/TransactionBuffer.d.ts +16 -6
- package/dist/types/lib/memory/index.d.ts +2 -1
- package/dist/types/lib/memory/types.d.ts +45 -1
- package/dist/types/lib/recorder/CombinedRecorder.d.ts +15 -9
- package/dist/types/lib/runner/ExecutionRuntime.d.ts +10 -1
- package/dist/types/lib/runner/FlowChartExecutor.d.ts +25 -3
- package/dist/types/lib/scope/ScopeFacade.d.ts +24 -1
- package/dist/types/lib/scope/types.d.ts +11 -0
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@ import { DiagnosticCollector } from './DiagnosticCollector.js';
|
|
|
11
11
|
import { EventLog } from './EventLog.js';
|
|
12
12
|
import { SharedMemory } from './SharedMemory.js';
|
|
13
13
|
import { TransactionBuffer } from './TransactionBuffer.js';
|
|
14
|
-
import type { FlowControlType, StageSnapshot } from './types.js';
|
|
14
|
+
import type { FlowControlType, ReadTrackingMode, StageSnapshot } from './types.js';
|
|
15
15
|
export declare class StageContext {
|
|
16
16
|
private sharedMemory;
|
|
17
17
|
/**
|
|
@@ -28,6 +28,13 @@ export declare class StageContext {
|
|
|
28
28
|
*/
|
|
29
29
|
private redactedSharedMemory?;
|
|
30
30
|
private buffer?;
|
|
31
|
+
/**
|
|
32
|
+
* Committed-state view captured at this stage's FIRST touch (first read OR
|
|
33
|
+
* first write) — held by REFERENCE, never cloned. See
|
|
34
|
+
* {@link firstTouchState} for the algorithm and the immutability invariant
|
|
35
|
+
* that makes a bare reference safe.
|
|
36
|
+
*/
|
|
37
|
+
private stateView?;
|
|
31
38
|
private eventLog?;
|
|
32
39
|
stageName: string;
|
|
33
40
|
/** Unique stage identifier from the builder (matches spec node id). */
|
|
@@ -50,6 +57,17 @@ export declare class StageContext {
|
|
|
50
57
|
private _stageWrites;
|
|
51
58
|
/** Tracks user-level reads (pre-namespace) for the memory view. */
|
|
52
59
|
private _stageReads;
|
|
60
|
+
/**
|
|
61
|
+
* How tracked reads are recorded into `_stageReads` (#14). Default `'full'`
|
|
62
|
+
* preserves the historical per-read `structuredClone`. Inherited by every
|
|
63
|
+
* context created via {@link createNext} / {@link createChild} (same
|
|
64
|
+
* propagation pattern as the redacted mirror), and pushed into subflow
|
|
65
|
+
* root contexts by `SubflowExecutor`. Affects ONLY the snapshot's
|
|
66
|
+
* `stageReads` payload — `ScopeRecorder.onRead` (and therefore narrative)
|
|
67
|
+
* is dispatched at the scope tier and never cloned, so it is identical in
|
|
68
|
+
* every mode.
|
|
69
|
+
*/
|
|
70
|
+
private readTracking;
|
|
53
71
|
/** Observer called after commit() — used by ScopeFacade to fire ScopeRecorder.onCommit. */
|
|
54
72
|
private _commitObserver?;
|
|
55
73
|
constructor(runId: string, name: string, stageId: string, sharedMemory: SharedMemory, branchId?: string, eventLog?: EventLog, isDecider?: boolean);
|
|
@@ -66,9 +84,69 @@ export declare class StageContext {
|
|
|
66
84
|
useRedactedMirror(mirror: SharedMemory): void;
|
|
67
85
|
/** Returns the redacted mirror if installed, else undefined. */
|
|
68
86
|
getRedactedSharedMemory(): SharedMemory | undefined;
|
|
69
|
-
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
87
|
+
/**
|
|
88
|
+
* Set the read-tracking policy for this context (#14). Called at the root
|
|
89
|
+
* by `ExecutionRuntime.useReadTracking()` (plumbed from
|
|
90
|
+
* `FlowChartExecutor`); descendants inherit via `createNext`/`createChild`,
|
|
91
|
+
* and `SubflowExecutor` pushes the parent context's mode into each subflow
|
|
92
|
+
* root so nested charts inherit too.
|
|
93
|
+
*/
|
|
94
|
+
useReadTracking(mode: ReadTrackingMode): void;
|
|
95
|
+
/** Returns the active read-tracking policy (used for subflow propagation). */
|
|
96
|
+
getReadTracking(): ReadTrackingMode;
|
|
97
|
+
/**
|
|
98
|
+
* ── The first-touch state view (#13) ────────────────────────────────────
|
|
99
|
+
*
|
|
100
|
+
* WHAT: returns the committed shared state as it was at this stage's FIRST
|
|
101
|
+
* touch (first read or first write), capturing the reference on first call.
|
|
102
|
+
* Serves two consumers: reads before the first write ({@link readState})
|
|
103
|
+
* and the transaction buffer's diff base ({@link getTransactionBuffer}).
|
|
104
|
+
*
|
|
105
|
+
* WHY A BARE REFERENCE IS SAFE — the invariant this rests on: committed
|
|
106
|
+
* state is immutable-after-swap. `SharedMemory.applyPatch` routes through
|
|
107
|
+
* `applySmartMerge`, which `structuredClone`s the current state, mutates
|
|
108
|
+
* only the clone, and swaps `SharedMemory.context` to it — the object a
|
|
109
|
+
* stage captured here is never edited afterwards. (`SharedMemory.setValue`/
|
|
110
|
+
* `updateValue` DO mutate in place, but have no callers during traversal;
|
|
111
|
+
* every runtime write reaches state through a stage commit's `applyPatch`.)
|
|
112
|
+
* Holding the reference therefore gives this stage a stable snapshot at
|
|
113
|
+
* zero cost — no clone, which is the entire point of #13.
|
|
114
|
+
*
|
|
115
|
+
* WHY FIRST TOUCH, not first write: the pre-#13 eager engine cloned the
|
|
116
|
+
* state into the buffer at the stage's first ACCESS, anchoring both its
|
|
117
|
+
* snapshot reads and its commit baseline (the net-change diff base) there.
|
|
118
|
+
* #13's first cut anchored the lazy buffer at first WRITE — observably
|
|
119
|
+
* different when something else commits in the gap between this stage's
|
|
120
|
+
* first read and its first write. That gap is REACHABLE: fork siblings are
|
|
121
|
+
* namespace-isolated for run-scoped keys (each child writes under
|
|
122
|
+
* `runs/<childId>/`), but ROOT-level keys are shared — written via
|
|
123
|
+
* `setGlobal` from consumer scope code and, critically, by
|
|
124
|
+
* `SubflowInputMapper`'s output mapping (`parentContext.setGlobal`), which
|
|
125
|
+
* is exactly what runs when a subflow is a fork branch. A sibling's
|
|
126
|
+
* root-key commit landing in the gap would shift this stage's diff base,
|
|
127
|
+
* making its CommitBundle record a phantom change (or swallow a real one)
|
|
128
|
+
* relative to the eager engine. Anchoring the view at first touch restores
|
|
129
|
+
* the EXACT eager semantics — sequential AND parallel — at zero clone cost.
|
|
130
|
+
*
|
|
131
|
+
* Read visibility is two-tier, matching eager byte-for-byte: keys present
|
|
132
|
+
* in the view at first touch read repeatably from it; keys ABSENT from it
|
|
133
|
+
* fall back to LIVE state (the eager engine's exact fallback — a
|
|
134
|
+
* mid-flight sibling root-key write was always visible to reads, and
|
|
135
|
+
* stays visible; only the DIFF BASE is pinned).
|
|
136
|
+
*/
|
|
137
|
+
private firstTouchState;
|
|
138
|
+
/** Lazily creates the transaction buffer on the stage's FIRST WRITE (#13).
|
|
139
|
+
*
|
|
140
|
+
* Reads NEVER construct it: read-your-writes only matters once a staged
|
|
141
|
+
* write exists, so before that {@link getValue}/{@link getValueDirect}
|
|
142
|
+
* serve from the first-touch state view and {@link commit} records an
|
|
143
|
+
* empty bundle — all with ZERO `structuredClone`s of the shared state.
|
|
144
|
+
*
|
|
145
|
+
* The buffer's base is the FIRST-TOUCH view, NOT the live state at write
|
|
146
|
+
* time: under parallel forks a sibling may have committed between this
|
|
147
|
+
* stage's first read and this write, and the net-change diff base must
|
|
148
|
+
* stay anchored at first touch to match the eager engine — see
|
|
149
|
+
* {@link firstTouchState}. */
|
|
72
150
|
getTransactionBuffer(): TransactionBuffer;
|
|
73
151
|
/** Builds an absolute path inside the shared memory (run namespace). */
|
|
74
152
|
private withNamespace;
|
|
@@ -82,7 +160,27 @@ export declare class StageContext {
|
|
|
82
160
|
updateGlobalContext(key: string, value: unknown): void;
|
|
83
161
|
appendToArray(path: string[], key: string, items: unknown[], description?: string): void;
|
|
84
162
|
mergeObject(path: string[], key: string, obj: Record<string, unknown>, description?: string): void;
|
|
85
|
-
|
|
163
|
+
/** Buffer-aware read, mirroring the eager engine's read order byte-for-byte:
|
|
164
|
+
*
|
|
165
|
+
* 1. staged writes + first-touch snapshot — `buffer.get` over its
|
|
166
|
+
* workingCopy when the buffer exists, else `nativeGet` over the
|
|
167
|
+
* zero-clone state view (the buffer's base IS that view, so the two
|
|
168
|
+
* tiers agree on content);
|
|
169
|
+
* 2. LIVE state via `sharedMemory.getValue` for keys absent from the
|
|
170
|
+
* snapshot — including its run→global namespace fallback. The eager
|
|
171
|
+
* engine had this exact live fallback for snapshot-missing keys;
|
|
172
|
+
* byte-identity over purity.
|
|
173
|
+
*
|
|
174
|
+
* Reads never construct the buffer (#13): a stage that never writes
|
|
175
|
+
* performs zero clones of the shared state. */
|
|
176
|
+
private readState;
|
|
177
|
+
/**
|
|
178
|
+
* Tracked read. The returned value is BORROWED — see the contract on
|
|
179
|
+
* `ScopeFacade.getValue`. Read-tracking cost is policy-gated (#14):
|
|
180
|
+
* `'full'` clones the value into `_stageReads` (historical default),
|
|
181
|
+
* `'summary'` records a cheap marker, `'off'` records nothing.
|
|
182
|
+
*/
|
|
183
|
+
getValue(path: string[], key?: string, description?: string): unknown;
|
|
86
184
|
/** Read state without tracking in _stageReads or paying structuredClone cost.
|
|
87
185
|
* Used by ScopeFacade.getValueSilent() for array proxy internal operations. */
|
|
88
186
|
getValueDirect(path: string[], key?: string): unknown;
|
|
@@ -97,6 +195,16 @@ export declare class StageContext {
|
|
|
97
195
|
operation: 'set' | 'update' | 'delete';
|
|
98
196
|
}>) => void): void;
|
|
99
197
|
commit(): void;
|
|
198
|
+
/**
|
|
199
|
+
* Create (or return) this context's linked successor.
|
|
200
|
+
*
|
|
201
|
+
* MEMOIZED: the first call creates `this.next`; every later call returns
|
|
202
|
+
* that SAME context and IGNORES its arguments. In normal traversal each
|
|
203
|
+
* context advances exactly once, so the memo never bites — but a caller
|
|
204
|
+
* expecting a fresh context for different `stageName`/`stageId` args gets
|
|
205
|
+
* the old one silently. Dev mode (`enableDevMode()`) warns on that
|
|
206
|
+
* mismatch (backlog B4).
|
|
207
|
+
*/
|
|
100
208
|
createNext(path: string, stageName: string, stageId: string, isDecider?: boolean): StageContext;
|
|
101
209
|
createChild(runId: string, branchId: string, stageName: string, stageId: string, isDecider?: boolean): StageContext;
|
|
102
210
|
createDecider(path: string, stageName: string, stageId: string): StageContext;
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TransactionBuffer —
|
|
2
|
+
* TransactionBuffer — Per-stage STAGING buffer for state mutations
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
4
|
+
* What it IS: a staging buffer with read-your-writes and net-change commits.
|
|
5
|
+
* - Changes are staged here during stage execution and flushed to
|
|
6
|
+
* SharedMemory in ONE batch per stage (`commit()`) — other stages and
|
|
7
|
+
* parallel siblings never observe a stage's half-finished writes.
|
|
8
|
+
* - Read-after-write consistency within a stage — a stage sees its own
|
|
9
|
+
* staged writes immediately.
|
|
10
|
+
* - `commit()` records the stage's NET change (see {@link commit}), plus an
|
|
11
|
+
* operation trace for deterministic replay.
|
|
12
|
+
*
|
|
13
|
+
* What it is NOT: a rollback mechanism. Despite the name, there is no
|
|
14
|
+
* abort/rollback path — when a stage THROWS, the engine still commits
|
|
15
|
+
* everything staged so far before re-throwing (commit-on-error in
|
|
16
|
+
* `FlowchartTraverser`). That is deliberate: the audit trail must record
|
|
17
|
+
* what the failing stage changed. Do not rely on "stage failed → its
|
|
18
|
+
* writes vanished".
|
|
9
19
|
*/
|
|
10
20
|
import type { MemoryPatch } from './types.js';
|
|
11
21
|
export declare class TransactionBuffer {
|
|
@@ -9,5 +9,6 @@ export { EventLog } from './EventLog.js';
|
|
|
9
9
|
export { SharedMemory } from './SharedMemory.js';
|
|
10
10
|
export { StageContext } from './StageContext.js';
|
|
11
11
|
export { TransactionBuffer } from './TransactionBuffer.js';
|
|
12
|
-
export type { CommitBundle, FlowControlType, FlowMessage, MemoryPatch, ScopeFactory, StageSnapshot, TraceEntry, } from './types.js';
|
|
12
|
+
export type { CommitBundle, FlowControlType, FlowMessage, MemoryPatch, ReadSummaryMarker, ReadTrackingMode, ScopeFactory, StageSnapshot, TraceEntry, } from './types.js';
|
|
13
|
+
export { READ_PREVIEW_LENGTH } from './types.js';
|
|
13
14
|
export { applySmartMerge, deepSmartMerge, DELIM, getNestedValue, getRunAndGlobalPaths, normalisePath, redactPatch, setNestedValue, updateNestedValue, updateValue, } from './utils.js';
|
|
@@ -45,6 +45,47 @@ export interface FlowMessage {
|
|
|
45
45
|
iteration?: number;
|
|
46
46
|
timestamp?: number;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Policy for how tracked reads are recorded into `StageSnapshot.stageReads`.
|
|
50
|
+
*
|
|
51
|
+
* - `'full'` (default) — every tracked read `structuredClone`s the value into
|
|
52
|
+
* the stage's read view. Byte-identical to the historical behavior; this is
|
|
53
|
+
* what snapshot consumers (lens, agentfootprint) see today.
|
|
54
|
+
* - `'summary'` — reads record a cheap {@link ReadSummaryMarker} (type + size
|
|
55
|
+
* proxy + short preview) instead of the cloned value. O(1)-ish per read —
|
|
56
|
+
* no value clone, no serialization of large objects.
|
|
57
|
+
* - `'off'` — reads are not recorded at all; `stageReads` is absent from the
|
|
58
|
+
* snapshot. Zero per-read cost. Values are still readable, and the
|
|
59
|
+
* `ScopeRecorder.onRead` event still fires (it passes the live reference and
|
|
60
|
+
* never cloned) — so narrative output is identical in every mode. The policy
|
|
61
|
+
* scopes ONLY the snapshot's `stageReads` payload.
|
|
62
|
+
*
|
|
63
|
+
* Set via `new FlowChartExecutor(chart, { readTracking })` or
|
|
64
|
+
* `executor.setReadTracking(mode)` (before `run()`).
|
|
65
|
+
*/
|
|
66
|
+
export type ReadTrackingMode = 'full' | 'summary' | 'off';
|
|
67
|
+
/**
|
|
68
|
+
* Marker recorded in `StageSnapshot.stageReads` under `readTracking: 'summary'`.
|
|
69
|
+
*
|
|
70
|
+
* Honest cost note: `size` is a cheap proxy (string length / array length /
|
|
71
|
+
* object key count), NOT a serialized byte count — computing real byte size
|
|
72
|
+
* would require an O(value) serialization, which is exactly the cost the
|
|
73
|
+
* summary mode removes. `preview` is only produced for primitives and strings
|
|
74
|
+
* (first {@link READ_PREVIEW_LENGTH} characters); objects and arrays carry no
|
|
75
|
+
* preview for the same reason.
|
|
76
|
+
*/
|
|
77
|
+
export interface ReadSummaryMarker {
|
|
78
|
+
/** Discriminant — lets snapshot consumers detect marker entries. */
|
|
79
|
+
__readSummary: true;
|
|
80
|
+
/** `typeof` result, refined to 'array' / 'null' for objects. */
|
|
81
|
+
type: 'string' | 'number' | 'boolean' | 'bigint' | 'symbol' | 'function' | 'object' | 'array' | 'null';
|
|
82
|
+
/** Size proxy: string length, array length, or object key count. */
|
|
83
|
+
size?: number;
|
|
84
|
+
/** First {@link READ_PREVIEW_LENGTH} chars — primitives and strings only. */
|
|
85
|
+
preview?: string;
|
|
86
|
+
}
|
|
87
|
+
/** Max characters captured in {@link ReadSummaryMarker.preview}. */
|
|
88
|
+
export declare const READ_PREVIEW_LENGTH = 80;
|
|
48
89
|
/** Serialisable representation of a stage's state (for debugging / visualisation). */
|
|
49
90
|
export type StageSnapshot = {
|
|
50
91
|
id: string;
|
|
@@ -59,7 +100,10 @@ export type StageSnapshot = {
|
|
|
59
100
|
isFork?: boolean;
|
|
60
101
|
/** User-level writes made by this stage (pre-namespace keys → values). */
|
|
61
102
|
stageWrites?: Record<string, unknown>;
|
|
62
|
-
/** User-level reads made by this stage (pre-namespace keys → values at read
|
|
103
|
+
/** User-level reads made by this stage (pre-namespace keys → values at read
|
|
104
|
+
* time). Shape depends on {@link ReadTrackingMode}: cloned values under
|
|
105
|
+
* `'full'` (default), {@link ReadSummaryMarker}s under `'summary'`, absent
|
|
106
|
+
* under `'off'`. */
|
|
63
107
|
stageReads?: Record<string, unknown>;
|
|
64
108
|
logs: Record<string, unknown>;
|
|
65
109
|
errors: Record<string, unknown>;
|
|
@@ -61,7 +61,8 @@ import type { EmitRecorder } from './EmitRecorder.js';
|
|
|
61
61
|
* Method names that appear on BOTH `ScopeRecorder` and `FlowRecorder` but with
|
|
62
62
|
* different event payload types. For these, a `CombinedRecorder` declares
|
|
63
63
|
* ONE handler that receives the union of both payloads — consumers
|
|
64
|
-
* discriminate
|
|
64
|
+
* discriminate with the exported `isFlowEvent()` helper (explicit `channel`
|
|
65
|
+
* discriminant, with a legacy pipelineId-absence fallback).
|
|
65
66
|
*/
|
|
66
67
|
type SharedLifecycleOverlap = 'onError' | 'onPause' | 'onResume';
|
|
67
68
|
/** Lifecycle hooks (not event-specific) that both interfaces share identically. */
|
|
@@ -81,8 +82,8 @@ type SharedLifecycle = 'id' | 'clear' | 'toSnapshot';
|
|
|
81
82
|
* Both `ScopeRecorder` and `FlowRecorder` declare these with DIFFERENT payload
|
|
82
83
|
* shapes. In a combined recorder, each such handler is called by BOTH
|
|
83
84
|
* channels with its own variant. The parameter type is a union — consumers
|
|
84
|
-
* can either handle both variants uniformly, or discriminate
|
|
85
|
-
*
|
|
85
|
+
* can either handle both variants uniformly, or discriminate with
|
|
86
|
+
* `isFlowEvent()` (explicit `channel` discriminant stamped by the engine).
|
|
86
87
|
*
|
|
87
88
|
* ## Forward compatibility
|
|
88
89
|
*
|
|
@@ -110,12 +111,17 @@ export type CombinedRecorder = Partial<Omit<ScopeRecorder, SharedLifecycleOverla
|
|
|
110
111
|
*
|
|
111
112
|
* ## How it discriminates
|
|
112
113
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* `
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
114
|
+
* 1. **Explicit `channel` field first** (backlog B3): every engine-dispatched
|
|
115
|
+
* shared-method event is stamped `channel: 'flow'` (control-flow) or
|
|
116
|
+
* `channel: 'scope'` (data-flow) at construction. This is the positive,
|
|
117
|
+
* schema-robust signal — it survives wrappers that add/strip fields.
|
|
118
|
+
* 2. **Legacy fallback** for unstamped events (consumer-fabricated tests,
|
|
119
|
+
* persisted traces from <9.2): scope-channel events extend
|
|
120
|
+
* `RecorderContext`, which carries `pipelineId`; flow-channel events do
|
|
121
|
+
* not. The flow variant is detected by the ABSENCE of `pipelineId` —
|
|
122
|
+
* schema-stable as long as scope events continue to extend
|
|
123
|
+
* `RecorderContext`, but fragile against field-stripping wrappers, which
|
|
124
|
+
* is why the explicit discriminant now exists.
|
|
119
125
|
*
|
|
120
126
|
* @example
|
|
121
127
|
* ```ts
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { EventLog } from '../memory/EventLog.js';
|
|
13
13
|
import { SharedMemory } from '../memory/SharedMemory.js';
|
|
14
14
|
import { StageContext } from '../memory/StageContext.js';
|
|
15
|
-
import type { CommitBundle, StageSnapshot } from '../memory/types.js';
|
|
15
|
+
import type { CommitBundle, ReadTrackingMode, StageSnapshot } from '../memory/types.js';
|
|
16
16
|
/** Snapshot of a single recorder's collected data. */
|
|
17
17
|
export interface RecorderSnapshot {
|
|
18
18
|
id: string;
|
|
@@ -63,6 +63,15 @@ export declare class ExecutionRuntime {
|
|
|
63
63
|
* allocation, no functional difference in the snapshot.
|
|
64
64
|
*/
|
|
65
65
|
enableRedactedMirror(): void;
|
|
66
|
+
/**
|
|
67
|
+
* Set the read-tracking policy (#14) on the root stage context. Descendant
|
|
68
|
+
* contexts inherit via `createNext`/`createChild`; subflow root contexts
|
|
69
|
+
* inherit from their parent-mount context via `SubflowExecutor`. Called by
|
|
70
|
+
* `FlowChartExecutor.createTraverser()` when the executor's policy is not
|
|
71
|
+
* the default `'full'` — including the resume path, where it is applied to
|
|
72
|
+
* the freshly-created continuation root.
|
|
73
|
+
*/
|
|
74
|
+
useReadTracking(mode: ReadTrackingMode): void;
|
|
66
75
|
/** Preserve the current rootStageContext for snapshots before changing it for resume. */
|
|
67
76
|
preserveSnapshotRoot(): void;
|
|
68
77
|
getPipelines(): string[];
|
|
@@ -22,6 +22,7 @@ import type { CombinedNarrativeEntry } from '../engine/narrative/narrativeTypes.
|
|
|
22
22
|
import type { ManifestEntry } from '../engine/narrative/recorders/ManifestFlowRecorder.js';
|
|
23
23
|
import type { FlowRecorder } from '../engine/narrative/types.js';
|
|
24
24
|
import { type ExecutorResult, type RunOptions, type ScopeFactory, type SerializedPipelineStructure, type StageNode, type StreamHandlers, type SubflowResult } from '../engine/types.js';
|
|
25
|
+
import type { ReadTrackingMode } from '../memory/types.js';
|
|
25
26
|
import type { FlowchartCheckpoint } from '../pause/types.js';
|
|
26
27
|
import type { CombinedRecorder } from '../recorder/CombinedRecorder.js';
|
|
27
28
|
import type { EmitRecorder } from '../recorder/EmitRecorder.js';
|
|
@@ -63,6 +64,19 @@ export interface FlowChartExecutorOptions<TScope = any> {
|
|
|
63
64
|
initialContext?: unknown;
|
|
64
65
|
/** Read-only input accessible via `scope.getArgs()` — never tracked or written. */
|
|
65
66
|
readOnlyContext?: unknown;
|
|
67
|
+
/**
|
|
68
|
+
* Policy for `StageSnapshot.stageReads` (#14). Default `'full'` — every
|
|
69
|
+
* tracked read `structuredClone`s the value into the stage's read view
|
|
70
|
+
* (the historical behavior; what lens/agentfootprint snapshots show).
|
|
71
|
+
* `'summary'` records a cheap type/size/preview marker per read; `'off'`
|
|
72
|
+
* records nothing — zero per-read clone cost (reads of large values become
|
|
73
|
+
* ~free). Narrative and `ScopeRecorder.onRead` are identical in every mode.
|
|
74
|
+
* Caveat: under `'off'` a stage's snapshot is indistinguishable from one
|
|
75
|
+
* that read nothing — auditing consumers that need "did it read?" without
|
|
76
|
+
* the value cost should prefer `'summary'`.
|
|
77
|
+
* Equivalent to calling `executor.setReadTracking(mode)` before `run()`.
|
|
78
|
+
*/
|
|
79
|
+
readTracking?: ReadTrackingMode;
|
|
66
80
|
/**
|
|
67
81
|
* Custom error classifier for throttling detection. Return `true` if the
|
|
68
82
|
* error represents a rate-limit or backpressure condition (the executor will
|
|
@@ -142,6 +156,13 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
142
156
|
* Must be called before run().
|
|
143
157
|
*/
|
|
144
158
|
setRedactionPolicy(policy: RedactionPolicy): void;
|
|
159
|
+
/**
|
|
160
|
+
* Set the read-tracking policy for `StageSnapshot.stageReads` (#14).
|
|
161
|
+
* Must be called before run(). Equivalent to the `readTracking`
|
|
162
|
+
* constructor option — see {@link FlowChartExecutorOptions.readTracking}
|
|
163
|
+
* for the mode semantics ('full' default / 'summary' / 'off').
|
|
164
|
+
*/
|
|
165
|
+
setReadTracking(mode: ReadTrackingMode): void;
|
|
145
166
|
/**
|
|
146
167
|
* Returns a compliance-friendly report of all redaction activity from the
|
|
147
168
|
* most recent run. Never includes actual values.
|
|
@@ -243,9 +264,10 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
243
264
|
* on clone failure we sanitize the diagnostic bags (non-cloneable values
|
|
244
265
|
* become '[non-serializable: …]' markers — the live engine bags are never
|
|
245
266
|
* touched) and retry. If the retry STILL fails, the violation is in
|
|
246
|
-
* consumer-owned data (realistically `pauseData` — a function
|
|
247
|
-
*
|
|
248
|
-
*
|
|
267
|
+
* consumer-owned data (realistically `pauseData` — a function can never
|
|
268
|
+
* reach shared state in the first place: TransactionBuffer clones every
|
|
269
|
+
* written value at write time, so the offending write already rejected)
|
|
270
|
+
* and we throw a DESCRIPTIVE contract error naming the offending
|
|
249
271
|
* checkpoint field(s). A naked DataCloneError never escapes.
|
|
250
272
|
*
|
|
251
273
|
* Subflow scope capture (`subflowStates`) survives ONLY on the signal — the
|
|
@@ -134,7 +134,30 @@ export declare class ScopeFacade {
|
|
|
134
134
|
* the existing getValue() contract where user code always receives raw data. */
|
|
135
135
|
getValueSilent(key?: string): unknown;
|
|
136
136
|
getInitialValueFor(key: string): any;
|
|
137
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Tracked read of shared state.
|
|
139
|
+
*
|
|
140
|
+
* **Read values are BORROWED — do not mutate them.** Since the lazy buffer
|
|
141
|
+
* (#13), reads before the stage's first write return references INTO
|
|
142
|
+
* COMMITTED SHARED STATE, and reads after a write return references into
|
|
143
|
+
* the stage's private transaction-buffer working copy (the eager engine
|
|
144
|
+
* returned references into that working copy for ALL reads). Mutating a
|
|
145
|
+
* returned value in place would corrupt state without a commit record —
|
|
146
|
+
* write changes back via `setValue`/`updateValue` instead. TypedScope
|
|
147
|
+
* consumers are safe automatically: the proxy routes every mutation
|
|
148
|
+
* through `setValue`/`updateValue`/copy-on-write array ops.
|
|
149
|
+
*
|
|
150
|
+
* There is deliberately NO dev-mode freeze guard here: deep-freezing a
|
|
151
|
+
* buffer-served read would freeze the stage's own working copy and make a
|
|
152
|
+
* legitimate read-then-deep-write throw, and freezing a committed-state
|
|
153
|
+
* read mutates an object shared with every other consumer of the live
|
|
154
|
+
* state. See `src/lib/memory/README.md` ("Read values are borrowed").
|
|
155
|
+
*
|
|
156
|
+
* Recorder note: the `onRead` event below passes the SAME live reference
|
|
157
|
+
* (no clone) unless field-level redaction scrubs a copy — recorders must
|
|
158
|
+
* treat event values as read-only too.
|
|
159
|
+
*/
|
|
160
|
+
getValue(key?: string): unknown;
|
|
138
161
|
setValue(key: string, value: unknown, shouldRedact?: boolean, description?: string): void;
|
|
139
162
|
updateValue(key: string, value: unknown, description?: string): void;
|
|
140
163
|
deleteValue(key: string, description?: string): void;
|
|
@@ -38,15 +38,26 @@ export interface ErrorEvent extends RecorderContext {
|
|
|
38
38
|
error: Error;
|
|
39
39
|
operation: 'read' | 'write' | 'commit';
|
|
40
40
|
key?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Explicit channel discriminant — `'scope'` on every engine-dispatched
|
|
43
|
+
* event. `isFlowEvent()` checks it first (backlog B3); optional so
|
|
44
|
+
* consumer-fabricated events (tests, replays) remain type-valid and fall
|
|
45
|
+
* back to the legacy pipelineId-presence heuristic.
|
|
46
|
+
*/
|
|
47
|
+
channel?: 'scope';
|
|
41
48
|
}
|
|
42
49
|
export interface StageEvent extends RecorderContext {
|
|
43
50
|
duration?: number;
|
|
44
51
|
}
|
|
45
52
|
export interface PauseEvent extends RecorderContext {
|
|
46
53
|
pauseData?: unknown;
|
|
54
|
+
/** Explicit channel discriminant — see {@link ErrorEvent.channel}. */
|
|
55
|
+
channel?: 'scope';
|
|
47
56
|
}
|
|
48
57
|
export interface ResumeEvent extends RecorderContext {
|
|
49
58
|
hasInput: boolean;
|
|
59
|
+
/** Explicit channel discriminant — see {@link ErrorEvent.channel}. */
|
|
60
|
+
channel?: 'scope';
|
|
50
61
|
}
|
|
51
62
|
/**
|
|
52
63
|
* Declarative redaction configuration — define once, applied everywhere.
|
package/package.json
CHANGED