@sensigo/realm 0.23.0 → 0.24.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/engine/execution-loop.d.ts.map +1 -1
- package/dist/engine/execution-loop.js +13 -2
- package/dist/engine/execution-loop.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/store/failed-attempt-store.d.ts +12 -1
- package/dist/store/failed-attempt-store.d.ts.map +1 -1
- package/dist/store/failed-attempt-store.js +57 -11
- package/dist/store/failed-attempt-store.js.map +1 -1
- package/dist/store/fs-io.d.ts +65 -0
- package/dist/store/fs-io.d.ts.map +1 -0
- package/dist/store/fs-io.js +159 -0
- package/dist/store/fs-io.js.map +1 -0
- package/dist/store/json-file-store.d.ts +56 -1
- package/dist/store/json-file-store.d.ts.map +1 -1
- package/dist/store/json-file-store.js +204 -42
- package/dist/store/json-file-store.js.map +1 -1
- package/dist/store/orphan-sweepable-store.d.ts +29 -0
- package/dist/store/orphan-sweepable-store.d.ts.map +1 -0
- package/dist/store/orphan-sweepable-store.js +18 -0
- package/dist/store/orphan-sweepable-store.js.map +1 -0
- package/dist/store/per-run-artifact-store.d.ts +6 -0
- package/dist/store/per-run-artifact-store.d.ts.map +1 -1
- package/dist/types/workflow-error.d.ts +1 -1
- package/dist/types/workflow-error.d.ts.map +1 -1
- package/dist/types/workflow-error.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Stats } from 'node:fs';
|
|
2
|
+
import { WorkflowError } from '../types/workflow-error.js';
|
|
3
|
+
/**
|
|
4
|
+
* Thrown by the fs-io primitives for any non-ENOENT errno. Callers map this to a typed,
|
|
5
|
+
* domain-specific `WorkflowError` (e.g. `ENGINE_ARTIFACT_DELETE_FAILED` via
|
|
6
|
+
* {@link toArtifactDeleteFailedError} below) at the aggregate layer — this class exists only to
|
|
7
|
+
* carry the raw path + errno + cause across that boundary without losing information.
|
|
8
|
+
*/
|
|
9
|
+
export declare class FsIoError extends Error {
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly code: string;
|
|
12
|
+
constructor(op: string, path: string, cause: unknown);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Exported ONLY so the retry policy itself can be unit-tested directly with an injected fake
|
|
16
|
+
* `op` (mocking `node:fs/promises`'s real `unlink`/`readFile`/`stat` under Vitest+ESM is brittle —
|
|
17
|
+
* "Cannot spy on export ... Module namespace is not configurable in ESM" — so the policy is
|
|
18
|
+
* decoupled from any specific fs call and tested in isolation instead). Not part of the module's
|
|
19
|
+
* intended per-call public surface (`deleteIfExists`/`readIfExists`/`statIfExists` are).
|
|
20
|
+
*/
|
|
21
|
+
export declare function withWin32Retry<T>(op: () => Promise<T>): Promise<T>;
|
|
22
|
+
/**
|
|
23
|
+
* Deletes `path`. ENOENT → `false` (already gone — success, idempotent). Any other errno → throws
|
|
24
|
+
* {@link FsIoError}. Returns `true` if this call actually deleted the file.
|
|
25
|
+
*/
|
|
26
|
+
export declare function deleteIfExists(path: string): Promise<boolean>;
|
|
27
|
+
/**
|
|
28
|
+
* Reads `path` as utf8. ENOENT → `undefined` (absent). Any other errno → throws {@link FsIoError}.
|
|
29
|
+
* Parsing is the CALLER's job — this returns raw bytes only.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readIfExists(path: string): Promise<string | undefined>;
|
|
32
|
+
/**
|
|
33
|
+
* Stats `path`. ENOENT → `undefined` (absent). Any other errno → throws {@link FsIoError}.
|
|
34
|
+
*/
|
|
35
|
+
export declare function statIfExists(path: string): Promise<Stats | undefined>;
|
|
36
|
+
/**
|
|
37
|
+
* Retryable classification for the `ENGINE_ARTIFACT_DELETE_FAILED` aggregate error (issue #183):
|
|
38
|
+
* `EACCES`/`EISDIR`/`EPERM`/`EROFS` are permanent (retrying won't help — same permission/mount
|
|
39
|
+
* state); `EBUSY`/`EIO` are transient (a lock held elsewhere, a flaky disk read) — worth
|
|
40
|
+
* retrying. Any other or unknown errno defaults to `false` — conservative, since advertising a
|
|
41
|
+
* retry that might not help is worse than not advertising one that would have.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isRetryableArtifactErrno(code: string): boolean;
|
|
44
|
+
/** One artifact this store failed to delete, at the aggregate layer. */
|
|
45
|
+
export interface ArtifactDeleteFailure {
|
|
46
|
+
artifact: string;
|
|
47
|
+
code: string;
|
|
48
|
+
message: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Builds the aggregate `ENGINE_ARTIFACT_DELETE_FAILED` `WorkflowError` every
|
|
52
|
+
* `PerRunArtifactStore.deleteAllForRun` implementation throws on a genuine (non-ENOENT) failure —
|
|
53
|
+
* the single typed shape `purge` (and any other caller) can rely on regardless of which store, or
|
|
54
|
+
* which internal read/delete step, actually failed. `retryable` is true iff ANY listed failure is
|
|
55
|
+
* itself retryable (a partial success followed by one transient failure is worth retrying as a
|
|
56
|
+
* whole).
|
|
57
|
+
*/
|
|
58
|
+
export declare function artifactDeleteFailedError(runId: string, store: string, deleted: string[], failures: ArtifactDeleteFailure[]): WorkflowError;
|
|
59
|
+
/**
|
|
60
|
+
* Convenience for the common single-failure case: extracts the errno (from an {@link FsIoError}
|
|
61
|
+
* or a raw `NodeJS.ErrnoException`) and message from `err`, then wraps it via
|
|
62
|
+
* {@link artifactDeleteFailedError}.
|
|
63
|
+
*/
|
|
64
|
+
export declare function toArtifactDeleteFailedError(runId: string, store: string, deleted: string[], artifact: string, err: unknown): WorkflowError;
|
|
65
|
+
//# sourceMappingURL=fs-io.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs-io.d.ts","sourceRoot":"","sources":["../../src/store/fs-io.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAkB3D;;;;;GAKG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;CASrD;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAgBxE;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQnE;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAO5E;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAO3E;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED,wEAAwE;AACxE,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,EACjB,QAAQ,EAAE,qBAAqB,EAAE,GAChC,aAAa,CASf;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,EACjB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,OAAO,GACX,aAAa,CAIf"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// fs-io.ts — the ENOENT-discriminating filesystem primitive (issue #183).
|
|
2
|
+
//
|
|
3
|
+
// Every store I/O site must distinguish three outcomes, never conflate them:
|
|
4
|
+
// 1. ENOENT → absence → success (idempotent: a delete of a missing file succeeds; a read of a
|
|
5
|
+
// missing file returns "absent").
|
|
6
|
+
// 2. Any OTHER errno (EACCES/EPERM/EIO/EROFS/EBUSY/EISDIR/ENOTDIR/…) → THROW a typed error.
|
|
7
|
+
// ENOTDIR is corruption, not absence — it must be loud.
|
|
8
|
+
// 3. Parse/JSON corruption → RECOVER (per-line skip / self-heal) + a loud structured warning.
|
|
9
|
+
// Never silence, never throw. This is the CALLER's job — these primitives return raw
|
|
10
|
+
// bytes/booleans/stats only, never parse.
|
|
11
|
+
//
|
|
12
|
+
// A `process.platform === 'win32'`-gated bounded retry absorbs transient EBUSY/EPERM/ENOTEMPTY —
|
|
13
|
+
// the same shape Node's own `fs.rm` uses (`maxRetries`/`retryDelay`) — because Windows
|
|
14
|
+
// file-locking (an antivirus scan, an open handle from another process) can make a delete/read
|
|
15
|
+
// transiently fail where POSIX would succeed immediately. No retry/delay on POSIX: a genuine
|
|
16
|
+
// EACCES there is not transient, and retrying would only slow down a real permission failure.
|
|
17
|
+
import { unlink, readFile, stat } from 'node:fs/promises';
|
|
18
|
+
import { WorkflowError } from '../types/workflow-error.js';
|
|
19
|
+
const WIN32_RETRY_COUNT = 3;
|
|
20
|
+
const WIN32_RETRY_DELAY_MS = 50;
|
|
21
|
+
const WIN32_RETRYABLE_CODES = new Set(['EBUSY', 'EPERM', 'ENOTEMPTY']);
|
|
22
|
+
/** Errno classified as transient (worth retrying) for the ENGINE_ARTIFACT_DELETE_FAILED
|
|
23
|
+
* aggregate's `retryable` field — a lock held elsewhere, a flaky disk read. */
|
|
24
|
+
const RETRYABLE_ARTIFACT_ERRNO = new Set(['EBUSY', 'EIO']);
|
|
25
|
+
/** Errno classified as permanent (retrying will not help — same permission/mount state). Listed
|
|
26
|
+
* explicitly (rather than "everything else") so the default for an unrecognized/unknown errno
|
|
27
|
+
* stays the conservative `false` — never advertise a retry that might not help. */
|
|
28
|
+
const NON_RETRYABLE_ARTIFACT_ERRNO = new Set(['EACCES', 'EISDIR', 'EPERM', 'EROFS']);
|
|
29
|
+
function errnoCode(err) {
|
|
30
|
+
return err?.code;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Thrown by the fs-io primitives for any non-ENOENT errno. Callers map this to a typed,
|
|
34
|
+
* domain-specific `WorkflowError` (e.g. `ENGINE_ARTIFACT_DELETE_FAILED` via
|
|
35
|
+
* {@link toArtifactDeleteFailedError} below) at the aggregate layer — this class exists only to
|
|
36
|
+
* carry the raw path + errno + cause across that boundary without losing information.
|
|
37
|
+
*/
|
|
38
|
+
export class FsIoError extends Error {
|
|
39
|
+
path;
|
|
40
|
+
code;
|
|
41
|
+
constructor(op, path, cause) {
|
|
42
|
+
const code = errnoCode(cause) ?? 'UNKNOWN';
|
|
43
|
+
const causeMessage = cause instanceof Error ? cause.message : String(cause);
|
|
44
|
+
super(`${op} failed for '${path}' (${code}): ${causeMessage}`);
|
|
45
|
+
this.name = 'FsIoError';
|
|
46
|
+
this.path = path;
|
|
47
|
+
this.code = code;
|
|
48
|
+
if (cause instanceof Error)
|
|
49
|
+
this.cause = cause;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Exported ONLY so the retry policy itself can be unit-tested directly with an injected fake
|
|
54
|
+
* `op` (mocking `node:fs/promises`'s real `unlink`/`readFile`/`stat` under Vitest+ESM is brittle —
|
|
55
|
+
* "Cannot spy on export ... Module namespace is not configurable in ESM" — so the policy is
|
|
56
|
+
* decoupled from any specific fs call and tested in isolation instead). Not part of the module's
|
|
57
|
+
* intended per-call public surface (`deleteIfExists`/`readIfExists`/`statIfExists` are).
|
|
58
|
+
*/
|
|
59
|
+
export async function withWin32Retry(op) {
|
|
60
|
+
if (process.platform !== 'win32')
|
|
61
|
+
return op();
|
|
62
|
+
let lastErr;
|
|
63
|
+
for (let attempt = 0; attempt <= WIN32_RETRY_COUNT; attempt++) {
|
|
64
|
+
try {
|
|
65
|
+
return await op();
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
lastErr = err;
|
|
69
|
+
const code = errnoCode(err);
|
|
70
|
+
if (code === undefined || !WIN32_RETRYABLE_CODES.has(code))
|
|
71
|
+
throw err;
|
|
72
|
+
if (attempt < WIN32_RETRY_COUNT) {
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, WIN32_RETRY_DELAY_MS));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
throw lastErr;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Deletes `path`. ENOENT → `false` (already gone — success, idempotent). Any other errno → throws
|
|
81
|
+
* {@link FsIoError}. Returns `true` if this call actually deleted the file.
|
|
82
|
+
*/
|
|
83
|
+
export async function deleteIfExists(path) {
|
|
84
|
+
try {
|
|
85
|
+
await withWin32Retry(() => unlink(path));
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
if (errnoCode(err) === 'ENOENT')
|
|
90
|
+
return false;
|
|
91
|
+
throw new FsIoError('unlink', path, err);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Reads `path` as utf8. ENOENT → `undefined` (absent). Any other errno → throws {@link FsIoError}.
|
|
96
|
+
* Parsing is the CALLER's job — this returns raw bytes only.
|
|
97
|
+
*/
|
|
98
|
+
export async function readIfExists(path) {
|
|
99
|
+
try {
|
|
100
|
+
return await withWin32Retry(() => readFile(path, 'utf8'));
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
if (errnoCode(err) === 'ENOENT')
|
|
104
|
+
return undefined;
|
|
105
|
+
throw new FsIoError('readFile', path, err);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Stats `path`. ENOENT → `undefined` (absent). Any other errno → throws {@link FsIoError}.
|
|
110
|
+
*/
|
|
111
|
+
export async function statIfExists(path) {
|
|
112
|
+
try {
|
|
113
|
+
return await withWin32Retry(() => stat(path));
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
if (errnoCode(err) === 'ENOENT')
|
|
117
|
+
return undefined;
|
|
118
|
+
throw new FsIoError('stat', path, err);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Retryable classification for the `ENGINE_ARTIFACT_DELETE_FAILED` aggregate error (issue #183):
|
|
123
|
+
* `EACCES`/`EISDIR`/`EPERM`/`EROFS` are permanent (retrying won't help — same permission/mount
|
|
124
|
+
* state); `EBUSY`/`EIO` are transient (a lock held elsewhere, a flaky disk read) — worth
|
|
125
|
+
* retrying. Any other or unknown errno defaults to `false` — conservative, since advertising a
|
|
126
|
+
* retry that might not help is worse than not advertising one that would have.
|
|
127
|
+
*/
|
|
128
|
+
export function isRetryableArtifactErrno(code) {
|
|
129
|
+
return RETRYABLE_ARTIFACT_ERRNO.has(code) && !NON_RETRYABLE_ARTIFACT_ERRNO.has(code);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Builds the aggregate `ENGINE_ARTIFACT_DELETE_FAILED` `WorkflowError` every
|
|
133
|
+
* `PerRunArtifactStore.deleteAllForRun` implementation throws on a genuine (non-ENOENT) failure —
|
|
134
|
+
* the single typed shape `purge` (and any other caller) can rely on regardless of which store, or
|
|
135
|
+
* which internal read/delete step, actually failed. `retryable` is true iff ANY listed failure is
|
|
136
|
+
* itself retryable (a partial success followed by one transient failure is worth retrying as a
|
|
137
|
+
* whole).
|
|
138
|
+
*/
|
|
139
|
+
export function artifactDeleteFailedError(runId, store, deleted, failures) {
|
|
140
|
+
const summary = failures.map((f) => `${f.artifact} (${f.code})`).join(', ');
|
|
141
|
+
return new WorkflowError(`${store} failed to delete artifact(s) for run '${runId}': ${summary}`, {
|
|
142
|
+
code: 'ENGINE_ARTIFACT_DELETE_FAILED',
|
|
143
|
+
category: 'ENGINE',
|
|
144
|
+
agentAction: 'report_to_user',
|
|
145
|
+
retryable: failures.some((f) => isRetryableArtifactErrno(f.code)),
|
|
146
|
+
details: { runId, store, deleted, failures },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Convenience for the common single-failure case: extracts the errno (from an {@link FsIoError}
|
|
151
|
+
* or a raw `NodeJS.ErrnoException`) and message from `err`, then wraps it via
|
|
152
|
+
* {@link artifactDeleteFailedError}.
|
|
153
|
+
*/
|
|
154
|
+
export function toArtifactDeleteFailedError(runId, store, deleted, artifact, err) {
|
|
155
|
+
const code = err instanceof FsIoError ? err.code : (errnoCode(err) ?? 'UNKNOWN');
|
|
156
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
157
|
+
return artifactDeleteFailedError(runId, store, deleted, [{ artifact, code, message }]);
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=fs-io.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs-io.js","sourceRoot":"","sources":["../../src/store/fs-io.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,EAAE;AACF,6EAA6E;AAC7E,gGAAgG;AAChG,uCAAuC;AACvC,8FAA8F;AAC9F,6DAA6D;AAC7D,gGAAgG;AAChG,0FAA0F;AAC1F,+CAA+C;AAC/C,EAAE;AACF,iGAAiG;AACjG,uFAAuF;AACvF,+FAA+F;AAC/F,6FAA6F;AAC7F,8FAA8F;AAC9F,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE3D,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAC5B,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAChC,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;AAEvE;gFACgF;AAChF,MAAM,wBAAwB,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;AAC3D;;oFAEoF;AACpF,MAAM,4BAA4B,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;AAErF,SAAS,SAAS,CAAC,GAAY;IAC7B,OAAQ,GAAyC,EAAE,IAAI,CAAC;AAC1D,CAAC;AAED;;;;;GAKG;AACH,MAAM,OAAO,SAAU,SAAQ,KAAK;IACzB,IAAI,CAAS;IACb,IAAI,CAAS;IAEtB,YAAY,EAAU,EAAE,IAAY,EAAE,KAAc;QAClD,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,IAAI,SAAS,CAAC;QAC3C,MAAM,YAAY,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5E,KAAK,CAAC,GAAG,EAAE,gBAAgB,IAAI,MAAM,IAAI,MAAM,YAAY,EAAE,CAAC,CAAC;QAC/D,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,KAAK,YAAY,KAAK;YAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACjD,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAI,EAAoB;IAC1D,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,EAAE,EAAE,CAAC;IAC9C,IAAI,OAAgB,CAAC;IACrB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,iBAAiB,EAAE,OAAO,EAAE,EAAE,CAAC;QAC9D,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,EAAE,CAAC;QACpB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,GAAG,GAAG,CAAC;YACd,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;YAC5B,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC;gBAAE,MAAM,GAAG,CAAC;YACtE,IAAI,OAAO,GAAG,iBAAiB,EAAE,CAAC;gBAChC,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,oBAAoB,CAAC,CAAC,CAAC;YAC5E,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,OAAO,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAY;IAC/C,IAAI,CAAC;QACH,MAAM,cAAc,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QACzC,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC9C,MAAM,IAAI,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;IAC3C,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY;IAC7C,IAAI,CAAC;QACH,OAAO,MAAM,cAAc,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QAClD,MAAM,IAAI,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY;IAC7C,IAAI,CAAC;QACH,OAAO,MAAM,cAAc,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAChD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QAClD,MAAM,IAAI,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;IACzC,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAAY;IACnD,OAAO,wBAAwB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,4BAA4B,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACvF,CAAC;AASD;;;;;;;GAOG;AACH,MAAM,UAAU,yBAAyB,CACvC,KAAa,EACb,KAAa,EACb,OAAiB,EACjB,QAAiC;IAEjC,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5E,OAAO,IAAI,aAAa,CAAC,GAAG,KAAK,0CAA0C,KAAK,MAAM,OAAO,EAAE,EAAE;QAC/F,IAAI,EAAE,+BAA+B;QACrC,QAAQ,EAAE,QAAQ;QAClB,WAAW,EAAE,gBAAgB;QAC7B,SAAS,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,wBAAwB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACjE,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE;KAC7C,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,2BAA2B,CACzC,KAAa,EACb,KAAa,EACb,OAAiB,EACjB,QAAgB,EAChB,GAAY;IAEZ,MAAM,IAAI,GAAG,GAAG,YAAY,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,CAAC;IACjF,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACjE,OAAO,yBAAyB,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;AACzF,CAAC"}
|
|
@@ -27,6 +27,17 @@ export interface ReconcileSummary {
|
|
|
27
27
|
extra_live_run_ids: string[];
|
|
28
28
|
}>;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* GLOBAL LOCK ORDER (issue #184): run-file lock BEFORE key lock, never the reverse.
|
|
32
|
+
* `deleteAllForRun` is the only site that nests them (run-file lock held, key lock acquired
|
|
33
|
+
* inside it, key lock released, then the run file is deleted and the run-file lock released).
|
|
34
|
+
* Every other method that touches both takes them SEQUENTIALLY, never nested: `save()` releases
|
|
35
|
+
* its run-file lock before `registerImportedKey()` acquires the key lock; `create()` only ever
|
|
36
|
+
* takes the key lock (the run file it writes under that lock is a fresh path, not yet lockable
|
|
37
|
+
* meaningfully — no run-file lock is held during `create()`). Violating this order (acquiring a
|
|
38
|
+
* key lock and THEN a run-file lock while still holding the key lock) is the classic two-lock
|
|
39
|
+
* deadlock shape and must never be introduced.
|
|
40
|
+
*/
|
|
30
41
|
export declare class JsonFileStore implements RunStore, PerRunArtifactStore {
|
|
31
42
|
private readonly runsDir;
|
|
32
43
|
/** This store round-trips the per-claim `claims` liveness clock (issue #101). */
|
|
@@ -41,7 +52,14 @@ export declare class JsonFileStore implements RunStore, PerRunArtifactStore {
|
|
|
41
52
|
private keyPath;
|
|
42
53
|
private ensureDir;
|
|
43
54
|
private ensureKeysDir;
|
|
44
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Read a pointer file. Absence (ENOENT) → undefined. A genuine I/O failure (permissions, a
|
|
57
|
+
* torn mount, …) THROWS — issue #183: previously any error here, I/O or parse alike, silently
|
|
58
|
+
* self-healed to "absent," which let a real read failure masquerade as a missing pointer. Parse
|
|
59
|
+
* corruption (a torn/garbage pointer file) still self-heals to undefined — `create()` and
|
|
60
|
+
* `reconcileKeys()` both depend on that recovery — but is no longer SILENT: a loud, structured
|
|
61
|
+
* warning is emitted so an operator/log consumer can see the corruption happened.
|
|
62
|
+
*/
|
|
45
63
|
private readPointer;
|
|
46
64
|
/** Write (or overwrite) a pointer file for `run` under `key`. params_hash is derived from the owned run. */
|
|
47
65
|
private writePointer;
|
|
@@ -88,6 +106,34 @@ export declare class JsonFileStore implements RunStore, PerRunArtifactStore {
|
|
|
88
106
|
*/
|
|
89
107
|
reconcileKeys(workflowId?: string, dryRun?: boolean): Promise<ReconcileSummary>;
|
|
90
108
|
list(workflowId?: string): Promise<RunRecord[]>;
|
|
109
|
+
/**
|
|
110
|
+
* The raw set of run IDs present in `runsDir` (issue #163) — every `<id>.json` BASENAME, with
|
|
111
|
+
* NO record parse. Deliberately NOT `list()`: `list()` parses each file as a `RunRecord`
|
|
112
|
+
* (`JSON.parse(raw) as RunRecord`, uncaught) — a syntactically corrupt-but-PRESENT `<id>.json`
|
|
113
|
+
* would throw there, whereas here it correctly still counts as a live run (the file exists on
|
|
114
|
+
* disk; whatever reads it later is a separate concern from whether `realm run gc`'s orphan
|
|
115
|
+
* sweep should treat this run's WAL/sidecar as run-less). Using `list()` for the orphan sweep's
|
|
116
|
+
* `liveRunIds` would wrongly orphan — and reap — a live-but-corrupt run's artifacts.
|
|
117
|
+
*
|
|
118
|
+
* Concrete method, NOT on the `RunStore` interface (issue #163) — `gc` constructs a concrete
|
|
119
|
+
* `JsonFileStore` directly (as it already does for `runsDirPath`), so no external `RunStore`
|
|
120
|
+
* implementer is forced to add this.
|
|
121
|
+
*
|
|
122
|
+
* FAIL-CLOSED, load-bearing: `ENOENT` (no `runsDir` at all — nothing has ever been created) is
|
|
123
|
+
* the ONLY tolerated case, yielding an empty set (no runs exist, so nothing is "wrongly" live or
|
|
124
|
+
* orphaned). Any OTHER `readdir` error (permissions, a torn mount) THROWS — a fabricated empty
|
|
125
|
+
* set here would make the orphan sweep believe NO runs exist, and reap every live run's
|
|
126
|
+
* artifacts as "run-less." This is the single most dangerous failure mode in the whole feature;
|
|
127
|
+
* see `orphan-sweepable-store.ts`'s own doc for the matching contract on the artifact-store side.
|
|
128
|
+
*/
|
|
129
|
+
listRunIds(): Promise<ReadonlySet<string>>;
|
|
130
|
+
/**
|
|
131
|
+
* Builds the `STATE_RUN_BUSY` error thrown when `deleteAllForRun` cannot proceed because
|
|
132
|
+
* another writer holds the run-file (or key) lock, or because the run is no longer terminal
|
|
133
|
+
* (issue #184). Retryable: a live writer self-heals (the lock is released), and a genuinely
|
|
134
|
+
* stale lock is eventually stolen by the next contender.
|
|
135
|
+
*/
|
|
136
|
+
private runBusyError;
|
|
91
137
|
/**
|
|
92
138
|
* Deletes every artifact this store owns for `runId` (issue #107): the idempotency-key
|
|
93
139
|
* pointer (conditionally) and the run file itself. Exact-path — ignores `dirEntries` (this
|
|
@@ -95,6 +141,15 @@ export declare class JsonFileStore implements RunStore, PerRunArtifactStore {
|
|
|
95
141
|
*
|
|
96
142
|
* Idempotent: if the run file is already gone (a concurrent purge, or a double-invocation),
|
|
97
143
|
* this is a no-op, not an error.
|
|
144
|
+
*
|
|
145
|
+
* issue #184 — the resurrect-race fix: the WHOLE body runs under the run-file lock (matching
|
|
146
|
+
* `update()`/`claimStep()`), and the run's terminal state is RE-VERIFIED under that lock, not
|
|
147
|
+
* just at selection time. `RESUMABLE_PHASES ⊂ TERMINAL_PHASES` and `resume.ts` sets
|
|
148
|
+
* `terminal_state: false`, and `atomicWriteFile` (a temp write + `rename`) recreates a
|
|
149
|
+
* deleted target on `rename` — so, unlocked, a concurrent `realm resume` racing a batch purge
|
|
150
|
+
* could resurrect the run file as a LIVE run with its WAL/sidecar/pointer already gone, while
|
|
151
|
+
* purge still reported `purged`. Re-checking terminal state under the SAME lock `update()`
|
|
152
|
+
* uses closes that window: `deleteAllForRun` now unconditionally refuses a non-terminal run.
|
|
98
153
|
*/
|
|
99
154
|
deleteAllForRun(runId: string, _dirEntries?: readonly string[]): Promise<void>;
|
|
100
155
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-file-store.d.ts","sourceRoot":"","sources":["../../src/store/json-file-store.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAE1E,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACvE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"json-file-store.d.ts","sourceRoot":"","sources":["../../src/store/json-file-store.ts"],"names":[],"mappings":"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"}
|