@modelprofile.com/flexharness 14.0.0 → 16.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 (51) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.flexharness.cachewarm.d.ts +2 -2
  3. package/dist_ts/classes.flexharness.cachewarm.js +1 -1
  4. package/dist_ts/classes.flexharness.core.d.ts +8 -1
  5. package/dist_ts/classes.flexharness.core.js +44 -31
  6. package/dist_ts/classes.flexharness.d.ts +5 -4
  7. package/dist_ts/classes.flexharness.js +10 -7
  8. package/dist_ts/classes.flexharness.prompts.d.ts +9 -5
  9. package/dist_ts/classes.flexharness.prompts.js +93 -18
  10. package/dist_ts/classes.flexharness.runs.d.ts +9 -4
  11. package/dist_ts/classes.flexharness.runs.js +70 -18
  12. package/dist_ts/classes.flexharness.sessions.d.ts +4 -5
  13. package/dist_ts/classes.flexharness.sessions.js +45 -12
  14. package/dist_ts/classes.flexharness.subagents.js +1 -1
  15. package/dist_ts/errors.d.ts +9 -0
  16. package/dist_ts/errors.js +14 -1
  17. package/dist_ts/index.d.ts +2 -0
  18. package/dist_ts/index.js +2 -1
  19. package/dist_ts/interfaces.d.ts +119 -11
  20. package/dist_ts/interfaces.flexharness.d.ts +12 -8
  21. package/dist_ts/interfaces.js +1 -1
  22. package/dist_ts/plugins.d.ts +3 -3
  23. package/dist_ts/plugins.js +3 -3
  24. package/dist_ts/utils.harnessoptions.js +6 -3
  25. package/dist_ts/utils.json.js +74 -18
  26. package/dist_ts/utils.sessionmetrics.d.ts +12 -1
  27. package/dist_ts/utils.sessionmetrics.js +18 -1
  28. package/dist_ts/utils.transfer.js +14 -1
  29. package/dist_ts/utils.validation.d.ts +7 -2
  30. package/dist_ts/utils.validation.js +28 -7
  31. package/package.json +3 -3
  32. package/readme.md +75 -6
  33. package/ts/00_commitinfo_data.ts +1 -1
  34. package/ts/classes.flexharness.cachewarm.ts +2 -2
  35. package/ts/classes.flexharness.core.ts +49 -30
  36. package/ts/classes.flexharness.prompts.ts +124 -39
  37. package/ts/classes.flexharness.runs.ts +78 -24
  38. package/ts/classes.flexharness.sessions.ts +54 -9
  39. package/ts/classes.flexharness.subagents.ts +2 -1
  40. package/ts/classes.flexharness.ts +25 -5
  41. package/ts/errors.ts +15 -0
  42. package/ts/index.ts +6 -0
  43. package/ts/interfaces.flexharness.ts +12 -8
  44. package/ts/interfaces.ts +125 -11
  45. package/ts/plugins.ts +13 -0
  46. package/ts/readme.md +75 -6
  47. package/ts/utils.harnessoptions.ts +9 -2
  48. package/ts/utils.json.ts +75 -18
  49. package/ts/utils.sessionmetrics.ts +23 -0
  50. package/ts/utils.transfer.ts +13 -0
  51. package/ts/utils.validation.ts +31 -5
package/ts/interfaces.ts CHANGED
@@ -92,8 +92,36 @@ export interface IFlexSessionMetricsState {
92
92
  reportedLifetimeCacheWriteTokens?: number;
93
93
  /** The breakdown counts every call since the session began, without saturation. */
94
94
  lifetimeTokenBreakdownComplete?: boolean;
95
+ /** The served context window of the last settled run's model; see `IFlexSessionMetrics`. */
96
+ contextWindow?: number;
97
+ /** The last settled run's compaction point; exclusive with `contextCompactionOff`. */
98
+ contextCompactionTokens?: number;
99
+ /** Why the last settled run had no compaction point; exclusive with `contextCompactionTokens`. */
100
+ contextCompactionOff?: TFlexContextCompactionOff;
95
101
  }
96
102
 
103
+ /**
104
+ * A session's choice of its proactive compaction point, in tokens of the model in use.
105
+ *
106
+ * - `{ tokens }`: a positive integer, clamped at run time to the ceiling of the run's model (90%
107
+ * of its served context window). Without a known served window it applies unclamped.
108
+ * - `{ percent }`: an integer from 1 through 90 of the served window. Without a known served
109
+ * window the run has no proactive compaction.
110
+ * - `false`: no proactive compaction.
111
+ *
112
+ * Omitted, the model's default applies: the catalog's automatic compaction point
113
+ * (`IFlexResolvedModel.contextCompactionDefaultTokens`) when stated, at most the ceiling,
114
+ * otherwise the ceiling itself.
115
+ */
116
+ export type TFlexContextCompactionSetting = plugins.TAgentContextCompactionSetting;
117
+
118
+ /**
119
+ * Why a run had no proactive compaction point: the setting is `false` (`disabled`), the run's
120
+ * model states no served context window and the setting needs one (`no-context-window`), or the
121
+ * harness has no `contextCompactor` (`no-compactor`).
122
+ */
123
+ export type TFlexContextCompactionOff = plugins.TAgentContextCompactionOff | 'no-compactor';
124
+
97
125
  /** No member estimates current context size or includes child-session / tool-owned model calls. */
98
126
  export interface IFlexSessionMetrics {
99
127
  /** Sum of durably settled per-call reports; a lower bound when incomplete. */
@@ -120,6 +148,19 @@ export interface IFlexSessionMetrics {
120
148
  * the session's calls were counted by input and cache since it began.
121
149
  */
122
150
  lifetimeTokenBreakdownComplete: boolean;
151
+ /**
152
+ * The served context window, in tokens, of the model the last settled run resolved; absent when
153
+ * that model states none or before the first run.
154
+ */
155
+ contextWindow?: number;
156
+ /**
157
+ * The proactive compaction point the last settled run used, in tokens. Exactly one of it and
158
+ * `contextCompactionOff` is present once a run settled; neither before. A changed session
159
+ * setting shows from the next run; `resolveAgentContextCompactionPoint` states it before.
160
+ */
161
+ contextCompactionTokens?: number;
162
+ /** Why the last settled run had no proactive compaction point. */
163
+ contextCompactionOff?: TFlexContextCompactionOff;
123
164
  }
124
165
 
125
166
  export interface IFlexModelIdentity {
@@ -222,6 +263,21 @@ export interface IFlexMessage {
222
263
  rejectedPermissionId?: string;
223
264
  /** On a user message that steered its running run: the caller's steer identity. */
224
265
  steerId?: string;
266
+ /** On the user message that heads a run: the caller's prompt identity, when the prompt named one. */
267
+ promptId?: string;
268
+ /**
269
+ * On a user message, once its run reached its terminal state: whether the run's result kept this
270
+ * message in the model context of later turns, the value the run's `keptInContext` reports. It
271
+ * records that outcome and is never rewritten afterwards: an undo is reported by `undone`, and a
272
+ * later compaction summarizes the message, which was still delivered. Absent on a message whose
273
+ * run has not ended, and on messages written before FlexHarness 15.0.0.
274
+ */
275
+ keptInContext?: boolean;
276
+ /**
277
+ * Set only by a read with `includeUndone`: the message belongs to an undone turn that a redo can
278
+ * bring back. Never stored.
279
+ */
280
+ undone?: true;
225
281
  /**
226
282
  * On both messages of a cache-warm turn: the harness sent the prompt only to keep the model's
227
283
  * prompt cache warm. Hosts leave these out of what they show and report as work.
@@ -268,6 +324,8 @@ export interface IFlexSession {
268
324
  depth?: number;
269
325
  /** Set only on a session the staged session import created; such a session is read-only. */
270
326
  origin?: IFlexSessionOrigin;
327
+ /** The session's compaction setting; absent for the harness default. */
328
+ contextCompaction?: TFlexContextCompactionSetting;
271
329
  }
272
330
 
273
331
  /** The position of an imported session when one source conversation was split into several sessions. */
@@ -394,14 +452,25 @@ export interface IFlexCreateSessionOptions {
394
452
  sessionGenerationId?: string;
395
453
  configurationRef?: string;
396
454
  title?: string;
455
+ /** The session's compaction setting; omitted for the harness default. */
456
+ contextCompaction?: TFlexContextCompactionSetting;
397
457
  }
398
458
 
399
459
  export interface IFlexUpdateSessionOptions {
400
460
  title?: string | null;
401
461
  archived?: boolean;
462
+ /** The session's compaction setting; `null` returns the session to the harness default. */
463
+ contextCompaction?: TFlexContextCompactionSetting | null;
402
464
  }
403
465
 
404
466
  export interface IFlexPromptOptions {
467
+ /**
468
+ * The caller's identity for this prompt, a non-empty string of at most 512 UTF-8 bytes. It is on
469
+ * the prompt's queue entry and on the user message that heads its run, so a caller can tell
470
+ * exactly whether the prompt was taken in. An outstanding prompt of the session holding the same
471
+ * id rejects the admission with `FlexHarnessDuplicatePromptError`.
472
+ */
473
+ promptId?: string;
405
474
  modelHint?: string;
406
475
  system?: string;
407
476
  /** Optional positive safe integer step cap; no limit unless configured. */
@@ -528,7 +597,11 @@ export interface IFlexPromptQueueEntry {
528
597
  status: TFlexPromptQueueStatus;
529
598
  queuedAt: string;
530
599
  runId?: string;
600
+ /** The prompt's `promptId`, when it named one. */
601
+ promptId?: string;
531
602
  scheduleKey?: string;
603
+ /** This entry answers existing context without adding a prompt message. */
604
+ continuation?: true;
532
605
  /** The entry is a cache-warm prompt; see {@link IFlexMessage.cacheWarm}. */
533
606
  cacheWarm?: true;
534
607
  startedAt?: string;
@@ -537,8 +610,9 @@ export interface IFlexPromptQueueEntry {
537
610
  /**
538
611
  * On a finished entry: whether its prompt, the steers its run took in and what its model
539
612
  * produced stay in the model context of later turns. True for a completed run and a run
540
- * stopped after its model produced output; false for a failed run, a run stopped before any
541
- * model output and a prompt that never ran. Send the prompt again to have it answered.
613
+ * stopped or failed after its model produced output; false for an unsuccessful run without
614
+ * model output and a prompt that never ran. For a continuation, this describes only the new
615
+ * run's history; it does not change the retention of the existing input.
542
616
  */
543
617
  keptInContext?: boolean;
544
618
  /**
@@ -549,10 +623,10 @@ export interface IFlexPromptQueueEntry {
549
623
  unappliedSteerIds?: string[];
550
624
  }
551
625
 
552
- export interface IFlexPromptResult {
626
+ /** The result of answering existing context, without adding a prompt message. */
627
+ export interface IFlexContinuePromptResult {
553
628
  runId: string;
554
629
  sessionId: string;
555
- userMessage: IFlexMessage;
556
630
  /** The run's last assistant message; a steered run completed earlier ones as it took steers in. */
557
631
  assistantMessage: IFlexMessage;
558
632
  model: IFlexModelIdentity;
@@ -563,6 +637,10 @@ export interface IFlexPromptResult {
563
637
  unappliedSteerIds: string[];
564
638
  }
565
639
 
640
+ export interface IFlexPromptResult extends IFlexContinuePromptResult {
641
+ userMessage: IFlexMessage;
642
+ }
643
+
566
644
  /** A message for the running run of a session, taken in at the run's next step boundary. */
567
645
  export interface IFlexSteerInput {
568
646
  /** Caller-chosen identity, unique among the steers of the run. */
@@ -576,12 +654,13 @@ export interface IFlexSteerAdmission {
576
654
  runId: string;
577
655
  }
578
656
 
579
- export interface IFlexPromptQueueAdmission {
657
+ export interface IFlexPromptQueueAdmission<TResult extends IFlexContinuePromptResult = IFlexPromptResult> {
580
658
  queueId: string;
581
- completion: Promise<IFlexPromptResult>;
659
+ completion: Promise<TResult>;
582
660
  }
583
661
 
584
- export interface IFlexPromptAdmission extends IFlexPromptQueueAdmission {
662
+ export interface IFlexPromptAdmission<TResult extends IFlexContinuePromptResult = IFlexPromptResult>
663
+ extends IFlexPromptQueueAdmission<TResult> {
585
664
  runId: string;
586
665
  }
587
666
 
@@ -602,11 +681,27 @@ export interface IFlexEnqueuePromptOptions extends IFlexPromptOptions {
602
681
  steers?: readonly IFlexSteerInput[];
603
682
  }
604
683
 
684
+ /** Options for continuation through the same FIFO, optionally with scheduled execution. */
685
+ export interface IFlexContinuePromptOptions extends Omit<IFlexEnqueuePromptOptions, 'promptId'> {
686
+ scheduleKey?: string;
687
+ /** Requires scheduleKey; defaults to 50 ms when scheduled. */
688
+ debounceMs?: number;
689
+ }
690
+
605
691
  export interface IFlexScheduledPromptAdmission extends IFlexPromptAdmission {
606
692
  scheduleKey: string;
607
693
  }
608
694
 
609
- export interface IFlexMessagePageOptions {
695
+ /** Options of the transcript reads. */
696
+ export interface IFlexMessageReadOptions {
697
+ /**
698
+ * Also return the messages of undone turns, each marked `undone: true`, in their place after the
699
+ * visible ones. Messages a committed branch removed are gone and never returned.
700
+ */
701
+ includeUndone?: boolean;
702
+ }
703
+
704
+ export interface IFlexMessagePageOptions extends IFlexMessageReadOptions {
610
705
  limit?: number;
611
706
  before?: string;
612
707
  }
@@ -669,6 +764,19 @@ export interface IFlexResolvedModel {
669
764
  cache?: TFlexAgentCacheSetting;
670
765
  /** Optional positive safe integer step cap; no limit unless configured. */
671
766
  maxSteps?: number;
767
+ /**
768
+ * The model's served context window in tokens, from the provider's catalog: the window the
769
+ * provider serves by default, never an extended maximum (for a Codex catalog `context_window`,
770
+ * not `max_context_window`; for Anthropic's Models API `max_input_tokens`). Omitted when the
771
+ * catalog states none: the run then has a compaction point only from a `{ tokens }` setting.
772
+ */
773
+ contextWindow?: number;
774
+ /**
775
+ * The catalog's own automatic compaction point in tokens, when it states one (a Codex catalog's
776
+ * `auto_compact_token_limit`). It lowers the default point, never the ceiling. Requires
777
+ * `contextWindow`.
778
+ */
779
+ contextCompactionDefaultTokens?: number;
672
780
  }
673
781
 
674
782
  export interface IFlexModelResolver<TScope> {
@@ -1167,7 +1275,8 @@ export interface IFlexScopeSnapshot {
1167
1275
  export interface IFlexTerminalProjection {
1168
1276
  runId: string;
1169
1277
  status: 'completed' | 'failed' | 'cancelled';
1170
- userMessage: IFlexMessage;
1278
+ /** Absent for a continuation, which creates no prompt message. */
1279
+ userMessage?: IFlexMessage;
1171
1280
  assistantMessage: IFlexMessage;
1172
1281
  model?: IFlexModelIdentity;
1173
1282
  /** The usage the provider reported for the run's model calls, whatever the status. */
@@ -1195,7 +1304,8 @@ export interface IFlexAffectedWorkspace {
1195
1304
 
1196
1305
  interface IFlexReversionSegmentV2 {
1197
1306
  runId: string;
1198
- userMessageId: string;
1307
+ /** Absent for an assistant-only continuation segment. */
1308
+ userMessageId?: string;
1199
1309
  status: TFlexReversionSegmentStatus;
1200
1310
  contextAvailable: boolean;
1201
1311
  eventIds: string[];
@@ -1891,7 +2001,11 @@ export type TFlexAgentContextCompactor<TScope = unknown> = (
1891
2001
  export interface IFlexAgentSessionPolicy<TScope = unknown> {
1892
2002
  contextBuilder?: plugins.TAgentContextBuilder;
1893
2003
  contextCompactor?: TFlexAgentContextCompactor<TScope>;
1894
- contextCompactionBytes?: plugins.IAgentSessionOptions['contextCompactionBytes'];
2004
+ /**
2005
+ * The harness default compaction setting, for sessions without their own. Omitted, the model's
2006
+ * default applies. Requires `contextCompactor` unless `false`.
2007
+ */
2008
+ contextCompaction?: TFlexContextCompactionSetting;
1895
2009
  eventRetention?: TFlexAgentSessionOptions['eventRetention'];
1896
2010
  changeListenerTimeoutMs?: number;
1897
2011
  maxPendingSessionChanges?: number;
package/ts/plugins.ts CHANGED
@@ -92,6 +92,7 @@ export const importCodeSandboxEngine = async (): Promise<ICodeSandboxEngine> =>
92
92
 
93
93
  // @push.rocks scope
94
94
  import {
95
+ AGENT_CONTEXT_COMPACTION_CEILING_PERCENT,
95
96
  AgentEventStoreConflictError,
96
97
  AgentPendingInputQueue,
97
98
  applyAgentEventChange,
@@ -100,13 +101,16 @@ import {
100
101
  AgentPendingInputRejectedError,
101
102
  AgentSession,
102
103
  AgentToolCallScheduler,
104
+ assertAgentContextCompactionSetting,
103
105
  buildModelMessages,
106
+ getAgentContextCompactionCeiling,
104
107
  ToolJobStoreConflictError,
105
108
  createAgentEvent,
106
109
  filterModelVisibleAgentEvents,
107
110
  getAgentGenerationTransactions,
108
111
  isGenerationInModelContext,
109
112
  modelMessagesToAgentEvents,
113
+ resolveAgentContextCompactionPoint,
110
114
  tool,
111
115
  validateAgentEventArchive,
112
116
  validateAgentEventSnapshot,
@@ -114,6 +118,7 @@ import {
114
118
  } from '@modelprofile.com/flexharness-agent';
115
119
 
116
120
  export {
121
+ AGENT_CONTEXT_COMPACTION_CEILING_PERCENT,
117
122
  AgentEventStoreConflictError,
118
123
  AgentPendingInputQueue,
119
124
  applyAgentEventChange,
@@ -122,13 +127,16 @@ export {
122
127
  AgentPendingInputRejectedError,
123
128
  AgentSession,
124
129
  AgentToolCallScheduler,
130
+ assertAgentContextCompactionSetting,
125
131
  buildModelMessages,
132
+ getAgentContextCompactionCeiling,
126
133
  ToolJobStoreConflictError,
127
134
  createAgentEvent,
128
135
  filterModelVisibleAgentEvents,
129
136
  getAgentGenerationTransactions,
130
137
  isGenerationInModelContext,
131
138
  modelMessagesToAgentEvents,
139
+ resolveAgentContextCompactionPoint,
132
140
  tool,
133
141
  validateAgentEventArchive,
134
142
  validateAgentEventSnapshot,
@@ -141,6 +149,7 @@ export type {
141
149
  TSmartAiCacheBudgetBasis,
142
150
  IAgentEventArchive,
143
151
  IAgentEventChange,
152
+ IAgentContextCompactionInput,
144
153
  IAgentContextCompactionOptions,
145
154
  IAgentEventSnapshot,
146
155
  IAgentEventStore,
@@ -150,6 +159,7 @@ export type {
150
159
  IAgentRetryEvent,
151
160
  IAgentRunResult,
152
161
  IAgentSession,
162
+ IAgentContinuationSession,
153
163
  IAgentSessionOptions,
154
164
  IAgentToolCallUpdateEvent,
155
165
  IAgentToolExecutionOptions,
@@ -165,6 +175,9 @@ export type {
165
175
  IToolJobStore,
166
176
  IToolPermissionRequest,
167
177
  TAgentContextBuilder,
178
+ TAgentContextCompactionOff,
179
+ TAgentContextCompactionPoint,
180
+ TAgentContextCompactionSetting,
168
181
  TAgentContextCompactor,
169
182
  TAgentEvent,
170
183
  TAgentGenerationOutcome,
package/ts/readme.md CHANGED
@@ -111,6 +111,9 @@ A warm read joins outstanding projection writes and surfaces accounting failures
111
111
  | `lastObservedInputTokensFreshness` | Always `historical`. This is **not** current context size: output, steering, compaction and reversion can change context. |
112
112
  | `reportedLifetimeInputTokens` | Sum of the settled per-call reports' input tokens, prompt-cache reads included. |
113
113
  | `reportedLifetimeCacheReadTokens`, `reportedLifetimeCacheWriteTokens` | Sums of the settled per-call reports' input tokens read from and written to the prompt cache. |
114
+ | `contextWindow` | The served context window, in tokens, of the model the last settled run resolved; absent when that model states none. |
115
+ | `contextCompactionTokens` | The compaction point the last settled run used, in tokens. |
116
+ | `contextCompactionOff` | Why the last settled run had no compaction point: `disabled`, `no-context-window` or `no-compactor`. Exactly one of it and `contextCompactionTokens` is present once a run settled. |
114
117
  | `lifetimeTokenBreakdownComplete` | The three sums cover every call `lifetimeUsedTokens` covers: lifetime usage is complete and the session's calls were counted by input and cache since it began. Otherwise they are lower bounds. |
115
118
 
116
119
  Accounting covers this session's generation calls and reported compaction calls,
@@ -495,7 +498,7 @@ Sending requires the exact active parent run, its currently delegated direct chi
495
498
 
496
499
  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`.
497
500
 
498
- 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()` accepts `sessionId`, `sessionGenerationId`, `configurationRef`, and `title`. A child inherits its parent's `configurationRef` and cannot resume under a different reference. Child sessions reject direct `prompt()`, `startPrompt()`, `enqueuePrompt()`, and `schedulePrompt()` calls and run only through the `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`.
501
+ 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()` accepts `sessionId`, `sessionGenerationId`, `configurationRef`, and `title`. A child inherits its parent's `configurationRef` and cannot resume under a different reference. Child sessions reject direct `continuePrompt()`, `prompt()`, `startPrompt()`, `enqueuePrompt()`, and `schedulePrompt()` calls and run only through the `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`.
499
502
 
500
503
  The parent tool part receives `childSessionId` as soon as the child is acquired, so a host can offer an authorized live transcript. A foreground call also adds the resolved child model to its part and returns bounded JSON:
501
504
 
@@ -706,7 +709,7 @@ Queue payloads, status records, and `prompt.*` queue events are process-local. T
706
709
 
707
710
  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.
708
711
 
709
- `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. Each page carries `startIndex`, the zero-based index of its first message in the session's visible history (for an empty page, the index the page ends at), so a consumer can order and page by absolute position. Appending messages leaves every index unchanged; a branch after an undo reuses the positions of the history it replaces, so a consumer re-reads its pages after `session.history.changed`. `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.
712
+ `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. Each page carries `startIndex`, the zero-based index of its first message in the session's visible history (for an empty page, the index the page ends at), so a consumer can order and page by absolute position. Appending messages leaves every index unchanged; a branch after an undo reuses the positions of the history it replaces, so a consumer re-reads its pages after `session.history.changed`. `getMessage()` performs an exact lookup. `getMessages()`, `listMessagePage()`, `getMessage()` and `getSessionView()` take `includeUndone: true` to also return the messages of undone turns, which a redo can bring back: they follow the visible messages in their place, each marked `undone: true`, and page indexes count them. `undone` is set only by such a read and never stored. Messages a committed branch removed are gone and no read returns them. 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.
710
713
 
711
714
  `updateSession()` supports title replacement, explicit title clearing with `null`, and archive state through `archived`. Title-only updates remain available while prompts are queued or running, while permission is pending, and after archival. Requests containing `archived` are rejected while the session has any outstanding prompt or pending permission; a mixed title-and-archive request is rejected atomically without changing the title. Archived sessions expose `archivedAt`. Every session exposes `preview` once a run with prompt text was admitted: its first prompt as one line (whitespace runs collapsed, cut at a character boundary to `maxTitleBytes`), with no model call, so a host can name a session nobody titled (`title || preview`). It is set once, a session stored before previews existed takes it from its first prompt at its next run, and no caller sets or clears it. Deleting a session cascades through its complete descendant subtree. One durable root-keyed tombstone group hides every newly affected live session, and the delete also joins any already-separate descendant cleanup groups without rewriting their roots. FlexHarness then cancels queued and active subtree work, emits terminal queue events, waits for admitted initialization, and purges runtime queue status while cleaning runtime and persisted domains child-first. The requested root tombstone is removed last after every domain confirms cleanup; project-management cleanup confirmation is a retained durable project tombstone rather than physical removal. An imported session can hold no project-management state, so its deletion writes no project tombstone. A successful live `deleteSession()` call emits `session.deleted` for each session it newly tombstoned; retries of an existing tombstone and automatic load, retirement, or disposal cleanup emit no deletion events. Direct deletion of a descendant cascades only through that descendant's subtree. Cleanup authority follows the resolved storage namespace, so scope aliases share the same groups. A partial failure retains durable ownership for retry by a later `deleteSession()`, namespace load, `retireScope()`, or `dispose()` call.
712
715
 
@@ -714,6 +717,55 @@ The reservation save is the admission point. A save failure produces no start ev
714
717
 
715
718
  `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.
716
719
 
720
+ ### Continuing Without New Input
721
+
722
+ Use `continuePrompt()` after a failed or stopped run whose input remains in context
723
+ (`keptInContext: true`), when the conversation still needs an answer:
724
+
725
+ ```typescript
726
+ const continuation = await harness.continuePrompt(scopeId, sessionId, {
727
+ modelHint: 'available-model',
728
+ });
729
+ console.log(continuation.queueId);
730
+ const result = await continuation.completion;
731
+ console.log(result.assistantMessage.parts);
732
+ ```
733
+
734
+ The signature is `continuePrompt(scopeId: string, sessionId: string,
735
+ options?: IFlexContinuePromptOptions): Promise<IFlexPromptQueueAdmission<IFlexContinuePromptResult>>`.
736
+ It accepts `modelHint`, `system`, `maxSteps` and initial `steers`, plus optional
737
+ `scheduleKey` and `debounceMs` for the same scheduling behavior as `schedulePrompt()`.
738
+ `debounceMs` requires a schedule key and defaults to 50 ms when scheduled. There is
739
+ no `prompt` or `promptId`: continuation creates a new run and an assistant message
740
+ without appending or modifying a user message. Its result has the prompt result's
741
+ fields except `userMessage`. Explicit steers still become user messages normally.
742
+
743
+ Admission returns `{ queueId, completion }` once queued, like `enqueuePrompt()`.
744
+ The entry and its `prompt.*` events carry `continuation: true`. Inspect its `runId`
745
+ after `prompt.started`, or read it through `getPromptQueueEntry()`. Use the same
746
+ `cancelPrompt()`, `cancelScheduledPrompt()`, `abort()` and `steerPrompt()` methods.
747
+ Queue bounds and FIFO order, permissions, resource cleanup, compaction, usage
748
+ accounting and provider limits follow the ordinary run path.
749
+
750
+ At the front of the FIFO, the harness validates the visible transcript and model
751
+ context with undone and excluded runs filtered out, before committing an undone
752
+ branch or reserving a generation or audit message. A refusal preserves redo history.
753
+ Empty context, an already answered turn, and an unsuccessful input that did not stay
754
+ in context refuse with `FlexHarnessValidationError` through `completion`. A retained
755
+ failure whose model context ends in partial assistant text with no tool call is also
756
+ refused; `keptInContext: true` alone does not guarantee it can be continued. An earlier
757
+ queued answer can therefore invalidate a waiting continuation. Cancellation before
758
+ execution leaves the retained input available for another continuation. Continuing never replays
759
+ previous tool calls; unknown external effects still require reconciliation.
760
+
761
+ Existing projections and Agent event stores remain readable without migration.
762
+ Assistant-only runs require two optional fields in the existing schema-3 projection:
763
+ a staged terminal omits `userMessage`, and its reversion segment omits
764
+ `userMessageId`. Requiring either would invent a user message. Generation claims
765
+ use existing events without a new `user-message` event. Older strict projection
766
+ readers cannot open a session containing these continuation records; do not mix
767
+ versions or downgrade those sessions.
768
+
717
769
  ### Steering a Running Turn
718
770
 
719
771
  `steerPrompt()` hands a message to the running run of a prompt instead of queueing it behind that run:
@@ -734,6 +786,8 @@ The run takes a steer in at its next step boundary: after the tool results of it
734
786
 
735
787
  A prompt can bring further user messages into its run: `startPrompt()`, `enqueuePrompt()` and `prompt()` take `steers`, a list of `{ steerId, prompt }`, in their options. The run accepts them when it starts, emitting `prompt.steer.accepted` for each in the given order right after `prompt.started`, and takes them in before its first model call, so the model sees the prompt and its steers as consecutive user messages of their own in one call, each with its own `steerId`, `messageId` and attachments. From then on they report like every other steer of the run. They count against the session's outstanding prompt limits from admission on. The whole prompt is refused with `FlexHarnessValidationError` when a `steerId` repeats, and with `FlexHarnessQueueFullError` when it brings more steers than `maxOutstandingPromptsPerSession` or its steers' bytes exceed the outstanding byte limit. A prompt cancelled before its run started lists them in `unappliedSteerIds` on its finished queue entry and `prompt.finished`, with `keptInContext: false`. A caller holding several waiting messages for a session delivers them in one turn this way, without merging them into one message.
736
788
 
789
+ A prompt can name itself with `promptId` in its options (`startPrompt()`, `enqueuePrompt()`, `prompt()` and `schedulePrompt()`): a non-empty string of at most 512 UTF-8 bytes, validated like a `steerId`. It is on the prompt's queue entry and its queue events, and on the user message that heads its run, also after a restart, so a caller that is unsure whether a prompt was taken in finds out from the transcript. A prompt whose `promptId` an outstanding prompt of the same session, queued or running, already holds is refused with `FlexHarnessDuplicatePromptError`; once that prompt finished, the id is free again. Other sessions' prompts do not count.
790
+
737
791
  ### Keeping the Prompt Cache Warm
738
792
 
739
793
  A provider's prompt cache expires when nothing reads it for its TTL, and the next turn then writes the whole prefix again. `keepCacheWarm()` keeps a root session's cache warm until a deadline:
@@ -812,7 +866,7 @@ At most 128 custom commands may be registered. Every registration must be a plai
812
866
 
813
867
  `/undo` and `/redo`, plus `undoSession()` and `redoSession()`, move a durable history cursor. The direct methods return `{ revertedRunId }` and `{ restoredRunId }`; the slash forms return `{ type: 'operation', name: 'undo' | 'redo' }`. Each committed cursor move emits one `session.history.changed` event with `direction`, `runId`, and the selected session identity. A committed branch emits the same event with `direction: 'branch'` and no `runId`, so controllers should refresh the complete selected session. Capture finalization, cleanup, and metadata-only changes do not emit this event.
814
868
 
815
- A completed root-session turn defines an operation-group boundary. Failed and cancelled turns after it belong to that group; leading failed or cancelled turns belong to the first completed group. No completed boundary means there is nothing to undo; retained leading unsuccessful history is grouped with the next completed turn rather than silently discarded. Undo hides that group's canonical partial history and outcome markers as well as its visible messages, redo restores them, and branching keeps the hidden history excluded. Transcript-only undo never claims to reverse external tool effects. Undo applies selected segments in reverse order and redo applies them in forward order. Without `turnReversionProvider`, only transcript and future model context move. Hidden messages disappear from `getMessages()`, message pages, exact message lookup, and future model context. Starting a new prompt, template, handler, or compaction from an undone position commits a branch: hidden messages and segments are removed durably and cannot be redone. Successful event archival also commits hidden redo history; a missing compaction or failed archive leaves it intact. Automatic budget compaction archives covered events after generation finalization and advances the same undo horizon; set `contextCompactionBytes: false` to disable that automatic policy.
869
+ A completed root-session turn defines an operation-group boundary. Failed and cancelled turns after it belong to that group; leading failed or cancelled turns belong to the first completed group. No completed boundary means there is nothing to undo; retained leading unsuccessful history is grouped with the next completed turn rather than silently discarded. Undo hides that group's canonical partial history and outcome markers as well as its visible messages, redo restores them, and branching keeps the hidden history excluded. Transcript-only undo never claims to reverse external tool effects. Undo applies selected segments in reverse order and redo applies them in forward order. Without `turnReversionProvider`, only transcript and future model context move. Hidden messages disappear from `getMessages()`, message pages, exact message lookup, and future model context. Starting a new prompt, template, handler, or compaction from an undone position commits a branch: hidden messages and segments are removed durably and cannot be redone. Successful event archival also commits hidden redo history; a missing compaction or failed archive leaves it intact. Automatic budget compaction archives covered events after generation finalization and advances the same undo horizon; `contextCompaction: false` turns budget compaction off.
816
870
 
817
871
  Two horizons bound undo. Retention pruning removes the oldest complete visible units when `reversionLimits` is exceeded. Explicit event archival marks covered turns context-unavailable and prunes complete prefixes that can no longer be rebuilt; FlexHarness never crosses that archive horizon. Manual compaction without archival retains the original events and remains undoable. Schema-1 projection history and sessions migrated from `2.x` have no reversion segments, so historical turns are not retroactively undoable; newly written turns are tracked normally.
818
872
 
@@ -950,7 +1004,7 @@ The model context is built by the session's canonical AgentSession event history
950
1004
  2. The normalized current user message.
951
1005
  3. AgentSession's result messages.
952
1006
 
953
- A failed or cancelled prompt remains visible through `getMessages()` with its truthful `failed` or `cancelled` status. Its canonical partial history also remains in future model context when execution produced output; an unsuccessful generation without output is omitted. Retention is not acceptance and does not roll back or replay external effects. Canonical Agent events remain private and are not exposed by the session or message APIs.
1007
+ A failed or cancelled prompt remains visible through `getMessages()` with its truthful `failed` or `cancelled` status. Its canonical partial history also remains in future model context when execution produced output; an unsuccessful generation without output is omitted. Each user message, the prompt and every steer its run took in, carries `keptInContext` once its run reached its terminal state: the run's `keptInContext`, true when the run's result kept the message in the model context of later turns, false when it did not, as for an unsuccessful run without model or tool output; a failed or stopped run that produced output stays in context and reports true. It records that outcome and is never rewritten: a later undo is reported by `undone` on an `includeUndone` read, and a later compaction summarizes the message, which was still delivered. A message whose run has not ended carries none, and neither do messages written before FlexHarness 15.0.0. Retention is not acceptance and does not roll back or replay external effects. Canonical Agent events remain private and are not exposed by the session or message APIs.
954
1008
 
955
1009
  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.
956
1010
 
@@ -1005,7 +1059,7 @@ if (started.status === 'started') {
1005
1059
 
1006
1060
  **Read-only.** An imported session never runs, and nothing writes model context for it: it has no Agent events and no tool jobs. Every operation that would change its history or model context throws `FlexHarnessImportedSessionError` (code `FLEX_SESSION_READ_ONLY`):
1007
1061
 
1008
- - `prompt`, `startPrompt`, `enqueuePrompt` and `schedulePrompt`;
1062
+ - `prompt`, `startPrompt`, `enqueuePrompt`, `schedulePrompt` and `continuePrompt`;
1009
1063
  - `executeSlashCommand` and `compactSession`;
1010
1064
  - `undoSession` and `redoSession`;
1011
1065
  - `pushRuntimeEvent`, `reconcileToolExecution` and `archiveSessionEvents`;
@@ -1082,6 +1136,21 @@ Transactional tool calls persist an execution intent before the tool side effect
1082
1136
 
1083
1137
  `executionContextProvider` can construct a AgentSession 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.
1084
1138
 
1139
+ ### Compaction Point
1140
+
1141
+ With a compactor, a generation compacts settled history before its first inference once the session's context reaches its compaction point, in tokens of the run's model. The point never exceeds its ceiling: 90% of the model's served context window, which the model resolver states as `IFlexResolvedModel.contextWindow`. That is the window the provider serves by default, never an extended maximum: a Codex catalog's `context_window`, not its `max_context_window`; Anthropic's Models API `max_input_tokens`, where 1M is the default window of the 1M-token models without any beta header. By default the point is the ceiling, or the catalog's own automatic compaction point when it states a lower one (`IFlexResolvedModel.contextCompactionDefaultTokens`, a Codex catalog's `auto_compact_token_limit`). A session chooses its own point with `contextCompaction` on `createSession()` or `updateSession()`, where `null` returns it to the default; `agentSessionPolicy.contextCompaction` is the default for sessions without one.
1142
+
1143
+ | `contextCompaction` | Compaction point |
1144
+ | --- | --- |
1145
+ | omitted | The model's default; none when the model states no served window |
1146
+ | `{ tokens }` | The count, clamped at run time to the ceiling of the run's model; unclamped when the model states no served window |
1147
+ | `{ percent }` | An integer from 1 through 90 of the served window; none when the model states none |
1148
+ | `false` | None |
1149
+
1150
+ A percent outside 1 through 90, a token count that is not a positive integer, or a setting other than `false` on a harness without a `contextCompactor` is rejected with `FlexHarnessValidationError`. `getSessionMetrics()` states the last settled run's `contextWindow` and either its `contextCompactionTokens` or, when it had none, `contextCompactionOff`: `disabled`, `no-context-window` or `no-compactor`. A changed setting shows there from the next run; `resolveAgentContextCompactionPoint({ contextWindow, contextCompactionDefaultTokens, setting })` states the point a session would have on a model before any run.
1151
+
1152
+ The context is measured with the provider's own count where there is one. Each transactional generation's outcome event carries `contextUsage`: the input tokens its last model call reported, with the events that call saw. The reading counts while those events still build the same model messages and no compaction followed them; what was committed after it is estimated at 4 UTF-8 JSON bytes per token. After a compaction, an undo or a branch, and for history written before readings existed, the whole context is estimated that way. The check runs once per generation, before its first inference: a generation that grows past the point in its own tool loop is compacted only when its context overflows.
1153
+
1085
1154
  ### Context Compaction Model
1086
1155
 
1087
1156
  A `contextCompactor` states which model to compact with from the model the compacted work was dispatched on, never from a host default that may name another account:
@@ -1089,7 +1158,7 @@ A `contextCompactor` states which model to compact with from the model the compa
1089
1158
  | `reason` | Compaction | Model members |
1090
1159
  | --- | --- | --- |
1091
1160
  | `context-overflow` | A run's context overflowed, including a delegated run's | `resolvedModel`: the `IFlexResolvedModel` the model resolver returned for that run, frozen; a delegated run's overflow carries the delegated run's own model |
1092
- | `context-budget` | Settled history exceeds `contextCompactionBytes` before a generation's first inference (256 KiB by default with a compactor) | `resolvedModel`: the exact model resolved for that run, including its delegated model |
1161
+ | `context-budget` | The context reaches the session's compaction point before a generation's first inference; see Compaction Point | `resolvedModel`: the exact model resolved for that run, including its delegated model |
1093
1162
  | `manual` | `/compact` or `compactSession()` | `modelHint`: the `modelHint` of the `/compact` execution options or of `compactSession(scopeId, sessionId, { modelHint })`, when one was passed |
1094
1163
  | `retention` | Event retention | none |
1095
1164
 
@@ -33,7 +33,7 @@ import {
33
33
  isReservedSlashCommandName,
34
34
  isValidSlashCommandName,
35
35
  } from './utils.slashcommands.js';
36
- import { validateUtf8String } from './utils.validation.js';
36
+ import { validateContextCompactionSetting, validateUtf8String } from './utils.validation.js';
37
37
 
38
38
  export type TRegisteredSlashCommand<TScope> =
39
39
  | Readonly<IFlexSlashCommandTemplateRegistration>
@@ -548,10 +548,17 @@ export function resolveResultLimits(
548
548
  export function normalizeAgentSessionPolicy<TScope>(
549
549
  policy: IFlexAgentSessionPolicy<TScope> = {},
550
550
  ): IFlexAgentSessionPolicy<TScope> {
551
+ if (policy.contextCompaction !== undefined) {
552
+ validateContextCompactionSetting(
553
+ policy.contextCompaction,
554
+ policy.contextCompactor !== undefined,
555
+ 'agentSessionPolicy.contextCompaction',
556
+ );
557
+ }
551
558
  return {
552
559
  ...(policy.contextBuilder === undefined ? {} : { contextBuilder: policy.contextBuilder }),
553
560
  ...(policy.contextCompactor === undefined ? {} : { contextCompactor: policy.contextCompactor }),
554
- ...(policy.contextCompactionBytes === undefined ? {} : { contextCompactionBytes: policy.contextCompactionBytes }),
561
+ ...(policy.contextCompaction === undefined ? {} : { contextCompaction: policy.contextCompaction }),
555
562
  ...(policy.eventRetention === undefined ? {} : { eventRetention: policy.eventRetention }),
556
563
  ...(policy.changeListenerTimeoutMs === undefined
557
564
  ? {}