@modelprofile.com/flexharness 13.1.1 → 13.2.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.cachewarm.d.ts +64 -0
- package/dist_ts/classes.flexharness.cachewarm.js +353 -0
- package/dist_ts/classes.flexharness.callbacks.js +2 -1
- package/dist_ts/classes.flexharness.core.d.ts +9 -1
- package/dist_ts/classes.flexharness.core.js +55 -6
- package/dist_ts/classes.flexharness.d.ts +10 -1
- package/dist_ts/classes.flexharness.deletion.js +2 -1
- package/dist_ts/classes.flexharness.js +14 -1
- package/dist_ts/classes.flexharness.lifecycle.js +4 -1
- package/dist_ts/classes.flexharness.prompts.d.ts +1 -1
- package/dist_ts/classes.flexharness.prompts.js +25 -5
- package/dist_ts/classes.flexharness.reversions.d.ts +2 -0
- package/dist_ts/classes.flexharness.reversions.js +7 -2
- package/dist_ts/classes.flexharness.runs.js +23 -2
- package/dist_ts/interfaces.d.ts +87 -1
- package/dist_ts/interfaces.flexharness.d.ts +15 -0
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/plugins.d.ts +1 -1
- package/dist_ts/plugins.js +1 -1
- package/dist_ts/utils.harnessoptions.d.ts +8 -1
- package/dist_ts/utils.harnessoptions.js +50 -6
- package/dist_ts/utils.json.js +11 -2
- package/dist_ts/utils.sessionpreview.d.ts +3 -3
- package/dist_ts/utils.sessionpreview.js +5 -5
- package/dist_ts_chat_harness/classes.transcript.js +17 -4
- package/package.json +3 -3
- package/readme.md +27 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.cachewarm.ts +433 -0
- package/ts/classes.flexharness.callbacks.ts +1 -0
- package/ts/classes.flexharness.core.ts +50 -4
- package/ts/classes.flexharness.deletion.ts +1 -0
- package/ts/classes.flexharness.lifecycle.ts +3 -0
- package/ts/classes.flexharness.prompts.ts +22 -3
- package/ts/classes.flexharness.reversions.ts +7 -1
- package/ts/classes.flexharness.runs.ts +19 -1
- package/ts/classes.flexharness.ts +21 -0
- package/ts/interfaces.flexharness.ts +16 -0
- package/ts/interfaces.ts +98 -1
- package/ts/plugins.ts +1 -0
- package/ts/readme.md +27 -0
- package/ts/utils.harnessoptions.ts +74 -5
- package/ts/utils.json.ts +10 -1
- package/ts/utils.sessionpreview.ts +4 -4
- package/ts_chat_harness/classes.transcript.ts +14 -3
- package/ts_chat_harness/readme.md +1 -0
package/ts/readme.md
CHANGED
|
@@ -418,6 +418,7 @@ The tools use strict action-discriminated inputs:
|
|
|
418
418
|
- `goal`: `get`, `set`, or `clear`.
|
|
419
419
|
- `scratchpad`: `get`, `set`, `append`, or `clear`. Append concatenates the supplied content exactly.
|
|
420
420
|
- `rename_session`: sets the active session title and returns the authoritative session.
|
|
421
|
+
- `keep_cache_warm` (`cacheWarm: true`, root sessions only): keeps the session's prompt cache warm; see [Keeping the Prompt Cache Warm](#keeping-the-prompt-cache-warm).
|
|
421
422
|
|
|
422
423
|
Every project action returns the authoritative revision and state; task mutations also return the affected task, and clear returns the removed tasks. Reads never save. A set, clear, append, update, idempotent create, or empty task clear that makes no state change returns the current revision without writing. Mutations load once, apply once, validate the complete next snapshot, and issue one compare-and-swap save at `revision + 1`. FlexHarness never retries or merges an external conflict.
|
|
423
424
|
|
|
@@ -729,6 +730,32 @@ The run takes a steer in at its next step boundary: after the tool results of it
|
|
|
729
730
|
|
|
730
731
|
`steerPrompt()` resolves with `{ steerId, queueId, runId }` once the run accepted the steer and `prompt.steer.accepted` was emitted; `prompt.steer.applied` carries the `messageId` of the user message the steer became. Every accepted steer ends in exactly one of two ways: it is in the model context of later turns, or it is listed in `unappliedSteerIds`, in arrival order, on `IFlexPromptResult` and `run.finished`, and on the finished queue entry when that list is not empty. A steer is listed when the run did not take it in before it ended, because its last model step was already running or because it was stopped or failed, and also when the run itself does not stay in context. `keptInContext` on `run.finished` and on the finished queue entry says whether it does: it is `false` for a failed run and for a run stopped before any model output, and such a run lists every steer it accepted, including the ones it took in and reported with `prompt.steer.applied`. Send its prompt and its unapplied steers again to have them answered. The run stops taking steers atomically when its generation ends or `abort()`/`cancelPrompt()` stops it. A steer for a prompt without a running run, or whose run is ending, is refused with `FlexHarnessSteerRejectedError` reason `not-running`; the caller sends it as the next turn instead. Reusing a `steerId` within a run is refused with reason `duplicate`, an unknown `queueId` throws `FlexHarnessNotFoundError`, and child sessions reject this general API (their exact parent run uses `sendSubagentInput()` or `delegate_send` instead). A run holds at most `maxOutstandingPromptsPerSession` pending steers, and their bytes count against the session's outstanding prompt byte limit until the run takes them in or ends, so `startPrompt()` and `enqueuePrompt()` are refused with `FlexHarnessQueueFullError` while pending steers fill it. They are process-local like queue entries: a process restart ends the run and drops its pending steers, while steers it took in are durable parts of the turn.
|
|
731
732
|
|
|
733
|
+
### Keeping the Prompt Cache Warm
|
|
734
|
+
|
|
735
|
+
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:
|
|
736
|
+
|
|
737
|
+
```typescript
|
|
738
|
+
await harness.keepCacheWarm(scopeId, sessionId, { durationMs: 60 * 60 * 1000 }); // warm until 1 h from now
|
|
739
|
+
await harness.keepCacheWarm(scopeId, sessionId, { durationMs: 0 }); // stop
|
|
740
|
+
const state = await harness.getCacheWarm(scopeId, sessionId); // { sessionId, warmUntil?, nextWarmAt?, idleMs?, basis? }
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
Each call replaces the deadline with `durationMs` from now, capped at `cacheWarm.maxDurationMs` (default 4 h, at most 24 h); `0` stops it. While the deadline has not passed and the session is idle, the harness sends a cache-warm prompt once the session has had no model call for the cache TTL less one minute: 4 minutes for a 5-minute cache, 29 for a 30-minute one, 59 for a 1-hour one. The TTL comes from the request the provider actually sent last, its `basis` in `getCacheWarm()`:
|
|
744
|
+
|
|
745
|
+
| `basis` | TTL | Sent request |
|
|
746
|
+
| --- | --- | --- |
|
|
747
|
+
| `anthropic-5m`, `anthropic-1h` | 5 min, 1 h | Anthropic: the shortest `cache_control` lifetime of the request |
|
|
748
|
+
| `openai-30m` | 30 min | OpenAI's API, GPT-5.6 and later: `prompt_cache_options.ttl` is 30 minutes after the last write or reuse |
|
|
749
|
+
| `openai-24h` | 30 min | OpenAI's API, an earlier model with `prompt_cache_retention: '24h'` (`cache: { retention: '24h' }`): OpenAI keeps the prefix typically around 30 minutes and up to 24 hours, without promising more |
|
|
750
|
+
| `openai-in-memory` | 5 min | OpenAI's API, an earlier model with `in_memory` or no retention; or the ChatGPT backend of ChatGPT accounts, which takes no retention and documents none. In-memory entries last 5 to 10 minutes of inactivity |
|
|
751
|
+
| `configured` | `cache.warmDurationMs` | any |
|
|
752
|
+
|
|
753
|
+
With no basis (another provider or endpoint, or a custom transport that may change the request) the TTL is 5 minutes. No OpenAI retention promises a hit beyond about 30 minutes, so none makes warming unnecessary; extended retention only spaces warm turns further apart. Source: OpenAI's [prompt caching guide](https://developers.openai.com/api/docs/guides/prompt-caching), read 2026-10-11. A prompt, a steer or a running turn resets the wait, and no warm prompt is sent while a turn runs, a prompt is queued, an undo's redo history is open, a reversion is pending or a slash command runs. The warm prompt is the fixed text `FLEX_CACHE_WARM_PROMPT`, starting with `[cache-warm]`, which asks for the single word `ok`. Its turn uses the model and system of the session's last turn and offers the same tools, so it reads the cached prefix, but it refuses every tool call, takes at most two steps, caps each model call at `cacheWarm.maxOutputTokens` (default 1024) and takes no steer: `steerPrompt()` refuses one with reason `not-running`. Keeping a session warm costs a cache read of its prefix and a few new tokens per warm turn; the warm exchanges stay in the model context.
|
|
754
|
+
|
|
755
|
+
Warm turns are marked so hosts can leave them out of what they show and of what they count as work: both messages, the session `activity`, the prompt queue entry, `run.started`, `run.finished` and `model.usage` carry `cacheWarm: true`. A warm turn changes neither the session's `updatedAt` nor its `preview`, and adds no undo step. Three events report the keeper: `cache.warm.changed` (`source` `host` or `agent`, `warmUntil` absent once stopped), `cache.warm.completed` with the warm turn's `usage`, whose `cacheReadTokens` and `cacheWriteTokens` are the provider's own counts and show whether it hit the cache (OpenAI's `input_tokens_details.cached_tokens` and `cache_write_tokens`, Anthropic's `cache_read_input_tokens` and `cache_creation_input_tokens`), its `idleForMs` since the session's previous model call and its `basis`, and `cache.warm.stopped` with `reason` `deadline`, `cleared`, `failed` (the warm turn failed; `error` set) or `unavailable` (the session or its scope went away).
|
|
756
|
+
|
|
757
|
+
`builtInTools: { cacheWarm: true }` gives root sessions the `keep_cache_warm` tool, so the agent can keep its own cache warm: `{ duration: '1h' }` sets the deadline (amounts of `s`, `m` or `h`), `'off'` or `'0'` stops it. The keeper is process-local: it ends with `dispose()` and does not survive a restart. It warms only the loaded scope it was set in: retiring the scope or deleting the session stops it with `unavailable`, and it never loads a scope again. Tests pass `cacheWarm.clock` (`now`, `setTimeout`, `clearTimeout`) to drive it with a fake clock.
|
|
758
|
+
|
|
732
759
|
## Slash Commands
|
|
733
760
|
|
|
734
761
|
`parseSlashCommand()` is the public strict pure parser. It accepts at most 768 KiB, returns `not-command` for input not starting with `/`, `malformed` for invalid slash syntax, and `parsed` with the exact input, lowercase command name, separator-stripped raw argument text, and OpenCode-compatible quoted tokenization. Command names match `[a-z][a-z0-9_-]{0,63}`. The public `isValidSlashCommandName(name)` and `isReservedSlashCommandName(name)` helpers let applications validate registration names against the same rules. Single and double quotes group tokens and are stripped; escapes are not interpreted.
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
import type {
|
|
13
13
|
IFlexAgentSessionPolicy,
|
|
14
14
|
IFlexAttachmentResolver,
|
|
15
|
+
IFlexCacheWarmClock,
|
|
16
|
+
IFlexCacheWarmOptions,
|
|
15
17
|
IFlexCallbackLimits,
|
|
16
18
|
IFlexHarnessOptions,
|
|
17
19
|
IFlexHarnessStores,
|
|
@@ -46,8 +48,21 @@ export interface INormalizedProjectManagementTools {
|
|
|
46
48
|
export interface INormalizedBuiltInTools {
|
|
47
49
|
readonly renameSession: boolean;
|
|
48
50
|
readonly projectManagement?: INormalizedProjectManagementTools;
|
|
51
|
+
readonly cacheWarm: boolean;
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
export interface INormalizedCacheWarmOptions {
|
|
55
|
+
readonly maxDurationMs: number;
|
|
56
|
+
readonly maxOutputTokens: number;
|
|
57
|
+
readonly clock: IFlexCacheWarmClock;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const defaultCacheWarmMaxDurationMs = 4 * 60 * 60 * 1000;
|
|
61
|
+
// One call warms for at most a day; a host extends it by calling again.
|
|
62
|
+
const maximumCacheWarmMaxDurationMs = 24 * 60 * 60 * 1000;
|
|
63
|
+
const defaultCacheWarmMaxOutputTokens = 1024;
|
|
64
|
+
const maximumCacheWarmMaxOutputTokens = 32_768;
|
|
65
|
+
|
|
51
66
|
const DEFAULT_CALLBACK_LIMITS: Required<IFlexCallbackLimits> = {
|
|
52
67
|
maxEvents: 10_000,
|
|
53
68
|
maxOutputBytes: 1024 * 1024,
|
|
@@ -242,7 +257,7 @@ function normalizeCodeModeLimits(codeMode: unknown): Readonly<IFlexCodeModeLimit
|
|
|
242
257
|
export function normalizeBuiltInTools(
|
|
243
258
|
options: IFlexHarnessOptions<unknown>['builtInTools'],
|
|
244
259
|
): INormalizedBuiltInTools {
|
|
245
|
-
if (options === undefined) return Object.freeze({ renameSession: false });
|
|
260
|
+
if (options === undefined) return Object.freeze({ renameSession: false, cacheWarm: false });
|
|
246
261
|
if (
|
|
247
262
|
!options
|
|
248
263
|
|| typeof options !== 'object'
|
|
@@ -252,14 +267,19 @@ export function normalizeBuiltInTools(
|
|
|
252
267
|
throw new FlexHarnessValidationError('builtInTools must be a plain object.');
|
|
253
268
|
}
|
|
254
269
|
const unsupported = Object.keys(options)
|
|
255
|
-
.find((key) => !['renameSession', 'projectManagement'].includes(key));
|
|
270
|
+
.find((key) => !['renameSession', 'projectManagement', 'cacheWarm'].includes(key));
|
|
256
271
|
if (unsupported) throw new FlexHarnessValidationError(`builtInTools does not support "${unsupported}".`);
|
|
257
|
-
|
|
258
|
-
|
|
272
|
+
for (const key of ['renameSession', 'cacheWarm'] as const) {
|
|
273
|
+
if (options[key] !== undefined && typeof options[key] !== 'boolean') {
|
|
274
|
+
throw new FlexHarnessValidationError(`builtInTools.${key} must be a boolean.`);
|
|
275
|
+
}
|
|
259
276
|
}
|
|
260
277
|
const projectManagement = options.projectManagement;
|
|
261
278
|
if (projectManagement === undefined) {
|
|
262
|
-
return Object.freeze({
|
|
279
|
+
return Object.freeze({
|
|
280
|
+
renameSession: options.renameSession ?? false,
|
|
281
|
+
cacheWarm: options.cacheWarm ?? false,
|
|
282
|
+
});
|
|
263
283
|
}
|
|
264
284
|
if (
|
|
265
285
|
!projectManagement
|
|
@@ -283,6 +303,7 @@ export function normalizeBuiltInTools(
|
|
|
283
303
|
}
|
|
284
304
|
return Object.freeze({
|
|
285
305
|
renameSession: options.renameSession ?? false,
|
|
306
|
+
cacheWarm: options.cacheWarm ?? false,
|
|
286
307
|
projectManagement: Object.freeze({
|
|
287
308
|
task: projectManagement.task ?? true,
|
|
288
309
|
goal: projectManagement.goal ?? true,
|
|
@@ -374,6 +395,54 @@ export function normalizeSlashCommands<TScope>(
|
|
|
374
395
|
return Object.freeze(normalized);
|
|
375
396
|
}
|
|
376
397
|
|
|
398
|
+
const systemCacheWarmClock: IFlexCacheWarmClock = Object.freeze({
|
|
399
|
+
now: () => Date.now(),
|
|
400
|
+
setTimeout: (callback: () => void, delayMs: number) => {
|
|
401
|
+
const handle = setTimeout(callback, delayMs);
|
|
402
|
+
// A warm period never holds the process open.
|
|
403
|
+
handle.unref?.();
|
|
404
|
+
return handle;
|
|
405
|
+
},
|
|
406
|
+
clearTimeout: (handle: unknown) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
export function normalizeCacheWarmOptions(
|
|
410
|
+
input: IFlexHarnessOptions<unknown>['cacheWarm'],
|
|
411
|
+
): INormalizedCacheWarmOptions {
|
|
412
|
+
if (input !== undefined && !isPlainOptions(input as unknown)) {
|
|
413
|
+
throw new FlexHarnessValidationError('cacheWarm must be a plain object.');
|
|
414
|
+
}
|
|
415
|
+
const options: IFlexCacheWarmOptions = input ?? {};
|
|
416
|
+
const unsupported = Object.keys(options)
|
|
417
|
+
.find((key) => !['maxDurationMs', 'maxOutputTokens', 'clock'].includes(key));
|
|
418
|
+
if (unsupported) throw new FlexHarnessValidationError(`cacheWarm does not support "${unsupported}".`);
|
|
419
|
+
const clock: IFlexCacheWarmClock = options.clock ?? systemCacheWarmClock;
|
|
420
|
+
if (
|
|
421
|
+
!clock
|
|
422
|
+
|| typeof clock !== 'object'
|
|
423
|
+
|| typeof clock.now !== 'function'
|
|
424
|
+
|| typeof clock.setTimeout !== 'function'
|
|
425
|
+
|| typeof clock.clearTimeout !== 'function'
|
|
426
|
+
) {
|
|
427
|
+
throw new FlexHarnessValidationError('cacheWarm.clock must provide now, setTimeout and clearTimeout.');
|
|
428
|
+
}
|
|
429
|
+
return Object.freeze({
|
|
430
|
+
maxDurationMs: resolveBoundedPositiveInteger(
|
|
431
|
+
options.maxDurationMs,
|
|
432
|
+
'cacheWarm.maxDurationMs',
|
|
433
|
+
defaultCacheWarmMaxDurationMs,
|
|
434
|
+
maximumCacheWarmMaxDurationMs,
|
|
435
|
+
),
|
|
436
|
+
maxOutputTokens: resolveBoundedPositiveInteger(
|
|
437
|
+
options.maxOutputTokens,
|
|
438
|
+
'cacheWarm.maxOutputTokens',
|
|
439
|
+
defaultCacheWarmMaxOutputTokens,
|
|
440
|
+
maximumCacheWarmMaxOutputTokens,
|
|
441
|
+
),
|
|
442
|
+
clock,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
377
446
|
export function resolveBoundedPositiveInteger(
|
|
378
447
|
value: number | undefined,
|
|
379
448
|
name: string,
|
package/ts/utils.json.ts
CHANGED
|
@@ -566,7 +566,7 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
|
|
|
566
566
|
const activity = requireRecord(session.activity, `${path}.activity`);
|
|
567
567
|
requireOnlyKeys(
|
|
568
568
|
activity,
|
|
569
|
-
['runId', 'status', 'startedAt', 'completedAt', 'error', 'limit', 'rejectedPermissionId'],
|
|
569
|
+
['runId', 'status', 'startedAt', 'completedAt', 'error', 'limit', 'rejectedPermissionId', 'cacheWarm'],
|
|
570
570
|
`${path}.activity`,
|
|
571
571
|
);
|
|
572
572
|
if (!activityStatuses.includes(activity.status as (typeof activityStatuses)[number])) {
|
|
@@ -578,6 +578,11 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
|
|
|
578
578
|
requireOptionalString(activity.error, `${path}.activity.error`);
|
|
579
579
|
requireOptionalModelLimit(activity.limit, `${path}.activity.limit`);
|
|
580
580
|
requireOptionalString(activity.rejectedPermissionId, `${path}.activity.rejectedPermissionId`);
|
|
581
|
+
// FlexHarness 13.2.0 stored a warm turn's activity with `cacheWarm`; session repair on load
|
|
582
|
+
// removes it and restores the latest real turn's activity.
|
|
583
|
+
if (activity.cacheWarm !== undefined && activity.cacheWarm !== true) {
|
|
584
|
+
throw new FlexHarnessStoreFormatError(`${path}.activity.cacheWarm must be true when present.`);
|
|
585
|
+
}
|
|
581
586
|
const relationship = [
|
|
582
587
|
session.parentSessionId,
|
|
583
588
|
session.parentRunId,
|
|
@@ -776,6 +781,7 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
776
781
|
'limit',
|
|
777
782
|
'rejectedPermissionId',
|
|
778
783
|
'steerId',
|
|
784
|
+
'cacheWarm',
|
|
779
785
|
],
|
|
780
786
|
path,
|
|
781
787
|
);
|
|
@@ -799,6 +805,9 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
799
805
|
throw new FlexHarnessStoreFormatError(`${path}.steerId is only valid on a user message.`);
|
|
800
806
|
}
|
|
801
807
|
}
|
|
808
|
+
if (message.cacheWarm !== undefined && message.cacheWarm !== true) {
|
|
809
|
+
throw new FlexHarnessStoreFormatError(`${path}.cacheWarm must be true when present.`);
|
|
810
|
+
}
|
|
802
811
|
if (message.model !== undefined) validateModel(message.model, `${path}.model`);
|
|
803
812
|
if (message.usage !== undefined) validateUsage(message.usage, `${path}.usage`);
|
|
804
813
|
if (!Array.isArray(message.parts)) {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { FLEX_PROJECT_MANAGEMENT_LIMITS, type IFlexMessage } from './interfaces.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* What a session is about before anyone titles it: the text of its first prompt that has any,
|
|
5
|
-
* one line (every run of whitespace becomes one space), cut to the
|
|
6
|
-
* boundary. Undefined while no prompt carries text.
|
|
4
|
+
* What a session is about before anyone titles it: the text of its first prompt that has any,
|
|
5
|
+
* cache-warm prompts aside, as one line (every run of whitespace becomes one space), cut to the
|
|
6
|
+
* title bound at a character boundary. Undefined while no prompt carries text.
|
|
7
7
|
*/
|
|
8
8
|
export function sessionPreview(messagesArg: readonly IFlexMessage[]): string | undefined {
|
|
9
9
|
for (const message of messagesArg) {
|
|
10
|
-
if (message.role !== 'user') continue;
|
|
10
|
+
if (message.role !== 'user' || message.cacheWarm) continue;
|
|
11
11
|
const text = message.parts
|
|
12
12
|
.map((part) => (part.type === 'text' ? part.text : ''))
|
|
13
13
|
.join(' ')
|
|
@@ -180,7 +180,7 @@ export class FlexHarnessTranscript {
|
|
|
180
180
|
public view(): IFlexHarnessTranscriptView {
|
|
181
181
|
return structuredClone({
|
|
182
182
|
messages: this.flexMessages.flatMap((message, index) =>
|
|
183
|
-
flexMessageToHarnessRows(message, index, this.timings)),
|
|
183
|
+
message.cacheWarm ? [] : flexMessageToHarnessRows(message, index, this.timings)),
|
|
184
184
|
permissions: [...this.permissionRows.values()],
|
|
185
185
|
questions: [],
|
|
186
186
|
status: this.status,
|
|
@@ -217,6 +217,11 @@ export class FlexHarnessTranscript {
|
|
|
217
217
|
}
|
|
218
218
|
if (event.scopeId !== this.scopeId || event.sessionId !== this.sessionId) return change;
|
|
219
219
|
const timestamp = Date.parse(event.timestamp);
|
|
220
|
+
// A cache-warm turn's messages are held but not shown, so their events change no row.
|
|
221
|
+
const warmMessage = 'message' in event
|
|
222
|
+
? event.message.cacheWarm === true
|
|
223
|
+
: 'messageId' in event && this.flexMessages.some((message) =>
|
|
224
|
+
message.messageId === event.messageId && message.cacheWarm);
|
|
220
225
|
switch (event.type) {
|
|
221
226
|
case 'session.created':
|
|
222
227
|
case 'session.updated':
|
|
@@ -227,7 +232,7 @@ export class FlexHarnessTranscript {
|
|
|
227
232
|
change.messages = change.permissions = change.status = change.usage = true;
|
|
228
233
|
break;
|
|
229
234
|
case 'run.started':
|
|
230
|
-
this.setStatus(change, { type: 'busy' });
|
|
235
|
+
if (!event.cacheWarm) this.setStatus(change, { type: 'busy' });
|
|
231
236
|
break;
|
|
232
237
|
case 'message.created':
|
|
233
238
|
case 'message.updated':
|
|
@@ -280,13 +285,15 @@ export class FlexHarnessTranscript {
|
|
|
280
285
|
this.usage = sumUsage(this.usage, flexUsageToHarness(event.message.usage));
|
|
281
286
|
change.usage = true;
|
|
282
287
|
}
|
|
288
|
+
if (event.cacheWarm) break;
|
|
283
289
|
this.setStatus(change, event.status === 'failed'
|
|
284
290
|
? { type: 'error', ...(event.error ? { message: event.error.message } : {}) }
|
|
285
291
|
: { type: 'idle' });
|
|
286
292
|
break;
|
|
287
293
|
case 'prompt.started':
|
|
288
294
|
case 'prompt.running':
|
|
289
|
-
|
|
295
|
+
// A steer cannot join a cache-warm turn.
|
|
296
|
+
if (!event.entry.cacheWarm) this.runningQueueId = event.queueId;
|
|
290
297
|
break;
|
|
291
298
|
case 'prompt.finished':
|
|
292
299
|
if (this.runningQueueId === event.queueId) this.runningQueueId = undefined;
|
|
@@ -297,6 +304,10 @@ export class FlexHarnessTranscript {
|
|
|
297
304
|
default:
|
|
298
305
|
break;
|
|
299
306
|
}
|
|
307
|
+
if (warmMessage) {
|
|
308
|
+
change.messages = false;
|
|
309
|
+
change.deltas = [];
|
|
310
|
+
}
|
|
300
311
|
return change;
|
|
301
312
|
}
|
|
302
313
|
|
|
@@ -78,6 +78,7 @@ The host forwards every event of the harness instance, of every session, in orde
|
|
|
78
78
|
- Permission rows stay in the transcript once answered, showing the decision. A request its run leaves unanswered, because the run was stopped or failed, is removed when the run finishes.
|
|
79
79
|
- FlexHarness emits no event for project state. After a run, or after a project tool's row completes, read `getProjectState()` and pass it to `applyProjectState()` for the task list and the scratchpad.
|
|
80
80
|
- `questions` is always empty: FlexHarness has no question requests.
|
|
81
|
+
- A cache-warm turn (see the FlexHarness readme, Keeping the Prompt Cache Warm) has no rows and leaves the status and the session card as the last real turn left them and `activeQueueId` unset, so a steer never joins it. Its usage still counts in `usage`: it is spent.
|
|
81
82
|
- Messages the session has not taken in yet (a queued prompt, a steer) carry no text in FlexHarness events. The host that sent them shows them as pending rows.
|
|
82
83
|
- An attachment row needs a name, a media type and a size. The public projection keeps no payload and states these only when the prompt did, so an attachment without all three, such as a remote URL of unknown size, has no row.
|
|
83
84
|
|