@sema-agent/core 7.5.2 → 7.6.1
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 +67 -0
- package/dist/agents/agent-transcript-tool.d.ts +2 -2
- package/dist/agents/cascade.d.ts +4 -5
- package/dist/agents/cascade.js +12 -10
- package/dist/agents/repair-loop.d.ts +7 -5
- package/dist/agents/repair-loop.js +13 -15
- package/dist/agents/retain-ledger.d.ts +2 -3
- package/dist/agents/send-message-tool.d.ts +2 -2
- package/dist/agents/session-util.d.ts +2 -2
- package/dist/agents/subagent.d.ts +27 -46
- package/dist/agents/subagent.js +119 -105
- package/dist/agents/suspend-guard.d.ts +31 -19
- package/dist/agents/suspend-guard.js +14 -8
- package/dist/agents/teacher.d.ts +2 -2
- package/dist/agents/teacher.js +9 -9
- package/dist/agents/team.d.ts +6 -5
- package/dist/agents/team.js +10 -8
- package/dist/agents/verify.d.ts +8 -9
- package/dist/agents/verify.js +17 -17
- package/dist/core/a2a.js +2 -1
- package/dist/core/agent-definition.d.ts +172 -0
- package/dist/core/agent-definition.js +1 -0
- package/dist/core/ask-origin.d.ts +60 -7
- package/dist/core/ask-origin.js +26 -1
- package/dist/core/checkpoint-store.d.ts +78 -76
- package/dist/core/checkpoint-store.js +17 -1
- package/dist/core/delegation-frames.d.ts +298 -0
- package/dist/core/delegation-frames.js +21 -0
- package/dist/core/engine-notice.d.ts +555 -0
- package/dist/core/engine-notice.js +55 -0
- package/dist/core/gate-fold.d.ts +12 -0
- package/dist/core/gate-fold.js +158 -0
- package/dist/core/gate-lanes.d.ts +93 -0
- package/dist/core/gate-lanes.js +626 -0
- package/dist/core/gate-outcome.d.ts +189 -0
- package/dist/core/gate-outcome.js +70 -0
- package/dist/core/hands-band.d.ts +134 -0
- package/dist/core/hands-band.js +1 -0
- package/dist/core/hooks.d.ts +22 -177
- package/dist/core/hooks.js +53 -851
- package/dist/core/mcp-failure.d.ts +142 -0
- package/dist/core/mcp-failure.js +145 -0
- package/dist/core/mcp-server-spec.d.ts +217 -0
- package/dist/core/mcp-server-spec.js +1 -0
- package/dist/core/mcp.d.ts +21 -77
- package/dist/core/mcp.js +76 -150
- package/dist/core/model-seat.d.ts +99 -0
- package/dist/core/model-seat.js +1 -0
- package/dist/core/pause-registry.d.ts +131 -0
- package/dist/core/pause-registry.js +27 -0
- package/dist/core/reminder-mint.d.ts +10 -0
- package/dist/core/reminder-mint.js +3 -0
- package/dist/core/runner/assemble-result.d.ts +32 -41
- package/dist/core/runner/assemble-result.js +55 -74
- package/dist/core/runner/contracts.d.ts +427 -69
- package/dist/core/runner/denial-limit-arms.d.ts +1 -1
- package/dist/core/runner/denial-limit-arms.js +3 -3
- package/dist/core/runner/gate-exit.d.ts +242 -0
- package/dist/core/runner/gate-exit.js +124 -0
- package/dist/core/runner/park-commit.d.ts +17 -23
- package/dist/core/runner/park-commit.js +14 -15
- package/dist/core/runner/prepare-ask-lane.d.ts +0 -3
- package/dist/core/runner/prepare-ask-lane.js +3 -5
- package/dist/core/runner/prepare-boundary-parks.d.ts +3 -6
- package/dist/core/runner/prepare-boundary-parks.js +3 -3
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -7
- package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
- package/dist/core/runner/prepare-delegation-surface.d.ts +2 -7
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -7
- package/dist/core/runner/prepare-gate-stations.js +29 -54
- package/dist/core/runner/prepare-inherited-gate.js +1 -1
- package/dist/core/runner/prepare-memory.d.ts +44 -26
- package/dist/core/runner/prepare-park-ask.d.ts +2 -4
- package/dist/core/runner/prepare-park-ask.js +5 -5
- package/dist/core/runner/prepare-task.d.ts +2 -2
- package/dist/core/runner/prepare-task.js +8 -9
- package/dist/core/runner/prepare-wiring-manifest.d.ts +7 -15
- package/dist/core/runner/prepare-wiring-manifest.js +9 -10
- package/dist/core/runner/runtask.d.ts +20 -102
- package/dist/core/runner/runtask.js +119 -121
- package/dist/core/runner/terminal-projection.d.ts +22 -0
- package/dist/core/runner/terminal-projection.js +28 -0
- package/dist/core/runner-deps.d.ts +1416 -0
- package/dist/core/runner-deps.js +1 -0
- package/dist/core/runtime-caps.d.ts +164 -0
- package/dist/core/runtime-caps.js +1 -0
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
- package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
- package/dist/core/task-event.d.ts +910 -0
- package/dist/core/task-event.js +1 -0
- package/dist/core/task-limits.d.ts +110 -0
- package/dist/core/task-limits.js +1 -0
- package/dist/core/task-result.d.ts +809 -0
- package/dist/core/task-result.js +1 -0
- package/dist/core/task-spec.d.ts +1370 -0
- package/dist/core/task-spec.js +1 -0
- package/dist/core/task-stream.d.ts +382 -0
- package/dist/core/task-stream.js +1 -0
- package/dist/core/terminal-cause.d.ts +137 -0
- package/dist/core/terminal-cause.js +9 -0
- package/dist/core/tool-policy.d.ts +43 -139
- package/dist/core/tool-policy.js +79 -112
- package/dist/core/tool-spec.d.ts +1174 -0
- package/dist/core/tool-spec.js +1 -0
- package/dist/core/types.d.ts +27 -7789
- package/dist/core/types.js +2 -76
- package/dist/core/warm-resume.d.ts +2 -2
- package/dist/core/wiring-manifest.d.ts +6 -3
- package/dist/core/workflow-journal-store.js +3 -4
- package/dist/engine/harness/agent-harness.d.ts +1 -1
- package/dist/index.d.ts +12 -8
- package/dist/index.js +9 -6
- package/dist/orchestration/builtin-workflows.d.ts +2 -2
- package/dist/orchestration/builtin-workflows.js +1 -1
- package/dist/orchestration/goal.d.ts +2 -2
- package/dist/orchestration/goal.js +8 -7
- package/dist/orchestration/run-spec.d.ts +2 -2
- package/dist/orchestration/run-spec.js +5 -3
- package/dist/orchestration/run-workflow-tool.d.ts +4 -4
- package/dist/orchestration/run-workflow-tool.js +4 -4
- package/dist/orchestration/workflow-governance.d.ts +4 -4
- package/dist/orchestration/workflow-governance.js +4 -2
- package/dist/orchestration/workflow-primitives.d.ts +1 -1
- package/dist/orchestration/workflow-primitives.js +1 -1
- package/dist/orchestration/workflow.d.ts +15 -4
- package/dist/orchestration/workflow.js +64 -39
- package/dist/prompts/supervisor.d.ts +1 -1
- package/dist/prompts/supervisor.js +3 -3
- package/dist/scenarios/scenario-registry.d.ts +3 -3
- package/dist/scenarios/scenario-registry.js +1 -1
- package/dist/scenarios/teacher-quickstart.d.ts +2 -2
- package/dist/server/http.d.ts +2 -2
- package/dist/stores/file/fs-atomic.d.ts +88 -12
- package/dist/stores/file/fs-atomic.js +184 -55
- package/dist/stores/file/index.d.ts +1 -0
- package/dist/stores/file/index.js +1 -0
- package/package.json +3 -1
- package/test/export-surface.snapshot.json +82 -22
|
@@ -184,10 +184,11 @@ export declare class FileStoreLockError extends Error {
|
|
|
184
184
|
ownerPid?: number | undefined);
|
|
185
185
|
}
|
|
186
186
|
/**
|
|
187
|
-
* The coarse boot guard (§2.4): an `O_EXCL`
|
|
187
|
+
* The coarse boot guard (§2.4): an `O_EXCL` owner file (`root/LOCK` for the whole data dir; one per store
|
|
188
188
|
* ledger directory via {@link acquireStoreDirLock}) that forbids two processes sharing it. A second
|
|
189
|
-
* instance fails fast ("another instance owns this data dir"). A STALE lock (the
|
|
190
|
-
*
|
|
189
|
+
* instance fails fast ("another instance owns this data dir"). A STALE lock (the process that WROTE it is
|
|
190
|
+
* gone — see {@link isProcessLive}, which is not the same question as "is that pid free") is pruned and
|
|
191
|
+
* re-acquired — `proper-lockfile`/CC `concurrentSessions` parity.
|
|
191
192
|
*
|
|
192
193
|
* This is the family's ONE lock MECHANISM (the per-operation CAS stays in-process: one event loop + a
|
|
193
194
|
* per-token async mutex, so there is NO per-operation flock). A holder of one of these fences guarantees
|
|
@@ -201,7 +202,7 @@ export declare class BootLock {
|
|
|
201
202
|
/** The verdict of the ONE release this holding gets, replayed to any later caller (see {@link release}). */
|
|
202
203
|
private spentVerdict;
|
|
203
204
|
constructor(lockPath: string);
|
|
204
|
-
/** Acquire the lock or throw. Reclaims a lock whose recorded
|
|
205
|
+
/** Acquire the lock or throw. Reclaims a lock whose recorded OWNER is gone (see below). */
|
|
205
206
|
acquire(): void;
|
|
206
207
|
/**
|
|
207
208
|
* @param depth how many stale reclaim gates deep we already are (see the gate below). Bounded so a
|
|
@@ -240,8 +241,8 @@ export declare class BootLock {
|
|
|
240
241
|
*
|
|
241
242
|
* ONE-SHOT, and that is load-bearing. This object stops being a holder the moment it is ASKED to release
|
|
242
243
|
* — before the filesystem is touched — and a second call REPEATS the first verdict instead of acting on
|
|
243
|
-
* it again. A lock file
|
|
244
|
-
* on disk: after a FAILED release the path can legitimately be re-published (a later acquisition here
|
|
244
|
+
* it again. A lock file names its publishing PROCESS, so two BootLocks over one path in this process are
|
|
245
|
+
* byte-identical on disk: after a FAILED release the path can legitimately be re-published (a later acquisition here
|
|
245
246
|
* finishing our cleanup), and a stale object that "removed its own lock" a second time would be deleting
|
|
246
247
|
* the SUCCESSOR's fence while both sides believed they held the directory — the double-writer this whole
|
|
247
248
|
* mechanism exists to refuse, minted from bookkeeping instead of from a race. So a second release answers
|
|
@@ -320,11 +321,86 @@ export declare function acquireStoreDirLock(dir: string, opts: {
|
|
|
320
321
|
* stops two processes consolidating the SAME scope at once (the in-process model the file backend assumes —
|
|
321
322
|
* see {@link BootLock} — does not cover a second OS process with its own timer).
|
|
322
323
|
*
|
|
323
|
-
* `acquire(scope)`
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
324
|
+
* `acquire(scope)` takes a {@link BootLock} over `lockDir/<sanitizedScope>.consolidate.lock`. It returns a
|
|
325
|
+
* `release` callback on success, or `undefined` for EVERY refusal that lock knows how to make — a live
|
|
326
|
+
* owner (including this process: a second pass here is still a conflict), a takeover in flight, an
|
|
327
|
+
* unreadable file — because the caller's whole vocabulary is "busy → skip this pass", a no-op that costs
|
|
328
|
+
* one deferred consolidation. A stale lock is reclaimed by the same protocol as any other, which is the
|
|
329
|
+
* point of not hand-rolling one: this factory used to prune with a bare `unlink` and re-publish, and two
|
|
330
|
+
* passes that had both read the same dead owner would then both prune and both publish — one deleting the
|
|
331
|
+
* other's fresh lock, leaving two processes consolidating the SAME scope, which is exactly what the lock
|
|
332
|
+
* exists to prevent. The reclaim gate serialises that, the depth cap bounds it, and the identity re-read
|
|
333
|
+
* makes sure a reclaim prunes the owner it came for. One lock MECHANISM in this family, not two.
|
|
334
|
+
*
|
|
335
|
+
* The implementation lives HERE (the deployment shell's persist/exec axis), NOT core — core only DEFINES
|
|
336
|
+
* the injection point (the constitutional split: gate-presence in core, mechanism in the profile).
|
|
329
337
|
*/
|
|
330
338
|
export declare function createFileConsolidationLock(lockDir: string): (scope: string) => (() => void) | undefined;
|
|
339
|
+
/**
|
|
340
|
+
* A process identity that survives PID REUSE: a pid, WHEN that pid's process started, and the host the
|
|
341
|
+
* observation was made on.
|
|
342
|
+
*
|
|
343
|
+
* A pid alone is not an identity. The OS hands the same number out again once its holder exits, so a lock
|
|
344
|
+
* (or lease, or any other durable "who owns this" record) carrying only a pid says no more than "somebody
|
|
345
|
+
* with that number" — and after a crash, an unrelated process that inherits the number makes the dead
|
|
346
|
+
* owner's record read as a LIVE owner for as long as that process lives. Adding the START TIME makes the
|
|
347
|
+
* record falsifiable: the pid narrows the candidate to one process, the start time decides whether it is
|
|
348
|
+
* still the SAME one.
|
|
349
|
+
*
|
|
350
|
+
* The start time is carried TWICE, against two different clocks, and that is the whole robustness of the
|
|
351
|
+
* thing. Both numbers come from ONE reading of the process's elapsed time, but one is subtracted from the
|
|
352
|
+
* WALL clock and the other from the host's UPTIME — and a platform that leaves one of them unmoved when
|
|
353
|
+
* the wall clock is corrected moves the other. (Where `ps` measures elapsed time against the boot clock, a
|
|
354
|
+
* corrected wall clock shifts the derived wall-clock start and leaves the uptime-relative one alone; where
|
|
355
|
+
* it measures against a stored absolute start, it is the other way round.) A comparison that insists on
|
|
356
|
+
* BOTH before calling a record stale therefore cannot be talked into a reclaim by a clock correction —
|
|
357
|
+
* which matters because that reclaim would be of a LIVE writer's lock. See {@link isProcessLive}.
|
|
358
|
+
*
|
|
359
|
+
* Both are `null` when this host cannot produce a start time (no `ps`, a cut-down `ps` that rejects the
|
|
360
|
+
* query, a probe too slow for its own reading to be worth anything). That is a plain "unknown" — never a
|
|
361
|
+
* zero and never a guess — and every consumer treats an unobtainable fingerprint as UNFALSIFIABLE, keeping
|
|
362
|
+
* the pid-only verdict: the degrade lands on yesterday's behaviour rather than on a wrong reclaim, and
|
|
363
|
+
* says so out loud once.
|
|
364
|
+
*/
|
|
365
|
+
export interface ProcessFingerprint {
|
|
366
|
+
/** The process id the rest of the record is about. */
|
|
367
|
+
readonly pid: number;
|
|
368
|
+
/** Start of that pid's process against the WALL clock, to the second; `null` when unobtainable here. */
|
|
369
|
+
readonly startedAtMs: number | null;
|
|
370
|
+
/** The same start against this host's UPTIME (ms after boot), to the second; `null` likewise. */
|
|
371
|
+
readonly startedSinceBootMs: number | null;
|
|
372
|
+
/** The host both were read on — a fingerprint from ANOTHER host is comparable to nothing here. */
|
|
373
|
+
readonly host: string;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* The fingerprint of a process: this one when called with no pid, any other pid when named.
|
|
377
|
+
*
|
|
378
|
+
* ONE derivation for both halves of the mechanism — the record a holder WRITES and the probe a later reader
|
|
379
|
+
* verifies it with — on purpose. Deriving our own start time from `process.uptime()` instead would be
|
|
380
|
+
* cheaper, but it measures from Node's own initialisation rather than from the kernel's `exec`, so a busy
|
|
381
|
+
* machine can put the two derivations seconds apart. A reader comparing across them would then read a
|
|
382
|
+
* healthy holder as stale and RECLAIM A LIVE LOCK — a strictly worse failure than the pid reuse this fixes.
|
|
383
|
+
* One derivation makes the tolerance a rounding allowance instead of a guess about bootstrap latency.
|
|
384
|
+
*
|
|
385
|
+
* The self case is memoized: our start time cannot change, the probe is a subprocess, and a lock publish is
|
|
386
|
+
* the only place that needs it — so a process pays for it at most once, whatever it opens.
|
|
387
|
+
*/
|
|
388
|
+
export declare function processFingerprint(pid?: number): ProcessFingerprint;
|
|
389
|
+
/**
|
|
390
|
+
* Is the process a record names still the one that wrote it? ONE predicate, two terms: the pid is running,
|
|
391
|
+
* AND the fingerprint has not been falsified.
|
|
392
|
+
*
|
|
393
|
+
* "Not falsified" is the load-bearing wording, and it is what every degrade lands on. A record with no
|
|
394
|
+
* `startedAtMs` (the pre-fingerprint spelling, or a host that cannot produce one), a record written on
|
|
395
|
+
* ANOTHER host (its start time was measured against a different boot — comparing it here would be
|
|
396
|
+
* arithmetic on unrelated numbers), and a probe that cannot answer all reach the same verdict: LIVE. The
|
|
397
|
+
* asymmetry is deliberate and is the whole safety argument — a false "dead" reclaims a directory out from
|
|
398
|
+
* under a working process (two writers, and the ledger loses whichever rows the loser never loaded), while
|
|
399
|
+
* a false "live" costs a refusal an operator can act on.
|
|
400
|
+
*/
|
|
401
|
+
export declare function isProcessLive(record: {
|
|
402
|
+
readonly pid: number;
|
|
403
|
+
readonly startedAtMs?: number | null;
|
|
404
|
+
readonly startedSinceBootMs?: number | null;
|
|
405
|
+
readonly host?: string | null;
|
|
406
|
+
}): boolean;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { closeSync, constants as FS, existsSync, fstatSync, linkSync, mkdirSync, openSync, readFileSync, readSync, realpathSync, renameSync, truncateSync, unlinkSync, writeFileSync, writeSync, fsyncSync, } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
2
|
+
import { homedir, hostname, uptime } from "node:os";
|
|
3
|
+
import { hrtime } from "node:process";
|
|
3
4
|
import { basename, dirname, join, resolve as resolvePath } from "node:path";
|
|
4
5
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
5
7
|
const SAFE_COMPONENT = /^[A-Za-z0-9_.-]+$/;
|
|
6
8
|
export function sanitizePathComponent(raw) {
|
|
7
9
|
if (raw === "" || raw === "." || raw === ".." || !SAFE_COMPONENT.test(raw)) {
|
|
@@ -270,6 +272,64 @@ export class FileStoreLockError extends Error {
|
|
|
270
272
|
const MAX_RECLAIM_DEPTH = 8;
|
|
271
273
|
const MAX_ACQUIRE_ATTEMPTS = 4;
|
|
272
274
|
const abandonedLocks = new Set();
|
|
275
|
+
const LOCK_RECORD_VERSION = 2;
|
|
276
|
+
function serializeLockRecord() {
|
|
277
|
+
const fp = processFingerprint();
|
|
278
|
+
return JSON.stringify({
|
|
279
|
+
v: LOCK_RECORD_VERSION,
|
|
280
|
+
pid: fp.pid,
|
|
281
|
+
startedAtMs: fp.startedAtMs,
|
|
282
|
+
startedSinceBootMs: fp.startedSinceBootMs,
|
|
283
|
+
host: fp.host,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
function parseLockRecord(raw) {
|
|
287
|
+
if (/^[1-9][0-9]*$/.test(raw)) {
|
|
288
|
+
const barePid = Number.parseInt(raw, 10);
|
|
289
|
+
return Number.isSafeInteger(barePid) && barePid > 0
|
|
290
|
+
? { pid: barePid, startedAtMs: null, startedSinceBootMs: null, host: null }
|
|
291
|
+
: null;
|
|
292
|
+
}
|
|
293
|
+
if (!raw.startsWith("{"))
|
|
294
|
+
return null;
|
|
295
|
+
let parsed;
|
|
296
|
+
try {
|
|
297
|
+
parsed = JSON.parse(raw);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
303
|
+
return null;
|
|
304
|
+
const rec = parsed;
|
|
305
|
+
if (typeof rec.pid !== "number" || !Number.isSafeInteger(rec.pid) || rec.pid <= 0)
|
|
306
|
+
return null;
|
|
307
|
+
const known = rec.v === LOCK_RECORD_VERSION;
|
|
308
|
+
const measure = (raw) => (known && typeof raw === "number" && Number.isFinite(raw) ? raw : null);
|
|
309
|
+
return {
|
|
310
|
+
pid: rec.pid,
|
|
311
|
+
startedAtMs: measure(rec.startedAtMs),
|
|
312
|
+
startedSinceBootMs: measure(rec.startedSinceBootMs),
|
|
313
|
+
host: known && typeof rec.host === "string" && rec.host !== "" ? rec.host : null,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function readLockRecord(lockPath) {
|
|
317
|
+
let raw;
|
|
318
|
+
try {
|
|
319
|
+
raw = readFileSync(lockPath, "utf8").trim();
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
return err.code === "ENOENT" ? { state: "absent" } : { state: "unreadable" };
|
|
323
|
+
}
|
|
324
|
+
const owner = parseLockRecord(raw);
|
|
325
|
+
return owner === null ? { state: "unreadable" } : { state: "owner", ...owner };
|
|
326
|
+
}
|
|
327
|
+
function sameOwner(a, b) {
|
|
328
|
+
return (a.pid === b.pid &&
|
|
329
|
+
a.startedAtMs === b.startedAtMs &&
|
|
330
|
+
a.startedSinceBootMs === b.startedSinceBootMs &&
|
|
331
|
+
a.host === b.host);
|
|
332
|
+
}
|
|
273
333
|
export class BootLock {
|
|
274
334
|
lockPath;
|
|
275
335
|
held = false;
|
|
@@ -302,7 +362,7 @@ export class BootLock {
|
|
|
302
362
|
this.pruneOwnAbandoned();
|
|
303
363
|
return;
|
|
304
364
|
}
|
|
305
|
-
if (
|
|
365
|
+
if (isProcessLive(owner)) {
|
|
306
366
|
throw new FileStoreLockError("store.dir_in_use", `file store: another instance (pid ${owner.pid}) owns this data dir (${dirnameOf(this.lockPath)})`, this.lockPath, owner.pid);
|
|
307
367
|
}
|
|
308
368
|
if (depth >= MAX_RECLAIM_DEPTH) {
|
|
@@ -322,7 +382,7 @@ export class BootLock {
|
|
|
322
382
|
}
|
|
323
383
|
try {
|
|
324
384
|
const under = this.inspectLock();
|
|
325
|
-
if (under.state === "owner" && under
|
|
385
|
+
if (under.state === "owner" && sameOwner(under, owner)) {
|
|
326
386
|
try {
|
|
327
387
|
unlinkSync(this.lockPath);
|
|
328
388
|
}
|
|
@@ -368,23 +428,13 @@ export class BootLock {
|
|
|
368
428
|
this.publishOrLoseRace();
|
|
369
429
|
}
|
|
370
430
|
writeLock() {
|
|
371
|
-
writeThenLink(this.lockPath,
|
|
431
|
+
writeThenLink(this.lockPath, serializeLockRecord());
|
|
372
432
|
}
|
|
373
433
|
get abandonKey() {
|
|
374
434
|
return resolvePath(this.lockPath);
|
|
375
435
|
}
|
|
376
436
|
inspectLock() {
|
|
377
|
-
|
|
378
|
-
try {
|
|
379
|
-
raw = readFileSync(this.lockPath, "utf8").trim();
|
|
380
|
-
}
|
|
381
|
-
catch (err) {
|
|
382
|
-
return err.code === "ENOENT" ? { state: "absent" } : { state: "unreadable" };
|
|
383
|
-
}
|
|
384
|
-
if (!/^[1-9][0-9]*$/.test(raw))
|
|
385
|
-
return { state: "unreadable" };
|
|
386
|
-
const pid = Number.parseInt(raw, 10);
|
|
387
|
-
return Number.isInteger(pid) && pid > 0 ? { state: "owner", pid } : { state: "unreadable" };
|
|
437
|
+
return readLockRecord(this.lockPath);
|
|
388
438
|
}
|
|
389
439
|
release() {
|
|
390
440
|
if (!this.held) {
|
|
@@ -500,49 +550,18 @@ export function acquireStoreDirLock(dir, opts) {
|
|
|
500
550
|
export function createFileConsolidationLock(lockDir) {
|
|
501
551
|
return (scope) => {
|
|
502
552
|
ensureDir(lockDir);
|
|
503
|
-
const
|
|
504
|
-
const tryWrite = () => {
|
|
505
|
-
try {
|
|
506
|
-
writeThenLink(lockPath, String(process.pid));
|
|
507
|
-
return true;
|
|
508
|
-
}
|
|
509
|
-
catch (err) {
|
|
510
|
-
if (err.code !== "EEXIST")
|
|
511
|
-
throw err;
|
|
512
|
-
return false;
|
|
513
|
-
}
|
|
514
|
-
};
|
|
515
|
-
const readPid = () => {
|
|
516
|
-
try {
|
|
517
|
-
const raw = readFileSync(lockPath, "utf8").trim();
|
|
518
|
-
if (!/^[1-9][0-9]*$/.test(raw))
|
|
519
|
-
return undefined;
|
|
520
|
-
const pid = Number.parseInt(raw, 10);
|
|
521
|
-
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
522
|
-
}
|
|
523
|
-
catch {
|
|
524
|
-
return undefined;
|
|
525
|
-
}
|
|
526
|
-
};
|
|
527
|
-
const release = () => {
|
|
528
|
-
try {
|
|
529
|
-
if (readPid() === process.pid)
|
|
530
|
-
unlinkSync(lockPath);
|
|
531
|
-
}
|
|
532
|
-
catch {
|
|
533
|
-
}
|
|
534
|
-
};
|
|
535
|
-
if (tryWrite())
|
|
536
|
-
return release;
|
|
537
|
-
const ownerPid = readPid();
|
|
538
|
-
if (ownerPid === undefined || isProcessRunning(ownerPid))
|
|
539
|
-
return undefined;
|
|
553
|
+
const lock = new BootLock(join(lockDir, `${sanitizeScope(scope)}.consolidate.lock`));
|
|
540
554
|
try {
|
|
541
|
-
|
|
555
|
+
lock.acquire();
|
|
542
556
|
}
|
|
543
|
-
catch {
|
|
557
|
+
catch (err) {
|
|
558
|
+
if (err instanceof FileStoreLockError)
|
|
559
|
+
return undefined;
|
|
560
|
+
throw err;
|
|
544
561
|
}
|
|
545
|
-
return
|
|
562
|
+
return () => {
|
|
563
|
+
lock.release();
|
|
564
|
+
};
|
|
546
565
|
};
|
|
547
566
|
}
|
|
548
567
|
function isProcessRunning(pid) {
|
|
@@ -554,3 +573,113 @@ function isProcessRunning(pid) {
|
|
|
554
573
|
return err.code === "EPERM";
|
|
555
574
|
}
|
|
556
575
|
}
|
|
576
|
+
const START_TIME_TOLERANCE_MS = 2_000;
|
|
577
|
+
const START_TIME_PROBE_TIMEOUT_MS = 2_000;
|
|
578
|
+
const START_TIME_PROBE_PRECISION_MS = 250;
|
|
579
|
+
const ETIME_PATTERN = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/;
|
|
580
|
+
let cachedHostname;
|
|
581
|
+
let selfFingerprint;
|
|
582
|
+
let startTimeProbeUnavailable = false;
|
|
583
|
+
function thisHost() {
|
|
584
|
+
return (cachedHostname ??= hostname());
|
|
585
|
+
}
|
|
586
|
+
function announceStartTimeDegrade(reason) {
|
|
587
|
+
if (startTimeProbeUnavailable)
|
|
588
|
+
return;
|
|
589
|
+
startTimeProbeUnavailable = true;
|
|
590
|
+
console.warn(`[sema] file store: this host cannot report process start times (${reason}), so a directory lock's owner ` +
|
|
591
|
+
`is judged by PID ALONE. A lock left behind by a crashed owner whose pid the OS has since handed to an ` +
|
|
592
|
+
`unrelated process then reads as a LIVE owner, and that directory stays refused until the lock file is ` +
|
|
593
|
+
`removed by hand.`);
|
|
594
|
+
}
|
|
595
|
+
const START_TIME_PROBE_ATTEMPTS = 3;
|
|
596
|
+
function probeStartTimes(pid) {
|
|
597
|
+
for (let attempt = 0; attempt < START_TIME_PROBE_ATTEMPTS; attempt++) {
|
|
598
|
+
const reading = probeStartTimesOnce(pid);
|
|
599
|
+
if (reading !== "imprecise")
|
|
600
|
+
return reading;
|
|
601
|
+
}
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
function probeStartTimesOnce(pid) {
|
|
605
|
+
if (startTimeProbeUnavailable)
|
|
606
|
+
return null;
|
|
607
|
+
const knownToExist = pid === process.pid;
|
|
608
|
+
if (process.platform === "win32") {
|
|
609
|
+
announceStartTimeDegrade("no ps(1) on win32");
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
const monotonicBefore = hrtime.bigint();
|
|
613
|
+
const wallBefore = Date.now();
|
|
614
|
+
const uptimeBefore = Math.round(uptime() * 1000);
|
|
615
|
+
let raw;
|
|
616
|
+
try {
|
|
617
|
+
raw = execFileSync("ps", ["-p", String(pid), "-o", "etime="], {
|
|
618
|
+
encoding: "utf8",
|
|
619
|
+
timeout: START_TIME_PROBE_TIMEOUT_MS,
|
|
620
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
621
|
+
env: { ...process.env, LC_ALL: "C", LANG: "C" },
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
catch (err) {
|
|
625
|
+
if (typeof err.status === "number") {
|
|
626
|
+
if (knownToExist)
|
|
627
|
+
announceStartTimeDegrade("ps refused a query about this very process");
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
announceStartTimeDegrade(`ps failed (${err.code ?? "unknown"})`);
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
const latency = Number(hrtime.bigint() - monotonicBefore) / 1e6;
|
|
634
|
+
const wallDelta = Date.now() - wallBefore;
|
|
635
|
+
const trimmed = raw.trim();
|
|
636
|
+
const parts = ETIME_PATTERN.exec(trimmed);
|
|
637
|
+
if (parts === null) {
|
|
638
|
+
if (trimmed !== "" || knownToExist)
|
|
639
|
+
announceStartTimeDegrade("ps produced no elapsed time");
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
if (latency > START_TIME_PROBE_PRECISION_MS)
|
|
643
|
+
return "imprecise";
|
|
644
|
+
if (Math.abs(wallDelta - latency) > START_TIME_PROBE_PRECISION_MS)
|
|
645
|
+
return "imprecise";
|
|
646
|
+
const elapsedMs = (Number(parts[1] ?? "0") * 86_400 + Number(parts[2] ?? "0") * 3_600 + Number(parts[3] ?? "0") * 60 + Number(parts[4] ?? "0")) *
|
|
647
|
+
1000;
|
|
648
|
+
return {
|
|
649
|
+
startedAtMs: Math.round((wallBefore - elapsedMs) / 1000) * 1000,
|
|
650
|
+
startedSinceBootMs: Math.round((uptimeBefore - elapsedMs) / 1000) * 1000,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
export function processFingerprint(pid) {
|
|
654
|
+
const host = thisHost();
|
|
655
|
+
const of = (id, probed) => ({
|
|
656
|
+
pid: id,
|
|
657
|
+
startedAtMs: probed?.startedAtMs ?? null,
|
|
658
|
+
startedSinceBootMs: probed?.startedSinceBootMs ?? null,
|
|
659
|
+
host,
|
|
660
|
+
});
|
|
661
|
+
if (pid === undefined || pid === process.pid) {
|
|
662
|
+
if (selfFingerprint === undefined || selfFingerprint.startedAtMs === null) {
|
|
663
|
+
const answer = of(process.pid, probeStartTimes(process.pid));
|
|
664
|
+
if (answer.startedAtMs === null)
|
|
665
|
+
return answer;
|
|
666
|
+
selfFingerprint = answer;
|
|
667
|
+
}
|
|
668
|
+
return selfFingerprint;
|
|
669
|
+
}
|
|
670
|
+
if (processFingerprint().startedAtMs === null)
|
|
671
|
+
return of(pid, null);
|
|
672
|
+
return of(pid, probeStartTimes(pid));
|
|
673
|
+
}
|
|
674
|
+
export function isProcessLive(record) {
|
|
675
|
+
if (!isProcessRunning(record.pid))
|
|
676
|
+
return false;
|
|
677
|
+
if (record.host !== undefined && record.host !== null && record.host !== thisHost())
|
|
678
|
+
return true;
|
|
679
|
+
const observed = processFingerprint(record.pid);
|
|
680
|
+
const falsified = (was, now) => was !== undefined && was !== null && now !== null && now - was > START_TIME_TOLERANCE_MS;
|
|
681
|
+
const bootIsOver = (was) => was !== undefined && was !== null && was - Math.round(uptime() * 1000) > START_TIME_TOLERANCE_MS;
|
|
682
|
+
const wallSaysGone = falsified(record.startedAtMs, observed.startedAtMs);
|
|
683
|
+
const uptimeSaysGone = falsified(record.startedSinceBootMs, observed.startedSinceBootMs) || bootIsOver(record.startedSinceBootMs);
|
|
684
|
+
return !(wallSaysGone && uptimeSaysGone);
|
|
685
|
+
}
|
|
@@ -25,6 +25,7 @@ export { FileStrategyStore, type FileStrategyStoreOptions } from "./strategy-sto
|
|
|
25
25
|
export { resolveDataRoot, sanitizeScope, sanitizePathComponent, createFileConsolidationLock } from "./fs-atomic.js";
|
|
26
26
|
export { FileStoreLockError, type FileStoreLockErrorCode } from "./fs-atomic.js";
|
|
27
27
|
export { atomicWriteFile, writeThenLink, ensureDir, readJsonlRecords, AppendLog } from "./fs-atomic.js";
|
|
28
|
+
export { processFingerprint, isProcessLive, type ProcessFingerprint } from "./fs-atomic.js";
|
|
28
29
|
export { AdoptionError, assertAdoptionBootGate, readRootAdoptionFile, writeRootAdoptionFile, ROOT_ADOPTION_FILE, type AdoptionErrorCode, type AdoptionSource, type AdoptionReport, type AdoptionReceipt, type AdoptionLegReport, type AffectedDeploymentConfig, type RootAdoptionFile, } from "./adoption/marker.js";
|
|
29
30
|
export { adoptLocalDataRoot, ackAdoptionConfig, witnessAdoptionConfig, listAdoptionQuarantine, readAdoptionStatus, type AdoptionStatus, type AdoptLocalDataRootOptions, type AdoptLocalDataRootResult, type AdoptionCarriageLeg, type AdoptionCarriageLegContext, type AdoptionConfigWitnessReceipt, } from "./adoption/adopt.js";
|
|
30
31
|
/**
|
|
@@ -27,6 +27,7 @@ export { FileStrategyStore } from "./strategy-store.js";
|
|
|
27
27
|
export { resolveDataRoot, sanitizeScope, sanitizePathComponent, createFileConsolidationLock } from "./fs-atomic.js";
|
|
28
28
|
export { FileStoreLockError } from "./fs-atomic.js";
|
|
29
29
|
export { atomicWriteFile, writeThenLink, ensureDir, readJsonlRecords, AppendLog } from "./fs-atomic.js";
|
|
30
|
+
export { processFingerprint, isProcessLive } from "./fs-atomic.js";
|
|
30
31
|
export { AdoptionError, assertAdoptionBootGate, readRootAdoptionFile, writeRootAdoptionFile, ROOT_ADOPTION_FILE, } from "./adoption/marker.js";
|
|
31
32
|
export { adoptLocalDataRoot, ackAdoptionConfig, witnessAdoptionConfig, listAdoptionQuarantine, readAdoptionStatus, } from "./adoption/adopt.js";
|
|
32
33
|
export class FileStorageBackend {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/core",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.6.1",
|
|
4
4
|
"description": "Stateless, task-oriented AI agent core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -76,7 +76,9 @@
|
|
|
76
76
|
"gate:wire": "node scripts/wire-changes.mjs",
|
|
77
77
|
"gate:layering": "node scripts/verify-layering.mjs",
|
|
78
78
|
"gate:phase-api": "node scripts/verify-phase-api.mjs",
|
|
79
|
+
"gate:closed-sets": "node scripts/verify-closed-sets.mjs",
|
|
79
80
|
"gate:agent-returns": "node scripts/verify-agent-returns.mjs",
|
|
81
|
+
"gate:contract-sentences": "node scripts/verify-contract-sentences.mjs",
|
|
80
82
|
"gate:all": "node scripts/gate-all.mjs"
|
|
81
83
|
},
|
|
82
84
|
"dependencies": {
|