@modelprofile.com/flexharness 2.1.0 → 3.0.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/readme.hints.md CHANGED
@@ -4,25 +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
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 cleanup errors are owned by the exact loaded storage state. Scope retirement cannot await or consume another namespace's cleanup, while full disposal settles all storage drains before aggregating failures.
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.
26
28
 
27
29
  ## Tool output boundary
28
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,44 +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);
164
183
  await harness.retireScope(scopeId);
165
184
  await harness.dispose();
166
185
  ```
167
186
 
168
- 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.
188
+
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.
169
190
 
170
- 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.
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.
171
192
 
172
- `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.
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.
173
194
 
174
- `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.
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.
175
196
 
176
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.
177
198
 
178
- `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. Calls through storage-key aliases share the same retirement drain. A later call can load the durable namespace again if the application still resolves it.
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.
179
200
 
180
- Normal retirement-induced cancellation does not make `retireScope()` reject. Unexpected failures observed through run finalization or scoped detached cleanup are surfaced after the namespace has been drained and evicted. One such failure is thrown directly; multiple failures are reported through `FlexHarnessRunError`. State-load failures and already-started non-run persistence failures remain reported to their originating operations and are not reported a second time by retirement.
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.
181
202
 
182
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.
183
204
 
184
205
  ## History And Audit Behavior
185
206
 
186
- Successful model context is accumulated as:
207
+ The model context is built by the session's canonical SmartAgent event history as:
187
208
 
188
- 1. Previous successful model history.
209
+ 1. Previous canonically accepted generations.
189
210
  2. The normalized current user message.
190
211
  3. SmartAgent's result messages.
191
212
 
192
- 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.
193
214
 
194
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.
195
216
 
196
- 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.
197
218
 
198
- 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.
199
220
 
200
221
  ## Permissions
201
222
 
@@ -227,12 +248,22 @@ FlexHarness wraps every provided tool `execute` method before SmartAgent receive
227
248
 
228
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.
229
250
 
230
- 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.
231
252
 
232
- `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.
233
254
 
234
255
  `normalizeJsonValue()` is also exported for integrations that need the same conversion independently.
235
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
+
236
267
  ## Events
237
268
 
238
269
  ```typescript
@@ -263,34 +294,70 @@ Events are discriminated, sequenced, deeply immutable snapshots. Listener except
263
294
 
264
295
  ## Stores
265
296
 
266
- `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.
267
304
 
268
- `JsonFileFlexHarnessStore` stores one `sha256(storageKey).json` file per resolved key. It provides:
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.
269
306
 
270
- - Snapshot schema version 1 and optimistic revisions.
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.
308
+
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:
310
+
311
+ - Strict domain-specific schema validation and optimistic revisions.
271
312
  - Static process-wide queues shared by all store instances for the same absolute file.
272
313
  - Revision re-reads inside the queue before every save.
273
- - Atomic temporary-file write and rename.
314
+ - Atomic temporary-file write, file fsync, rename, and parent-directory fsync.
274
315
  - Directory mode `0700` and file mode `0600`, including existing paths.
275
316
  - Stale temporary-file cleanup and strict snapshot validation.
276
317
 
277
- 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.
319
+
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.
278
321
 
279
- 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.
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
+ ```
280
347
 
281
- 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.
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. A run left streaming by a process crash is deterministically repaired to the same cancelled state that the `2.x` loader produced in memory. The migration then 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.
282
349
 
283
350
  ## Shutdown
284
351
 
285
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.
286
353
 
287
- 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.
288
355
 
289
- `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`.
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`.
290
357
 
291
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`.
292
359
 
293
- 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.
294
361
 
295
362
  ## License and Legal Information
296
363
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/flexharness',
6
- version: '2.1.0',
6
+ version: '3.0.1',
7
7
  description: 'Provider-neutral model-session runtime with durable history, permissions, typed events, and pluggable local or remote tool execution.'
8
8
  }