@modelprofile.com/flexharness 13.2.0 → 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.
@@ -969,32 +969,42 @@ export class FlexHarnessCore<TScope> {
969
969
  ...stored.messages.map((message) => message.runId),
970
970
  ...stored.stagedTerminals.map((terminal) => terminal.runId),
971
971
  ]);
972
+ // The session's state is the latest real turn's: a cache-warm turn's terminal never sets it.
972
973
  let latestTerminal: IFlexTerminalProjection | undefined;
973
974
  for (const runId of runIds) {
974
975
  const outcome = outcomes.get(runId);
975
976
  const stage = stages.get(runId);
976
977
  if (outcome === 'accepted' && stage?.status === 'completed') {
977
978
  this.runs.replaceVisibleRun(stored, stage);
978
- latestTerminal = stage;
979
+ if (!stage.userMessage.cacheWarm) latestTerminal = stage;
979
980
  } else if (outcome !== 'accepted') {
980
981
  const visible = stored.messages.filter((message) => message.runId === runId);
981
- if (visible.length > 0 || stage) {
982
+ // A run that failed or was stopped keeps its terminal and its error: the staged one, or the
983
+ // one it published. Only a run that never reached its terminal is repaired as interrupted.
984
+ const ended = stage && stage.status !== 'completed'
985
+ ? stage
986
+ : stage ? undefined : this.publishedTerminal(runId, visible);
987
+ if (ended) {
988
+ if (ended === stage) this.runs.replaceVisibleRun(stored, stage);
989
+ if (!ended.userMessage.cacheWarm) latestTerminal = ended;
990
+ } else if (visible.length > 0 || stage) {
982
991
  const status = outcome === 'rejected' ? 'failed' : 'cancelled';
983
992
  const terminal = this.repairTerminal(stored.session.sessionId, runId, visible, stage, status);
984
993
  this.runs.replaceVisibleRun(stored, terminal);
985
- latestTerminal = terminal;
994
+ if (!terminal.userMessage.cacheWarm) latestTerminal = terminal;
986
995
  }
987
996
  } else {
988
997
  const visibleSuccess = stored.messages.some((message) =>
989
998
  message.runId === runId && message.status === 'completed');
990
999
  if (visibleSuccess) {
991
- latestTerminal = this.repairTerminal(
1000
+ const terminal = this.repairTerminal(
992
1001
  stored.session.sessionId,
993
1002
  runId,
994
1003
  stored.messages.filter((message) => message.runId === runId),
995
1004
  undefined,
996
1005
  'completed',
997
1006
  );
1007
+ if (!terminal.userMessage.cacheWarm) latestTerminal = terminal;
998
1008
  }
999
1009
  }
1000
1010
  }
@@ -1033,17 +1043,22 @@ export class FlexHarnessCore<TScope> {
1033
1043
  }
1034
1044
  await this.reversions.recoverPendingCapture(state, stored, scopeId, scope, outcomes);
1035
1045
  }
1046
+ // FlexHarness 13.2.0 marked a warm turn's activity with `cacheWarm`; no activity carries it now.
1047
+ delete (stored.session.activity as { cacheWarm?: true }).cacheWarm;
1036
1048
  if (latestTerminal) {
1037
1049
  const completedAt = latestTerminal.assistantMessage.completedAt ?? new Date().toISOString();
1050
+ const startedAt = stored.session.activity.runId === latestTerminal.runId
1051
+ ? stored.session.activity.startedAt
1052
+ : undefined;
1038
1053
  stored.session.status = latestTerminal.status === 'completed' ? 'idle' : latestTerminal.status;
1039
1054
  stored.session.activity = {
1040
1055
  runId: latestTerminal.runId,
1041
1056
  status: latestTerminal.status === 'completed' ? 'idle' : latestTerminal.status,
1057
+ ...(startedAt === undefined ? {} : { startedAt }),
1042
1058
  completedAt,
1043
1059
  ...terminalActivityError(latestTerminal.assistantMessage),
1044
- ...(latestTerminal.userMessage.cacheWarm ? { cacheWarm: true as const } : {}),
1045
1060
  };
1046
- if (!latestTerminal.userMessage.cacheWarm) stored.session.updatedAt = completedAt;
1061
+ stored.session.updatedAt = completedAt;
1047
1062
  } else if (['scheduled', 'running', 'waiting_permission'].includes(stored.session.status)) {
1048
1063
  const timestamp = new Date().toISOString();
1049
1064
  stored.session.status = 'cancelled';
@@ -1083,6 +1098,30 @@ export class FlexHarnessCore<TScope> {
1083
1098
  return beforeSession !== JSON.stringify(stored.session);
1084
1099
  }
1085
1100
 
1101
+ /**
1102
+ * The failed or cancelled terminal a run published, read from its projected messages. A run
1103
+ * still running has its prompt or its last assistant message streaming; it has none.
1104
+ */
1105
+ private publishedTerminal(
1106
+ runId: string,
1107
+ visible: IFlexMessage[],
1108
+ ): IFlexTerminalProjection | undefined {
1109
+ if (visible.some((message) => message.status === 'streaming')) return undefined;
1110
+ const userMessage = visible.find((message) => message.role === 'user' && message.steerId === undefined);
1111
+ const assistantMessage = [...visible].reverse().find((message) => message.role === 'assistant');
1112
+ if (!userMessage || !assistantMessage) return undefined;
1113
+ const status = assistantMessage.status;
1114
+ if (status !== 'failed' && status !== 'cancelled') return undefined;
1115
+ return {
1116
+ runId,
1117
+ status,
1118
+ userMessage,
1119
+ assistantMessage,
1120
+ ...(assistantMessage.model ? { model: assistantMessage.model } : {}),
1121
+ ...(assistantMessage.usage ? { usage: assistantMessage.usage } : {}),
1122
+ };
1123
+ }
1124
+
1086
1125
  private repairTerminal(
1087
1126
  sessionId: string,
1088
1127
  runId: string,
@@ -642,18 +642,18 @@ export class FlexHarnessPromptQueue<TScope> {
642
642
  if (run.state.tombstones.has(run.sessionId) || run.state.sessions.get(run.sessionId) !== run.stored) {
643
643
  throw new FlexHarnessAbortError('The session was deleted during prompt admission.');
644
644
  }
645
+ // A cache-warm turn leaves the session's status, activity, updatedAt and preview as the
646
+ // last real turn left them; the active run alone keeps the session busy.
647
+ if (run.cacheWarm) return;
645
648
  run.stored.session.status = run.scheduleKey ? 'scheduled' : 'running';
646
649
  run.stored.session.activity = {
647
650
  runId: run.runId,
648
651
  status: run.scheduleKey ? 'scheduled' : 'running',
649
652
  startedAt: reservation.userMessage.createdAt,
650
- ...(run.cacheWarm ? { cacheWarm: true as const } : {}),
651
653
  };
652
- if (!run.cacheWarm) {
653
- run.stored.session.updatedAt = reservation.userMessage.createdAt;
654
- const preview = run.stored.session.preview ?? sessionPreview(run.stored.messages);
655
- if (preview !== undefined) run.stored.session.preview = preview;
656
- }
654
+ run.stored.session.updatedAt = reservation.userMessage.createdAt;
655
+ const preview = run.stored.session.preview ?? sessionPreview(run.stored.messages);
656
+ if (preview !== undefined) run.stored.session.preview = preview;
657
657
  });
658
658
  } catch (error) {
659
659
  if (containsStoreCommitUncertainty(error)) run.state.lifecycle = 'fenced';
@@ -965,7 +965,7 @@ export class FlexHarnessRuns<TScope> {
965
965
  this.replaceVisibleRun(run.stored, terminal);
966
966
  run.stored.stagedTerminals = run.stored.stagedTerminals.filter((entry) => entry.runId !== run.runId);
967
967
  }, true);
968
- if (!this.core.isTombstoned(run)) {
968
+ if (!this.core.isTombstoned(run) && !run.cacheWarm) {
969
969
  await this.core.mutateScope(run.state, () => {
970
970
  if (run.state.sessions.get(run.sessionId) !== run.stored) return;
971
971
  const timestamp = terminal.assistantMessage.completedAt!;
@@ -975,9 +975,8 @@ export class FlexHarnessRuns<TScope> {
975
975
  status: 'idle',
976
976
  startedAt: run.stored.session.activity.startedAt,
977
977
  completedAt: timestamp,
978
- ...(run.cacheWarm ? { cacheWarm: true as const } : {}),
979
978
  };
980
- if (!run.cacheWarm) run.stored.session.updatedAt = timestamp;
979
+ run.stored.session.updatedAt = timestamp;
981
980
  }, true);
982
981
  }
983
982
  }
@@ -990,6 +989,8 @@ export class FlexHarnessRuns<TScope> {
990
989
  this.replaceVisibleRun(run.stored, terminal);
991
990
  run.stored.stagedTerminals = run.stored.stagedTerminals.filter((entry) => entry.runId !== run.runId);
992
991
  }, true);
992
+ // A failed cache-warm turn reports through `cache.warm.stopped`; the session keeps its state.
993
+ if (run.cacheWarm) return;
993
994
  await this.core.mutateScope(run.state, () => {
994
995
  if (run.state.sessions.get(run.sessionId) !== run.stored) return;
995
996
  const timestamp = terminal.assistantMessage.completedAt!;
@@ -1001,9 +1002,8 @@ export class FlexHarnessRuns<TScope> {
1001
1002
  startedAt: run.stored.session.activity.startedAt,
1002
1003
  completedAt: timestamp,
1003
1004
  ...terminalActivityError(terminal.assistantMessage),
1004
- ...(run.cacheWarm ? { cacheWarm: true as const } : {}),
1005
1005
  };
1006
- if (!run.cacheWarm) run.stored.session.updatedAt = timestamp;
1006
+ run.stored.session.updatedAt = timestamp;
1007
1007
  }, true);
1008
1008
  }
1009
1009
 
package/ts/interfaces.ts CHANGED
@@ -239,8 +239,6 @@ export interface IFlexSessionActivity {
239
239
  limit?: TFlexModelLimit;
240
240
  /** The permission whose rejection stopped the last run, next to its error. */
241
241
  rejectedPermissionId?: string;
242
- /** The run is or was a cache-warm turn; see {@link IFlexMessage.cacheWarm}. */
243
- cacheWarm?: true;
244
242
  }
245
243
 
246
244
  export interface IFlexSession {
package/ts/utils.json.ts CHANGED
@@ -578,6 +578,8 @@ 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.
581
583
  if (activity.cacheWarm !== undefined && activity.cacheWarm !== true) {
582
584
  throw new FlexHarnessStoreFormatError(`${path}.activity.cacheWarm must be true when present.`);
583
585
  }
@@ -215,9 +215,7 @@ export function flexPermissionToHarness(
215
215
  };
216
216
  }
217
217
 
218
- /** A cache-warm turn is idle time to the user: its activity shows as idle. */
219
218
  export function flexSessionToHarnessStatus(session: IFlexSession): plugins.IHarnessStatus {
220
- if (session.activity.cacheWarm) return { type: 'idle' };
221
219
  switch (session.activity.status) {
222
220
  case 'running':
223
221
  case 'scheduled':
@@ -249,7 +247,7 @@ export function flexSessionToHarnessMeta(session: IFlexSession): plugins.IHarnes
249
247
  title: session.title ?? session.sessionId,
250
248
  ...(epoch(session.createdAt) === undefined ? {} : { createdAt: epoch(session.createdAt) }),
251
249
  ...(epoch(session.updatedAt) === undefined ? {} : { updatedAt: epoch(session.updatedAt) }),
252
- state: session.activity.cacheWarm ? 'normal' : sessionCardStates[session.activity.status],
250
+ state: sessionCardStates[session.activity.status],
253
251
  };
254
252
  }
255
253
 
@@ -78,7 +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 idle, the session card normal and `activeQueueId` unset, so a steer never joins it. Its usage still counts in `usage`: it is spent.
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.
82
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.
83
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.
84
84