@modelprofile.com/flexharness 10.1.0 → 10.2.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.
@@ -90,6 +90,7 @@ import type {
90
90
  IFlexSchedulePromptOptions,
91
91
  IFlexScopeSnapshot,
92
92
  IFlexSession,
93
+ IFlexSessionView,
93
94
  IFlexSessionPermissionPolicyContext,
94
95
  IFlexSessionActivity,
95
96
  IFlexSessionGeneration,
@@ -360,6 +361,11 @@ interface IStorageState {
360
361
  activeRuns: Map<string, IActiveRun>;
361
362
  delegatedRunAdmissionOwners: Set<IDelegatedRunAdmissionOwner>;
362
363
  pendingPermissions: Map<string, IPendingPermission>;
364
+ /**
365
+ * Per session, the open sections that change what a session view shows and emit the events
366
+ * describing it. A view waits until none is open, so it never shows a change without its event.
367
+ */
368
+ publications: Map<string, Set<Promise<void>>>;
363
369
  scopeQueue: Promise<void>;
364
370
  lifecycle: 'active' | 'retiring' | 'fenced' | 'retired';
365
371
  fenceInProgress: boolean;
@@ -652,6 +658,12 @@ type TEventDetails = Record<string, unknown> & {
652
658
 
653
659
  const maxScheduleDebounceMs = 24 * 60 * 60 * 1000;
654
660
  const maxBackgroundExecutions = 100;
661
+ /**
662
+ * Consecutive waits a session view makes for queued writes and open sections to settle. Each wait
663
+ * ends only when real work finishes, so the bound limits starvation by a session that never stops
664
+ * changing, not the time one slow save may take.
665
+ */
666
+ const maxSessionViewPasses = 64;
655
667
  const maxSubagentTaskDescriptionBytes = 256;
656
668
  const maxSubagentPromptBytes = 64 * 1024;
657
669
  const maxSubagentTaskIdBytes = 512;
@@ -1924,6 +1936,75 @@ export class FlexHarness<TScope = unknown> {
1924
1936
  }
1925
1937
  }
1926
1938
 
1939
+ /**
1940
+ * Reads the session's metadata, visible messages and pending permissions in one step, together
1941
+ * with the instance's event sequence they include. It waits until no queued projection or scope
1942
+ * write and no change that emits events for the session is in progress.
1943
+ */
1944
+ public async getSessionView(scopeId: string, sessionId: string): Promise<IFlexSessionView> {
1945
+ const { state } = await this.resolveState(scopeId);
1946
+ let stored = this.requireSession(state, sessionId);
1947
+ for (let pass = 0; ; pass++) {
1948
+ if (pass === maxSessionViewPasses) {
1949
+ throw new FlexHarnessSessionBusyError(
1950
+ sessionId,
1951
+ `kept changing through ${maxSessionViewPasses} consecutive waits for a settled view`,
1952
+ );
1953
+ }
1954
+ const scopeQueue = state.scopeQueue;
1955
+ const projectionQueue = stored.projectionQueue;
1956
+ const publications = [...(state.publications.get(sessionId) ?? [])];
1957
+ await Promise.all([scopeQueue, projectionQueue, ...publications]);
1958
+ this.assertStateAcceptingWork(state);
1959
+ stored = this.requireSession(state, sessionId);
1960
+ if (
1961
+ state.scopeQueue === scopeQueue
1962
+ && stored.projectionQueue === projectionQueue
1963
+ && !state.publications.get(sessionId)?.size
1964
+ ) break;
1965
+ }
1966
+ const run = state.activeRuns.get(sessionId);
1967
+ const messages = this.visibleMessages(stored).map((message) => (
1968
+ run && run.stored === stored && message.messageId === run.assistantMessageId
1969
+ && message.status === 'streaming'
1970
+ ? { ...message, parts: run.callbackParts }
1971
+ : message
1972
+ ));
1973
+ return publicSnapshot({
1974
+ session: stored.session,
1975
+ messages,
1976
+ pendingPermissions: [...state.pendingPermissions.values()]
1977
+ .map((pending) => pending.request)
1978
+ .filter((request) => request.sessionId === sessionId)
1979
+ .sort((left, right) => left.createdAt.localeCompare(right.createdAt)),
1980
+ sequence: this.sequence,
1981
+ });
1982
+ }
1983
+
1984
+ /**
1985
+ * Opens a section that changes what a view of the session shows and emits the events that
1986
+ * describe the change. Closing is idempotent; close it once the events are emitted or the
1987
+ * change is rolled back.
1988
+ */
1989
+ private openPublication(state: IStorageState, sessionId: string): () => void {
1990
+ let release!: () => void;
1991
+ const section = new Promise<void>((resolve) => {
1992
+ release = resolve;
1993
+ });
1994
+ let sections = state.publications.get(sessionId);
1995
+ if (!sections) {
1996
+ sections = new Set();
1997
+ state.publications.set(sessionId, sections);
1998
+ }
1999
+ sections.add(section);
2000
+ return () => {
2001
+ const current = state.publications.get(sessionId);
2002
+ if (!current?.delete(section)) return;
2003
+ if (current.size === 0) state.publications.delete(sessionId);
2004
+ release();
2005
+ };
2006
+ }
2007
+
1927
2008
  public async getMessages(scopeId: string, sessionId: string): Promise<IFlexMessage[]> {
1928
2009
  const { state } = await this.resolveState(scopeId);
1929
2010
  const stored = this.requireSession(state, sessionId);
@@ -3397,12 +3478,17 @@ export class FlexHarness<TScope = unknown> {
3397
3478
  .map((runId) => stored.reversionSegments.find((segment) => segment.runId === runId))
3398
3479
  .find((segment) => segment?.status === 'completed');
3399
3480
  if (!target) throw new FlexHarnessValidationError('Pending reversion has no target candidate.');
3400
- await this.resumePendingApply(state, stored, scopeId, scope, operationSignal);
3401
- this.emitEvent(scopeId, stored.session, {
3402
- type: 'session.history.changed',
3403
- direction,
3404
- runId: target.runId,
3405
- });
3481
+ const closePublication = this.openPublication(state, sessionId);
3482
+ try {
3483
+ await this.resumePendingApply(state, stored, scopeId, scope, operationSignal);
3484
+ this.emitEvent(scopeId, stored.session, {
3485
+ type: 'session.history.changed',
3486
+ direction,
3487
+ runId: target.runId,
3488
+ });
3489
+ } finally {
3490
+ closePublication();
3491
+ }
3406
3492
  return target.runId;
3407
3493
  }
3408
3494
  const unit = this.reversionUnit(stored, direction);
@@ -3442,18 +3528,23 @@ export class FlexHarness<TScope = unknown> {
3442
3528
  appliedRunIds: [],
3443
3529
  };
3444
3530
  }, true);
3445
- await this.resumePendingApply(
3446
- state,
3447
- stored,
3448
- scopeId,
3449
- scope,
3450
- operationSignal,
3451
- );
3452
- this.emitEvent(scopeId, stored.session, {
3453
- type: 'session.history.changed',
3454
- direction,
3455
- runId: unit.target.runId,
3456
- });
3531
+ const closePublication = this.openPublication(state, sessionId);
3532
+ try {
3533
+ await this.resumePendingApply(
3534
+ state,
3535
+ stored,
3536
+ scopeId,
3537
+ scope,
3538
+ operationSignal,
3539
+ );
3540
+ this.emitEvent(scopeId, stored.session, {
3541
+ type: 'session.history.changed',
3542
+ direction,
3543
+ runId: unit.target.runId,
3544
+ });
3545
+ } finally {
3546
+ closePublication();
3547
+ }
3457
3548
  return unit.target.runId;
3458
3549
  },
3459
3550
  );
@@ -4405,123 +4496,130 @@ export class FlexHarness<TScope = unknown> {
4405
4496
  const prompt = queued.prompt!;
4406
4497
  const options = queued.options!;
4407
4498
  let projectionReserved = false;
4499
+ // Open from the reservation until its messages are announced or rolled back.
4500
+ let closeReservationPublication = (): void => undefined;
4408
4501
  try {
4409
- this.assertPromptPromotion(queued, run);
4410
- await this.acquireDelegatedRunAdmission(queued, run);
4411
- this.assertPromptPromotion(queued, run);
4412
- await this.commitRevertedBranch(run.state, run.stored, run.scopeId, run.scope as TScope);
4413
- this.assertPromptPromotion(queued, run);
4414
- const generationPrompt = cloneSerializable(prompt.modelMessage.content) as Parameters<
4415
- plugins.IAgentSession['beginGeneration']
4416
- >[0];
4417
- this.assertPromptPromotion(queued, run);
4418
- run.transaction = await this.withRunCompactorContext(
4419
- run,
4420
- () => run.stored.agentSession.beginGeneration(
4421
- generationPrompt,
4422
- { generationId: run.runId },
4423
- ),
4424
- );
4425
- this.assertPromptPromotion(queued, run);
4426
- const reservation = this.createReservation(run, prompt);
4427
- await this.mutateProjection(run.state, run.stored, () => {
4428
- run.stored.messages.push(reservation.userMessage, reservation.assistantMessage);
4429
- if (run.stored.session.agent === undefined) {
4430
- this.pruneReversionState(run.stored, true);
4431
- const captureId = this.turnReversionProvider
4432
- ? this.reversionId('capture', run.state.storageKey, run.sessionId, run.runId)
4433
- : undefined;
4434
- const protocolVersion = this.reversionProviderProtocolVersion() ?? 1;
4435
- run.stored.reversionSegments.push({
4436
- runId: run.runId,
4437
- userMessageId: reservation.userMessage.messageId,
4438
- status: 'capturing',
4439
- contextAvailable: true,
4440
- eventIds: [],
4441
- workspaceCaptured: captureId !== undefined,
4442
- protocolVersion,
4443
- provenance: captureId === undefined ? 'transcript' : 'workspace',
4444
- ...(captureId === undefined ? {} : { disposition: 'pending' }),
4445
- ...(captureId === undefined ? {} : { captureId }),
4446
- });
4447
- if (captureId) {
4448
- run.stored.pendingReversion = {
4449
- kind: 'capture',
4502
+ try {
4503
+ this.assertPromptPromotion(queued, run);
4504
+ await this.acquireDelegatedRunAdmission(queued, run);
4505
+ this.assertPromptPromotion(queued, run);
4506
+ await this.commitRevertedBranch(run.state, run.stored, run.scopeId, run.scope as TScope);
4507
+ this.assertPromptPromotion(queued, run);
4508
+ const generationPrompt = cloneSerializable(prompt.modelMessage.content) as Parameters<
4509
+ plugins.IAgentSession['beginGeneration']
4510
+ >[0];
4511
+ this.assertPromptPromotion(queued, run);
4512
+ run.transaction = await this.withRunCompactorContext(
4513
+ run,
4514
+ () => run.stored.agentSession.beginGeneration(
4515
+ generationPrompt,
4516
+ { generationId: run.runId },
4517
+ ),
4518
+ );
4519
+ this.assertPromptPromotion(queued, run);
4520
+ const reservation = this.createReservation(run, prompt);
4521
+ closeReservationPublication = this.openPublication(run.state, run.sessionId);
4522
+ await this.mutateProjection(run.state, run.stored, () => {
4523
+ run.stored.messages.push(reservation.userMessage, reservation.assistantMessage);
4524
+ if (run.stored.session.agent === undefined) {
4525
+ this.pruneReversionState(run.stored, true);
4526
+ const captureId = this.turnReversionProvider
4527
+ ? this.reversionId('capture', run.state.storageKey, run.sessionId, run.runId)
4528
+ : undefined;
4529
+ const protocolVersion = this.reversionProviderProtocolVersion() ?? 1;
4530
+ run.stored.reversionSegments.push({
4450
4531
  runId: run.runId,
4451
- captureId,
4452
- state: 'preparing',
4532
+ userMessageId: reservation.userMessage.messageId,
4533
+ status: 'capturing',
4534
+ contextAvailable: true,
4535
+ eventIds: [],
4536
+ workspaceCaptured: captureId !== undefined,
4453
4537
  protocolVersion,
4454
- };
4538
+ provenance: captureId === undefined ? 'transcript' : 'workspace',
4539
+ ...(captureId === undefined ? {} : { disposition: 'pending' }),
4540
+ ...(captureId === undefined ? {} : { captureId }),
4541
+ });
4542
+ if (captureId) {
4543
+ run.stored.pendingReversion = {
4544
+ kind: 'capture',
4545
+ runId: run.runId,
4546
+ captureId,
4547
+ state: 'preparing',
4548
+ protocolVersion,
4549
+ };
4550
+ }
4551
+ }
4552
+ });
4553
+ projectionReserved = true;
4554
+ run.reservedUserMessage = publicSnapshot(reservation.userMessage);
4555
+ run.reservedAssistantMessage = publicSnapshot(reservation.assistantMessage);
4556
+ this.assertPromptPromotion(queued, run);
4557
+ await this.mutateScope(run.state, () => {
4558
+ if (run.controller.signal.aborted) {
4559
+ throw run.controller.signal.reason ?? new FlexHarnessAbortError();
4560
+ }
4561
+ if (run.state.tombstones.has(run.sessionId) || run.state.sessions.get(run.sessionId) !== run.stored) {
4562
+ throw new FlexHarnessAbortError('The session was deleted during prompt admission.');
4455
4563
  }
4564
+ run.stored.session.status = run.scheduleKey ? 'scheduled' : 'running';
4565
+ run.stored.session.activity = {
4566
+ runId: run.runId,
4567
+ status: run.scheduleKey ? 'scheduled' : 'running',
4568
+ startedAt: reservation.userMessage.createdAt,
4569
+ };
4570
+ run.stored.session.updatedAt = reservation.userMessage.createdAt;
4571
+ });
4572
+ } catch (error) {
4573
+ if (containsStoreCommitUncertainty(error)) run.state.lifecycle = 'fenced';
4574
+ const safeError = this.projectExternalError(run, error, projectionReserved ? 'persistence' : 'agentSession');
4575
+ let cleanupErrors: unknown[] = [];
4576
+ let cleanupFailure: unknown;
4577
+ try {
4578
+ await this.closeRunDelegatedRunAdmission(
4579
+ run,
4580
+ !run.delegatedRunAdmissionOwner?.acquisitionDetached,
4581
+ );
4582
+ } catch (closeError) {
4583
+ cleanupErrors.push(closeError);
4456
4584
  }
4457
- });
4458
- projectionReserved = true;
4459
- run.reservedUserMessage = publicSnapshot(reservation.userMessage);
4460
- run.reservedAssistantMessage = publicSnapshot(reservation.assistantMessage);
4461
- this.assertPromptPromotion(queued, run);
4462
- await this.mutateScope(run.state, () => {
4463
- if (run.controller.signal.aborted) {
4464
- throw run.controller.signal.reason ?? new FlexHarnessAbortError();
4585
+ try {
4586
+ cleanupErrors.push(...await this.rollbackAdmission(run, projectionReserved, safeError));
4587
+ } catch (rollbackError) {
4588
+ cleanupFailure = rollbackError;
4465
4589
  }
4466
- if (run.state.tombstones.has(run.sessionId) || run.state.sessions.get(run.sessionId) !== run.stored) {
4467
- throw new FlexHarnessAbortError('The session was deleted during prompt admission.');
4590
+ if (cleanupFailure !== undefined && cleanupErrors.length > 0) {
4591
+ cleanupFailure = combineErrors([cleanupFailure, ...cleanupErrors]);
4468
4592
  }
4469
- run.stored.session.status = run.scheduleKey ? 'scheduled' : 'running';
4470
- run.stored.session.activity = {
4471
- runId: run.runId,
4472
- status: run.scheduleKey ? 'scheduled' : 'running',
4473
- startedAt: reservation.userMessage.createdAt,
4474
- };
4475
- run.stored.session.updatedAt = reservation.userMessage.createdAt;
4476
- });
4477
- } catch (error) {
4478
- if (containsStoreCommitUncertainty(error)) run.state.lifecycle = 'fenced';
4479
- const safeError = this.projectExternalError(run, error, projectionReserved ? 'persistence' : 'agentSession');
4480
- let cleanupErrors: unknown[] = [];
4481
- let cleanupFailure: unknown;
4482
- try {
4483
- await this.closeRunDelegatedRunAdmission(
4484
- run,
4485
- !run.delegatedRunAdmissionOwner?.acquisitionDetached,
4486
- );
4487
- } catch (closeError) {
4488
- cleanupErrors.push(closeError);
4489
- }
4490
- try {
4491
- cleanupErrors.push(...await this.rollbackAdmission(run, projectionReserved, safeError));
4492
- } catch (rollbackError) {
4493
- cleanupFailure = rollbackError;
4494
- }
4495
- if (cleanupFailure !== undefined && cleanupErrors.length > 0) {
4496
- cleanupFailure = combineErrors([cleanupFailure, ...cleanupErrors]);
4497
- }
4498
- if (run.state.lifecycle === 'fenced' && cleanupFailure === undefined) {
4499
- const combined = combineErrors([safeError, ...cleanupErrors]);
4500
- try {
4501
- await this.fenceNamespace(run.state, run, combined);
4502
- } catch (fenceError) {
4503
- cleanupFailure = fenceError;
4593
+ if (run.state.lifecycle === 'fenced' && cleanupFailure === undefined) {
4594
+ const combined = combineErrors([safeError, ...cleanupErrors]);
4595
+ try {
4596
+ await this.fenceNamespace(run.state, run, combined);
4597
+ } catch (fenceError) {
4598
+ cleanupFailure = fenceError;
4599
+ }
4504
4600
  }
4601
+ const rejection = cleanupFailure
4602
+ ?? (cleanupErrors.length > 0 ? combineErrors([safeError, ...cleanupErrors]) : safeError);
4603
+ throw rejection;
4505
4604
  }
4506
- const rejection = cleanupFailure
4507
- ?? (cleanupErrors.length > 0 ? combineErrors([safeError, ...cleanupErrors]) : safeError);
4508
- throw rejection;
4509
- }
4510
4605
 
4511
- const runType = run.scheduleKey ? 'run.scheduled' : 'run.started';
4512
- this.emitEvent(run.scopeId, run.stored.session, {
4513
- type: runType,
4514
- runId: run.runId,
4515
- messageId: run.userMessageId,
4516
- session: publicSnapshot(run.stored.session),
4517
- });
4518
- for (const message of [run.reservedUserMessage!, run.reservedAssistantMessage!]) {
4606
+ const runType = run.scheduleKey ? 'run.scheduled' : 'run.started';
4519
4607
  this.emitEvent(run.scopeId, run.stored.session, {
4520
- type: 'message.created',
4608
+ type: runType,
4521
4609
  runId: run.runId,
4522
- messageId: message.messageId,
4523
- message,
4610
+ messageId: run.userMessageId,
4611
+ session: publicSnapshot(run.stored.session),
4524
4612
  });
4613
+ for (const message of [run.reservedUserMessage!, run.reservedAssistantMessage!]) {
4614
+ this.emitEvent(run.scopeId, run.stored.session, {
4615
+ type: 'message.created',
4616
+ runId: run.runId,
4617
+ messageId: message.messageId,
4618
+ message,
4619
+ });
4620
+ }
4621
+ } finally {
4622
+ closeReservationPublication();
4525
4623
  }
4526
4624
 
4527
4625
  queued.status = run.scheduleKey ? 'scheduled' : 'starting';
@@ -4729,15 +4827,20 @@ export class FlexHarness<TScope = unknown> {
4729
4827
  run.steering.keptInContext = true;
4730
4828
  this.refreshAutomaticArchiveHorizon(run.stored);
4731
4829
  run.phase = 'promoting';
4830
+ const closePublication = this.openPublication(run.state, run.sessionId);
4732
4831
  try {
4733
- await this.promoteCompletedTerminal(run, terminal);
4734
- } catch (error) {
4735
- const projected = this.projectExternalError(run, error, 'persistence');
4736
- await this.fenceNamespace(run.state, run, projected);
4737
- throw projected;
4832
+ try {
4833
+ await this.promoteCompletedTerminal(run, terminal);
4834
+ } catch (error) {
4835
+ const projected = this.projectExternalError(run, error, 'persistence');
4836
+ await this.fenceNamespace(run.state, run, projected);
4837
+ throw projected;
4838
+ }
4839
+ this.emitTerminalProjection(run, terminal);
4840
+ this.emitFinalRunEvent(run, terminal.assistantMessage, undefined, false);
4841
+ } finally {
4842
+ closePublication();
4738
4843
  }
4739
- this.emitTerminalProjection(run, terminal);
4740
- this.emitFinalRunEvent(run, terminal.assistantMessage, undefined, false);
4741
4844
  await this.drainReversionReleases(
4742
4845
  run.state,
4743
4846
  run.stored,
@@ -4858,16 +4961,21 @@ export class FlexHarness<TScope = unknown> {
4858
4961
  : safeError;
4859
4962
  const terminal = this.buildTerminal(run, publicStatus, undefined, terminalError);
4860
4963
  if (!this.isTombstoned(run)) {
4964
+ const closePublication = this.openPublication(run.state, run.sessionId);
4861
4965
  try {
4862
- await this.publishFailedTerminal(run, terminal);
4863
- } catch (persistenceError) {
4864
- errors.push(this.projectExternalError(run, persistenceError, 'persistence'));
4865
- const combined = combineErrors(errors);
4866
- await this.fenceNamespace(run.state, run, combined);
4867
- throw combined;
4966
+ try {
4967
+ await this.publishFailedTerminal(run, terminal);
4968
+ } catch (persistenceError) {
4969
+ errors.push(this.projectExternalError(run, persistenceError, 'persistence'));
4970
+ const combined = combineErrors(errors);
4971
+ await this.fenceNamespace(run.state, run, combined);
4972
+ throw combined;
4973
+ }
4974
+ this.emitTerminalProjection(run, terminal);
4975
+ this.emitFinalRunEvent(run, terminal.assistantMessage, terminalError, publicStatus === 'cancelled');
4976
+ } finally {
4977
+ closePublication();
4868
4978
  }
4869
- this.emitTerminalProjection(run, terminal);
4870
- this.emitFinalRunEvent(run, terminal.assistantMessage, terminalError, publicStatus === 'cancelled');
4871
4979
  }
4872
4980
  await this.drainReversionReleases(
4873
4981
  run.state,
@@ -6561,61 +6669,66 @@ export class FlexHarness<TScope = unknown> {
6561
6669
  parts: [],
6562
6670
  }
6563
6671
  : undefined;
6564
- await this.mutateProjection(run.state, run.stored, () => {
6565
- const index = run.stored.messages.findIndex((message) => message.messageId === answeredAssistantId);
6566
- if (index < 0) throw new Error('The steered run has no projected assistant message.');
6567
- if (!nextAssistant) {
6568
- run.stored.messages.splice(index, 0, cloneSerializable(steerMessage));
6569
- return;
6672
+ const closePublication = this.openPublication(run.state, run.sessionId);
6673
+ try {
6674
+ await this.mutateProjection(run.state, run.stored, () => {
6675
+ const index = run.stored.messages.findIndex((message) => message.messageId === answeredAssistantId);
6676
+ if (index < 0) throw new Error('The steered run has no projected assistant message.');
6677
+ if (!nextAssistant) {
6678
+ run.stored.messages.splice(index, 0, cloneSerializable(steerMessage));
6679
+ return;
6680
+ }
6681
+ completedAssistant = {
6682
+ ...cloneSerializable(run.stored.messages[index]),
6683
+ status: 'completed',
6684
+ completedAt: timestamp,
6685
+ parts: cloneSerializable(run.callbackParts),
6686
+ ...(run.modelResolution ? { model: cloneSerializable(run.modelResolution.identity) } : {}),
6687
+ };
6688
+ run.stored.messages.splice(
6689
+ index,
6690
+ 1,
6691
+ cloneSerializable(completedAssistant),
6692
+ cloneSerializable(steerMessage),
6693
+ cloneSerializable(nextAssistant),
6694
+ );
6695
+ });
6696
+ if (completedAssistant && nextAssistant) {
6697
+ run.assistantMessageId = nextAssistant.messageId;
6698
+ run.reservedAssistantMessage = publicSnapshot(nextAssistant);
6699
+ run.callbackParts = [];
6700
+ run.reasoningPartIds.clear();
6701
+ this.emitEvent(run.scopeId, run.stored.session, {
6702
+ type: 'message.updated',
6703
+ runId: run.runId,
6704
+ messageId: completedAssistant.messageId,
6705
+ message: publicSnapshot(completedAssistant),
6706
+ });
6570
6707
  }
6571
- completedAssistant = {
6572
- ...cloneSerializable(run.stored.messages[index]),
6573
- status: 'completed',
6574
- completedAt: timestamp,
6575
- parts: cloneSerializable(run.callbackParts),
6576
- ...(run.modelResolution ? { model: cloneSerializable(run.modelResolution.identity) } : {}),
6577
- };
6578
- run.stored.messages.splice(
6579
- index,
6580
- 1,
6581
- cloneSerializable(completedAssistant),
6582
- cloneSerializable(steerMessage),
6583
- cloneSerializable(nextAssistant),
6584
- );
6585
- });
6586
- if (completedAssistant && nextAssistant) {
6587
- run.assistantMessageId = nextAssistant.messageId;
6588
- run.reservedAssistantMessage = publicSnapshot(nextAssistant);
6589
- run.callbackParts = [];
6590
- run.reasoningPartIds.clear();
6591
6708
  this.emitEvent(run.scopeId, run.stored.session, {
6592
- type: 'message.updated',
6709
+ type: 'message.created',
6593
6710
  runId: run.runId,
6594
- messageId: completedAssistant.messageId,
6595
- message: publicSnapshot(completedAssistant),
6711
+ messageId: steerMessage.messageId,
6712
+ message: publicSnapshot(steerMessage),
6596
6713
  });
6597
- }
6598
- this.emitEvent(run.scopeId, run.stored.session, {
6599
- type: 'message.created',
6600
- runId: run.runId,
6601
- messageId: steerMessage.messageId,
6602
- message: publicSnapshot(steerMessage),
6603
- });
6604
- if (nextAssistant) {
6714
+ if (nextAssistant) {
6715
+ this.emitEvent(run.scopeId, run.stored.session, {
6716
+ type: 'message.created',
6717
+ runId: run.runId,
6718
+ messageId: nextAssistant.messageId,
6719
+ message: publicSnapshot(nextAssistant),
6720
+ });
6721
+ }
6605
6722
  this.emitEvent(run.scopeId, run.stored.session, {
6606
- type: 'message.created',
6723
+ type: 'prompt.steer.applied',
6724
+ queueId: run.queueId,
6607
6725
  runId: run.runId,
6608
- messageId: nextAssistant.messageId,
6609
- message: publicSnapshot(nextAssistant),
6726
+ steerId: applied.inputId,
6727
+ messageId: steerMessage.messageId,
6610
6728
  });
6729
+ } finally {
6730
+ closePublication();
6611
6731
  }
6612
- this.emitEvent(run.scopeId, run.stored.session, {
6613
- type: 'prompt.steer.applied',
6614
- queueId: run.queueId,
6615
- runId: run.runId,
6616
- steerId: applied.inputId,
6617
- messageId: steerMessage.messageId,
6618
- });
6619
6732
  }
6620
6733
 
6621
6734
  private buildTerminal(
@@ -8255,28 +8368,33 @@ export class FlexHarness<TScope = unknown> {
8255
8368
  responseQueue: Promise.resolve(),
8256
8369
  abortListener,
8257
8370
  };
8258
- state.pendingPermissions.set(request.permissionId, pending);
8259
- run.pendingPermissionIds.add(request.permissionId);
8260
- run.controller.signal.addEventListener('abort', abortListener, { once: true });
8371
+ const closePublication = this.openPublication(state, run.sessionId);
8261
8372
  try {
8262
- await this.mutateScope(state, () => {
8263
- if (state.sessions.get(run.sessionId) !== run.stored) {
8264
- throw new FlexHarnessAbortError('The session was deleted while permission was pending.');
8265
- }
8266
- run.stored.session.status = 'waiting_permission';
8267
- run.stored.session.activity.status = 'waiting_permission';
8268
- run.stored.session.updatedAt = new Date().toISOString();
8269
- }, true);
8270
- } catch (error) {
8271
- this.forceRejectPending(state, pending, this.projectExternalError(run, error, 'persistence'));
8272
- return promise;
8273
- }
8274
- if (!pending.settled) {
8275
- this.emitEvent(run.scopeId, run.stored.session, {
8276
- type: 'permission.requested',
8277
- runId: run.runId,
8278
- request: publicSnapshot(request),
8279
- });
8373
+ state.pendingPermissions.set(request.permissionId, pending);
8374
+ run.pendingPermissionIds.add(request.permissionId);
8375
+ run.controller.signal.addEventListener('abort', abortListener, { once: true });
8376
+ try {
8377
+ await this.mutateScope(state, () => {
8378
+ if (state.sessions.get(run.sessionId) !== run.stored) {
8379
+ throw new FlexHarnessAbortError('The session was deleted while permission was pending.');
8380
+ }
8381
+ run.stored.session.status = 'waiting_permission';
8382
+ run.stored.session.activity.status = 'waiting_permission';
8383
+ run.stored.session.updatedAt = new Date().toISOString();
8384
+ }, true);
8385
+ } catch (error) {
8386
+ this.forceRejectPending(state, pending, this.projectExternalError(run, error, 'persistence'));
8387
+ return promise;
8388
+ }
8389
+ if (!pending.settled) {
8390
+ this.emitEvent(run.scopeId, run.stored.session, {
8391
+ type: 'permission.requested',
8392
+ runId: run.runId,
8393
+ request: publicSnapshot(request),
8394
+ });
8395
+ }
8396
+ } finally {
8397
+ closePublication();
8280
8398
  }
8281
8399
  return promise;
8282
8400
  }
@@ -9085,6 +9203,7 @@ export class FlexHarness<TScope = unknown> {
9085
9203
  activeRuns: new Map(),
9086
9204
  delegatedRunAdmissionOwners: new Set(),
9087
9205
  pendingPermissions: new Map(),
9206
+ publications: new Map(),
9088
9207
  scopeQueue: Promise.resolve(),
9089
9208
  lifecycle: 'active',
9090
9209
  fenceInProgress: false,
package/ts/interfaces.ts CHANGED
@@ -513,6 +513,25 @@ export interface IFlexMessagePage {
513
513
  nextCursor?: string;
514
514
  }
515
515
 
516
+ /**
517
+ * A session as the events up to `sequence` describe it, read in one step. Replaying the events of
518
+ * the same harness instance after `sequence` onto it gives the session's later states.
519
+ */
520
+ export interface IFlexSessionView {
521
+ /** Current metadata. Its status can be ahead of `sequence`: status changes emit no event. */
522
+ session: IFlexSession;
523
+ /** The visible messages, including what a running turn has streamed so far. */
524
+ messages: IFlexMessage[];
525
+ /**
526
+ * The session's pending permission requests, oldest first. A request its run leaves unanswered,
527
+ * because the run was stopped or failed, leaves this list without an event of its own; the run's
528
+ * `run.finished` follows.
529
+ */
530
+ pendingPermissions: IFlexPermissionRequest[];
531
+ /** The `sequence` of the instance's last event this view includes; 0 before the first event. */
532
+ sequence: number;
533
+ }
534
+
516
535
  export interface IFlexResolvedScope<TScope> {
517
536
  storageKey: string;
518
537
  scope: TScope;