@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.
Files changed (138) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +2 -2
  3. package/dist/agents/cascade.d.ts +4 -5
  4. package/dist/agents/cascade.js +12 -10
  5. package/dist/agents/repair-loop.d.ts +7 -5
  6. package/dist/agents/repair-loop.js +13 -15
  7. package/dist/agents/retain-ledger.d.ts +2 -3
  8. package/dist/agents/send-message-tool.d.ts +2 -2
  9. package/dist/agents/session-util.d.ts +2 -2
  10. package/dist/agents/subagent.d.ts +27 -46
  11. package/dist/agents/subagent.js +119 -105
  12. package/dist/agents/suspend-guard.d.ts +31 -19
  13. package/dist/agents/suspend-guard.js +14 -8
  14. package/dist/agents/teacher.d.ts +2 -2
  15. package/dist/agents/teacher.js +9 -9
  16. package/dist/agents/team.d.ts +6 -5
  17. package/dist/agents/team.js +10 -8
  18. package/dist/agents/verify.d.ts +8 -9
  19. package/dist/agents/verify.js +17 -17
  20. package/dist/core/a2a.js +2 -1
  21. package/dist/core/agent-definition.d.ts +172 -0
  22. package/dist/core/agent-definition.js +1 -0
  23. package/dist/core/ask-origin.d.ts +60 -7
  24. package/dist/core/ask-origin.js +26 -1
  25. package/dist/core/checkpoint-store.d.ts +78 -76
  26. package/dist/core/checkpoint-store.js +17 -1
  27. package/dist/core/delegation-frames.d.ts +298 -0
  28. package/dist/core/delegation-frames.js +21 -0
  29. package/dist/core/engine-notice.d.ts +555 -0
  30. package/dist/core/engine-notice.js +55 -0
  31. package/dist/core/gate-fold.d.ts +12 -0
  32. package/dist/core/gate-fold.js +158 -0
  33. package/dist/core/gate-lanes.d.ts +93 -0
  34. package/dist/core/gate-lanes.js +626 -0
  35. package/dist/core/gate-outcome.d.ts +189 -0
  36. package/dist/core/gate-outcome.js +70 -0
  37. package/dist/core/hands-band.d.ts +134 -0
  38. package/dist/core/hands-band.js +1 -0
  39. package/dist/core/hooks.d.ts +22 -177
  40. package/dist/core/hooks.js +53 -851
  41. package/dist/core/mcp-failure.d.ts +142 -0
  42. package/dist/core/mcp-failure.js +145 -0
  43. package/dist/core/mcp-server-spec.d.ts +217 -0
  44. package/dist/core/mcp-server-spec.js +1 -0
  45. package/dist/core/mcp.d.ts +21 -77
  46. package/dist/core/mcp.js +76 -150
  47. package/dist/core/model-seat.d.ts +99 -0
  48. package/dist/core/model-seat.js +1 -0
  49. package/dist/core/pause-registry.d.ts +131 -0
  50. package/dist/core/pause-registry.js +27 -0
  51. package/dist/core/reminder-mint.d.ts +10 -0
  52. package/dist/core/reminder-mint.js +3 -0
  53. package/dist/core/runner/assemble-result.d.ts +32 -41
  54. package/dist/core/runner/assemble-result.js +55 -74
  55. package/dist/core/runner/contracts.d.ts +427 -69
  56. package/dist/core/runner/denial-limit-arms.d.ts +1 -1
  57. package/dist/core/runner/denial-limit-arms.js +3 -3
  58. package/dist/core/runner/gate-exit.d.ts +242 -0
  59. package/dist/core/runner/gate-exit.js +124 -0
  60. package/dist/core/runner/park-commit.d.ts +17 -23
  61. package/dist/core/runner/park-commit.js +14 -15
  62. package/dist/core/runner/prepare-ask-lane.d.ts +0 -3
  63. package/dist/core/runner/prepare-ask-lane.js +3 -5
  64. package/dist/core/runner/prepare-boundary-parks.d.ts +3 -6
  65. package/dist/core/runner/prepare-boundary-parks.js +3 -3
  66. package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -7
  67. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  68. package/dist/core/runner/prepare-delegation-surface.d.ts +2 -7
  69. package/dist/core/runner/prepare-gate-stations.d.ts +4 -7
  70. package/dist/core/runner/prepare-gate-stations.js +29 -54
  71. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  72. package/dist/core/runner/prepare-memory.d.ts +44 -26
  73. package/dist/core/runner/prepare-park-ask.d.ts +2 -4
  74. package/dist/core/runner/prepare-park-ask.js +5 -5
  75. package/dist/core/runner/prepare-task.d.ts +2 -2
  76. package/dist/core/runner/prepare-task.js +8 -9
  77. package/dist/core/runner/prepare-wiring-manifest.d.ts +7 -15
  78. package/dist/core/runner/prepare-wiring-manifest.js +9 -10
  79. package/dist/core/runner/runtask.d.ts +20 -102
  80. package/dist/core/runner/runtask.js +119 -121
  81. package/dist/core/runner/terminal-projection.d.ts +22 -0
  82. package/dist/core/runner/terminal-projection.js +28 -0
  83. package/dist/core/runner-deps.d.ts +1416 -0
  84. package/dist/core/runner-deps.js +1 -0
  85. package/dist/core/runtime-caps.d.ts +164 -0
  86. package/dist/core/runtime-caps.js +1 -0
  87. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
  88. package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
  89. package/dist/core/task-event.d.ts +910 -0
  90. package/dist/core/task-event.js +1 -0
  91. package/dist/core/task-limits.d.ts +110 -0
  92. package/dist/core/task-limits.js +1 -0
  93. package/dist/core/task-result.d.ts +809 -0
  94. package/dist/core/task-result.js +1 -0
  95. package/dist/core/task-spec.d.ts +1370 -0
  96. package/dist/core/task-spec.js +1 -0
  97. package/dist/core/task-stream.d.ts +382 -0
  98. package/dist/core/task-stream.js +1 -0
  99. package/dist/core/terminal-cause.d.ts +137 -0
  100. package/dist/core/terminal-cause.js +9 -0
  101. package/dist/core/tool-policy.d.ts +43 -139
  102. package/dist/core/tool-policy.js +79 -112
  103. package/dist/core/tool-spec.d.ts +1174 -0
  104. package/dist/core/tool-spec.js +1 -0
  105. package/dist/core/types.d.ts +27 -7789
  106. package/dist/core/types.js +2 -76
  107. package/dist/core/warm-resume.d.ts +2 -2
  108. package/dist/core/wiring-manifest.d.ts +6 -3
  109. package/dist/core/workflow-journal-store.js +3 -4
  110. package/dist/engine/harness/agent-harness.d.ts +1 -1
  111. package/dist/index.d.ts +12 -8
  112. package/dist/index.js +9 -6
  113. package/dist/orchestration/builtin-workflows.d.ts +2 -2
  114. package/dist/orchestration/builtin-workflows.js +1 -1
  115. package/dist/orchestration/goal.d.ts +2 -2
  116. package/dist/orchestration/goal.js +8 -7
  117. package/dist/orchestration/run-spec.d.ts +2 -2
  118. package/dist/orchestration/run-spec.js +5 -3
  119. package/dist/orchestration/run-workflow-tool.d.ts +4 -4
  120. package/dist/orchestration/run-workflow-tool.js +4 -4
  121. package/dist/orchestration/workflow-governance.d.ts +4 -4
  122. package/dist/orchestration/workflow-governance.js +4 -2
  123. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  124. package/dist/orchestration/workflow-primitives.js +1 -1
  125. package/dist/orchestration/workflow.d.ts +15 -4
  126. package/dist/orchestration/workflow.js +64 -39
  127. package/dist/prompts/supervisor.d.ts +1 -1
  128. package/dist/prompts/supervisor.js +3 -3
  129. package/dist/scenarios/scenario-registry.d.ts +3 -3
  130. package/dist/scenarios/scenario-registry.js +1 -1
  131. package/dist/scenarios/teacher-quickstart.d.ts +2 -2
  132. package/dist/server/http.d.ts +2 -2
  133. package/dist/stores/file/fs-atomic.d.ts +88 -12
  134. package/dist/stores/file/fs-atomic.js +184 -55
  135. package/dist/stores/file/index.d.ts +1 -0
  136. package/dist/stores/file/index.js +1 -0
  137. package/package.json +3 -1
  138. 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` PID file (`root/LOCK` for the whole data dir; one per store
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 writing PID is dead) is
190
- * pruned and re-acquired `proper-lockfile`/CC `concurrentSessions` parity.
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 PID is not running (see below). */
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 is a bare pid, so two BootLocks over one path in this process are byte-identical
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)` writes `lockDir/<sanitizedScope>.consolidate.lock` carrying THIS pid (atomic
324
- * {@link writeThenLink}, so the lock appears with its pid already in it no empty-window prune race). It
325
- * returns a `release` callback on success, or `undefined` when a LIVE owner already holds it (the caller
326
- * treats `undefined` as "busy → skip this pass", a no-op). A STALE lock (recorded pid is dead) is pruned and
327
- * re-acquired once. The implementation lives HERE (the deployment shell's persist/exec axis), NOT core — core
328
- * only DEFINES the injection point (the constitutional split: gate-presence in core, mechanism in the profile).
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 (isProcessRunning(owner.pid)) {
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.pid === owner.pid) {
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, String(process.pid));
431
+ writeThenLink(this.lockPath, serializeLockRecord());
372
432
  }
373
433
  get abandonKey() {
374
434
  return resolvePath(this.lockPath);
375
435
  }
376
436
  inspectLock() {
377
- let raw;
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 lockPath = join(lockDir, `${sanitizeScope(scope)}.consolidate.lock`);
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
- unlinkSync(lockPath);
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 tryWrite() ? release : undefined;
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.5.2",
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": {