@modelprofile.com/flexharness 2.0.0 → 3.0.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.hints.md CHANGED
@@ -4,23 +4,27 @@ Implementation findings for flexharness.
4
4
 
5
5
  ## SmartAgent boundary
6
6
 
7
- - SmartAgent is the only external runtime dependency. FlexHarness imports only `@push.rocks/smartagent`; its model, prompt, message, provider-option, tool-set, runner-option, and runner-result aliases are derived from SmartAgent's exported `IAgentRunOptions` and `IAgentRunResult`.
7
+ - SmartAgent is the canonical private conversation and execution runtime. FlexHarness creates one transactional `AgentSession` per public session and derives its public model, prompt, provider-option, tool-set, and result aliases from SmartAgent exports.
8
8
  - Public prompts remain JSON-safe. URL strings are converted to `URL` instances only at the private SmartAgent invocation boundary.
9
- - Prompt attachment payloads exist only in the private SmartAgent prompt and, for successful turns, persisted model history. Public messages, prompt results, store audit messages, and events project them to `attachmentType`, source kind, optional media/name, and decoded size when determinable.
9
+ - Prompt attachment payloads exist only in canonical private Agent events. Public messages, prompt results, projection snapshots, and events expose only `attachmentType`, source kind, optional media/name, and decoded size when determinable.
10
10
  - Resolver calls start in promise continuations so synchronous throws are observed. The first failure aborts the shared internal signal without awaiting an ignoring sibling; detached tool-provider settlement is observed and late handles are closed.
11
11
 
12
12
  ## Persistence boundary
13
13
 
14
14
  - Snapshot schema version 1 uses optimistic revisions. Harness mutations are serialized per resolved storage key, while the JSON store adds a process-wide per-file queue shared by all instances.
15
- - Every queued mutation snapshots `revision` and persistent sessions first. Mutation, schema, save, and CAS failures restore that snapshot without replacing `activeRuns`, pending permission objects, or the save queue. Code that crosses the save `await` must re-fetch session records because restoration intentionally recreates them.
16
- - `JsonFileFlexHarnessStore` is intentionally not cross-process safe. It provides atomic rename and in-process CAS, not an operating-system lock.
17
- - Active persisted session states are normalized to idle when loaded. Incomplete messages and tool parts are marked cancelled in memory so a restarted process never presents them as still running.
18
- - Session update/delete checks runtime runs and pending permissions inside the queued mutation. Deletion removes the entire stored session record, including private history and remembered grants.
19
- - Tool close settles before success/history is decided. Final persistence is still attempted after original or close failure. A final save failure rolls back the store mutation, then terminalizes only the current in-memory audit before emitting terminal events once.
20
- - Disposal waits finalizers and every loaded state save tail before clearing listeners, session histories, and the state-load cache.
21
- - Terminal persistence is the cancellation linearization point. Abort returns false once a run starts committing, while the active-run entry continues to block new prompts until the commit settles.
15
+ - Every queued mutation snapshots its revision and domain state first. A thrown save is reconciled against the store before memory is restored: a proven prior snapshot rolls back, a proven new snapshot remains current, and an unknown or explicitly uncertain commit fences the namespace. Code that crosses the save `await` must re-fetch session records because a proven rollback intentionally restores them.
16
+ - `JsonFileFlexHarnessStores` is intentionally not cross-process safe. It provides atomic rename and in-process CAS, not an operating-system lock.
17
+ - Loaded state is repaired from canonical generation outcomes. Accepted hidden terminal stages are promoted, while interrupted or incomplete public messages and tool parts become cancelled so a restarted process never presents them as still running.
18
+ - Session updates reject while runtime work is active. Deletion tombstones the session, cancels active work, and retains failed runtime cleanup for a later deletion, retirement, or disposal retry before removing every persisted domain.
19
+ - Tool close settles before canonical acceptance. A completed hidden terminal projection is staged first, the Agent generation is finalized second, and only then is the public projection promoted. Earlier execution failures finalize as interrupted and publish from that durable outcome.
20
+ - Disposal waits admitted session initialization, finalizers, every loaded-state save tail, tombstone cleanup, provider release, and detached tool cleanup. Listeners always clear; state caches clear only after cleanup succeeds and otherwise remain owned for retry.
21
+ - Canonical finalization is the cancellation linearization point. Abort returns false once a run starts committing, while the active-run entry continues to block new prompts until finalization and public promotion settle.
22
22
  - Permission callbacks wait for the current save tail and then revalidate their run before reading remembered grants, so neither uncommitted grants nor captured callbacks can authorize later work.
23
- - Detached tool-provider settlement remains tracked after prompt finalization. Disposal awaits late handle closure and reports cleanup failure.
23
+ - Detached tool-provider settlement remains tracked after prompt finalization. Failed late handle closure stays owned by its storage state and is retried by a later retirement or disposal call.
24
+ - Scope retirement fences older resolver calls and late state admissions, drains the complete resolved storage namespace, and evicts only the exact cached state promise. It never deletes the durable snapshot. Applications must close admission across every resolver alias before retirement because aliases are unknowable before resolution.
25
+ - Detached tool-provider cleanup and retained session cleanup are owned by the exact loaded storage state. Failed drains retain that ownership and the cached state for retry. Scope retirement cannot await or consume another namespace's cleanup, while full disposal settles all storage drains and orphaned initialization cleanup before aggregating failures.
26
+ - JSON provider release hooks evict non-destructive session wrapper caches after AgentSession and execution-context ownership ends. Failed partial-initialization releases remain storage-scoped orphan ownership and are retried by retirement or disposal.
27
+ - `JsonFileFlexHarnessStores.dispose()` is the explicit final drain for file handles whose close failed on the last store operation; failed disposal retains the handle for another call.
24
28
 
25
29
  ## Tool output boundary
26
30
 
package/readme.md CHANGED
@@ -16,14 +16,14 @@ Node.js 24 or newer is required.
16
16
 
17
17
  ## Overview
18
18
 
19
- FlexHarness owns session state, audit messages, successful model context, permission decisions, event delivery, cancellation, and persistence. Model selection and tool execution remain application-defined extension points. The package depends on SmartAgent but does not expose AI SDK or SmartAI imports as part of its API.
19
+ FlexHarness owns scope isolation, public session and message projections, permission decisions, event delivery, cancellation, and persistence coordination. Each session is backed by a SmartAgent `AgentSession`, which owns the canonical private conversation and runtime event history. Model selection, tool execution, and optional execution-context creation remain application-defined extension points. The package depends on SmartAgent but does not expose AI SDK or SmartAI imports as part of its API.
20
20
 
21
21
  ## Core Setup
22
22
 
23
23
  ```typescript
24
24
  import {
25
25
  FlexHarness,
26
- JsonFileFlexHarnessStore,
26
+ JsonFileFlexHarnessStores,
27
27
  type IFlexResolvedModel,
28
28
  type TFlexAgentToolSet,
29
29
  } from '@modelprofile.com/flexharness';
@@ -70,7 +70,7 @@ const harness = new FlexHarness<IProjectScope>({
70
70
  };
71
71
  },
72
72
  },
73
- store: new JsonFileFlexHarnessStore({
73
+ stores: new JsonFileFlexHarnessStores({
74
74
  directory: '/var/lib/my-app/model-sessions',
75
75
  }),
76
76
  toolOutputLimits: {
@@ -140,7 +140,7 @@ Attachment payloads are never copied into public audit messages or events. Publi
140
140
  }
141
141
  ```
142
142
 
143
- When the turn succeeds, the original string remains only in private persisted model history so a later model turn can receive the attachment again. Failed, cancelled, resolver-failed, cleanup-failed, and persistence-failed turns do not add it to future context.
143
+ When the turn succeeds, the original string remains only in canonical private Agent events so a later model turn can receive the attachment again. Failed, cancelled, resolver-failed, cleanup-failed, and persistence-failed turns do not add it to future context.
144
144
 
145
145
  The main session methods are:
146
146
 
@@ -158,37 +158,65 @@ await harness.prompt(scopeId, sessionId, prompt, options);
158
158
  const admission = await harness.startPrompt(scopeId, sessionId, prompt, options);
159
159
  console.log(admission.runId);
160
160
  await admission.completion;
161
+ const scheduled = await harness.schedulePrompt(
162
+ scopeId,
163
+ sessionId,
164
+ 'refresh-index',
165
+ prompt,
166
+ { debounceMs: 250 },
167
+ );
168
+ await harness.cancelScheduledPrompt(scopeId, sessionId, scheduled.scheduleKey);
161
169
  await harness.abort(scopeId, sessionId);
162
170
  await harness.listPendingPermissions(scopeId, sessionId);
163
171
  await harness.respondToPermission(scopeId, sessionId, permissionId, 'once');
172
+ await harness.pushRuntimeEvent(scopeId, sessionId, { type: 'workspace.changed', path: 'src/' });
173
+ await harness.listUncertainToolExecutions(scopeId, sessionId);
174
+ await harness.reconcileToolExecution(scopeId, sessionId, intentId, {
175
+ resolution: 'executed',
176
+ output: { committed: true },
177
+ });
178
+ await harness.compactSession(scopeId, sessionId);
179
+ await harness.archiveSessionEvents(scopeId, sessionId, compactionEventId);
180
+ await harness.listBackgroundExecutions(scopeId, sessionId);
181
+ await harness.getBackgroundExecution(scopeId, sessionId, executionId);
182
+ await harness.abortBackgroundExecution(scopeId, sessionId, executionId);
183
+ await harness.retireScope(scopeId);
164
184
  await harness.dispose();
165
185
  ```
166
186
 
167
- Only one run may be active in a session. Different sessions can run concurrently. `startPrompt()` resolves with `{ runId, completion }` only after the run ID and initial audit messages have been reserved in the configured store and the corresponding start events have been emitted. `prompt()` preserves the simpler behavior by awaiting that completion internally.
187
+ Only one run may be active in a session. Different sessions can run concurrently. `startPrompt()` resolves with `{ runId, completion }` only after the canonical generation claim, run ID, and initial public audit messages have been durably reserved and the corresponding start events have been emitted. `prompt()` preserves the simpler behavior by awaiting that completion internally.
168
188
 
169
- The reservation save is the admission point. A save failure produces no start events or active audit. If disposal begins while that save is in flight and the save commits, admission still resolves and its completion settles as cancelled; disposal waits for terminal finalization. The optional legacy abort reason argument is accepted solely for call compatibility and is ignored completely, including by the completion error, audit, and events.
189
+ `schedulePrompt()` performs the same durable admission immediately, exposes session status `scheduled`, and starts model preparation after its bounded `debounceMs` delay. `cancelScheduledPrompt()` returns `true` only while the matching schedule key can still be cancelled; its completion rejects with `FlexHarnessAbortError` and the reserved audit messages become cancelled.
170
190
 
171
- `listMessagePage()` returns the newest contiguous page in chronological order. `limit` must be an integer from 1 through 50 and defaults to 50. `nextCursor` is opaque, limited to 4096 UTF-8 bytes, bound to the resolved storage namespace and session, and remains stable when newer messages are appended. Mismatched and stale cursors fail validation. `getMessage()` performs an exact lookup. Transfer identifiers are limited to 512 bytes, text and reasoning parts to 96 KiB, complete messages to 480 KiB, and complete page envelopes to 512 KiB. A page may therefore contain fewer messages than requested. Oversized text is truncated and an otherwise oversized parts collection is replaced with an explicit elision marker; metadata that still cannot fit fails validation. Private model history is unchanged.
191
+ The reservation save is the admission point. A save failure produces no start events or active audit. If disposal begins while that save is in flight and the save commits, admission still resolves and its completion settles as cancelled; disposal waits for terminal finalization.
172
192
 
173
- `updateSession()` supports title replacement, explicit title clearing with `null`, and archive state through `archived`. Archived sessions expose `archivedAt`. Update and deletion are rejected while the session has an active run or pending permission. Deletion removes the complete persisted session, including messages, private model history, and remembered permission grants.
193
+ `listMessagePage()` returns the newest contiguous page in chronological order. `limit` must be an integer from 1 through 50 and defaults to 50. `nextCursor` is opaque, limited to 4096 UTF-8 bytes, bound to the resolved storage namespace and session, and remains stable when newer messages are appended. Mismatched and stale cursors fail validation. `getMessage()` performs an exact lookup. Transfer identifiers are limited to 512 bytes, text and reasoning parts to 96 KiB, complete messages to 480 KiB, and complete page envelopes to 512 KiB. A page may therefore contain fewer messages than requested. Oversized text is truncated and an otherwise oversized parts collection is replaced with an explicit elision marker; metadata that still cannot fit fails validation. Canonical private Agent events are unchanged.
194
+
195
+ `updateSession()` supports title replacement, explicit title clearing with `null`, and archive state through `archived`. Archived sessions expose `archivedAt`. Updates are rejected while the session has an active run or pending permission. Deletion first hides the session behind a durable tombstone, then cancels active work, rejects pending permissions, waits for runtime cleanup, and removes the complete persisted session across public projections, canonical Agent events and archives, remembered permission grants, and background job state. If cleanup fails, the tombstone remains and the operation is retried by a later `deleteSession()`, namespace load, `retireScope()`, or `dispose()` call.
174
196
 
175
197
  `abort()` returns `true` only while cancellation is still accepted. Terminal persistence is the run's commit point; once it starts, `abort()` returns `false` and the already-fixed terminal outcome completes while the session remains busy.
176
198
 
199
+ `retireScope()` stops runtime ownership for the complete resolved storage namespace without deleting its durable snapshot. It does not load a namespace that has no cached or in-flight state. For loaded state, it preserves and waits for persistence that has already started, while later queued reads, writes, and run admissions reject with `FlexHarnessAbortError`. It aborts cancellable runs, rejects pending permissions, waits for committing runs, terminal persistence, tool-handle closure, and detached tool-provider cleanup, then clears and evicts the cached state. Failed cleanup ownership remains cached so a later `retireScope()` or `dispose()` call can retry it. Calls through storage-key aliases share the same retirement drain. A later call can load the durable namespace again after successful retirement if the application still resolves it.
200
+
201
+ Normal retirement-induced cancellation does not make `retireScope()` reject. Unexpected failures observed through run finalization or scoped cleanup are surfaced without dropping the resources that still require cleanup. One such failure is thrown directly; multiple failures are reported through `FlexHarnessRunError`. Calling retirement or disposal again retries retained cleanup ownership.
202
+
203
+ Applications removing a scope must stop and serialize new admission across every alias before calling `retireScope()`, await retirement, and only then remove or purge application-owned durable records. FlexHarness cannot discover aliases before the application resolver returns. Integrations must not use retirement itself as durable deletion.
204
+
177
205
  ## History And Audit Behavior
178
206
 
179
- Successful model context is accumulated as:
207
+ The model context is built by the session's canonical SmartAgent event history as:
180
208
 
181
- 1. Previous successful model history.
209
+ 1. Previous canonically accepted generations.
182
210
  2. The normalized current user message.
183
211
  3. SmartAgent's result messages.
184
212
 
185
- A failed or cancelled prompt remains visible through `getMessages()`, with `failed` or `cancelled` status, but is not included in future model context. Model history is held only in the store snapshot and is not exposed by the session or message APIs.
213
+ A failed or cancelled prompt remains visible through `getMessages()`, with `failed` or `cancelled` status, but is not included in future model context. Canonical Agent events remain private and are not exposed by the session or message APIs.
186
214
 
187
215
  Resolved model identity contains provider and model IDs plus optional display name and effective `variant`. The identity, including its variant, is attached to a failed assistant message when resolution completed before a later failure, matching the provider/model behavior. Prompt results contain it only on success.
188
216
 
189
- Public audit history is safe to send to controllers: attachment parts contain source and size metadata, never inline base64, data URLs, or remote URL payloads. Private model history retains those values solely for subsequent model turns.
217
+ Public audit history is safe to send to controllers: attachment parts contain source and size metadata, never inline base64, data URLs, or remote URL payloads. Canonical private Agent events retain those values solely for subsequent model turns.
190
218
 
191
- Sessions expose `idle`, `running`, `waiting_permission`, `failed`, and `cancelled` status. Persisted `running` and `waiting_permission` states normalize to `idle` after process restart; incomplete messages and parts normalize to `cancelled`.
219
+ Sessions expose `idle`, `scheduled`, `running`, `waiting_permission`, `failed`, and `cancelled` status. Persisted non-terminal activity is repaired from canonical Agent generation outcomes after process restart; incomplete messages and parts normalize to `cancelled` unless an accepted hidden terminal stage can be promoted.
192
220
 
193
221
  ## Permissions
194
222
 
@@ -220,12 +248,22 @@ FlexHarness wraps every provided tool `execute` method before SmartAgent receive
220
248
 
221
249
  Streaming callbacks use run-local synchronous state rather than one persistence promise per delta. Adjacent text and reasoning deltas coalesce. `callbackLimits` bounds callback events, accumulated output bytes, and part count; overflow aborts internally with `FlexHarnessCallbackOverflowError` and the turn is recorded as failed. Reservation and terminal finalization are the normal persistence checkpoints, with permission state changes as explicit additional checkpoints.
222
250
 
223
- Model resolver, tool provider, runner, tool execution, tool callback, tool cleanup, and run-persistence failures cross an untrusted error boundary. By default they become a fixed immutable `FlexHarnessExternalError` before completion rejection, persistence, events, or detached-cleanup reporting. Raw external messages and aggregate members are not retained. A failed `onToolCallFinish` callback stores and accounts for only the bounded projected message; it does not otherwise reject completion, although exceeding the configured callback limits still fails the run. Scope resolution and the initial store load happen before a run exists and remain outside this boundary.
251
+ Model resolver, tool provider, AgentSession, tool execution, tool callback, tool cleanup, and run-persistence failures cross an untrusted error boundary. By default they become a fixed immutable `FlexHarnessExternalError` before completion rejection, persistence, events, or detached-cleanup reporting. Raw external messages and aggregate members are not retained. A failed `onToolCallFinish` callback stores and accounts for only the bounded projected message; it does not otherwise reject completion, although exceeding the configured callback limits still fails the run. Scope resolution and the initial store load happen before a run exists and remain outside this boundary.
224
252
 
225
- `externalErrorProjector` receives one of `modelResolver`, `toolProvider`, `runner`, `toolExecution`, `toolCallback`, `toolCleanup`, or `persistence` as its source. It may synchronously return an application-approved plain data object `{ name, message, code? }`, limited to a 128-byte name, 2048-byte message, and optional 128-byte code. Accessors, extra keys, throwing projectors, and malformed or oversized results fall back to the fixed error. Even exported FlexHarness error subclasses thrown by external integrations are reprojected. Internally created cancellation, callback-overflow, and permission errors retain their typed behavior.
253
+ `externalErrorProjector` receives one of `modelResolver`, `toolProvider`, `agentSession`, `toolExecution`, `toolCallback`, `toolCleanup`, or `persistence` as its source. It may synchronously return an application-approved plain data object `{ name, message, code? }`, limited to a 128-byte name, 2048-byte message, and optional 128-byte code. Accessors, extra keys, throwing projectors, and malformed or oversized results fall back to the fixed error. Even exported FlexHarness error subclasses thrown by external integrations are reprojected. Internally created cancellation, callback-overflow, and permission errors retain their typed behavior.
226
254
 
227
255
  `normalizeJsonValue()` is also exported for integrations that need the same conversion independently.
228
256
 
257
+ ## Agent Runtime Operations
258
+
259
+ `pushRuntimeEvent()` appends a validated JSON event through the canonical AgentSession event store. It is intended for controller-owned context such as workspace changes or external notifications; invalid or non-JSON values fail before persistence.
260
+
261
+ Transactional tool calls persist an execution intent before the tool side effect starts. After an interrupted process, `listUncertainToolExecutions()` exposes intents whose outcome cannot be proven. A controller must inspect the external system and call `reconcileToolExecution()` with `executed`, `not-executed`, or `abandoned-unknown` before allowing dependent work to continue. Reconciliation output is normalized using the same tool-output limits.
262
+
263
+ `agentSessionPolicy` forwards bounded SmartAgent session controls for context building, compaction, event retention, change-listener pressure, lease cleanup, archived transaction tombstones, and context-overflow retries. With a configured `contextCompactor`, `compactSession()` writes a canonical compaction event. `archiveSessionEvents()` moves events covered by that compaction into the configured Agent event archive store and returns public archive metadata.
264
+
265
+ `executionContextProvider` can construct a SmartAgent execution context for each session. FlexHarness supplies the resolved scope, storage key, and the session's private job store. The public background APIs expose only execution ID, type, state, exit code, and timestamps; command payloads, stdout, and stderr remain private. The provider's optional `close()` is owned by session deletion, scope retirement, and harness disposal.
266
+
229
267
  ## Events
230
268
 
231
269
  ```typescript
@@ -256,32 +294,70 @@ Events are discriminated, sequenced, deeply immutable snapshots. Listener except
256
294
 
257
295
  ## Stores
258
296
 
259
- `InMemoryFlexHarnessStore` provides revision-based compare-and-swap behavior for tests and ephemeral processes. It is also the default when `store` is omitted.
297
+ FlexHarness `3.x` separates persistence by trust and lifecycle domain through `IFlexHarnessStores`:
298
+
299
+ - `scopes`: session metadata and deletion tombstones for a resolved storage namespace.
300
+ - `projections`: public audit messages and hidden terminal stages per session.
301
+ - `permissions`: remembered permission keys per session.
302
+ - `agentEvents`: canonical private SmartAgent events and archives per session.
303
+ - `jobs`: private background execution state per session.
304
+
305
+ `InMemoryFlexHarnessStores` implements all five domains with revision-based compare-and-swap behavior for tests and ephemeral processes. It is the default when `stores` is omitted.
306
+
307
+ Custom Agent event and job providers may implement `releaseSession(storageKey, sessionId)` to release session-bound wrappers, handles, or caches without deleting durable data. FlexHarness calls these hooks only after the corresponding AgentSession or execution context has released runtime ownership. A failed release remains owned for a later retirement or disposal retry. `deleteSession()` remains the separate destructive operation for durable session data.
260
308
 
261
- `JsonFileFlexHarnessStore` stores one `sha256(storageKey).json` file per resolved key. It provides:
309
+ `JsonFileFlexHarnessStores` stores the domains in separate `scopes`, `projections`, `permissions`, `events`, `archives`, and `jobs` directories. Storage and session identifiers are SHA-256 hashed for filenames. It provides:
262
310
 
263
- - Snapshot schema version 1 and optimistic revisions.
311
+ - Strict domain-specific schema validation and optimistic revisions.
264
312
  - Static process-wide queues shared by all store instances for the same absolute file.
265
313
  - Revision re-reads inside the queue before every save.
266
- - Atomic temporary-file write and rename.
314
+ - Atomic temporary-file write, file fsync, rename, and parent-directory fsync.
267
315
  - Directory mode `0700` and file mode `0600`, including existing paths.
268
316
  - Stale temporary-file cleanup and strict snapshot validation.
269
317
 
270
- The JSON file store is explicitly not cross-process safe. Use a custom `IFlexHarnessStore` backed by a database or another cross-process CAS mechanism when several processes write the same storage key.
318
+ After every harness using a `JsonFileFlexHarnessStores` instance has been disposed and no store operation remains active, call `await stores.dispose()` to retry and drain any file handle whose earlier close failed. A failed store disposal retains that handle so the call can be retried.
271
319
 
272
- Direct store operations and non-run session mutations surface conflicts as `FlexHarnessStoreConflictError`; malformed, wrong-schema, or non-JSON snapshots are surfaced as `FlexHarnessStoreFormatError`. Run reservation and finalization save failures cross the run error boundary and therefore become `FlexHarnessExternalError`. FlexHarness does not merge conflicts.
320
+ The JSON stores are explicitly not cross-process safe. Use a custom `IFlexHarnessStores` implementation backed by a database or another cross-process CAS mechanism when several processes write the same storage namespace.
273
321
 
274
- Every harness mutation snapshots the persistent session state inside its per-storage queue. If the mutation itself or `store.save()` fails, the in-memory revision and sessions are restored before the queue settles. Runtime run controllers, pending permission objects, and queue identity are preserved. CAS conflicts therefore expose neither an uncommitted create/update/delete nor an automatic merge.
322
+ Direct store operations and non-run session mutations surface conflicts as `FlexHarnessStoreConflictError` or the corresponding SmartAgent store conflict. Malformed, wrong-schema, or non-JSON snapshots are surfaced as `FlexHarnessStoreFormatError`. A write or deletion that changed its target but cannot confirm parent-directory durability surfaces `FlexHarnessStoreCommitUncertainError` with the affected path, operation, and cause. Run persistence failures cross the external error boundary and therefore become `FlexHarnessExternalError`. FlexHarness does not merge conflicts.
323
+
324
+ Each domain serializes its own mutations. A successful run first persists a hidden completed projection, then finalizes the canonical Agent generation as `accepted`, then promotes the hidden projection publicly. Recovery uses the canonical generation outcome to promote an accepted stage or publish a failed/cancelled projection. Failed and cancelled generations remain auditable but never enter future model context.
325
+
326
+ ## Migrating From 2.x
327
+
328
+ Version `3.x` replaces the single `IFlexHarnessStore` snapshot with the split stores above. Run migration while every process that can access the storage namespace is stopped.
329
+
330
+ ```typescript
331
+ import {
332
+ JsonFileFlexHarnessStores,
333
+ } from '@modelprofile.com/flexharness';
334
+ import {
335
+ migrateLegacyFlexHarnessSnapshot,
336
+ type IFlexLegacyHarnessSnapshot,
337
+ } from '@modelprofile.com/flexharness/migration';
338
+
339
+ const storageKey = 'account/project';
340
+ const legacySnapshot: IFlexLegacyHarnessSnapshot = await loadLegacySnapshot(storageKey);
341
+ const stores = new JsonFileFlexHarnessStores({
342
+ directory: '/var/lib/my-app/model-sessions-v3',
343
+ });
344
+
345
+ await migrateLegacyFlexHarnessSnapshot(storageKey, legacySnapshot, stores);
346
+ ```
347
+
348
+ `loadLegacySnapshot()` is application-owned access to the snapshot written by the `2.x` store. The migration validates the complete source and every public run before writing, deterministically converts private model messages into generationless canonical Agent conversation events, and records terminal SmartAgent transactions for completed, failed, and cancelled public runs. It preflights the scope, projection, permission, Agent event, and job destinations before any write, applies missing per-session domains first, and publishes scope discovery last. It is safe to rerun after no work, a completed prefix, or a complete migration when existing destination content is identical. It fails closed when a destination contains conflicting content or non-empty jobs. Keep the legacy snapshot until the migrated application has loaded and verified every storage namespace.
275
349
 
276
350
  ## Shutdown
277
351
 
278
352
  Model and tool resolution share the run signal. Synchronous throws are observed as resolver failures; the first failure aborts that signal and finalizes immediately without waiting for an unresponsive sibling. A detached tool provider that resolves later is observed and its handle is closed; disposal waits for that settlement and reports a late close failure.
279
353
 
280
- Tool-handle close settles before a turn can be successful. Final persistence is attempted even when model execution or close fails. Cleanup failure prevents history append. If final persistence also fails, the stored reserved snapshot remains unchanged while the current in-memory audit is terminalized and terminal events are emitted exactly once.
354
+ Tool-handle close settles before a turn can be successful. After model generation resolves, FlexHarness stages its terminal projection before canonical acceptance; earlier execution failures are finalized as interrupted and then published from that durable outcome. Cleanup failure prevents canonical acceptance. If canonical finalization or public promotion fails, FlexHarness fences the namespace; the next load repairs public state from the durable canonical outcome and any hidden terminal stage.
355
+
356
+ `dispose()` is asynchronous and idempotent. It marks the harness closed, prevents operations waiting on persistence from reserving a run, aborts cancellable active runs, rejects pending permissions, waits for committing runs, all run finalizers, state save tails, and tracked detached tool-provider cleanup, then clears listeners. Loaded state caches are cleared after all cleanup succeeds; a failed drain retains its cache and cleanup ownership so a later `dispose()` call can retry it. Multiple run or cleanup failures are reported through `FlexHarnessRunError`.
281
357
 
282
- `dispose()` is asynchronous and idempotent. It marks the harness closed, prevents operations waiting on persistence from reserving a run, aborts cancellable active runs, rejects pending permissions, waits for committing runs, all run finalizers, state save tails, and tracked detached tool-provider cleanup, then clears listeners and loaded state caches. Multiple run or cleanup failures are reported through `FlexHarnessRunError`.
358
+ If `dispose()` overlaps a storage namespace already being retired, both calls await the same storage drain and cleanup runs once. A retirement call begun after disposal starts rejects with `FlexHarnessClosedError`.
283
359
 
284
- Cancellation is cooperative: model resolvers, tool providers, runners, tools, and cleanup functions must observe the supplied `AbortSignal` and settle tracked work. After a sibling resolver fails, FlexHarness deliberately does not wait for an unresponsive model resolver; a detached tool provider remains tracked because any late handle must be closed. A process host that needs a hard shutdown deadline must enforce that deadline outside FlexHarness and terminate only the process it owns.
360
+ Cancellation is cooperative: model resolvers, tool providers, AgentSession model execution, tools, execution contexts, and cleanup functions must observe the supplied `AbortSignal` and settle tracked work. After a sibling resolver fails, FlexHarness deliberately does not wait for an unresponsive model resolver; a detached tool provider remains tracked because any late handle must be closed. A process host that needs a hard shutdown deadline must enforce that deadline outside FlexHarness and terminate only the process it owns.
285
361
 
286
362
  ## License and Legal Information
287
363
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/flexharness',
6
- version: '2.0.0',
6
+ version: '3.0.0',
7
7
  description: 'Provider-neutral model-session runtime with durable history, permissions, typed events, and pluggable local or remote tool execution.'
8
8
  }