@modelprofile.com/flexharness 11.3.1 → 12.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.
Files changed (65) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.flexharness.core.d.ts +4 -0
  3. package/dist_ts/classes.flexharness.core.js +69 -25
  4. package/dist_ts/classes.flexharness.imports.js +4 -3
  5. package/dist_ts/classes.flexharness.reversions.d.ts +7 -1
  6. package/dist_ts/classes.flexharness.reversions.js +43 -29
  7. package/dist_ts/classes.flexharness.sessions.js +2 -6
  8. package/dist_ts/classes.stores.js +20 -18
  9. package/dist_ts/index.d.ts +1 -0
  10. package/dist_ts/index.js +2 -1
  11. package/dist_ts/interfaces.d.ts +24 -2
  12. package/dist_ts/interfaces.flexharness.d.ts +8 -1
  13. package/dist_ts/interfaces.js +1 -1
  14. package/dist_ts/plugins.d.ts +3 -3
  15. package/dist_ts/plugins.js +3 -3
  16. package/dist_ts/utils.projectionchange.d.ts +34 -0
  17. package/dist_ts/utils.projectionchange.js +135 -0
  18. package/dist_ts/utils.storecodecs.d.ts +10 -3
  19. package/dist_ts/utils.storecodecs.js +29 -4
  20. package/dist_ts/utils.storerules.d.ts +1 -1
  21. package/dist_ts/utils.storerules.js +1 -1
  22. package/dist_ts_migration/v3_legacyflexharness.js +6 -5
  23. package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +5 -4
  24. package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +44 -25
  25. package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +152 -20
  26. package/dist_ts_stores_nosqldb/classes.recordstore.js +369 -66
  27. package/dist_ts_stores_nosqldb/index.d.ts +1 -1
  28. package/dist_ts_stores_nosqldb/index.js +2 -2
  29. package/dist_ts_stores_nosqldb/plugins.d.ts +1 -1
  30. package/dist_ts_stores_nosqldb/plugins.js +1 -1
  31. package/dist_ts_stores_testing/contract.d.ts +33 -0
  32. package/dist_ts_stores_testing/contract.js +566 -0
  33. package/dist_ts_stores_testing/fixtures.d.ts +20 -0
  34. package/dist_ts_stores_testing/fixtures.js +112 -0
  35. package/dist_ts_stores_testing/index.d.ts +2 -0
  36. package/dist_ts_stores_testing/index.js +3 -0
  37. package/dist_ts_stores_testing/plugins.d.ts +5 -0
  38. package/dist_ts_stores_testing/plugins.js +7 -0
  39. package/package.json +9 -3
  40. package/readme.md +6 -3
  41. package/ts/00_commitinfo_data.ts +1 -1
  42. package/ts/classes.flexharness.core.ts +74 -24
  43. package/ts/classes.flexharness.imports.ts +3 -2
  44. package/ts/classes.flexharness.reversions.ts +53 -28
  45. package/ts/classes.flexharness.sessions.ts +2 -5
  46. package/ts/classes.stores.ts +40 -37
  47. package/ts/index.ts +5 -0
  48. package/ts/interfaces.flexharness.ts +8 -1
  49. package/ts/interfaces.ts +27 -2
  50. package/ts/plugins.ts +12 -7
  51. package/ts/readme.md +6 -3
  52. package/ts/utils.projectionchange.ts +190 -0
  53. package/ts/utils.storecodecs.ts +42 -7
  54. package/ts/utils.storerules.ts +2 -2
  55. package/ts_migration/v3_legacyflexharness.ts +11 -6
  56. package/ts_stores_nosqldb/classes.nosqldbstores.ts +59 -46
  57. package/ts_stores_nosqldb/classes.recordstore.ts +445 -64
  58. package/ts_stores_nosqldb/index.ts +1 -0
  59. package/ts_stores_nosqldb/plugins.ts +4 -3
  60. package/ts_stores_nosqldb/readme.md +6 -2
  61. package/ts_stores_testing/contract.ts +776 -0
  62. package/ts_stores_testing/fixtures.ts +127 -0
  63. package/ts_stores_testing/index.ts +17 -0
  64. package/ts_stores_testing/plugins.ts +20 -0
  65. package/ts_stores_testing/readme.md +62 -0
@@ -0,0 +1,20 @@
1
+ import * as plugins from './plugins.js';
2
+ import type { IFlexMessage, IFlexPermissionSnapshot, IFlexProjectionSnapshotCurrent, IFlexReversionSegment, IFlexScopeSnapshot } from '../dist_ts/interfaces.js';
3
+ /** A scope snapshot with one idle session. */
4
+ export declare function scopeSnapshot(revision: number, sessionId?: string): IFlexScopeSnapshot;
5
+ /** An empty projection. */
6
+ export declare function projectionSnapshot(revision: number): IFlexProjectionSnapshotCurrent;
7
+ /** The user and assistant message of one completed run of `session-1`. */
8
+ export declare function runMessages(runId: string): IFlexMessage[];
9
+ /** The reversion segment of one completed run, without a workspace capture. */
10
+ export declare function runSegment(runId: string): IFlexReversionSegment;
11
+ /** A projection of completed runs of `session-1`, each with its messages and reversion segment. */
12
+ export declare function projectionWithRuns(revision: number, runIds: string[]): IFlexProjectionSnapshotCurrent;
13
+ /** A permission snapshot that remembers one approval. */
14
+ export declare function permissionSnapshot(revision: number): IFlexPermissionSnapshot;
15
+ /** A system message event. */
16
+ export declare function agentEvent(id?: string, content?: string): plugins.ISystemMessageEvent;
17
+ /** The Agent event change that appends `events`. */
18
+ export declare function appendEvents(...events: plugins.TAgentEvent[]): plugins.IAgentEventChange;
19
+ /** A finished shell job. */
20
+ export declare function finishedJob(executionId?: string): plugins.IToolJobState;
@@ -0,0 +1,112 @@
1
+ import * as plugins from './plugins.js';
2
+ /** A scope snapshot with one idle session. */
3
+ export function scopeSnapshot(revision, sessionId = 'session-1') {
4
+ return {
5
+ schemaVersion: 1,
6
+ revision,
7
+ sessions: [
8
+ {
9
+ scopeId: 'scope-1',
10
+ sessionId,
11
+ createdAt: '2026-08-05T00:00:00.000Z',
12
+ updatedAt: '2026-08-05T00:00:00.000Z',
13
+ status: 'idle',
14
+ activity: { status: 'idle' },
15
+ },
16
+ ],
17
+ tombstones: [],
18
+ };
19
+ }
20
+ /** An empty projection. */
21
+ export function projectionSnapshot(revision) {
22
+ return {
23
+ schemaVersion: 3,
24
+ revision,
25
+ messages: [],
26
+ stagedTerminals: [],
27
+ reversionSegments: [],
28
+ revertCursor: 0,
29
+ excludedRunIds: [],
30
+ pendingReversionReleases: [],
31
+ };
32
+ }
33
+ /** The user and assistant message of one completed run of `session-1`. */
34
+ export function runMessages(runId) {
35
+ return [{
36
+ messageId: `user-${runId}`,
37
+ sessionId: 'session-1',
38
+ runId,
39
+ role: 'user',
40
+ status: 'completed',
41
+ createdAt: '2026-08-05T00:00:00.000Z',
42
+ completedAt: '2026-08-05T00:00:01.000Z',
43
+ parts: [{ partId: `user-part-${runId}`, type: 'text', text: runId }],
44
+ }, {
45
+ messageId: `assistant-${runId}`,
46
+ sessionId: 'session-1',
47
+ runId,
48
+ role: 'assistant',
49
+ status: 'completed',
50
+ createdAt: '2026-08-05T00:00:00.000Z',
51
+ completedAt: '2026-08-05T00:00:01.000Z',
52
+ parts: [{ partId: `assistant-part-${runId}`, type: 'text', text: 'answer' }],
53
+ }];
54
+ }
55
+ /** The reversion segment of one completed run, without a workspace capture. */
56
+ export function runSegment(runId) {
57
+ return {
58
+ runId,
59
+ userMessageId: `user-${runId}`,
60
+ status: 'completed',
61
+ contextAvailable: true,
62
+ eventIds: [`event-${runId}`],
63
+ workspaceCaptured: false,
64
+ protocolVersion: 1,
65
+ provenance: 'transcript',
66
+ };
67
+ }
68
+ /** A projection of completed runs of `session-1`, each with its messages and reversion segment. */
69
+ export function projectionWithRuns(revision, runIds) {
70
+ return {
71
+ ...projectionSnapshot(revision),
72
+ messages: runIds.flatMap(runMessages),
73
+ reversionSegments: runIds.map(runSegment),
74
+ revertCursor: runIds.length,
75
+ };
76
+ }
77
+ /** A permission snapshot that remembers one approval. */
78
+ export function permissionSnapshot(revision) {
79
+ return {
80
+ schemaVersion: 1,
81
+ revision,
82
+ rememberedPermissionKeys: ['shell:approved'],
83
+ };
84
+ }
85
+ /** A system message event. */
86
+ export function agentEvent(id = 'event-1', content = 'System prompt') {
87
+ return plugins.createAgentEvent({
88
+ id,
89
+ timestamp: 1,
90
+ type: 'system-message',
91
+ content,
92
+ });
93
+ }
94
+ /** The Agent event change that appends `events`. */
95
+ export function appendEvents(...events) {
96
+ return { remove: [], replace: [], append: events };
97
+ }
98
+ /** A finished shell job. */
99
+ export function finishedJob(executionId = 'job-1') {
100
+ return {
101
+ executionId,
102
+ type: 'shell',
103
+ state: 'finished',
104
+ exitCode: 0,
105
+ stdout: 'ok',
106
+ stderr: '',
107
+ startedAt: 1,
108
+ updatedAt: 2,
109
+ finishedAt: 2,
110
+ };
111
+ }
112
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZml4dHVyZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90c19zdG9yZXNfdGVzdGluZy9maXh0dXJlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQVN4Qyw4Q0FBOEM7QUFDOUMsTUFBTSxVQUFVLGFBQWEsQ0FBQyxRQUFnQixFQUFFLFNBQVMsR0FBRyxXQUFXO0lBQ3JFLE9BQU87UUFDTCxhQUFhLEVBQUUsQ0FBQztRQUNoQixRQUFRO1FBQ1IsUUFBUSxFQUFFO1lBQ1I7Z0JBQ0UsT0FBTyxFQUFFLFNBQVM7Z0JBQ2xCLFNBQVM7Z0JBQ1QsU0FBUyxFQUFFLDBCQUEwQjtnQkFDckMsU0FBUyxFQUFFLDBCQUEwQjtnQkFDckMsTUFBTSxFQUFFLE1BQU07Z0JBQ2QsUUFBUSxFQUFFLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBRTthQUM3QjtTQUNGO1FBQ0QsVUFBVSxFQUFFLEVBQUU7S0FDZixDQUFDO0FBQ0osQ0FBQztBQUVELDJCQUEyQjtBQUMzQixNQUFNLFVBQVUsa0JBQWtCLENBQUMsUUFBZ0I7SUFDakQsT0FBTztRQUNMLGFBQWEsRUFBRSxDQUFDO1FBQ2hCLFFBQVE7UUFDUixRQUFRLEVBQUUsRUFBRTtRQUNaLGVBQWUsRUFBRSxFQUFFO1FBQ25CLGlCQUFpQixFQUFFLEVBQUU7UUFDckIsWUFBWSxFQUFFLENBQUM7UUFDZixjQUFjLEVBQUUsRUFBRTtRQUNsQix3QkFBd0IsRUFBRSxFQUFFO0tBQzdCLENBQUM7QUFDSixDQUFDO0FBRUQsMEVBQTBFO0FBQzFFLE1BQU0sVUFBVSxXQUFXLENBQUMsS0FBYTtJQUN2QyxPQUFPLENBQUM7WUFDTixTQUFTLEVBQUUsUUFBUSxLQUFLLEVBQUU7WUFDMUIsU0FBUyxFQUFFLFdBQVc7WUFDdEIsS0FBSztZQUNMLElBQUksRUFBRSxNQUFNO1lBQ1osTUFBTSxFQUFFLFdBQVc7WUFDbkIsU0FBUyxFQUFFLDBCQUEwQjtZ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
@@ -0,0 +1,2 @@
1
+ export { createFlexStoreContractTests, type IFlexStoreContractBackend, type IFlexStoreContractFixture, type IFlexStoreContractTest, } from './contract.js';
2
+ export { agentEvent, appendEvents, finishedJob, permissionSnapshot, projectionSnapshot, projectionWithRuns, runMessages, runSegment, scopeSnapshot, } from './fixtures.js';
@@ -0,0 +1,3 @@
1
+ export { createFlexStoreContractTests, } from './contract.js';
2
+ export { agentEvent, appendEvents, finishedJob, permissionSnapshot, projectionSnapshot, projectionWithRuns, runMessages, runSegment, scopeSnapshot, } from './fixtures.js';
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90c19zdG9yZXNfdGVzdGluZy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsNEJBQTRCLEdBSTdCLE1BQU0sZUFBZSxDQUFDO0FBQ3ZCLE9BQU8sRUFDTCxVQUFVLEVBQ1YsWUFBWSxFQUNaLFdBQVcsRUFDWCxrQkFBa0IsRUFDbEIsa0JBQWtCLEVBQ2xCLGtCQUFrQixFQUNsQixXQUFXLEVBQ1gsVUFBVSxFQUNWLGFBQWEsR0FDZCxNQUFNLGVBQWUsQ0FBQyJ9
@@ -0,0 +1,5 @@
1
+ import { strict as assert } from 'node:assert';
2
+ export { assert };
3
+ import { AgentEventStoreConflictError, ToolJobStoreConflictError, createAgentEvent } from '@modelprofile.com/flexharness-agent';
4
+ export { AgentEventStoreConflictError, ToolJobStoreConflictError, createAgentEvent };
5
+ export type { IAgentEventArchive, IAgentEventChange, ISystemMessageEvent, IToolJobState, TAgentEvent, } from '@modelprofile.com/flexharness-agent';
@@ -0,0 +1,7 @@
1
+ // node native scope
2
+ import { strict as assert } from 'node:assert';
3
+ export { assert };
4
+ // @modelprofile.com scope
5
+ import { AgentEventStoreConflictError, ToolJobStoreConflictError, createAgentEvent, } from '@modelprofile.com/flexharness-agent';
6
+ export { AgentEventStoreConflictError, ToolJobStoreConflictError, createAgentEvent };
7
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzX3N0b3Jlc190ZXN0aW5nL3BsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsb0JBQW9CO0FBQ3BCLE9BQU8sRUFBRSxNQUFNLElBQUksTUFBTSxFQUFFLE1BQU0sYUFBYSxDQUFDO0FBRS9DLE9BQU8sRUFBRSxNQUFNLEVBQUUsQ0FBQztBQUVsQiwwQkFBMEI7QUFDMUIsT0FBTyxFQUNMLDRCQUE0QixFQUM1Qix5QkFBeUIsRUFDekIsZ0JBQWdCLEdBQ2pCLE1BQU0scUNBQXFDLENBQUM7QUFFN0MsT0FBTyxFQUFFLDRCQUE0QixFQUFFLHlCQUF5QixFQUFFLGdCQUFnQixFQUFFLENBQUMifQ==
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "Task Venture Capital GmbH",
3
3
  "license": "MIT",
4
4
  "name": "@modelprofile.com/flexharness",
5
- "version": "11.3.1",
5
+ "version": "12.0.0",
6
6
  "type": "module",
7
7
  "description": "Managed agent sessions with tools, MCP, chat, OCR, documents and browser control through focused subpath imports.",
8
8
  "main": "./dist_ts/index.js",
@@ -80,6 +80,10 @@
80
80
  "types": "./dist_ts_stores_nosqldb/index.d.ts",
81
81
  "import": "./dist_ts_stores_nosqldb/index.js"
82
82
  },
83
+ "./stores/testing": {
84
+ "types": "./dist_ts_stores_testing/index.d.ts",
85
+ "import": "./dist_ts_stores_testing/index.js"
86
+ },
83
87
  "./typedrequest": {
84
88
  "types": "./dist_ts_typedrequest/index.d.ts",
85
89
  "import": "./dist_ts_typedrequest/index.js"
@@ -87,8 +91,8 @@
87
91
  },
88
92
  "dependencies": {
89
93
  "@cfworker/json-schema": "^4.1.1",
90
- "@modelprofile.com/flexharness-agent": "11.3.1",
91
- "@modelprofile.com/flexharness-models": "11.3.1",
94
+ "@modelprofile.com/flexharness-agent": "12.0.0",
95
+ "@modelprofile.com/flexharness-models": "12.0.0",
92
96
  "@push.rocks/smartrequest": "^5.5.1",
93
97
  "ai": "^7.0.111",
94
98
  "zod": "^4.6.5"
@@ -216,6 +220,8 @@
216
220
  "dist_ts_chat_harness_typedsocket",
217
221
  "ts_stores_nosqldb",
218
222
  "dist_ts_stores_nosqldb",
223
+ "ts_stores_testing",
224
+ "dist_ts_stores_testing",
219
225
  "ts_typedrequest",
220
226
  "dist_ts_typedrequest",
221
227
  "readme.md",
package/readme.md CHANGED
@@ -68,6 +68,7 @@ or provider SDKs.
68
68
  | `/mcp` | MCP clients, AI SDK tool conversion and `createMcpToolCatalog`, the tool catalog of MCP tools | `@modelcontextprotocol/sdk` |
69
69
  | `/migration` | Existing versioned harness migrations | None |
70
70
  | `/stores/nosqldb` | `NoSqlFlexHarnessStores`: cross-process CAS stores on `@lossless.org/client/nosqldb` | `@lossless.org/client` |
71
+ | `/stores/testing` | `createFlexStoreContractTests`: the contract every built-in `IFlexHarnessStores` passes, to run against a custom store with any test runner | None |
71
72
 
72
73
  The integrations in the last column use optional peer dependencies. Install them
73
74
  only for the entrypoints your project imports, for example:
@@ -1128,7 +1129,9 @@ Current FlexHarness persistence is separated by trust and lifecycle domain throu
1128
1129
 
1129
1130
  `InMemoryFlexHarnessStores` implements all six required domains with revision-based compare-and-swap behavior for tests and ephemeral processes. It is the default when `stores` is omitted. Custom `IFlexHarnessStores` implementations must provide `projectManagement` even when project-management tools are disabled, because deletion cleanup always writes the generation fence.
1130
1131
 
1131
- A custom store validates what it loads and saves with the validators the built-in stores apply. Each throws `FlexHarnessStoreFormatError` for a malformed or wrong-schema snapshot: `assertFlexScopeSnapshot()`, `assertFlexProjectionSnapshot(value, sessionId)`, which also requires every message and staged terminal to belong to `sessionId`, `assertFlexPermissionSnapshot()`, `assertFlexProjectManagementRecord()` and `assertFlexToolJobSnapshot()`. Agent event snapshots and archives have `validateAgentEventSnapshotV2()` and `validateAgentEventArchiveV2()` in `@modelprofile.com/flexharness-agent`.
1132
+ A custom store validates what it loads and saves with the validators the built-in stores apply. Each throws `FlexHarnessStoreFormatError` for a malformed or wrong-schema snapshot: `assertFlexScopeSnapshot()`, `assertFlexProjectionSnapshot(value, sessionId)`, which also requires every message and staged terminal to belong to `sessionId`, `assertFlexPermissionSnapshot()`, `assertFlexProjectManagementRecord()` and `assertFlexToolJobSnapshot()`. Agent event snapshots and archives have `validateAgentEventSnapshot()` and `validateAgentEventArchive()` in `@modelprofile.com/flexharness-agent`.
1133
+
1134
+ Projections and Agent event histories are saved as changes, so a save costs what changed rather than the whole session. `IFlexProjectionStore.save(storageKey, sessionId, change, expectedRevision)` receives an `IFlexProjectionChange`: the schema-3 projection with `messages` and `reversionSegments` given as `IFlexListChange`s `{ start, end, items }`, each of which keeps the stored entries from `start` up to `end` and appends `items`. A list change with `start` and `end` at 0 replaces the list whole; FlexHarness sends whole lists when it cannot know which entries the store holds: for a projection it loaded in an older schema and after a save with an uncertain outcome. The Agent event store of each session receives an `IAgentEventChange` of removed, replaced and appended events, as the `@modelprofile.com/flexharness-agent` readme describes. A store that keeps snapshots checks a change's form with `assertFlexProjectionChange()` or `assertAgentEventChange()`, applies it with `applyFlexProjectionChange()` or `applyAgentEventChange()`, and validates the result as above; `createWholeProjectionChange(snapshot)` makes the change that stores a projection whole. FlexHarness freezes every message and reversion segment it saved and replaces an entry instead of editing it, which is how it tells the entries the store already holds. `/stores/testing` exports `createFlexStoreContractTests(backend)`, the cases every built-in store passes, for a custom store to run with any test runner; its readme shows how.
1132
1135
 
1133
1136
  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()` and `deleteSessionGenerationCohort()` are the separate destructive operations for durable session data.
1134
1137
 
@@ -1145,13 +1148,13 @@ After every harness using a `JsonFileFlexHarnessStores` instance has been dispos
1145
1148
 
1146
1149
  The JSON stores are explicitly not cross-process safe. When several processes can access the same storage namespace, every core `IFlexHarnessStores` domain and `stores.projectManagement` must use database-backed or equivalent cross-process CAS. Process-local CAS for the core stores or for PM alone is insufficient: session generation creation, cleanup tombstones, PM replacement, and stale-writer rejection must all retain their respective atomic preconditions across processes.
1147
1150
 
1148
- `NoSqlFlexHarnessStores` from `/stores/nosqldb` is that database-backed bundle for every domain. Pass it an initialized `SmartdataDb` and `await stores.prepare()` at startup, before the first harness uses it; the subpath readme describes chunked snapshots and failure reporting.
1151
+ `NoSqlFlexHarnessStores` from `/stores/nosqldb` is that database-backed bundle for every domain. Pass it an initialized `SmartdataDb` and `await stores.prepare()` at startup, before the first harness uses it; the subpath readme describes chunked snapshots, change logs and failure reporting.
1149
1152
 
1150
1153
  Direct store operations and non-run session mutations surface conflicts as `FlexHarnessStoreConflictError` or the corresponding AgentSession 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.
1151
1154
 
1152
1155
  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.
1153
1156
 
1154
- Projection stores accept schema version 1, 2, or 3 from `load()`, but every `save()` receives the current schema-3 shape. Schema 3 records explicit reversion protocol, transcript/workspace provenance, and V2 disposition. A terminal V2 segment must be conclusively `revertible`, `no-change`, or `nonrevertible`; a pending V2 capture remains owned by its capture WAL and is never treated as transcript history.
1157
+ Projection stores accept schema version 1, 2, or 3 from `load()`, but every `save()` receives a change that makes the current schema-3 shape. A change to a loaded schema-1 or schema-2 projection carries both lists whole. Schema 3 records explicit reversion protocol, transcript/workspace provenance, and V2 disposition. A terminal V2 segment must be conclusively `revertible`, `no-change`, or `nonrevertible`; a pending V2 capture remains owned by its capture WAL and is never treated as transcript history.
1155
1158
 
1156
1159
  Current schema-3 projections also persist optional `metrics` state. Older strict readers that do not recognize this key, or the token breakdown keys inside it (`reportedLifetimeInputTokens`, `reportedLifetimeCacheReadTokens`, `reportedLifetimeCacheWriteTokens`, `lifetimeTokenBreakdownComplete`), cannot reopen a projection after they have been written; downgrade or mixed-version access to that session is unsupported.
1157
1160
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/flexharness',
6
- version: '11.3.1',
6
+ version: '12.0.0',
7
7
  description: 'The FlexHarness toolbox source repository and shared dependency catalog.'
8
8
  }
@@ -21,6 +21,7 @@ import type {
21
21
  IFlexMessage,
22
22
  IFlexPartSnapshotEvent,
23
23
  IFlexPromptQueueLimits,
24
+ IFlexProjectionChange,
24
25
  IFlexProjectionSnapshotCurrent,
25
26
  IFlexReversionLimits,
26
27
  IFlexReversionSegment,
@@ -51,6 +52,7 @@ import {
51
52
  resolveJsonLimits,
52
53
  } from './utils.json.js';
53
54
  import { hydrateAgentMessages } from './utils.prompt.js';
55
+ import { createProjectionChange, freezeProjectionEntries } from './utils.projectionchange.js';
54
56
  import {
55
57
  normalizeAgentSessionPolicy,
56
58
  normalizeBuiltInTools,
@@ -550,7 +552,7 @@ export class FlexHarnessCore<TScope> {
550
552
  rememberedPermissionKeys: new Set(),
551
553
  permissionRevision: 0,
552
554
  permissionQueue: Promise.resolve(),
553
- eventStore: undefined as unknown as plugins.IAgentEventStoreV2,
555
+ eventStore: undefined as unknown as plugins.IAgentEventStore,
554
556
  agentSession: unavailable,
555
557
  agentSessionAbortCompleted: true,
556
558
  agentSessionCloseCompleted: true,
@@ -699,6 +701,8 @@ export class FlexHarnessCore<TScope> {
699
701
  terminalPromptQueueEntries: new Map(),
700
702
  outstandingPromptBytes: 0,
701
703
  };
704
+ freezeProjectionEntries(stored);
705
+ if (currentProjection) this.rememberSavedProjectionLists(stored);
702
706
  if (currentProjection?.metrics === undefined
703
707
  || stored.metrics.pendingGenerationId !== undefined
704
708
  || stored.metrics.pendingCompactionId !== undefined
@@ -971,18 +975,20 @@ export class FlexHarnessCore<TScope> {
971
975
  if (stored.stagedTerminals.length > 0) stored.stagedTerminals = [];
972
976
  const activeEvents = stored.agentSession.getEvents();
973
977
  const activeEventIds = new Set(activeEvents.map((event) => event.id));
974
- for (let index = 0; index < stored.reversionSegments.length; index++) {
975
- const segment = stored.reversionSegments[index];
976
- if (segment.eventIds.length > 0 && !segment.eventIds.some((id) => activeEventIds.has(id))) {
977
- segment.contextAvailable = false;
978
- }
979
- if (segment.status !== 'capturing') {
978
+ stored.reversionSegments = stored.reversionSegments.map((segment) => {
979
+ const contextAvailable = segment.contextAvailable
980
+ && (segment.eventIds.length === 0 || segment.eventIds.some((id) => activeEventIds.has(id)));
981
+ let status = segment.status;
982
+ if (status !== 'capturing') {
980
983
  const outcome = outcomes.get(segment.runId);
981
- if (outcome === 'accepted') segment.status = 'completed';
982
- else if (outcome === 'rejected') segment.status = 'failed';
983
- else if (outcome === 'interrupted' || segment.contextAvailable) segment.status = 'cancelled';
984
+ if (outcome === 'accepted') status = 'completed';
985
+ else if (outcome === 'rejected') status = 'failed';
986
+ else if (outcome === 'interrupted' || contextAvailable) status = 'cancelled';
984
987
  }
985
- }
988
+ return contextAvailable === segment.contextAvailable && status === segment.status
989
+ ? segment
990
+ : { ...segment, contextAvailable, status };
991
+ });
986
992
  stored.revertCursor = stored.reversionSegments
987
993
  .slice(0, reversionVisibilityBoundary)
988
994
  .filter((segment) => segment.status === 'completed')
@@ -1032,17 +1038,19 @@ export class FlexHarnessCore<TScope> {
1032
1038
  });
1033
1039
  if (projectionChanged) {
1034
1040
  const expected = stored.projectionRevision;
1035
- const snapshot = this.createProjectionSnapshot(stored, expected + 1);
1041
+ freezeProjectionEntries(stored);
1042
+ const change = this.createProjectionChange(stored, expected + 1);
1036
1043
  await this.stores.projections.save(
1037
1044
  storageKey,
1038
1045
  stored.session.sessionId,
1039
- snapshot,
1046
+ change,
1040
1047
  expected,
1041
1048
  );
1042
1049
  stored.projectionSchemaVersion = 3;
1043
1050
  delete stored.projectionBaseline;
1051
+ this.rememberSavedProjectionLists(stored);
1044
1052
  stored.projectionReconciliationRequired = false;
1045
- stored.projectionRevision = snapshot.revision;
1053
+ stored.projectionRevision = change.revision;
1046
1054
  }
1047
1055
  return beforeSession !== JSON.stringify(stored.session);
1048
1056
  }
@@ -1239,9 +1247,10 @@ export class FlexHarnessCore<TScope> {
1239
1247
  ? undefined
1240
1248
  : cloneSerializable(stored.projectionBaseline);
1241
1249
  const beforeMetrics = cloneSerializable(stored.metrics);
1242
- const beforeMessages = cloneSerializable(stored.messages);
1250
+ // List entries are frozen and replaced rather than edited, so copies of the lists suffice.
1251
+ const beforeMessages = stored.messages.slice();
1243
1252
  const beforeStages = cloneSerializable(stored.stagedTerminals);
1244
- const beforeSegments = cloneSerializable(stored.reversionSegments);
1253
+ const beforeSegments = stored.reversionSegments.slice();
1245
1254
  const beforeCursor = stored.revertCursor;
1246
1255
  const beforeExcludedRunIds = cloneSerializable(stored.excludedRunIds);
1247
1256
  const beforePendingReversion = stored.pendingReversion === undefined
@@ -1285,18 +1294,20 @@ export class FlexHarnessCore<TScope> {
1285
1294
  restore();
1286
1295
  throw error;
1287
1296
  }
1288
- const snapshot = this.createProjectionSnapshot(stored, beforeRevision + 1);
1297
+ freezeProjectionEntries(stored);
1298
+ const revision = beforeRevision + 1;
1289
1299
  try {
1290
1300
  await this.stores.projections.save(
1291
1301
  state.storageKey,
1292
1302
  stored.session.sessionId,
1293
- snapshot,
1303
+ this.createProjectionChange(stored, revision),
1294
1304
  beforeRevision,
1295
1305
  );
1296
1306
  stored.projectionSchemaVersion = 3;
1297
1307
  delete stored.projectionBaseline;
1308
+ this.rememberSavedProjectionLists(stored);
1298
1309
  stored.projectionReconciliationRequired = false;
1299
- stored.projectionRevision = snapshot.revision;
1310
+ stored.projectionRevision = revision;
1300
1311
  return result;
1301
1312
  } catch (error) {
1302
1313
  if (error instanceof FlexHarnessStoreConflictError) {
@@ -1305,11 +1316,13 @@ export class FlexHarnessCore<TScope> {
1305
1316
  }
1306
1317
  if (error instanceof FlexHarnessStoreCommitUncertainError) {
1307
1318
  stored.projectionSchemaVersion = 3;
1319
+ delete stored.savedProjectionLists;
1308
1320
  stored.projectionReconciliationRequired = true;
1309
- stored.projectionRevision = snapshot.revision;
1321
+ stored.projectionRevision = revision;
1310
1322
  state.lifecycle = 'fenced';
1311
1323
  throw error;
1312
1324
  }
1325
+ const snapshot = this.createProjectionSnapshot(stored, revision);
1313
1326
  let current: TFlexProjectionSnapshot | undefined;
1314
1327
  let reconciliationError: unknown;
1315
1328
  try {
@@ -1323,8 +1336,9 @@ export class FlexHarnessCore<TScope> {
1323
1336
  if (current && JSON.stringify(current) === JSON.stringify(snapshot)) {
1324
1337
  stored.projectionSchemaVersion = 3;
1325
1338
  delete stored.projectionBaseline;
1339
+ this.rememberSavedProjectionLists(stored);
1326
1340
  stored.projectionReconciliationRequired = false;
1327
- stored.projectionRevision = snapshot.revision;
1341
+ stored.projectionRevision = revision;
1328
1342
  throw error;
1329
1343
  }
1330
1344
  if (
@@ -1335,8 +1349,9 @@ export class FlexHarnessCore<TScope> {
1335
1349
  throw error;
1336
1350
  }
1337
1351
  stored.projectionSchemaVersion = 3;
1352
+ delete stored.savedProjectionLists;
1338
1353
  stored.projectionReconciliationRequired = true;
1339
- stored.projectionRevision = snapshot.revision;
1354
+ stored.projectionRevision = revision;
1340
1355
  state.lifecycle = 'fenced';
1341
1356
  throw reconciliationError === undefined
1342
1357
  ? error
@@ -1380,6 +1395,35 @@ export class FlexHarnessCore<TScope> {
1380
1395
  };
1381
1396
  }
1382
1397
 
1398
+ /** The change from the lists the store holds to the session's projection at `revision`. */
1399
+ private createProjectionChange(
1400
+ stored: IStoredSessionState,
1401
+ revision: number,
1402
+ ): IFlexProjectionChange {
1403
+ return createProjectionChange(stored.savedProjectionLists, {
1404
+ schemaVersion: 3,
1405
+ revision,
1406
+ metrics: cloneSerializable(stored.metrics),
1407
+ messages: stored.messages,
1408
+ stagedTerminals: cloneSerializable(stored.stagedTerminals),
1409
+ reversionSegments: stored.reversionSegments,
1410
+ revertCursor: stored.revertCursor,
1411
+ excludedRunIds: cloneSerializable(stored.excludedRunIds),
1412
+ ...(stored.pendingReversion === undefined
1413
+ ? {}
1414
+ : { pendingReversion: cloneSerializable(stored.pendingReversion) }),
1415
+ pendingReversionReleases: cloneSerializable(stored.pendingReversionReleases),
1416
+ });
1417
+ }
1418
+
1419
+ /** Records the session's lists as the ones the store now holds. */
1420
+ private rememberSavedProjectionLists(stored: IStoredSessionState): void {
1421
+ stored.savedProjectionLists = {
1422
+ messages: stored.messages.slice(),
1423
+ reversionSegments: stored.reversionSegments.slice(),
1424
+ };
1425
+ }
1426
+
1383
1427
  public upgradeProjectionSnapshot(
1384
1428
  projection: TFlexProjectionSnapshot,
1385
1429
  ): IFlexProjectionSnapshotCurrent {
@@ -1443,8 +1487,14 @@ export class FlexHarnessCore<TScope> {
1443
1487
  if (current.pendingReversion === undefined) delete stored.pendingReversion;
1444
1488
  else stored.pendingReversion = current.pendingReversion;
1445
1489
  stored.pendingReversionReleases = current.pendingReversionReleases;
1446
- if (projection.schemaVersion === 3) delete stored.projectionBaseline;
1447
- else stored.projectionBaseline = cloneSerializable(projection);
1490
+ freezeProjectionEntries(stored);
1491
+ if (projection.schemaVersion === 3) {
1492
+ delete stored.projectionBaseline;
1493
+ this.rememberSavedProjectionLists(stored);
1494
+ } else {
1495
+ stored.projectionBaseline = cloneSerializable(projection);
1496
+ delete stored.savedProjectionLists;
1497
+ }
1448
1498
  stored.projectionSchemaVersion = projection.schemaVersion;
1449
1499
  stored.projectionRevision = projection.revision;
1450
1500
  stored.projectionReconciliationRequired = false;
@@ -17,6 +17,7 @@ import {
17
17
  type TFlexBeginSessionImportResult,
18
18
  } from './interfaces.js';
19
19
  import { cloneSerializable } from './utils.json.js';
20
+ import { createWholeProjectionChange } from './utils.projectionchange.js';
20
21
  import { publicSnapshot } from './utils.transfer.js';
21
22
  import {
22
23
  normalizeBeginSessionImportOptions,
@@ -231,7 +232,7 @@ export class FlexHarnessSessionImports<TScope> {
231
232
  });
232
233
  });
233
234
  reserved = true;
234
- await this.core.stores.projections.save(state.storageKey, sessionId, {
235
+ await this.core.stores.projections.save(state.storageKey, sessionId, createWholeProjectionChange({
235
236
  schemaVersion: 3,
236
237
  revision: 1,
237
238
  metrics: createSessionMetricsState(false),
@@ -241,7 +242,7 @@ export class FlexHarnessSessionImports<TScope> {
241
242
  revertCursor: 0,
242
243
  excludedRunIds: [],
243
244
  pendingReversionReleases: [],
244
- }, 0);
245
+ }), 0);
245
246
  const runtime = await this.core.loadSessionRuntime(state, metadata, scopeId, scope);
246
247
  loaded = runtime;
247
248
  await this.core.mutateScope(state, () => {
@@ -28,6 +28,7 @@ import {
28
28
  type TJsonValue,
29
29
  } from './interfaces.js';
30
30
  import { cloneSerializable, normalizeJsonValue } from './utils.json.js';
31
+ import { createWholeProjectionChange } from './utils.projectionchange.js';
31
32
  import { publicSnapshot } from './utils.transfer.js';
32
33
  import type {
33
34
  TCanonicalOutcome,
@@ -402,13 +403,8 @@ export class FlexHarnessReversions<TScope> {
402
403
  && !segment.eventIds.some((eventId) => activeEvents.has(eventId)))) return false;
403
404
  let branchCommitted = false;
404
405
  await this.core.mutateProjection(state, stored, () => {
405
- for (const segment of stored.reversionSegments) {
406
- if (
407
- segment.contextAvailable
408
- && segment.eventIds.length > 0
409
- && !segment.eventIds.some((eventId) => activeEvents.has(eventId))
410
- ) segment.contextAvailable = false;
411
- }
406
+ this.markReversionContextUnavailable(stored, (segment) =>
407
+ segment.eventIds.length > 0 && !segment.eventIds.some((eventId) => activeEvents.has(eventId)));
412
408
  branchCommitted = this.commitArchivedHiddenBranchState(stored);
413
409
  this.pruneReversionState(stored, false, true);
414
410
  }, true);
@@ -618,16 +614,22 @@ export class FlexHarnessReversions<TScope> {
618
614
  return;
619
615
  }
620
616
  await this.core.mutateProjection(state, stored, () => {
621
- segment.status = outcome === 'accepted'
622
- ? 'completed'
623
- : outcome === 'rejected'
624
- ? 'failed'
625
- : 'cancelled';
626
- this.applyFinalizedReversionOutcome(stored, segment, finalizedOutcome);
627
- segment.eventIds = stored.agentSession.getEvents()
617
+ const index = stored.reversionSegments.findIndex((entry) => entry.runId === pending.runId);
618
+ if (index < 0) throw new FlexHarnessValidationError('Pending capture references a missing segment.');
619
+ const finalized: IFlexReversionSegment = {
620
+ ...stored.reversionSegments[index],
621
+ status: outcome === 'accepted'
622
+ ? 'completed'
623
+ : outcome === 'rejected'
624
+ ? 'failed'
625
+ : 'cancelled',
626
+ };
627
+ this.applyFinalizedReversionOutcome(stored, finalized, finalizedOutcome);
628
+ finalized.eventIds = stored.agentSession.getEvents()
628
629
  .filter((event) => event.generationId === pending.runId)
629
630
  .map((event) => event.id);
630
- if (segment.status === 'completed') stored.revertCursor++;
631
+ stored.reversionSegments[index] = finalized;
632
+ if (finalized.status === 'completed') stored.revertCursor++;
631
633
  if (outcome === undefined && !stored.excludedRunIds.includes(pending.runId)) {
632
634
  stored.excludedRunIds.push(pending.runId);
633
635
  }
@@ -1077,23 +1079,42 @@ export class FlexHarnessReversions<TScope> {
1077
1079
  };
1078
1080
  }
1079
1081
 
1080
- /** Records a run's terminal status and events on its segment and moves the revert cursor. */
1082
+ /**
1083
+ * Replaces a run's segment with a copy that records its terminal status and events, moves the
1084
+ * revert cursor and returns the copy.
1085
+ */
1081
1086
  private closeRunReversionSegment(
1082
1087
  run: IActiveRun,
1083
1088
  status: 'completed' | 'failed' | 'cancelled',
1084
1089
  ): IFlexReversionSegment {
1085
- const segment = run.stored.reversionSegments.find((entry) => entry.runId === run.runId);
1086
- if (!segment) throw new FlexHarnessValidationError('Reversion segment changed before finalization.');
1087
- const wasCompleted = segment.status === 'completed';
1088
- segment.status = status;
1089
- segment.eventIds = run.stored.agentSession.getEvents()
1090
- .filter((event) => event.generationId === run.runId)
1091
- .map((event) => event.id);
1090
+ const index = run.stored.reversionSegments.findIndex((entry) => entry.runId === run.runId);
1091
+ if (index < 0) throw new FlexHarnessValidationError('Reversion segment changed before finalization.');
1092
+ const wasCompleted = run.stored.reversionSegments[index].status === 'completed';
1093
+ const closed: IFlexReversionSegment = {
1094
+ ...run.stored.reversionSegments[index],
1095
+ status,
1096
+ eventIds: run.stored.agentSession.getEvents()
1097
+ .filter((event) => event.generationId === run.runId)
1098
+ .map((event) => event.id),
1099
+ };
1100
+ run.stored.reversionSegments[index] = closed;
1092
1101
  if (!wasCompleted && status === 'completed') run.stored.revertCursor++;
1093
1102
  else if (wasCompleted && status !== 'completed') run.stored.revertCursor--;
1094
- return segment;
1103
+ return closed;
1095
1104
  }
1096
1105
 
1106
+ /** Replaces the segments `unavailable` selects with copies that have no context available. */
1107
+ public markReversionContextUnavailable(
1108
+ stored: IStoredSessionState,
1109
+ unavailable: (segment: IFlexReversionSegment) => boolean,
1110
+ ): void {
1111
+ stored.reversionSegments = stored.reversionSegments.map((segment) =>
1112
+ segment.contextAvailable && unavailable(segment)
1113
+ ? { ...segment, contextAvailable: false }
1114
+ : segment);
1115
+ }
1116
+
1117
+ /** Records a finalized capture on `segment`, an unsaved copy the caller puts in place. */
1097
1118
  private applyFinalizedReversionOutcome(
1098
1119
  stored: IStoredSessionState,
1099
1120
  segment: IFlexReversionSegment,
@@ -1270,9 +1291,8 @@ export class FlexHarnessReversions<TScope> {
1270
1291
  const archivedIds = new Set(events.flatMap((event) => event.type === 'context-compaction' && event.automatic
1271
1292
  ? event.coveredEventIds.filter((id) => !activeIds.has(id)) : []));
1272
1293
  if (archivedIds.size === 0) return;
1273
- for (const segment of stored.reversionSegments) {
1274
- if (segment.eventIds.some((id) => archivedIds.has(id))) segment.contextAvailable = false;
1275
- }
1294
+ this.markReversionContextUnavailable(stored, (segment) =>
1295
+ segment.eventIds.some((id) => archivedIds.has(id)));
1276
1296
  this.commitArchivedHiddenBranchState(stored);
1277
1297
  this.pruneReversionState(stored, false, true);
1278
1298
  }
@@ -1394,7 +1414,12 @@ export class FlexHarnessReversions<TScope> {
1394
1414
  const persistDirect = async (next: IFlexProjectionSnapshotCurrent): Promise<void> => {
1395
1415
  const before = durableProjection;
1396
1416
  try {
1397
- await this.core.stores.projections.save(storageKey, sessionId, next, projection.revision);
1417
+ await this.core.stores.projections.save(
1418
+ storageKey,
1419
+ sessionId,
1420
+ createWholeProjectionChange(next),
1421
+ projection.revision,
1422
+ );
1398
1423
  durableProjection = next;
1399
1424
  projection = next;
1400
1425
  } catch (error) {
@@ -456,11 +456,8 @@ export class FlexHarnessSessions<TScope> {
456
456
  const archivedIds = new Set(archive.events.map((event) => event.id));
457
457
  await this.core.mutateProjection(state, stored, () => {
458
458
  branchCommitted = this.core.reversions.commitRevertedBranchState(stored);
459
- for (const segment of stored.reversionSegments) {
460
- if (segment.eventIds.some((eventId) => archivedIds.has(eventId))) {
461
- segment.contextAvailable = false;
462
- }
463
- }
459
+ this.core.reversions.markReversionContextUnavailable(stored, (segment) =>
460
+ segment.eventIds.some((eventId) => archivedIds.has(eventId)));
464
461
  this.core.reversions.pruneReversionState(stored);
465
462
  }, true);
466
463
  } catch (error) {