@particle-academy/prism-harness 0.9.1 → 0.10.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/README.md CHANGED
@@ -58,9 +58,17 @@ creates no session, thread or index entry and makes no model/provider request
58
58
  or logical state change. Reads may physically purge expired entries, exactly as
59
59
  an ordinary store read does. Explicit state writes during resolution refuse.
60
60
 
61
- For named agents, the mode list is the ceiling even when a caller supplies tool
62
- names. Legacy sessions retain caller-list selection by default; set
63
- `AgentRuntimeOptions.narrowRequestedTools` to `true` to apply the ceiling there.
61
+ The mode list is the ceiling for named and legacy sessions. Caller-supplied
62
+ names narrow it; omitting names uses the mode's full list, and an empty list
63
+ offers no tools. A requested `*` selects the mode's ceiling. Other names outside
64
+ the ceiling fail before the model is called.
65
+
66
+ Legacy callers that intentionally need the previous replacement behavior can
67
+ set `AgentRuntimeOptions.allowLegacyToolListWidening` to `true`. This permits
68
+ widening only in the legacy namespace; the configured authorizer still filters
69
+ the offer and tool calls. Named agents always retain their ceiling. Remove an
70
+ old explicit `narrowRequestedTools: false`: it now raises a migration error.
71
+ Explicit `true` remains supported, and conflicts with the widening opt-out.
64
72
  Named agents inherit no legacy authority. Builtins require explicit registration and
65
73
  mode selection. Named-agent duplicates are refused; use
66
74
  tools.forAgent('support').replace(replacement, 'app.support', 'app.support.v2')
@@ -393,6 +401,13 @@ badly.
393
401
  native boot/PID realm and a proven dead or explicitly released owner. Local
394
402
  filesystems on one host only; SMB/NFS are unsupported.
395
403
 
404
+ State readers close their handles before decoding the document. On Windows,
405
+ replacement of a state file can fail while another process holds it open,
406
+ even with delete sharing. Permission failures retry for up to 750 ms. If the
407
+ reader stays open or access is permanently denied, the original native error
408
+ is raised and the temporary file is removed; the previous state stays intact.
409
+ Other errors and POSIX replacement do not retry. Callbacks are never replayed.
410
+
396
411
  On Windows, the first file-lock operation makes one fixed, read-only metadata
397
412
  query using the system Windows PowerShell and CIM module, with a fifteen-second
398
413
  timeout. Established identities and detected container conditions are cached
package/dist/runtime.d.ts CHANGED
@@ -110,8 +110,10 @@ export interface PendingApproval {
110
110
  arguments: JsonObject;
111
111
  }
112
112
  export interface AgentRuntimeOptions {
113
- /** Legacy compatibility default. Named agents always enforce their mode ceiling. */
113
+ /** Explicit true retains strict behavior. Explicit false now requires migration to the widening opt-out. */
114
114
  narrowRequestedTools?: boolean;
115
+ /** Permit caller names to replace a LEGACY session's mode list. Named agents remain strict; authorizers still apply. */
116
+ allowLegacyToolListWidening?: boolean;
115
117
  config?: RuntimeConfig;
116
118
  agents?: Record<string, RuntimeConfig>;
117
119
  client: LlmClient;
package/dist/runtime.js CHANGED
@@ -39,7 +39,13 @@ export class AgentRuntime {
39
39
  constructor(options) {
40
40
  if (options.narrowRequestedTools !== undefined && typeof options.narrowRequestedTools !== 'boolean')
41
41
  throw new TypeError('narrowRequestedTools must be a boolean');
42
- this.#narrowRequestedTools = options.narrowRequestedTools ?? false;
42
+ if (options.allowLegacyToolListWidening !== undefined && typeof options.allowLegacyToolListWidening !== 'boolean')
43
+ throw new TypeError('allowLegacyToolListWidening must be a boolean');
44
+ if (options.narrowRequestedTools === false)
45
+ throw new TypeError('narrowRequestedTools: false is no longer supported. Remove it; use allowLegacyToolListWidening: true only to explicitly permit legacy tool-list widening.');
46
+ if (options.narrowRequestedTools === true && options.allowLegacyToolListWidening === true)
47
+ throw new TypeError('narrowRequestedTools: true conflicts with allowLegacyToolListWidening: true.');
48
+ this.#narrowRequestedTools = options.allowLegacyToolListWidening !== true;
43
49
  this.#client = options.client;
44
50
  this.#modes = options.modes;
45
51
  this.#tools = options.tools;
@@ -126,6 +126,9 @@ async function inspect(lockPath, local) {
126
126
  return 'held';
127
127
  if (!owner.released && !lockPidIsDead(owner.pid))
128
128
  return 'held';
129
+ // A proven stale owner's temporary file may still be open on Windows.
130
+ // Cleanup errors use inspection's held handling and the existing wait budget.
131
+ await cleanFileLockOwner(lockPath, owner, temporary);
129
132
  }
130
133
  catch (error) {
131
134
  if (code(error) === 'ENOENT')
@@ -134,7 +137,6 @@ async function inspect(lockPath, local) {
134
137
  return 'held';
135
138
  throw error;
136
139
  }
137
- await cleanFileLockOwner(lockPath, owner, temporary);
138
140
  return 'retry';
139
141
  }
140
142
  async function release(progress) {
@@ -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.1",
3
+ "version": "0.10.0",
4
4
  "description": "Durable agent sessions for TypeScript — threads, session state and store drivers.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -25,7 +25,8 @@
25
25
  "devDependencies": {
26
26
  "@types/node": "^26.3.0",
27
27
  "typescript": "^5.9.3",
28
- "vitest": "^4.1.11"
28
+ "vite": "^8.3.5",
29
+ "vitest": "^4.1.12"
29
30
  },
30
31
  "repository": {
31
32
  "type": "git",