@modelprofile.com/flexharness 5.3.0 → 5.3.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/changelog.md +9 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +2 -0
- package/dist_ts/classes.flexharness.js +76 -19
- package/dist_ts/interfaces.d.ts +17 -2
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/utils.json.js +77 -2
- package/package.json +1 -1
- package/readme.hints.md +2 -2
- package/readme.md +13 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +87 -16
- package/ts/interfaces.ts +19 -1
- package/ts/utils.json.ts +116 -1
package/readme.md
CHANGED
|
@@ -250,11 +250,15 @@ Public writes use `{ actor: 'application' }`. Tool writes use `{ actor: 'agent',
|
|
|
250
250
|
|
|
251
251
|
`FLEX_PROJECT_MANAGEMENT_LIMITS` exports the hard UTF-8 and aggregate limits: goal 8 KiB, scratchpad 128 KiB, task content 8 KiB, task ID 512 bytes, title 2048 bytes, 512 tasks, and a 1 MiB serialized snapshot. The aggregate bound leaves room for worst-case JSON escaping of a controller-valid scratchpad. Loaded snapshots reject extra fields, duplicate IDs, invalid status/priority/timestamps, non-JSON data, wrong schema/revision, and every exceeded bound before use.
|
|
252
252
|
|
|
253
|
-
`IFlexProjectManagementStore` is exact per `(storageKey, sessionId)`: `load`, CAS `save`, CAS `tombstoneSession`, and `purgeNamespace` must not collapse multiple sessions or storage namespaces. `load()` returns `TFlexProjectManagementRecord | undefined` and receives
|
|
253
|
+
`IFlexProjectManagementStore` is exact per `(storageKey, sessionId)`: `load`, CAS `save`, CAS `tombstoneSession`, and `purgeNamespace` must not collapse multiple sessions or storage namespaces. `load()` returns `TFlexProjectManagementRecord | undefined` and receives an optional `IFlexProjectManagementSessionContext` as its third argument; `tombstoneSession()` receives the same optional context as its fifth argument. FlexHarness always supplies both, while existing two-argument loads, four-argument tombstones, and shorter store implementations remain compatible.
|
|
254
|
+
|
|
255
|
+
`IFlexProjectManagementSessionContext` contains `sessionGenerationId`, `sessionGenerationSequence`, and optional `subagent`. The atomic `IFlexSubagentProvenance` block contains `parentSessionId`, `parentSessionGenerationId`, `parentSessionGenerationSequence`, `originParentRunId`, `originParentToolCallId`, `agent`, and actual session `depth`. Both the context and its separately cloned nested block are frozen.
|
|
256
|
+
|
|
257
|
+
Same-generation live saves use normal revision CAS, and a same-generation tombstone permanently rejects later live saves. A higher `sessionGenerationSequence` with a different `sessionGenerationId` may replace only an older tombstone using expected revision `0`; it cannot replace a live record. This resets the PM revision for a recreated core session while stale saves and tombstones from older generations remain fenced. Deleting a recreated session that made no PM writes still replaces the prior-generation tombstone with a revision-1 tombstone for the new generation.
|
|
254
258
|
|
|
255
259
|
Every newly created core session exposes and persists a `sessionGenerationId` plus its monotonic `sessionGenerationSequence`. FlexHarness generates a strong random ID when `sessionGenerationId` is omitted. Applications may supply the ID to `createSession()` when they need to persist creation authority before dispatch; a supplied ID must be nonblank, contain no control characters, and fit within `FLEX_SESSION_GENERATION_ID_MAX_BYTES` (128 UTF-8 bytes). FlexHarness still assigns the sequence atomically. A legacy scope session without those fields is assigned a deterministic bounded ID derived from its immutable `storageKey`, `sessionId`, and `createdAt`; FlexHarness persists the repaired scope snapshot before accepting work. Grouped core deletion tombstones retain both fields after live metadata is removed.
|
|
256
260
|
|
|
257
|
-
Normal Flex session cleanup always waits in-flight local project operations, then loads and CAS-tombstones `stores.projectManagement`, regardless of whether PM tools are enabled in that harness. If a concurrent same-generation save wins first, cleanup reloads and retries
|
|
261
|
+
Normal Flex session cleanup always waits in-flight local project operations, then loads and CAS-tombstones `stores.projectManagement`, regardless of whether PM tools are enabled in that harness. Child cleanup persists one complete `IFlexSubagentProvenance` block on its core tombstone before live metadata is removed. One cleanup invocation reuses the identical doubly frozen context object across bounded CAS retries; restart or a later cleanup invocation reconstructs a new frozen context from the persisted provenance. If a concurrent same-generation save wins first, cleanup reloads and retries; unresolved conflict or store failure retains the core Flex session cleanup tombstone for a later retry. The durable PM tombstone is not physically removed during normal session cleanup.
|
|
258
262
|
|
|
259
263
|
`purgeNamespace(storageKey)` is the explicit destructive reclamation operation and physically removes every live record and tombstone in that exact PM namespace. Applications may call it only after serializing every scope alias, preventing new admission, awaiting `retireScope()` on every harness owner, and deleting or purging the application-owned core scope namespace. `retireScope()` itself remains non-destructive and never calls `purgeNamespace()`. Purging PM first, purging only one alias, or racing a stale harness can remove the fence that makes session-generation reuse safe.
|
|
260
264
|
|
|
@@ -275,7 +279,7 @@ interface IDelegateInput {
|
|
|
275
279
|
}
|
|
276
280
|
```
|
|
277
281
|
|
|
278
|
-
Before creating or resuming a child, FlexHarness requests permission on the parent run with `kind: 'subagent.start'`, the parent `toolCallId`, and bounded agent/task metadata. The controller answers
|
|
282
|
+
Before creating or resuming a child, FlexHarness requests permission on the parent run with `kind: 'subagent.start'`, the parent `toolCallId`, and separately bounded harness-owned agent/task metadata. Its metadata includes `childSessionId`, the exact ID reserved for this call: FlexHarness derives it deterministically when `taskId` is omitted and copies the supplied candidate when `taskId` is present. A resume also retains that candidate as `taskId`. This block is not truncated by `toolOutputLimits`. The reserved ID binds permission handling before child creation but does not prove that the child exists or is owned: applications must treat the later delegated admission context as authoritative. The controller answers through the normal permission APIs. This request has no `rememberKey`, so `always` is invalid; controllers use `once` or `reject`.
|
|
279
283
|
|
|
280
284
|
Each new invocation creates a durable child `IFlexSession` with immutable `parentSessionId`, origin `parentRunId`, origin `parentToolCallId`, `agent`, and `depth`. New public roots persist `depth: 0`; legacy schema-1 roots may omit it. These fields are harness-owned; public `createSession()` remains limited to `sessionId`, `sessionGenerationId`, and `title`. Child sessions reject direct `prompt()`, `startPrompt()`, `enqueuePrompt()`, and `schedulePrompt()` calls and run only through the foreground `delegate` tool. The model and tool resolver contexts receive optional immutable `parentSessionId` and `agent` values so integrations can apply agent-specific model and tool policy. Child prompts use the definition's `modelHint`, `system`, and `maxSteps`.
|
|
281
285
|
|
|
@@ -294,7 +298,7 @@ Omitting `taskId` creates a deterministic child for the parent session, run, and
|
|
|
294
298
|
|
|
295
299
|
Supplying `taskId` deliberately resumes an idle, live child from a later run of the same immutable parent session and the same configured agent. The caller must use the exact ID returned by an earlier completed delegate call; an unknown ID fails with safe corrective guidance and never creates a child under the supplied label. Resume starts a new child prompt while retaining the child's original parent run and tool-call origin. A child owned by another parent or agent, a deleted child, an active child, a same-run resume, or a second acquisition of the same child within one later parent run is rejected. Parent cancellation propagates only to the exact child run started by that delegate call.
|
|
296
300
|
|
|
297
|
-
`delegatedRunAdmissionProvider` optionally adds an application-owned admission lease around each internally delegated child run. The public contracts are `IFlexDelegatedRunAdmissionProvider<TScope>`, `IFlexDelegatedRunAdmissionContext<TScope>`, and `IFlexDelegatedRunAdmissionLease`. The provider is never called for root prompts or direct public prompt APIs. It receives a frozen context containing the resolved `scopeId`, exact captured `scope`, and `storageKey`; the exact child `sessionId`, session generation, queue, and run; the exact current parent session generation, queue, run, and delegate `toolCallId`; the child agent and depth; and the child run `AbortSignal`. For `taskId` resume,
|
|
301
|
+
`delegatedRunAdmissionProvider` optionally adds an application-owned admission lease around each internally delegated child run. The public contracts are `IFlexDelegatedRunAdmissionProvider<TScope>`, `IFlexDelegatedRunAdmissionContext<TScope>`, and `IFlexDelegatedRunAdmissionLease`. The provider is never called for root prompts or direct public prompt APIs. It receives a frozen context containing the resolved `scopeId`, exact captured `scope`, and `storageKey`; the exact child `sessionId`, session generation, queue, and run; the exact current parent session generation, queue, run, and delegate `toolCallId`; immutable `originParentRunId` and `originParentToolCallId`; the child agent and depth; and the child run `AbortSignal`. For `taskId` resume, the current parent queue/run/tool-call fields identify this delegate invocation, while the origin fields remain fixed to the invocation that created the durable child.
|
|
298
302
|
|
|
299
303
|
```typescript
|
|
300
304
|
delegatedRunAdmissionProvider: {
|
|
@@ -314,6 +318,8 @@ delegatedRunAdmissionProvider: {
|
|
|
314
318
|
queueId: context.parentQueueId,
|
|
315
319
|
runId: context.parentRunId,
|
|
316
320
|
toolCallId: context.parentToolCallId,
|
|
321
|
+
originRunId: context.originParentRunId,
|
|
322
|
+
originToolCallId: context.originParentToolCallId,
|
|
317
323
|
},
|
|
318
324
|
signal: context.signal,
|
|
319
325
|
});
|
|
@@ -328,7 +334,9 @@ Acquisition completes before generation-side branch reversion, context compactio
|
|
|
328
334
|
|
|
329
335
|
For a normally acquired lease, FlexHarness gives `close()` an awaited attempt after SmartAgent generation and before canonical accepted, rejected, or interrupted finalization and the terminal `prompt.finished` event. If an abort detaches an acquisition that ignores its signal, the run may settle before acquisition returns; FlexHarness retains that owner and closes any late lease. `close()` must be idempotent and safe to retry after rejection. A close failure prevents successful child acceptance and remains owned by the exact child session generation for retry by later exact-session deletion, scope retirement, or disposal. Retirement and disposal truthfully wait for late acquisition and lease cleanup.
|
|
330
336
|
|
|
331
|
-
Limits are validated and frozen at construction: at most 32 unique definitions; names are non-empty and at most 128 UTF-8 bytes; descriptions 2048 bytes; optional model hints 512 bytes; optional system prompts 64 KiB; and optional `maxSteps` a positive safe integer. `maxSubagentDepth` defaults to 1 and must be a positive safe integer at most 8. `maxSubagentCallsPerRun` defaults to 32 and must be a positive safe integer at most 128. A call slot is consumed synchronously at the start of every schema-valid delegate execution, before semantic bounds, subagent type/depth validation, permission, or child work. Inputs rejected by the tool schema never start delegate execution and do not consume a slot. After successful semantic validation, the child ID is reserved for the rest of the parent run, including after permission rejection or later failure. Permission rejection creates no child session. Omitting `taskId` reserves a deterministic new child ID; supplying `taskId` reserves
|
|
337
|
+
Limits are validated and frozen at construction: at most 32 unique definitions; names are non-empty and at most 128 UTF-8 bytes; descriptions 2048 bytes; optional model hints 512 bytes; optional system prompts 64 KiB; and optional `maxSteps` a positive safe integer. `maxSubagentDepth` defaults to 1 and must be a positive safe integer at most 8. `maxSubagentCallsPerRun` defaults to 32 and must be a positive safe integer at most 128. A call slot is consumed synchronously at the start of every schema-valid delegate execution, before semantic bounds, subagent type/depth validation, permission, or child work. Inputs rejected by the tool schema never start delegate execution and do not consume a slot. After successful semantic validation, the child ID candidate is reserved for the rest of the parent run, including after permission rejection or later failure. Permission rejection creates no child session. Omitting `taskId` reserves a deterministic new child ID; supplying `taskId` reserves that unverified candidate and attempts resume after permission only if it identifies a resumable child. Delegate descriptions are non-empty and at most 256 UTF-8 bytes, prompts non-empty and at most 64 KiB, subagent types at most 128 bytes, and task IDs at most 512 bytes.
|
|
338
|
+
|
|
339
|
+
Scope snapshots remain schema 1 and legacy child tombstones without `subagent` provenance continue to load. New child tombstones write the atomic provenance block and reject partial, parent-generation-mismatched, depth-mismatched, or duplicate-origin records. FlexHarness versions before this provenance addition reject that new optional key under their strict reader, so downgrading or mixing old readers with newly written scope snapshots is unsupported.
|
|
332
340
|
|
|
333
341
|
## Sessions And Prompts
|
|
334
342
|
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@modelprofile.com/flexharness',
|
|
6
|
-
version: '5.3.
|
|
6
|
+
version: '5.3.1',
|
|
7
7
|
description: 'Provider-neutral model-session runtime with durable history, permissions, typed events, and pluggable local or remote tool execution.'
|
|
8
8
|
}
|
|
@@ -51,6 +51,7 @@ import type {
|
|
|
51
51
|
IFlexPermissionRequestInput,
|
|
52
52
|
IFlexPermissionSnapshot,
|
|
53
53
|
IFlexProjectGoalResult,
|
|
54
|
+
IFlexProjectManagementSessionContext,
|
|
54
55
|
IFlexProjectManagementSnapshot,
|
|
55
56
|
IFlexProjectManagementTombstone,
|
|
56
57
|
IFlexProjectManagementWriteContext,
|
|
@@ -87,6 +88,7 @@ import type {
|
|
|
87
88
|
IFlexSlashCommandExecutionOptions,
|
|
88
89
|
IFlexSlashCommandHandlerRegistration,
|
|
89
90
|
IFlexSubagentDefinition,
|
|
91
|
+
IFlexSubagentProvenance,
|
|
90
92
|
IFlexTerminalProjection,
|
|
91
93
|
IFlexToolHandle,
|
|
92
94
|
IFlexToolMessagePart,
|
|
@@ -545,6 +547,7 @@ const maxBackgroundExecutions = 100;
|
|
|
545
547
|
const maxSubagentTaskDescriptionBytes = 256;
|
|
546
548
|
const maxSubagentPromptBytes = 64 * 1024;
|
|
547
549
|
const maxSubagentTaskIdBytes = 512;
|
|
550
|
+
const maxSubagentPermissionMetadataBytes = 16 * 1024;
|
|
548
551
|
const maxSubagentResultTextBytes = 64 * 1024;
|
|
549
552
|
const defaultMaxSubagentDepth = 1;
|
|
550
553
|
const maximumMaxSubagentDepth = 8;
|
|
@@ -1380,8 +1383,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1380
1383
|
const rootDepth = stored.session.depth ?? 0;
|
|
1381
1384
|
const deletedAt = new Date().toISOString();
|
|
1382
1385
|
const deletedSessions = subtree.map((entry) => cloneSerializable(entry.session));
|
|
1386
|
+
const provenanceBySessionId = new Map(subtree.map((entry) => [
|
|
1387
|
+
entry.session.sessionId,
|
|
1388
|
+
this.createSubagentProvenance(state, entry.session),
|
|
1389
|
+
]));
|
|
1383
1390
|
for (const entry of subtree) {
|
|
1384
1391
|
const entrySessionId = entry.session.sessionId;
|
|
1392
|
+
const subagent = provenanceBySessionId.get(entrySessionId);
|
|
1385
1393
|
state.sessions.delete(entrySessionId);
|
|
1386
1394
|
state.retainedSessionCleanups.set(entrySessionId, {
|
|
1387
1395
|
stored: entry,
|
|
@@ -1396,6 +1404,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1396
1404
|
...(entry.session.parentSessionId === undefined
|
|
1397
1405
|
? {}
|
|
1398
1406
|
: { parentSessionId: entry.session.parentSessionId }),
|
|
1407
|
+
...(subagent === undefined ? {} : { subagent: cloneSerializable(subagent) }),
|
|
1399
1408
|
});
|
|
1400
1409
|
}
|
|
1401
1410
|
return {
|
|
@@ -3491,6 +3500,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3491
3500
|
parentQueueId: seed.parentQueueId,
|
|
3492
3501
|
parentRunId: seed.parentRunId,
|
|
3493
3502
|
parentToolCallId: seed.parentToolCallId,
|
|
3503
|
+
originParentRunId: seed.originParentRunId,
|
|
3504
|
+
originParentToolCallId: seed.originParentToolCallId,
|
|
3494
3505
|
agent: seed.agent,
|
|
3495
3506
|
depth: seed.depth,
|
|
3496
3507
|
signal: run.controller.signal,
|
|
@@ -4173,10 +4184,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4173
4184
|
): Promise<IFlexProjectManagementSnapshot> {
|
|
4174
4185
|
const store = this.stores.projectManagement;
|
|
4175
4186
|
const generation = requireSessionGeneration(stored.session);
|
|
4187
|
+
const sessionContext = this.createProjectManagementSessionContext(state, stored.session);
|
|
4176
4188
|
const record = await store.load(
|
|
4177
4189
|
state.storageKey,
|
|
4178
4190
|
stored.session.sessionId,
|
|
4179
|
-
|
|
4191
|
+
sessionContext,
|
|
4180
4192
|
);
|
|
4181
4193
|
if (record === undefined) {
|
|
4182
4194
|
return createEmptyFlexProjectManagementSnapshot(
|
|
@@ -4269,6 +4281,52 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4269
4281
|
return publicSnapshot({ revision: snapshot.revision, state: snapshot });
|
|
4270
4282
|
}
|
|
4271
4283
|
|
|
4284
|
+
private createSubagentProvenance(
|
|
4285
|
+
state: IStorageState,
|
|
4286
|
+
session: Readonly<IFlexSession>,
|
|
4287
|
+
): Readonly<IFlexSubagentProvenance> | undefined {
|
|
4288
|
+
if (session.parentSessionId === undefined) return undefined;
|
|
4289
|
+
if (
|
|
4290
|
+
session.parentRunId === undefined
|
|
4291
|
+
|| session.parentToolCallId === undefined
|
|
4292
|
+
|| session.agent === undefined
|
|
4293
|
+
|| session.depth === undefined
|
|
4294
|
+
) throw new FlexHarnessValidationError('Subagent session provenance is incomplete.');
|
|
4295
|
+
const parent = state.sessions.get(session.parentSessionId)?.session
|
|
4296
|
+
?? state.tombstones.get(session.parentSessionId);
|
|
4297
|
+
if (!parent) {
|
|
4298
|
+
throw new FlexHarnessValidationError(
|
|
4299
|
+
`Subagent session "${session.sessionId}" has no exact parent provenance.`,
|
|
4300
|
+
);
|
|
4301
|
+
}
|
|
4302
|
+
const parentGeneration = requireSessionGeneration(parent);
|
|
4303
|
+
return Object.freeze({
|
|
4304
|
+
parentSessionId: session.parentSessionId,
|
|
4305
|
+
parentSessionGenerationId: parentGeneration.sessionGenerationId,
|
|
4306
|
+
parentSessionGenerationSequence: parentGeneration.sessionGenerationSequence,
|
|
4307
|
+
originParentRunId: session.parentRunId,
|
|
4308
|
+
originParentToolCallId: session.parentToolCallId,
|
|
4309
|
+
agent: session.agent,
|
|
4310
|
+
depth: session.depth,
|
|
4311
|
+
});
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
private createProjectManagementSessionContext(
|
|
4315
|
+
state: IStorageState,
|
|
4316
|
+
session: Readonly<IFlexSession | IFlexSessionTombstone>,
|
|
4317
|
+
): Readonly<IFlexProjectManagementSessionContext> {
|
|
4318
|
+
const source = 'deletedAt' in session
|
|
4319
|
+
? session.subagent
|
|
4320
|
+
: this.createSubagentProvenance(state, session);
|
|
4321
|
+
const subagent = source === undefined
|
|
4322
|
+
? undefined
|
|
4323
|
+
: Object.freeze({ ...cloneSerializable(source) });
|
|
4324
|
+
return Object.freeze({
|
|
4325
|
+
...requireSessionGeneration(session),
|
|
4326
|
+
...(subagent === undefined ? {} : { subagent }),
|
|
4327
|
+
});
|
|
4328
|
+
}
|
|
4329
|
+
|
|
4272
4330
|
private projectTaskResult(
|
|
4273
4331
|
snapshot: IFlexProjectManagementSnapshot,
|
|
4274
4332
|
task: IFlexProjectTask,
|
|
@@ -5046,16 +5104,20 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5046
5104
|
run.controller.signal.addEventListener('abort', abortChild, { once: true });
|
|
5047
5105
|
if (run.controller.signal.aborted) abortChild();
|
|
5048
5106
|
try {
|
|
5107
|
+
const permissionMetadata = {
|
|
5108
|
+
agent: definition.name,
|
|
5109
|
+
description: input.description,
|
|
5110
|
+
childSessionId: reservedSessionId,
|
|
5111
|
+
...(input.taskId === undefined ? {} : { taskId: input.taskId }),
|
|
5112
|
+
};
|
|
5113
|
+
if (jsonBytes(permissionMetadata) > maxSubagentPermissionMetadataBytes) {
|
|
5114
|
+
throw new FlexHarnessValidationError('Subagent permission metadata exceeds its byte limit.');
|
|
5115
|
+
}
|
|
5049
5116
|
await this.requestPermission(run.state, run, {
|
|
5050
5117
|
kind: 'subagent.start',
|
|
5051
5118
|
description: `Start foreground subagent "${definition.name}": ${input.description}`,
|
|
5052
5119
|
toolCallId,
|
|
5053
|
-
|
|
5054
|
-
agent: definition.name,
|
|
5055
|
-
description: input.description,
|
|
5056
|
-
...(input.taskId === undefined ? {} : { taskId: input.taskId }),
|
|
5057
|
-
},
|
|
5058
|
-
});
|
|
5120
|
+
}, Object.freeze(permissionMetadata));
|
|
5059
5121
|
const acquired = await this.acquireSubagentSession(
|
|
5060
5122
|
run,
|
|
5061
5123
|
definition,
|
|
@@ -5343,6 +5405,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5343
5405
|
) throw projected;
|
|
5344
5406
|
if (state.tombstones.has(sessionId)) throw projected;
|
|
5345
5407
|
try {
|
|
5408
|
+
const subagent = this.createSubagentProvenance(state, metadata);
|
|
5346
5409
|
await this.mutateScope(state, () => {
|
|
5347
5410
|
if (state.sessions.get(sessionId) !== placeholder) return;
|
|
5348
5411
|
state.sessions.delete(sessionId);
|
|
@@ -5353,6 +5416,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
5353
5416
|
rootSessionId: sessionId,
|
|
5354
5417
|
depth: 0,
|
|
5355
5418
|
parentSessionId: run.sessionId,
|
|
5419
|
+
...(subagent === undefined ? {} : { subagent: cloneSerializable(subagent) }),
|
|
5356
5420
|
});
|
|
5357
5421
|
}, true);
|
|
5358
5422
|
completeInitialization();
|
|
@@ -6920,6 +6984,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6920
6984
|
state: IStorageState,
|
|
6921
6985
|
run: IActiveRun,
|
|
6922
6986
|
input: IFlexPermissionRequestInput,
|
|
6987
|
+
exactMetadata?: TJsonValue,
|
|
6923
6988
|
): Promise<void> {
|
|
6924
6989
|
validateIdentifier(input.kind, 'permission kind');
|
|
6925
6990
|
validateIdentifier(input.description, 'permission description');
|
|
@@ -6942,9 +7007,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6942
7007
|
description: truncateUtf8(input.description, maxTransferMetadataBytes),
|
|
6943
7008
|
...(input.toolCallId ? { toolCallId: input.toolCallId } : {}),
|
|
6944
7009
|
...(input.rememberKey ? { rememberKey: input.rememberKey } : {}),
|
|
6945
|
-
...(
|
|
6946
|
-
? {}
|
|
6947
|
-
:
|
|
7010
|
+
...(exactMetadata !== undefined
|
|
7011
|
+
? { metadata: cloneSerializable(exactMetadata) }
|
|
7012
|
+
: input.metadata === undefined
|
|
7013
|
+
? {}
|
|
7014
|
+
: { metadata: normalizeJsonValue(input.metadata, this.toolOutputLimits) }),
|
|
6948
7015
|
createdAt: new Date().toISOString(),
|
|
6949
7016
|
};
|
|
6950
7017
|
let resolvePermission!: () => void;
|
|
@@ -7785,7 +7852,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7785
7852
|
scopeId,
|
|
7786
7853
|
scope,
|
|
7787
7854
|
);
|
|
7788
|
-
await this.cleanupSessionDomains(
|
|
7855
|
+
await this.cleanupSessionDomains(state, tombstone);
|
|
7789
7856
|
}
|
|
7790
7857
|
for (const tombstone of group) state.tombstones.delete(tombstone.sessionId);
|
|
7791
7858
|
scopeChanged = true;
|
|
@@ -8766,16 +8833,17 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8766
8833
|
}
|
|
8767
8834
|
|
|
8768
8835
|
private async cleanupSessionDomains(
|
|
8769
|
-
|
|
8836
|
+
state: IStorageState,
|
|
8770
8837
|
tombstone: IFlexSessionTombstone,
|
|
8771
8838
|
): Promise<void> {
|
|
8839
|
+
const storageKey = state.storageKey;
|
|
8772
8840
|
const sessionId = tombstone.sessionId;
|
|
8773
8841
|
const projectManagementCleanup = async (): Promise<void> => {
|
|
8774
8842
|
await this.projectManagementQueues.get(
|
|
8775
8843
|
this.projectManagementKey(storageKey, sessionId),
|
|
8776
8844
|
);
|
|
8777
8845
|
await this.tombstoneProjectManagementSession(
|
|
8778
|
-
|
|
8846
|
+
state,
|
|
8779
8847
|
tombstone,
|
|
8780
8848
|
);
|
|
8781
8849
|
};
|
|
@@ -8793,18 +8861,20 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8793
8861
|
}
|
|
8794
8862
|
|
|
8795
8863
|
private async tombstoneProjectManagementSession(
|
|
8796
|
-
|
|
8864
|
+
state: IStorageState,
|
|
8797
8865
|
sessionTombstone: IFlexSessionTombstone,
|
|
8798
8866
|
): Promise<void> {
|
|
8867
|
+
const storageKey = state.storageKey;
|
|
8799
8868
|
const store = this.stores.projectManagement;
|
|
8800
8869
|
const sessionId = sessionTombstone.sessionId;
|
|
8801
8870
|
const generation = requireSessionGeneration(sessionTombstone);
|
|
8871
|
+
const sessionContext = this.createProjectManagementSessionContext(state, sessionTombstone);
|
|
8802
8872
|
let lastConflict: FlexHarnessStoreConflictError | undefined;
|
|
8803
8873
|
for (let attempt = 0; attempt <= maxProjectManagementTombstoneConflicts; attempt++) {
|
|
8804
8874
|
const current = await store.load(
|
|
8805
8875
|
storageKey,
|
|
8806
8876
|
sessionId,
|
|
8807
|
-
|
|
8877
|
+
sessionContext,
|
|
8808
8878
|
);
|
|
8809
8879
|
if (current !== undefined) {
|
|
8810
8880
|
assertFlexProjectManagementRecord(current);
|
|
@@ -8845,6 +8915,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8845
8915
|
sessionId,
|
|
8846
8916
|
tombstone,
|
|
8847
8917
|
expectedRevision,
|
|
8918
|
+
sessionContext,
|
|
8848
8919
|
);
|
|
8849
8920
|
} catch (error) {
|
|
8850
8921
|
if (!(error instanceof FlexHarnessStoreConflictError)) throw error;
|
|
@@ -9062,7 +9133,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9062
9133
|
releaseContext.scope,
|
|
9063
9134
|
retained?.stored,
|
|
9064
9135
|
);
|
|
9065
|
-
await this.cleanupSessionDomains(state
|
|
9136
|
+
await this.cleanupSessionDomains(state, tombstone);
|
|
9066
9137
|
if (retained) retained.domainsCompleted = true;
|
|
9067
9138
|
}
|
|
9068
9139
|
}
|
package/ts/interfaces.ts
CHANGED
|
@@ -441,6 +441,7 @@ export interface IFlexSessionTombstone {
|
|
|
441
441
|
rootSessionId?: string;
|
|
442
442
|
depth?: number;
|
|
443
443
|
parentSessionId?: string;
|
|
444
|
+
subagent?: IFlexSubagentProvenance;
|
|
444
445
|
}
|
|
445
446
|
|
|
446
447
|
export interface IFlexScopeSnapshot {
|
|
@@ -582,6 +583,16 @@ export interface IFlexSessionGeneration {
|
|
|
582
583
|
sessionGenerationSequence: number;
|
|
583
584
|
}
|
|
584
585
|
|
|
586
|
+
export interface IFlexSubagentProvenance {
|
|
587
|
+
readonly parentSessionId: string;
|
|
588
|
+
readonly parentSessionGenerationId: string;
|
|
589
|
+
readonly parentSessionGenerationSequence: number;
|
|
590
|
+
readonly originParentRunId: string;
|
|
591
|
+
readonly originParentToolCallId: string;
|
|
592
|
+
readonly agent: string;
|
|
593
|
+
readonly depth: number;
|
|
594
|
+
}
|
|
595
|
+
|
|
585
596
|
export interface IFlexSessionGenerationCohortEntry extends IFlexSessionGeneration {
|
|
586
597
|
sessionId: string;
|
|
587
598
|
}
|
|
@@ -649,11 +660,15 @@ export interface IFlexProjectManagementWriteContext {
|
|
|
649
660
|
toolCallId?: string;
|
|
650
661
|
}
|
|
651
662
|
|
|
663
|
+
export interface IFlexProjectManagementSessionContext extends IFlexSessionGeneration {
|
|
664
|
+
readonly subagent?: Readonly<IFlexSubagentProvenance>;
|
|
665
|
+
}
|
|
666
|
+
|
|
652
667
|
export interface IFlexProjectManagementStore {
|
|
653
668
|
load(
|
|
654
669
|
storageKey: string,
|
|
655
670
|
sessionId: string,
|
|
656
|
-
|
|
671
|
+
sessionContext?: Readonly<IFlexProjectManagementSessionContext>,
|
|
657
672
|
): Promise<TFlexProjectManagementRecord | undefined>;
|
|
658
673
|
save(
|
|
659
674
|
storageKey: string,
|
|
@@ -667,6 +682,7 @@ export interface IFlexProjectManagementStore {
|
|
|
667
682
|
sessionId: string,
|
|
668
683
|
tombstone: IFlexProjectManagementTombstone,
|
|
669
684
|
expectedRevision: number,
|
|
685
|
+
sessionContext?: Readonly<IFlexProjectManagementSessionContext>,
|
|
670
686
|
): Promise<void>;
|
|
671
687
|
purgeNamespace(storageKey: string): Promise<void>;
|
|
672
688
|
}
|
|
@@ -980,6 +996,8 @@ export interface IFlexDelegatedRunAdmissionContext<TScope> extends IFlexSessionG
|
|
|
980
996
|
readonly parentQueueId: string;
|
|
981
997
|
readonly parentRunId: string;
|
|
982
998
|
readonly parentToolCallId: string;
|
|
999
|
+
readonly originParentRunId: string;
|
|
1000
|
+
readonly originParentToolCallId: string;
|
|
983
1001
|
readonly agent: string;
|
|
984
1002
|
readonly depth: number;
|
|
985
1003
|
readonly signal: AbortSignal;
|
package/ts/utils.json.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
IFlexProjectionSnapshotV2,
|
|
16
16
|
IFlexProjectionSnapshotV3,
|
|
17
17
|
IFlexScopeSnapshot,
|
|
18
|
+
IFlexSubagentProvenance,
|
|
18
19
|
TFlexAgentModelMessage,
|
|
19
20
|
TJsonValue,
|
|
20
21
|
} from './interfaces.js';
|
|
@@ -546,6 +547,68 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
|
|
|
546
547
|
};
|
|
547
548
|
}
|
|
548
549
|
|
|
550
|
+
function validateSubagentProvenance(
|
|
551
|
+
value: unknown,
|
|
552
|
+
path: string,
|
|
553
|
+
): IFlexSubagentProvenance {
|
|
554
|
+
const provenance = requireRecord(value, path);
|
|
555
|
+
requireOnlyKeys(provenance, [
|
|
556
|
+
'parentSessionId',
|
|
557
|
+
'parentSessionGenerationId',
|
|
558
|
+
'parentSessionGenerationSequence',
|
|
559
|
+
'originParentRunId',
|
|
560
|
+
'originParentToolCallId',
|
|
561
|
+
'agent',
|
|
562
|
+
'depth',
|
|
563
|
+
], path);
|
|
564
|
+
const parentSessionId = requireString(
|
|
565
|
+
provenance.parentSessionId,
|
|
566
|
+
`${path}.parentSessionId`,
|
|
567
|
+
);
|
|
568
|
+
const parentSessionGenerationId = requireString(
|
|
569
|
+
provenance.parentSessionGenerationId,
|
|
570
|
+
`${path}.parentSessionGenerationId`,
|
|
571
|
+
);
|
|
572
|
+
if (
|
|
573
|
+
Buffer.byteLength(parentSessionGenerationId, 'utf8')
|
|
574
|
+
> FLEX_SESSION_GENERATION_ID_MAX_BYTES
|
|
575
|
+
) {
|
|
576
|
+
throw new FlexHarnessStoreFormatError(
|
|
577
|
+
`${path}.parentSessionGenerationId exceeds ${FLEX_SESSION_GENERATION_ID_MAX_BYTES} UTF-8 bytes.`,
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
if (
|
|
581
|
+
!Number.isSafeInteger(provenance.parentSessionGenerationSequence)
|
|
582
|
+
|| Number(provenance.parentSessionGenerationSequence) < 1
|
|
583
|
+
) {
|
|
584
|
+
throw new FlexHarnessStoreFormatError(
|
|
585
|
+
`${path}.parentSessionGenerationSequence must be a positive integer.`,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
const originParentRunId = requireString(
|
|
589
|
+
provenance.originParentRunId,
|
|
590
|
+
`${path}.originParentRunId`,
|
|
591
|
+
);
|
|
592
|
+
const originParentToolCallId = requireString(
|
|
593
|
+
provenance.originParentToolCallId,
|
|
594
|
+
`${path}.originParentToolCallId`,
|
|
595
|
+
);
|
|
596
|
+
const agent = requireString(provenance.agent, `${path}.agent`);
|
|
597
|
+
requireNonNegativeInteger(provenance.depth, `${path}.depth`);
|
|
598
|
+
if (provenance.depth === 0) {
|
|
599
|
+
throw new FlexHarnessStoreFormatError(`${path}.depth must be positive.`);
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
parentSessionId,
|
|
603
|
+
parentSessionGenerationId,
|
|
604
|
+
parentSessionGenerationSequence: provenance.parentSessionGenerationSequence as number,
|
|
605
|
+
originParentRunId,
|
|
606
|
+
originParentToolCallId,
|
|
607
|
+
agent,
|
|
608
|
+
depth: provenance.depth as number,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
549
612
|
function validateModel(value: unknown, path: string): void {
|
|
550
613
|
const model = requireRecord(value, path);
|
|
551
614
|
requireOnlyKeys(model, ['provider', 'model', 'displayName', 'variant'], path);
|
|
@@ -811,9 +874,12 @@ export function assertFlexScopeSnapshot(value: unknown): asserts value is IFlexS
|
|
|
811
874
|
const tombstoneIds = new Set<string>();
|
|
812
875
|
const tombstones = new Map<string, {
|
|
813
876
|
sessionId: string;
|
|
877
|
+
sessionGenerationId?: string;
|
|
878
|
+
sessionGenerationSequence?: number;
|
|
814
879
|
rootSessionId?: string;
|
|
815
880
|
depth?: number;
|
|
816
881
|
parentSessionId?: string;
|
|
882
|
+
subagent?: IFlexSubagentProvenance;
|
|
817
883
|
}>();
|
|
818
884
|
for (let index = 0; index < snapshot.tombstones.length; index++) {
|
|
819
885
|
const path = `$snapshot.tombstones[${index}]`;
|
|
@@ -828,11 +894,12 @@ export function assertFlexScopeSnapshot(value: unknown): asserts value is IFlexS
|
|
|
828
894
|
'rootSessionId',
|
|
829
895
|
'depth',
|
|
830
896
|
'parentSessionId',
|
|
897
|
+
'subagent',
|
|
831
898
|
],
|
|
832
899
|
path,
|
|
833
900
|
);
|
|
834
901
|
const sessionId = requireString(tombstone.sessionId, `${path}.sessionId`);
|
|
835
|
-
validateOptionalSessionGeneration(tombstone, path);
|
|
902
|
+
const generation = validateOptionalSessionGeneration(tombstone, path);
|
|
836
903
|
requireString(tombstone.deletedAt, `${path}.deletedAt`);
|
|
837
904
|
const parentSessionId = tombstone.parentSessionId === undefined
|
|
838
905
|
? undefined
|
|
@@ -840,6 +907,19 @@ export function assertFlexScopeSnapshot(value: unknown): asserts value is IFlexS
|
|
|
840
907
|
if (parentSessionId === sessionId) {
|
|
841
908
|
throw new FlexHarnessStoreFormatError(`${path} cannot be its own parent.`);
|
|
842
909
|
}
|
|
910
|
+
const subagent = tombstone.subagent === undefined
|
|
911
|
+
? undefined
|
|
912
|
+
: validateSubagentProvenance(tombstone.subagent, `${path}.subagent`);
|
|
913
|
+
if (subagent && generation.sessionGenerationId === undefined) {
|
|
914
|
+
throw new FlexHarnessStoreFormatError(
|
|
915
|
+
`${path}.subagent requires exact session generation fields.`,
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
if (subagent && subagent.parentSessionId !== parentSessionId) {
|
|
919
|
+
throw new FlexHarnessStoreFormatError(
|
|
920
|
+
`${path}.subagent does not match its parentSessionId.`,
|
|
921
|
+
);
|
|
922
|
+
}
|
|
843
923
|
const hasRoot = tombstone.rootSessionId !== undefined;
|
|
844
924
|
const hasDepth = tombstone.depth !== undefined;
|
|
845
925
|
if (hasRoot !== hasDepth) {
|
|
@@ -864,11 +944,26 @@ export function assertFlexScopeSnapshot(value: unknown): asserts value is IFlexS
|
|
|
864
944
|
);
|
|
865
945
|
}
|
|
866
946
|
tombstoneIds.add(sessionId);
|
|
947
|
+
if (subagent) {
|
|
948
|
+
const origin = JSON.stringify([
|
|
949
|
+
subagent.parentSessionId,
|
|
950
|
+
subagent.originParentRunId,
|
|
951
|
+
subagent.originParentToolCallId,
|
|
952
|
+
]);
|
|
953
|
+
if (origins.has(origin)) {
|
|
954
|
+
throw new FlexHarnessStoreFormatError(
|
|
955
|
+
'Snapshot contains duplicate subagent invocation origins.',
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
origins.add(origin);
|
|
959
|
+
}
|
|
867
960
|
tombstones.set(sessionId, {
|
|
868
961
|
sessionId,
|
|
962
|
+
...generation,
|
|
869
963
|
...(hasRoot ? { rootSessionId: tombstone.rootSessionId as string } : {}),
|
|
870
964
|
...(hasDepth ? { depth: tombstone.depth as number } : {}),
|
|
871
965
|
...(parentSessionId === undefined ? {} : { parentSessionId }),
|
|
966
|
+
...(subagent === undefined ? {} : { subagent }),
|
|
872
967
|
});
|
|
873
968
|
}
|
|
874
969
|
for (const [index, tombstone] of snapshot.tombstones.entries()) {
|
|
@@ -894,6 +989,26 @@ export function assertFlexScopeSnapshot(value: unknown): asserts value is IFlexS
|
|
|
894
989
|
`Snapshot tombstone "${tombstone.sessionId}" has no parent "${tombstone.parentSessionId}".`,
|
|
895
990
|
);
|
|
896
991
|
}
|
|
992
|
+
if (tombstone.subagent) {
|
|
993
|
+
const parent = parentSession ?? parentTombstone!;
|
|
994
|
+
if (
|
|
995
|
+
parent.sessionGenerationId !== tombstone.subagent.parentSessionGenerationId
|
|
996
|
+
|| parent.sessionGenerationSequence
|
|
997
|
+
!== tombstone.subagent.parentSessionGenerationSequence
|
|
998
|
+
) {
|
|
999
|
+
throw new FlexHarnessStoreFormatError(
|
|
1000
|
+
`Snapshot tombstone "${tombstone.sessionId}" has inconsistent parent generation provenance.`,
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
const parentDepth = parent.parentSessionId === undefined
|
|
1004
|
+
? 0
|
|
1005
|
+
: parentSession?.depth ?? parentTombstone?.subagent?.depth;
|
|
1006
|
+
if (parentDepth === undefined || tombstone.subagent.depth !== parentDepth + 1) {
|
|
1007
|
+
throw new FlexHarnessStoreFormatError(
|
|
1008
|
+
`Snapshot tombstone "${tombstone.sessionId}" has inconsistent session depth provenance.`,
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
897
1012
|
if (tombstone.depth !== undefined && tombstone.depth > 0) {
|
|
898
1013
|
if (
|
|
899
1014
|
!parentTombstone
|