@particle-academy/prism-harness 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -393,6 +393,13 @@ badly.
393
393
  native boot/PID realm and a proven dead or explicitly released owner. Local
394
394
  filesystems on one host only; SMB/NFS are unsupported.
395
395
 
396
+ State readers close their handles before decoding the document. On Windows,
397
+ replacement of a state file can fail while another process holds it open,
398
+ even with delete sharing. Permission failures retry for up to 750 ms. If the
399
+ reader stays open or access is permanently denied, the original native error
400
+ is raised and the temporary file is removed; the previous state stays intact.
401
+ Other errors and POSIX replacement do not retry. Callbacks are never replayed.
402
+
396
403
  On Windows, the first file-lock operation makes one fixed, read-only metadata
397
404
  query using the system Windows PowerShell and CIM module, with a fifteen-second
398
405
  timeout. Established identities and detected container conditions are cached
package/dist/runtime.js CHANGED
@@ -149,7 +149,7 @@ export class AgentRuntime {
149
149
  });
150
150
  try {
151
151
  let offered;
152
- if (session.agent === null) {
152
+ if (session.agent === null && !this.#narrowRequestedTools) {
153
153
  const resolved = await this.#tools.resolve(toolNames ?? mode.tools, session);
154
154
  offered = this.#authorizer ? await this.#authorizer.allowed(session, resolved) : [...resolved.values()];
155
155
  }
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { mkdirSync } from 'node:fs';
3
3
  import { mkdir, open, readFile, rename, rm, stat, truncate, writeFile } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
+ import { setTimeout as delay } from 'node:timers/promises';
5
6
  import { isJsonObject } from '../json.js';
6
7
  import { HarnessError } from '../errors.js';
7
8
  import { withFileOwnerLock, pendingFileOwnerReleases } from './file-owner-lock.js';
@@ -52,6 +53,8 @@ export class FileSessionStore {
52
53
  async get(key) {
53
54
  let raw;
54
55
  try {
56
+ // Node's whole-file helper closes its own handle on both outcomes before
57
+ // resolving; JSON decoding and expiry housekeeping happen after it closes.
55
58
  raw = await readFile(this.#pathFor(key), 'utf8');
56
59
  }
57
60
  catch (error) {
@@ -91,7 +94,7 @@ export class FileSessionStore {
91
94
  try {
92
95
  // Atomic on both POSIX and Windows. A reader either sees the whole old
93
96
  // payload or the whole new one, never half of either.
94
- await rename(temporary, target);
97
+ await replaceStateFile(temporary, target);
95
98
  }
96
99
  catch (error) {
97
100
  await rm(temporary, { force: true });
@@ -255,3 +258,36 @@ function digestOf(key) {
255
258
  function isMissing(error) {
256
259
  return typeof error === 'object' && error !== null && error.code === 'ENOENT';
257
260
  }
261
+ /** An open Windows target can block replacement even with delete sharing. */
262
+ async function replaceStateFile(temporary, target) {
263
+ try {
264
+ await rename(temporary, target);
265
+ return;
266
+ }
267
+ catch (original) {
268
+ if (process.platform !== 'win32' || !isReplacePermissionError(original))
269
+ throw original;
270
+ const deadline = performance.now() + 750;
271
+ while (true) {
272
+ const remaining = deadline - performance.now();
273
+ if (remaining <= 0)
274
+ throw original;
275
+ // This timer stays referenced: an awaited write must keep the process alive.
276
+ await delay(Math.min(10, remaining));
277
+ if (performance.now() >= deadline)
278
+ throw original;
279
+ try {
280
+ await rename(temporary, target);
281
+ return;
282
+ }
283
+ catch (error) {
284
+ if (!isReplacePermissionError(error))
285
+ throw error;
286
+ }
287
+ }
288
+ }
289
+ }
290
+ function isReplacePermissionError(error) {
291
+ const code = typeof error === 'object' && error !== null ? error.code : undefined;
292
+ return code === 'EPERM' || code === 'EACCES';
293
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-academy/prism-harness",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Durable agent sessions for TypeScript — threads, session state and store drivers.",
5
5
  "type": "module",
6
6
  "sideEffects": false,