@sensigo/realm-mcp 0.26.0 → 0.27.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/dist/index.d.ts CHANGED
@@ -3,5 +3,5 @@ export type { RealmMcpServerOptions } from './server.js';
3
3
  export { generateProtocol } from './protocol/generator.js';
4
4
  export type { WorkflowProtocol, ProtocolStep } from './protocol/generator.js';
5
5
  export { JsonTraceBufferStore } from './json-trace-buffer-store.js';
6
- export declare const VERSION = "0.26.0";
6
+ export declare const VERSION = "0.27.0";
7
7
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -4,5 +4,5 @@ export { generateProtocol } from './protocol/generator.js';
4
4
  // issue #107: exported so the operator run-purge CLI command (packages/cli) can construct one
5
5
  // and register it as a PerRunArtifactStore alongside JsonFileStore/FailedAttemptStore.
6
6
  export { JsonTraceBufferStore } from './json-trace-buffer-store.js';
7
- export const VERSION = '0.26.0';
7
+ export const VERSION = '0.27.0';
8
8
  //# sourceMappingURL=index.js.map
@@ -1,19 +1,162 @@
1
- import { type TraceBufferStore, type BufferedEntry, type AppendResult, type PerRunArtifactStore, type OrphanSweepableStore, type OrphanArtifact } from '@sensigo/realm';
1
+ import { type TraceBufferStore, type BufferedEntry, type AppendResult, type AppendOptions, type TraceCapability, type SealResult, type SealedArtifact, type PerRunArtifactStore, type OrphanSweepableStore, type OrphanArtifact } from '@sensigo/realm';
2
2
  import type { AgentTraceEntry } from '@sensigo/realm';
3
+ /** A `proper-lockfile` retry policy: either a bare retry count (its own simple default backoff)
4
+ * or an explicit `retry`-module-compatible options object. */
5
+ type LockRetries = number | {
6
+ retries: number;
7
+ minTimeout?: number;
8
+ maxTimeout?: number;
9
+ };
10
+ /** Lock-acquisition profile shared by every `lockWal` call in this file (issue #207).
11
+ * Constructor-injectable so a conformance suite can inflate the retry budget (e.g. to make a
12
+ * deliberately slow/latched guard's lock contention observable instead of exhausting the retry
13
+ * budget too quickly and masking the scenario under test). */
14
+ export interface TraceBufferLockProfile {
15
+ retries: LockRetries;
16
+ stale: number;
17
+ realpath: boolean;
18
+ }
3
19
  /**
4
20
  * File-based TraceBufferStore that persists WAL entries to JSONL files on disk.
5
21
  * WAL file path: <runsDir>/trace-buffer-<runId>-<base64url(stepId)>.jsonl
22
+ *
23
+ * Crash model: like `JsonFileStore`, this store never calls `fsync` — process-crash consistency
24
+ * comes from `appendFile`'s per-line granularity plus `readWal`'s per-line, torn-line-tolerant
25
+ * parsing (a crash mid-write can leave one unparseable trailing line, which is skipped; every
26
+ * earlier, already-written line survives intact). Durability across a true power loss (not just a
27
+ * process crash) is outside this store's contract, unchanged from its pre-#207 posture.
28
+ *
29
+ * Declares the fenced trio (issue #207): `read`, `delete`, and `deleteAllForRun` now serialize on
30
+ * the SAME per-(runId, stepId) critical section (a `proper-lockfile` lock on the WAL path)
31
+ * `appendFenced`/`deleteFenced`/`deleteAllForRunFenced` use — see `lockWal` and the interface's
32
+ * own doc for the full contract.
33
+ *
34
+ * Issue #197 PR-1 additionally declares BOTH capability-ladder rungs (`seal` and
35
+ * `writer_nonce_carriage`, `traceCapabilities`). `sealFenced` retires a live WAL file to a sealed
36
+ * artifact (`sealed-trace-<runId>-<base64url(stepId)>.<seq>.jsonl`) via the SAME `lockWal`
37
+ * chokepoint every other operation uses — see `sealFenced`'s own doc for the no-clobber move
38
+ * mechanics.
6
39
  */
7
40
  export declare class JsonTraceBufferStore implements TraceBufferStore, PerRunArtifactStore, OrphanSweepableStore {
41
+ readonly traceCapabilities: ReadonlySet<TraceCapability>;
8
42
  private readonly runsDir;
9
- constructor(runsDir: string);
43
+ private readonly lockProfile;
44
+ constructor(runsDir: string, lockProfile?: Partial<TraceBufferLockProfile>);
10
45
  private walPath;
46
+ /** Builds the on-disk path for one sealed artifact — see `SEALED_PREFIX`'s doc for the shape
47
+ * and why it never collides with a live WAL path. */
48
+ private sealedWalPath;
49
+ /**
50
+ * The SOLE `lockfile.lock(` call site in this file (issue #207) — every critical-section
51
+ * acquisition (`append`, `appendFenced`, `read`, `delete`, `deleteFenced`, `deleteAllForRun`,
52
+ * `deleteAllForRunFenced`) goes through this one chokepoint. Pins the shared base options
53
+ * (`stale`/`realpath`) and always installs an explicit `onCompromised` handler — a loud
54
+ * `console.warn` naming the WAL path — never `proper-lockfile`'s own default handler, which
55
+ * THROWS from inside a timer callback and crashes the process. `retriesOverride` lets
56
+ * `append`/`appendFenced` keep their own, more patient retry count; every other caller uses
57
+ * this store's configured `lockProfile.retries` (constructor-injectable — e.g. a conformance
58
+ * suite inflating it to make contention observable rather than exhausted-too-fast).
59
+ *
60
+ * Verified (issue #207): `lockfile.lock(path, { realpath: false })` acquires cleanly against a
61
+ * path that does not yet exist — no pre-lock placeholder file is needed for that. `append()`'s
62
+ * own placeholder-creation below predates this and stays byte-identical for that method, but no
63
+ * fenced method replicates it.
64
+ */
65
+ private lockWal;
11
66
  private readWal;
12
- append(runId: string, stepId: string, entries: AgentTraceEntry[]): Promise<AppendResult>;
67
+ /**
68
+ * Count + bytes for exactly the batches belonging to `writerNonce` (issue #197 PR-1, design §5's
69
+ * byte-attribution rule) — `undefined` = ⊥, the bare/anonymous writer class.
70
+ *
71
+ * A NONCED writer's stats are computed DIRECTLY: sum the `entries.length` and re-serialized
72
+ * byte size of exactly its own successfully-parsed lines. `JSON.stringify` of a parsed
73
+ * `WalLine` reproduces its ORIGINAL on-disk bytes exactly — this file only ever writes lines via
74
+ * `JSON.stringify({ts, entries[, nonce]})`, and V8 preserves string-key insertion order through
75
+ * parse→re-stringify, so re-serializing a parsed line is bit-for-bit identical to how it was
76
+ * actually written.
77
+ *
78
+ * ⊥'s stats are a RESIDUAL, not a direct sum: `bytes = fileBytes - Σ(every DISTINCT nonced
79
+ * partition's own bytes)`. This is what makes ⊥ "inherit all unattributable bytes" — a
80
+ * torn/unparseable line's raw bytes are captured in `fileBytes` (computed from the whole raw
81
+ * file content, not from re-stringifying parsed lines) but can never be subtracted out as part
82
+ * of any nonced partition (it never parsed into one), so they remain in ⊥'s residual exactly as
83
+ * they always silently were before this capability existed. For an all-bare file (no nonced
84
+ * lines at all), the residual is arithmetically the WHOLE file — byte-identical to the pre-#197
85
+ * formula, emergent rather than special-cased.
86
+ */
87
+ private partitionStats;
88
+ /**
89
+ * The actual write logic, shared by `append` and `appendFenced` (issue #207) so BUFFER_FULL /
90
+ * normalization / `AppendResult` shape live in exactly one place. Assumes the caller already
91
+ * holds the per-path critical section. `writerNonce` (issue #197 PR-1) is `undefined` for a bare
92
+ * call — the byte-identical legacy path (this file's own `newLine` shape omits the `nonce` key
93
+ * entirely when so, exactly matching the pre-#197 JSONL bytes for all-bare traffic).
94
+ */
95
+ private appendWithinCS;
96
+ append(runId: string, stepId: string, entries: AgentTraceEntry[], options?: AppendOptions): Promise<AppendResult>;
97
+ /**
98
+ * `guard` runs INSIDE the critical section, immediately before the physical write (issue #207)
99
+ * — see the interface doc for the full guard contract. NO pre-lock placeholder file: verified
100
+ * `lockfile.lock(path, { realpath: false })` acquires cleanly against a target that does not yet
101
+ * exist; the legacy `append()`'s placeholder above predates this and stays byte-identical there,
102
+ * but is not needed and is not replicated here — `appendFile`'s own `O_CREAT`, inside the
103
+ * critical section, is the sole creator of the WAL file on this path.
104
+ */
105
+ appendFenced(runId: string, stepId: string, entries: AgentTraceEntry[], guard: () => Promise<void>, options?: AppendOptions): Promise<AppendResult>;
106
+ /** `_nonce` is re-attached per-line ONLY when that line actually carried one — via the SAME
107
+ * `flattenWalBatches` core helper the in-memory store uses, so "never fabricate `_nonce` for a
108
+ * bare line" is enforced from exactly one shared code path rather than reimplemented twice. */
13
109
  read(runId: string, stepId: string): Promise<BufferedEntry[]>;
14
110
  delete(runId: string, stepId: string): Promise<void>;
15
111
  /**
16
- * Deletes every orphaned WAL file for `runId` (issue #107).
112
+ * `guard` runs INSIDE the same per-path critical section `append`/`appendFenced` use,
113
+ * immediately before the delete (issue #207) — see the interface doc for the full guard
114
+ * contract. Returns the number of entries actually deleted (`0` = buffer already absent; the
115
+ * guard still ran first). Counts via the already-open `readWal` internals — NEVER the public
116
+ * `read()`, which would re-acquire this same lock (a critical section must never be re-entered
117
+ * from within itself — `proper-lockfile` is not reentrant).
118
+ */
119
+ deleteFenced(runId: string, stepId: string, guard: () => Promise<void>): Promise<number>;
120
+ /**
121
+ * `guard` runs INSIDE the SAME per-path critical section every other operation on this key
122
+ * uses (issue #197 PR-1, the `seal` rung — design §4: "no second locking path"), immediately
123
+ * before the seal-move. Atomically retires the live WAL file to a new sealed artifact via the
124
+ * no-clobber `link`-then-`unlink` primitive (`linkNoClobberThenUnlink`) — plain `rename()` is
125
+ * FORBIDDEN here, since it would silently overwrite an existing sealed artifact at the same
126
+ * `seq`.
127
+ *
128
+ * `seq` is probed by the link attempt ITSELF (no pre-listing, no separate TOCTOU-prone scan):
129
+ * starting at 0, an `EEXIST` on the link means that `seq` is already taken by an earlier seal —
130
+ * bump and retry, bounded by `SEALED_ARTIFACTS_LIMIT_PER_STEP`. Exhausting the bound without
131
+ * success returns `{sealed: false, reason: 'capped'}` — the caller falls back to the existing
132
+ * destructive drain (`deleteFenced`), never a silent eviction of an already-sealed artifact.
133
+ *
134
+ * `{sealed: false, reason: 'absent'}` when no live WAL file exists for this key AT ALL (checked
135
+ * via `statIfExists` — #183's ENOENT-is-absence discipline) — nothing to seal is success, not a
136
+ * failure. A present-but-empty file (e.g. `append()`'s legacy placeholder) is NOT "absent" — it
137
+ * gets sealed like any other live WAL (a harmless, if pointless, empty sealed artifact).
138
+ */
139
+ sealFenced(runId: string, stepId: string, guard: () => Promise<void>): Promise<SealResult>;
140
+ /**
141
+ * Lock-free point-in-time read of every sealed artifact for `runId`, across all its steps
142
+ * (issue #197 PR-1, the `seal` rung) — matches `readAllForRun`'s deliberately-unlocked posture.
143
+ * Parses each sealed file torn-tolerant, per-line, exactly like `readWal`/`readAllForRun` (a
144
+ * sealed artifact's raw bytes moved verbatim from the live WAL, including any trailing torn
145
+ * line it already had at seal time — this is a READ concern, not something sealing fixes).
146
+ */
147
+ listSealedForRun(runId: string): Promise<SealedArtifact[]>;
148
+ /** Resolves the candidate WAL files for `runId`, sorted deterministically — shared by
149
+ * `deleteAllForRun` and `deleteAllForRunFenced` (issue #207). */
150
+ private matchingWalFiles;
151
+ /** Resolves the candidate SEALED artifact files for `runId`, sorted deterministically — the
152
+ * same shared-`dirEntries`-or-own-`readdir` shape as `matchingWalFiles` (issue #197 PR-1: a
153
+ * sealed artifact is retained only until its owning run itself is purged — design §4). */
154
+ private matchingSealedFiles;
155
+ /**
156
+ * Deletes every orphaned WAL file for `runId` (issue #107). Now serialized per-file on the same
157
+ * critical section `appendFenced`/`deleteFenced` use (issue #207) — declaring the fenced trio
158
+ * commits this legacy method too. Issue #197 PR-1: sealed artifacts for this run join the same
159
+ * sweep (`matchingSealedFiles`) — a sealed artifact outlives its step but not its run.
17
160
  *
18
161
  * @param dirEntries Optional pre-scanned `readdir(runsDir)` listing supplied by a batch purge —
19
162
  * when present, this method filters it in-memory instead of re-scanning the directory itself
@@ -21,6 +164,28 @@ export declare class JsonTraceBufferStore implements TraceBufferStore, PerRunArt
21
164
  * omitted, exactly as before.
22
165
  */
23
166
  deleteAllForRun(runId: string, dirEntries?: readonly string[]): Promise<void>;
167
+ /**
168
+ * `guard` is RE-INVOKED inside EACH per-file critical section, immediately before that file's
169
+ * delete (issue #207) — a refusal on any one file aborts the whole sweep with that file's error
170
+ * (stop-on-first-error, matching the legacy method's own semantics). When zero files match
171
+ * `runId` at all, `guard` is still consulted at least once (issue #207 correction: the scan
172
+ * — resolving which files match — necessarily runs FIRST, since there is nothing to invoke a
173
+ * per-file guard against otherwise; the guard is then invoked once for the empty case). If it
174
+ * throws, the sweep rejects with that error exactly as it would for a non-empty sweep —
175
+ * propagation is UNIFORM across the zero-match and non-empty cases (the TCK asserts rejection
176
+ * here, not merely invocation count: a refusing guard makes even a zero-match sweep reject). A
177
+ * guard rejection, and a per-file lock-contention failure (classified `STATE_RUN_BUSY`,
178
+ * mirroring `deleteAllForRun`'s own classification above), both propagate UNWRAPPED — never
179
+ * touched by `toArtifactDeleteFailedError`, which still wraps genuine unlink/I-O failures (the
180
+ * #183 absence/unreachable/corrupt trichotomy, extended with this third, distinct guard-refusal
181
+ * outcome). The guard call itself sits OUTSIDE the try/catch scope that performs this wrapping
182
+ * (see the loop body below) — so a guard that happens to throw an `FsIoError` (e.g. its own
183
+ * lock-free `runStore.get` hitting EACCES) is never mistaken for `deleteIfExists`'s own failure.
184
+ *
185
+ * Issue #197 PR-1: sealed artifacts for this run join the same fenced sweep, same as the
186
+ * unfenced `deleteAllForRun` above.
187
+ */
188
+ deleteAllForRunFenced(runId: string, guard: () => Promise<void>, dirEntries?: readonly string[]): Promise<void>;
24
189
  /**
25
190
  * Reads every WAL file for `runId` across all steps — the read-only counterpart to
26
191
  * `deleteAllForRun`, for `realm run export`'s evidence assembly (issue #159). Mirrors
@@ -55,4 +220,5 @@ export declare class JsonTraceBufferStore implements TraceBufferStore, PerRunArt
55
220
  */
56
221
  listOrphans(liveRunIds: ReadonlySet<string>): Promise<OrphanArtifact[]>;
57
222
  }
223
+ export {};
58
224
  //# sourceMappingURL=json-trace-buffer-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"json-trace-buffer-store.d.ts","sourceRoot":"","sources":["../src/json-trace-buffer-store.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EAUpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAkBtD;;;GAGG;AACH,qBAAa,oBACX,YAAW,gBAAgB,EAAE,mBAAmB,EAAE,oBAAoB;IAEtE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,OAAO,EAAE,MAAM;IAI3B,OAAO,CAAC,OAAO;YAKD,OAAO;IA6Bf,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC;IA6ExF,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAQ7D,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY1D;;;;;;;OAOG;IACG,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAmCnF;;;;;;;;;;;;;;;OAeG;IACG,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IAiDtE;;;;;;;;;;;;;;OAcG;IACG,WAAW,CAAC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;CAiC9E"}
1
+ {"version":3,"file":"json-trace-buffer-store.d.ts","sourceRoot":"","sources":["../src/json-trace-buffer-store.ts"],"names":[],"mappings":"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"}