@modelprofile.com/flexharness 10.1.0 → 10.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.
@@ -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);
@@ -3425,13 +3511,22 @@ export class FlexHarness<TScope = unknown> {
3425
3511
  'The turn has no available workspace reversion capture.',
3426
3512
  );
3427
3513
  }
3428
- const operationId = this.reversionId(
3429
- 'apply',
3430
- state.storageKey,
3431
- sessionId,
3432
- JSON.stringify([direction, stored.revertCursor, unit.toCursor, unit.target.runId]),
3433
- );
3434
3514
  await this.mutateProjection(state, stored, () => {
3515
+ // The write-ahead record commits at exactly projectionRevision + 1, so the
3516
+ // revision it replaces makes this apply's ID unique. Retries and recovery
3517
+ // read the persisted ID instead of deriving it again.
3518
+ const operationId = this.reversionId(
3519
+ 'apply',
3520
+ state.storageKey,
3521
+ sessionId,
3522
+ JSON.stringify([
3523
+ direction,
3524
+ stored.revertCursor,
3525
+ unit.toCursor,
3526
+ unit.target.runId,
3527
+ stored.projectionRevision,
3528
+ ]),
3529
+ );
3435
3530
  stored.pendingReversion = {
3436
3531
  kind: 'apply',
3437
3532
  operationId,
@@ -3442,18 +3537,23 @@ export class FlexHarness<TScope = unknown> {
3442
3537
  appliedRunIds: [],
3443
3538
  };
3444
3539
  }, 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
- });
3540
+ const closePublication = this.openPublication(state, sessionId);
3541
+ try {
3542
+ await this.resumePendingApply(
3543
+ state,
3544
+ stored,
3545
+ scopeId,
3546
+ scope,
3547
+ operationSignal,
3548
+ );
3549
+ this.emitEvent(scopeId, stored.session, {
3550
+ type: 'session.history.changed',
3551
+ direction,
3552
+ runId: unit.target.runId,
3553
+ });
3554
+ } finally {
3555
+ closePublication();
3556
+ }
3457
3557
  return unit.target.runId;
3458
3558
  },
3459
3559
  );
@@ -4405,123 +4505,130 @@ export class FlexHarness<TScope = unknown> {
4405
4505
  const prompt = queued.prompt!;
4406
4506
  const options = queued.options!;
4407
4507
  let projectionReserved = false;
4508
+ // Open from the reservation until its messages are announced or rolled back.
4509
+ let closeReservationPublication = (): void => undefined;
4408
4510
  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',
4511
+ try {
4512
+ this.assertPromptPromotion(queued, run);
4513
+ await this.acquireDelegatedRunAdmission(queued, run);
4514
+ this.assertPromptPromotion(queued, run);
4515
+ await this.commitRevertedBranch(run.state, run.stored, run.scopeId, run.scope as TScope);
4516
+ this.assertPromptPromotion(queued, run);
4517
+ const generationPrompt = cloneSerializable(prompt.modelMessage.content) as Parameters<
4518
+ plugins.IAgentSession['beginGeneration']
4519
+ >[0];
4520
+ this.assertPromptPromotion(queued, run);
4521
+ run.transaction = await this.withRunCompactorContext(
4522
+ run,
4523
+ () => run.stored.agentSession.beginGeneration(
4524
+ generationPrompt,
4525
+ { generationId: run.runId },
4526
+ ),
4527
+ );
4528
+ this.assertPromptPromotion(queued, run);
4529
+ const reservation = this.createReservation(run, prompt);
4530
+ closeReservationPublication = this.openPublication(run.state, run.sessionId);
4531
+ await this.mutateProjection(run.state, run.stored, () => {
4532
+ run.stored.messages.push(reservation.userMessage, reservation.assistantMessage);
4533
+ if (run.stored.session.agent === undefined) {
4534
+ this.pruneReversionState(run.stored, true);
4535
+ const captureId = this.turnReversionProvider
4536
+ ? this.reversionId('capture', run.state.storageKey, run.sessionId, run.runId)
4537
+ : undefined;
4538
+ const protocolVersion = this.reversionProviderProtocolVersion() ?? 1;
4539
+ run.stored.reversionSegments.push({
4450
4540
  runId: run.runId,
4451
- captureId,
4452
- state: 'preparing',
4541
+ userMessageId: reservation.userMessage.messageId,
4542
+ status: 'capturing',
4543
+ contextAvailable: true,
4544
+ eventIds: [],
4545
+ workspaceCaptured: captureId !== undefined,
4453
4546
  protocolVersion,
4454
- };
4547
+ provenance: captureId === undefined ? 'transcript' : 'workspace',
4548
+ ...(captureId === undefined ? {} : { disposition: 'pending' }),
4549
+ ...(captureId === undefined ? {} : { captureId }),
4550
+ });
4551
+ if (captureId) {
4552
+ run.stored.pendingReversion = {
4553
+ kind: 'capture',
4554
+ runId: run.runId,
4555
+ captureId,
4556
+ state: 'preparing',
4557
+ protocolVersion,
4558
+ };
4559
+ }
4455
4560
  }
4561
+ });
4562
+ projectionReserved = true;
4563
+ run.reservedUserMessage = publicSnapshot(reservation.userMessage);
4564
+ run.reservedAssistantMessage = publicSnapshot(reservation.assistantMessage);
4565
+ this.assertPromptPromotion(queued, run);
4566
+ await this.mutateScope(run.state, () => {
4567
+ if (run.controller.signal.aborted) {
4568
+ throw run.controller.signal.reason ?? new FlexHarnessAbortError();
4569
+ }
4570
+ if (run.state.tombstones.has(run.sessionId) || run.state.sessions.get(run.sessionId) !== run.stored) {
4571
+ throw new FlexHarnessAbortError('The session was deleted during prompt admission.');
4572
+ }
4573
+ run.stored.session.status = run.scheduleKey ? 'scheduled' : 'running';
4574
+ run.stored.session.activity = {
4575
+ runId: run.runId,
4576
+ status: run.scheduleKey ? 'scheduled' : 'running',
4577
+ startedAt: reservation.userMessage.createdAt,
4578
+ };
4579
+ run.stored.session.updatedAt = reservation.userMessage.createdAt;
4580
+ });
4581
+ } catch (error) {
4582
+ if (containsStoreCommitUncertainty(error)) run.state.lifecycle = 'fenced';
4583
+ const safeError = this.projectExternalError(run, error, projectionReserved ? 'persistence' : 'agentSession');
4584
+ let cleanupErrors: unknown[] = [];
4585
+ let cleanupFailure: unknown;
4586
+ try {
4587
+ await this.closeRunDelegatedRunAdmission(
4588
+ run,
4589
+ !run.delegatedRunAdmissionOwner?.acquisitionDetached,
4590
+ );
4591
+ } catch (closeError) {
4592
+ cleanupErrors.push(closeError);
4456
4593
  }
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();
4594
+ try {
4595
+ cleanupErrors.push(...await this.rollbackAdmission(run, projectionReserved, safeError));
4596
+ } catch (rollbackError) {
4597
+ cleanupFailure = rollbackError;
4465
4598
  }
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.');
4599
+ if (cleanupFailure !== undefined && cleanupErrors.length > 0) {
4600
+ cleanupFailure = combineErrors([cleanupFailure, ...cleanupErrors]);
4468
4601
  }
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;
4602
+ if (run.state.lifecycle === 'fenced' && cleanupFailure === undefined) {
4603
+ const combined = combineErrors([safeError, ...cleanupErrors]);
4604
+ try {
4605
+ await this.fenceNamespace(run.state, run, combined);
4606
+ } catch (fenceError) {
4607
+ cleanupFailure = fenceError;
4608
+ }
4504
4609
  }
4610
+ const rejection = cleanupFailure
4611
+ ?? (cleanupErrors.length > 0 ? combineErrors([safeError, ...cleanupErrors]) : safeError);
4612
+ throw rejection;
4505
4613
  }
4506
- const rejection = cleanupFailure
4507
- ?? (cleanupErrors.length > 0 ? combineErrors([safeError, ...cleanupErrors]) : safeError);
4508
- throw rejection;
4509
- }
4510
4614
 
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!]) {
4615
+ const runType = run.scheduleKey ? 'run.scheduled' : 'run.started';
4519
4616
  this.emitEvent(run.scopeId, run.stored.session, {
4520
- type: 'message.created',
4617
+ type: runType,
4521
4618
  runId: run.runId,
4522
- messageId: message.messageId,
4523
- message,
4619
+ messageId: run.userMessageId,
4620
+ session: publicSnapshot(run.stored.session),
4524
4621
  });
4622
+ for (const message of [run.reservedUserMessage!, run.reservedAssistantMessage!]) {
4623
+ this.emitEvent(run.scopeId, run.stored.session, {
4624
+ type: 'message.created',
4625
+ runId: run.runId,
4626
+ messageId: message.messageId,
4627
+ message,
4628
+ });
4629
+ }
4630
+ } finally {
4631
+ closeReservationPublication();
4525
4632
  }
4526
4633
 
4527
4634
  queued.status = run.scheduleKey ? 'scheduled' : 'starting';
@@ -4729,15 +4836,20 @@ export class FlexHarness<TScope = unknown> {
4729
4836
  run.steering.keptInContext = true;
4730
4837
  this.refreshAutomaticArchiveHorizon(run.stored);
4731
4838
  run.phase = 'promoting';
4839
+ const closePublication = this.openPublication(run.state, run.sessionId);
4732
4840
  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;
4841
+ try {
4842
+ await this.promoteCompletedTerminal(run, terminal);
4843
+ } catch (error) {
4844
+ const projected = this.projectExternalError(run, error, 'persistence');
4845
+ await this.fenceNamespace(run.state, run, projected);
4846
+ throw projected;
4847
+ }
4848
+ this.emitTerminalProjection(run, terminal);
4849
+ this.emitFinalRunEvent(run, terminal.assistantMessage, undefined, false);
4850
+ } finally {
4851
+ closePublication();
4738
4852
  }
4739
- this.emitTerminalProjection(run, terminal);
4740
- this.emitFinalRunEvent(run, terminal.assistantMessage, undefined, false);
4741
4853
  await this.drainReversionReleases(
4742
4854
  run.state,
4743
4855
  run.stored,
@@ -4858,16 +4970,21 @@ export class FlexHarness<TScope = unknown> {
4858
4970
  : safeError;
4859
4971
  const terminal = this.buildTerminal(run, publicStatus, undefined, terminalError);
4860
4972
  if (!this.isTombstoned(run)) {
4973
+ const closePublication = this.openPublication(run.state, run.sessionId);
4861
4974
  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;
4975
+ try {
4976
+ await this.publishFailedTerminal(run, terminal);
4977
+ } catch (persistenceError) {
4978
+ errors.push(this.projectExternalError(run, persistenceError, 'persistence'));
4979
+ const combined = combineErrors(errors);
4980
+ await this.fenceNamespace(run.state, run, combined);
4981
+ throw combined;
4982
+ }
4983
+ this.emitTerminalProjection(run, terminal);
4984
+ this.emitFinalRunEvent(run, terminal.assistantMessage, terminalError, publicStatus === 'cancelled');
4985
+ } finally {
4986
+ closePublication();
4868
4987
  }
4869
- this.emitTerminalProjection(run, terminal);
4870
- this.emitFinalRunEvent(run, terminal.assistantMessage, terminalError, publicStatus === 'cancelled');
4871
4988
  }
4872
4989
  await this.drainReversionReleases(
4873
4990
  run.state,
@@ -6561,61 +6678,66 @@ export class FlexHarness<TScope = unknown> {
6561
6678
  parts: [],
6562
6679
  }
6563
6680
  : 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;
6681
+ const closePublication = this.openPublication(run.state, run.sessionId);
6682
+ try {
6683
+ await this.mutateProjection(run.state, run.stored, () => {
6684
+ const index = run.stored.messages.findIndex((message) => message.messageId === answeredAssistantId);
6685
+ if (index < 0) throw new Error('The steered run has no projected assistant message.');
6686
+ if (!nextAssistant) {
6687
+ run.stored.messages.splice(index, 0, cloneSerializable(steerMessage));
6688
+ return;
6689
+ }
6690
+ completedAssistant = {
6691
+ ...cloneSerializable(run.stored.messages[index]),
6692
+ status: 'completed',
6693
+ completedAt: timestamp,
6694
+ parts: cloneSerializable(run.callbackParts),
6695
+ ...(run.modelResolution ? { model: cloneSerializable(run.modelResolution.identity) } : {}),
6696
+ };
6697
+ run.stored.messages.splice(
6698
+ index,
6699
+ 1,
6700
+ cloneSerializable(completedAssistant),
6701
+ cloneSerializable(steerMessage),
6702
+ cloneSerializable(nextAssistant),
6703
+ );
6704
+ });
6705
+ if (completedAssistant && nextAssistant) {
6706
+ run.assistantMessageId = nextAssistant.messageId;
6707
+ run.reservedAssistantMessage = publicSnapshot(nextAssistant);
6708
+ run.callbackParts = [];
6709
+ run.reasoningPartIds.clear();
6710
+ this.emitEvent(run.scopeId, run.stored.session, {
6711
+ type: 'message.updated',
6712
+ runId: run.runId,
6713
+ messageId: completedAssistant.messageId,
6714
+ message: publicSnapshot(completedAssistant),
6715
+ });
6570
6716
  }
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
6717
  this.emitEvent(run.scopeId, run.stored.session, {
6592
- type: 'message.updated',
6718
+ type: 'message.created',
6593
6719
  runId: run.runId,
6594
- messageId: completedAssistant.messageId,
6595
- message: publicSnapshot(completedAssistant),
6720
+ messageId: steerMessage.messageId,
6721
+ message: publicSnapshot(steerMessage),
6596
6722
  });
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) {
6723
+ if (nextAssistant) {
6724
+ this.emitEvent(run.scopeId, run.stored.session, {
6725
+ type: 'message.created',
6726
+ runId: run.runId,
6727
+ messageId: nextAssistant.messageId,
6728
+ message: publicSnapshot(nextAssistant),
6729
+ });
6730
+ }
6605
6731
  this.emitEvent(run.scopeId, run.stored.session, {
6606
- type: 'message.created',
6732
+ type: 'prompt.steer.applied',
6733
+ queueId: run.queueId,
6607
6734
  runId: run.runId,
6608
- messageId: nextAssistant.messageId,
6609
- message: publicSnapshot(nextAssistant),
6735
+ steerId: applied.inputId,
6736
+ messageId: steerMessage.messageId,
6610
6737
  });
6738
+ } finally {
6739
+ closePublication();
6611
6740
  }
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
6741
  }
6620
6742
 
6621
6743
  private buildTerminal(
@@ -8255,28 +8377,33 @@ export class FlexHarness<TScope = unknown> {
8255
8377
  responseQueue: Promise.resolve(),
8256
8378
  abortListener,
8257
8379
  };
8258
- state.pendingPermissions.set(request.permissionId, pending);
8259
- run.pendingPermissionIds.add(request.permissionId);
8260
- run.controller.signal.addEventListener('abort', abortListener, { once: true });
8380
+ const closePublication = this.openPublication(state, run.sessionId);
8261
8381
  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
- });
8382
+ state.pendingPermissions.set(request.permissionId, pending);
8383
+ run.pendingPermissionIds.add(request.permissionId);
8384
+ run.controller.signal.addEventListener('abort', abortListener, { once: true });
8385
+ try {
8386
+ await this.mutateScope(state, () => {
8387
+ if (state.sessions.get(run.sessionId) !== run.stored) {
8388
+ throw new FlexHarnessAbortError('The session was deleted while permission was pending.');
8389
+ }
8390
+ run.stored.session.status = 'waiting_permission';
8391
+ run.stored.session.activity.status = 'waiting_permission';
8392
+ run.stored.session.updatedAt = new Date().toISOString();
8393
+ }, true);
8394
+ } catch (error) {
8395
+ this.forceRejectPending(state, pending, this.projectExternalError(run, error, 'persistence'));
8396
+ return promise;
8397
+ }
8398
+ if (!pending.settled) {
8399
+ this.emitEvent(run.scopeId, run.stored.session, {
8400
+ type: 'permission.requested',
8401
+ runId: run.runId,
8402
+ request: publicSnapshot(request),
8403
+ });
8404
+ }
8405
+ } finally {
8406
+ closePublication();
8280
8407
  }
8281
8408
  return promise;
8282
8409
  }
@@ -9085,6 +9212,7 @@ export class FlexHarness<TScope = unknown> {
9085
9212
  activeRuns: new Map(),
9086
9213
  delegatedRunAdmissionOwners: new Set(),
9087
9214
  pendingPermissions: new Map(),
9215
+ publications: new Map(),
9088
9216
  scopeQueue: Promise.resolve(),
9089
9217
  lifecycle: 'active',
9090
9218
  fenceInProgress: false,