@timurproko/a1 0.1.8-dev.322 → 0.1.8-dev.335

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +22 -11
  2. package/dist/cli/dispatch.js +1 -1
  3. package/dist/features/owned-ui/project-trust-prompt.js +1 -1
  4. package/dist/features/owned-ui/settings-app.js +1 -1
  5. package/dist/features/prompt-history/service.d.ts +31 -1
  6. package/dist/features/prompt-history/service.js +294 -129
  7. package/dist/features/prompt-history/store.d.ts +2 -1
  8. package/dist/features/prompt-history/store.js +17 -4
  9. package/dist/features/prompt-history/worker.js +6 -3
  10. package/dist/foundation/launch-context/index.d.ts +36 -6
  11. package/dist/foundation/launch-context/index.js +64 -11
  12. package/dist/foundation/launch-guardian/main.js +2 -1
  13. package/dist/foundation/release/bootstrap.d.ts +7 -0
  14. package/dist/foundation/release/bootstrap.js +95 -19
  15. package/dist/foundation/release/cohort-state.js +0 -3
  16. package/dist/foundation/release/dependency-certification-retention.d.ts +7 -0
  17. package/dist/foundation/release/dependency-certification-retention.js +88 -0
  18. package/dist/foundation/release/dependency-certification.d.ts +24 -0
  19. package/dist/foundation/release/dependency-certification.js +208 -0
  20. package/dist/foundation/release/dependency-layer.d.ts +3 -4
  21. package/dist/foundation/release/dependency-layer.js +6 -37
  22. package/dist/foundation/release/index.d.ts +1 -0
  23. package/dist/foundation/release/index.js +1 -0
  24. package/dist/foundation/release/release-gc.js +72 -44
  25. package/dist/foundation/release/release-store.d.ts +2 -0
  26. package/dist/foundation/release/release-store.js +4 -3
  27. package/dist/foundation/release/restart-certification.js +16 -5
  28. package/dist/foundation/release/update-launch.d.ts +6 -0
  29. package/dist/foundation/release/update-launch.js +17 -0
  30. package/dist/foundation/release/update.d.ts +2 -0
  31. package/dist/foundation/release/update.js +21 -27
  32. package/dist/foundation/release/warmup.js +16 -5
  33. package/dist/foundation/supervision/main.js +5 -2
  34. package/dist/foundation/supervision/server.js +40 -13
  35. package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
  36. package/dist/integrations/pi/components/owned-editor-ux.js +51 -27
  37. package/dist/integrations/pi/engine/adapter.d.ts +20 -0
  38. package/dist/integrations/pi/engine/adapter.js +223 -87
  39. package/dist/integrations/pi/engine/pending-delivery.d.ts +26 -0
  40. package/dist/integrations/pi/engine/pending-delivery.js +149 -0
  41. package/dist/integrations/pi/session-ui/prompt-chips.js +38 -25
  42. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +0 -2
  43. package/dist/integrations/pi/session-ui/prompt-history-controller.js +3 -8
  44. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +1 -1
  45. package/dist/integrations/pi/session-ui/session-shell-root.js +5 -1
  46. package/dist/integrations/pi/session-ui/session-shell.js +9 -6
  47. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -2
  48. package/dist/integrations/pi/session-ui/session-viewport-controller.js +4 -5
  49. package/dist/integrations/pi/session-ui/text-paste.d.ts +5 -0
  50. package/dist/integrations/pi/session-ui/text-paste.js +16 -0
  51. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +2 -2
  52. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +82 -39
  53. package/dist/native/darwin-arm64/manifest.json +1 -1
  54. package/dist/native/linux-x64/manifest.json +1 -1
  55. package/dist/native/win32-x64/manifest.json +2 -2
  56. package/dist/native/win32-x64/process-guardian.exe +0 -0
  57. package/dist/product-identity.json +1 -1
  58. package/docs/architecture/internal-naming.md +5 -3
  59. package/docs/architecture/toolchain.md +1 -1
  60. package/docs/ci-release-runbook.md +45 -7
  61. package/docs/manual-code-streaming-cleanup.md +88 -0
  62. package/package.json +1 -1
@@ -14,6 +14,11 @@ import { PINNED_PI_SETTINGS_CALLBACKS, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from
14
14
  import { createPiRuntimeIntegration } from "./runtime-integration.js";
15
15
  import { PiSessionCommandIntegration } from "./session-integration.js";
16
16
  import { PiSettingsIntegration } from "./settings-integration.js";
17
+ import { PendingEngineDelivery } from "./pending-delivery.js";
18
+ /** Explicit flush failure when required delivery was interrupted rather than completed. */
19
+ export class EngineDeliveryError extends Error {
20
+ constructor() { super("Engine delivery did not complete"); this.name = "EngineDeliveryError"; }
21
+ }
17
22
  const execFileAsync = promisify(execFile);
18
23
  /**
19
24
  * Engine events delivered before the queue hands the event loop a turn. Small enough
@@ -24,7 +29,6 @@ const execFileAsync = promisify(execFile);
24
29
  // be expensive in long sessions; a larger synchronous batch starves terminal
25
30
  // input and makes an in-progress mouse selection appear frozen.
26
31
  const EVENT_DELIVERY_BATCH = 1;
27
- const TOOL_UPDATE_COALESCE_MS = 50;
28
32
  const AUTH_REFRESH_TIMEOUT_MS = 15_000;
29
33
  // Provenance: Pi 0.84.2 core/model-resolver.ts defaultModelPerProvider.
30
34
  const PINNED_DEFAULT_MODEL_BY_PROVIDER = Object.freeze({
@@ -87,7 +91,7 @@ export class PiEngineAdapter {
87
91
  #sessionForkPrompt;
88
92
  #workflowHost;
89
93
  #workflowInteraction;
90
- #listeners = new Set();
94
+ #listeners = new Map();
91
95
  #runtime;
92
96
  #session;
93
97
  #unsubscribe;
@@ -103,6 +107,7 @@ export class PiEngineAdapter {
103
107
  submitEnabled: false,
104
108
  };
105
109
  #sessionGeneration = 0;
110
+ #sessionBindingGeneration = 0;
106
111
  #status = {
107
112
  title: "Pi",
108
113
  workingMessage: null,
@@ -121,14 +126,20 @@ export class PiEngineAdapter {
121
126
  #messageFallbackIds = new Map();
122
127
  #toolBlockIds = new Map();
123
128
  #transcriptImageAssets = new Map();
124
- #pendingToolUpdates = new Map();
125
- #toolUpdateFlush = null;
126
129
  #usageCache;
127
130
  #nextBlockSequence = 0;
128
131
  #diagnostics = [];
129
- #eventQueue = [];
132
+ #eventQueue = new PendingEngineDelivery();
130
133
  #eventQueueProcessing;
131
- #droppedEventCount = 0;
134
+ #overload;
135
+ #overloads = 0;
136
+ #deliveryFailed = false;
137
+ #admissionStopped = false;
138
+ #runningCommands = 0;
139
+ #invalidatedEvents = 0;
140
+ #pendingCommands = new Map();
141
+ #reservedOutcomes = new Map();
142
+ #pendingWorkflows = new Set();
132
143
  #agentRunActive = false;
133
144
  #agentRunSequence = 0;
134
145
  #assistantResponseSequence = 0;
@@ -171,6 +182,8 @@ export class PiEngineAdapter {
171
182
  get sessionGeneration() {
172
183
  return this.#sessionGeneration;
173
184
  }
185
+ /** Actual session replacements, excluding delivery-only invalidation of callbacks. */
186
+ get sessionBindingGeneration() { return this.#sessionBindingGeneration; }
174
187
  get cwd() {
175
188
  return this.#runtime?.cwd ?? this.#cwd;
176
189
  }
@@ -209,7 +222,7 @@ export class PiEngineAdapter {
209
222
  const session = this.#session;
210
223
  const runtime = this.#runtime;
211
224
  const activeModel = this.#activeModel;
212
- if (this.#disposed || session === undefined || runtime === undefined || request.signal.aborted
225
+ if (this.#disposed || this.#overload !== undefined || this.#admissionStopped || session === undefined || runtime === undefined || request.signal.aborted
213
226
  || identity.sessionId !== this.#sessionId
214
227
  || identity.sessionGeneration !== this.#sessionGeneration
215
228
  || identity.runSequence !== this.#agentRunSequence
@@ -290,6 +303,8 @@ export class PiEngineAdapter {
290
303
  hardwareCursor: runtime.services.settingsManager?.getShowHardwareCursor?.() ?? this.#terminal.hardwareCursor,
291
304
  };
292
305
  runtime.setRebindSession(async (session) => {
306
+ if (this.#overload !== undefined || this.#admissionStopped || this.#disposed)
307
+ return;
293
308
  this.#bindSession(session);
294
309
  this.#emitView();
295
310
  });
@@ -343,14 +358,24 @@ export class PiEngineAdapter {
343
358
  this.#emitView();
344
359
  }
345
360
  onEvent(listener) {
346
- this.#listeners.add(listener);
347
- listener(this.#event({ type: "session-view", view: this.view() }));
361
+ const initial = this.#event({ type: "session-view", view: this.view() });
362
+ this.#listeners.set(listener, initial.sequence);
363
+ listener(initial);
348
364
  return () => this.#listeners.delete(listener);
349
365
  }
366
+ /** Developer-only pressure evidence; never mirrored into visible diagnostic/status arrays. */
367
+ deliveryDiagnostics() {
368
+ return { ...this.#eventQueue.diagnostics(), overloads: this.#overloads, recovering: this.#overload !== undefined,
369
+ pendingCommands: this.#pendingCommands.size + this.#pendingWorkflows.size, reservedOutcomes: this.#reservedOutcomes.size, invalidatedEvents: this.#invalidatedEvents };
370
+ }
350
371
  async flushEvents() {
351
- this.#flushPendingToolUpdates();
372
+ const failed = this.#deliveryFailed;
352
373
  while (this.#eventQueueProcessing)
353
374
  await this.#eventQueueProcessing;
375
+ if (failed || this.#deliveryFailed || this.#disposed && this.#lifecycle !== "stopped") {
376
+ this.#deliveryFailed = false;
377
+ throw new EngineDeliveryError();
378
+ }
354
379
  }
355
380
  nonVisualResources() {
356
381
  const loader = this.#runtime?.services.resourceLoader;
@@ -1028,6 +1053,24 @@ export class PiEngineAdapter {
1028
1053
  return null;
1029
1054
  }
1030
1055
  async executeWorkflow(request) {
1056
+ const cancelled = workflowResult(request.command, "cancelled", "", undefined, "silent");
1057
+ if (this.#overload !== undefined || this.#admissionStopped || this.#disposed
1058
+ || this.#pendingCommands.size + this.#pendingWorkflows.size >= 32)
1059
+ return cancelled;
1060
+ let cancel;
1061
+ const cancellation = new Promise(resolve => { cancel = () => resolve(cancelled); });
1062
+ const pending = { command: request.command, cancel };
1063
+ this.#pendingWorkflows.add(pending);
1064
+ this.#runningCommands++;
1065
+ const operation = this.#runWorkflow(request).finally(() => { this.#runningCommands--; });
1066
+ try {
1067
+ return await Promise.race([operation, cancellation]);
1068
+ }
1069
+ finally {
1070
+ this.#pendingWorkflows.delete(pending);
1071
+ }
1072
+ }
1073
+ async #runWorkflow(request) {
1031
1074
  try {
1032
1075
  return await this.#performWorkflow(request);
1033
1076
  }
@@ -1094,6 +1137,9 @@ export class PiEngineAdapter {
1094
1137
  if (this.#disposed || !this.#runtime || !this.#session) {
1095
1138
  return this.#finishCommand(command, "rejected", "engine adapter is not running");
1096
1139
  }
1140
+ // Invariant: the bounded out-of-band cancellation path has one slot per admitted command.
1141
+ if (this.#overload !== undefined || this.#admissionStopped || this.#pendingCommands.size + this.#pendingWorkflows.size >= 32)
1142
+ return { outcome: "rejected", diagnostic: null };
1097
1143
  const existing = this.#completedCommands.get(command.correlationId);
1098
1144
  if (existing)
1099
1145
  return existing;
@@ -1101,33 +1147,64 @@ export class PiEngineAdapter {
1101
1147
  return this.#finishCommand(command, "rejected", "duplicate engine command correlation id");
1102
1148
  }
1103
1149
  this.#activeCommandIds.push(command.correlationId);
1104
- this.#emitEvent({
1105
- type: "command-outcome",
1106
- correlationId: command.correlationId,
1107
- outcome: "accepted",
1108
- diagnostic: null,
1150
+ const generation = this.#sessionGeneration;
1151
+ let cancelled = false;
1152
+ const cancellation = new Promise(resolve => {
1153
+ this.#pendingCommands.set(command.correlationId, { type: command.type, cancel: () => {
1154
+ if (cancelled)
1155
+ return;
1156
+ cancelled = true;
1157
+ resolve(this.#recordCommand(command, "failed", null));
1158
+ } });
1109
1159
  });
1160
+ this.#emitEvent({ type: "command-outcome", correlationId: command.correlationId, outcome: "accepted", diagnostic: null });
1161
+ const operation = async () => {
1162
+ try {
1163
+ if (cancelled)
1164
+ return { outcome: "failed", diagnostic: null };
1165
+ this.#runningCommands++;
1166
+ try {
1167
+ await this.#perform(command);
1168
+ }
1169
+ finally {
1170
+ this.#runningCommands--;
1171
+ }
1172
+ if (cancelled)
1173
+ return { outcome: "failed", diagnostic: null };
1174
+ if (generation === this.#sessionGeneration)
1175
+ this.#emitView();
1176
+ if (cancelled)
1177
+ return { outcome: "failed", diagnostic: null };
1178
+ return this.#recordCommand(command, "completed", null);
1179
+ }
1180
+ catch (error) {
1181
+ if (cancelled)
1182
+ return { outcome: "failed", diagnostic: null };
1183
+ const diagnostic = error instanceof Error ? error.message : String(error);
1184
+ this.#addDiagnostic("error", "engine-command", diagnostic, true);
1185
+ if (generation === this.#sessionGeneration)
1186
+ this.#emitView();
1187
+ return this.#recordCommand(command, "failed", diagnostic);
1188
+ }
1189
+ };
1110
1190
  try {
1111
- await this.#perform(command);
1112
- this.#emitView();
1113
- return this.#recordCommand(command, "completed", null);
1191
+ return await Promise.race([operation(), cancellation]);
1114
1192
  }
1115
- catch (error) {
1116
- const diagnostic = error instanceof Error ? error.message : String(error);
1117
- this.#addDiagnostic("error", "engine-command", diagnostic, true);
1118
- this.#emitView();
1119
- return this.#recordCommand(command, "failed", diagnostic);
1193
+ finally {
1194
+ this.#pendingCommands.delete(command.correlationId);
1120
1195
  }
1121
1196
  }
1122
1197
  async dispose() {
1123
1198
  if (this.#disposed)
1124
1199
  return;
1125
1200
  this.#disposed = true;
1126
- if (this.#toolUpdateFlush !== null) {
1127
- clearTimeout(this.#toolUpdateFlush);
1128
- this.#toolUpdateFlush = null;
1129
- }
1130
- this.#pendingToolUpdates.clear();
1201
+ for (const pending of this.#pendingCommands.values())
1202
+ if (pending.type !== "shutdown")
1203
+ pending.cancel();
1204
+ // Compatibility: /quit owns normal disposal and must report its actual completion, not cancel itself.
1205
+ for (const pending of this.#pendingWorkflows)
1206
+ if (pending.command !== "quit")
1207
+ pending.cancel();
1131
1208
  this.#transcriptImageAssets.clear();
1132
1209
  this.#extensionBound = false;
1133
1210
  this.#extensionUi = undefined;
@@ -1140,7 +1217,13 @@ export class PiEngineAdapter {
1140
1217
  this.#lifecycle = "stopped";
1141
1218
  this.#emitEvent({ type: "session-lifecycle", lifecycle: "stopped", reason: null });
1142
1219
  this.#emitView();
1143
- await this.flushEvents();
1220
+ try {
1221
+ await this.flushEvents();
1222
+ }
1223
+ catch (error) {
1224
+ if (!(error instanceof EngineDeliveryError))
1225
+ throw error;
1226
+ }
1144
1227
  }
1145
1228
  async #performWorkflow(request) {
1146
1229
  const session = this.#requireWorkflowSession();
@@ -1821,6 +1904,7 @@ export class PiEngineAdapter {
1821
1904
  async #perform(command) {
1822
1905
  const runtime = this.#runtime;
1823
1906
  const session = this.#session;
1907
+ const generation = this.#sessionGeneration;
1824
1908
  if (!runtime || !session)
1825
1909
  throw new Error("engine session is unavailable");
1826
1910
  switch (command.type) {
@@ -1848,7 +1932,8 @@ export class PiEngineAdapter {
1848
1932
  throw new Error(`model is unavailable: ${command.model.providerId}/${command.model.modelId}`);
1849
1933
  }
1850
1934
  await session.setModel(model);
1851
- this.#activeModel = { ...command.model };
1935
+ if (generation === this.#sessionGeneration && this.#overload === undefined && !this.#admissionStopped)
1936
+ this.#activeModel = { ...command.model };
1852
1937
  return;
1853
1938
  }
1854
1939
  case "set-thinking-level":
@@ -1876,7 +1961,13 @@ export class PiEngineAdapter {
1876
1961
  }
1877
1962
  #bindSession(session) {
1878
1963
  this.#unsubscribe?.();
1964
+ for (const pending of this.#pendingCommands.values()) {
1965
+ if (pending.type !== "new-session" && pending.type !== "resume-session")
1966
+ pending.cancel();
1967
+ }
1879
1968
  this.#sessionGeneration += 1;
1969
+ this.#sessionBindingGeneration += 1;
1970
+ this.#invalidatedEvents += this.#eventQueue.discardObsolete(this.#sessionGeneration);
1880
1971
  this.#session = session;
1881
1972
  this.#activeCommandIds = [];
1882
1973
  this.#completedCommands.clear();
@@ -1899,7 +1990,11 @@ export class PiEngineAdapter {
1899
1990
  this.#thinkingLevel = readThinkingLevel(session.thinkingLevel);
1900
1991
  this.#transcriptImageAssets.clear();
1901
1992
  this.#rebuildTranscript(session.messages, "finalized");
1902
- this.#unsubscribe = session.subscribe(event => this.#handlePiEvent(event));
1993
+ const generation = this.#sessionGeneration;
1994
+ this.#unsubscribe = session.subscribe(event => {
1995
+ if (generation === this.#sessionGeneration && !this.#disposed)
1996
+ this.#handlePiEvent(event);
1997
+ });
1903
1998
  if (this.#extensionUi !== undefined)
1904
1999
  void this.#bindExtensionUiToSession();
1905
2000
  }
@@ -2059,15 +2154,11 @@ export class PiEngineAdapter {
2059
2154
  return;
2060
2155
  case "tool_execution_start":
2061
2156
  case "tool_execution_end": {
2062
- // Concurrency: the end supersedes any update still waiting on the coalescing timer.
2063
- const toolCallId = stringValue(event.toolCallId);
2064
- if (toolCallId !== undefined)
2065
- this.#pendingToolUpdates.delete(toolCallId);
2066
2157
  this.#upsertToolExecutionBlock(event);
2067
2158
  return;
2068
2159
  }
2069
2160
  case "tool_execution_update":
2070
- this.#coalesceToolExecutionUpdate(event);
2161
+ this.#upsertToolExecutionBlock(event);
2071
2162
  return;
2072
2163
  case "agent_settled":
2073
2164
  case "agent_end": {
@@ -2079,7 +2170,7 @@ export class PiEngineAdapter {
2079
2170
  if (finalMessages.length > 0)
2080
2171
  this.#rebuildTranscript(finalMessages, "finalized");
2081
2172
  else
2082
- this.#setTranscript(this.#transcript.map(block => block.status === "live" ? { ...block, status: "finalized" } : block));
2173
+ this.#setTranscript(this.#transcript.map(block => block.status === "live" ? { ...block, status: "finalized", revision: block.revision + 1 } : block));
2083
2174
  // Compatibility: ending a turn leaves the working state, as the recorded pinned baseline does, but
2084
2175
  // it leaves only that state: a compaction or retry being shown outlives the turn
2085
2176
  // that ended under it. Settlement ends the run, and with it every state — the
@@ -2401,30 +2492,6 @@ export class PiEngineAdapter {
2401
2492
  }
2402
2493
  return references;
2403
2494
  }
2404
- // Performance: coalescing each tool to its newest chunk bounds work by frames, not stream events.
2405
- #coalesceToolExecutionUpdate(event) {
2406
- const toolCallId = stringValue(event.toolCallId);
2407
- if (!toolCallId)
2408
- return;
2409
- this.#pendingToolUpdates.set(toolCallId, event);
2410
- this.#toolUpdateFlush ??= setTimeout(() => {
2411
- this.#toolUpdateFlush = null;
2412
- if (!this.#disposed)
2413
- this.#flushPendingToolUpdates();
2414
- }, TOOL_UPDATE_COALESCE_MS);
2415
- }
2416
- #flushPendingToolUpdates() {
2417
- if (this.#toolUpdateFlush !== null) {
2418
- clearTimeout(this.#toolUpdateFlush);
2419
- this.#toolUpdateFlush = null;
2420
- }
2421
- if (this.#pendingToolUpdates.size === 0)
2422
- return;
2423
- const pending = [...this.#pendingToolUpdates.values()];
2424
- this.#pendingToolUpdates.clear();
2425
- for (const update of pending)
2426
- this.#upsertToolExecutionBlock(update);
2427
- }
2428
2495
  #upsertToolExecutionBlock(event) {
2429
2496
  const toolCallId = stringValue(event.toolCallId);
2430
2497
  if (!toolCallId)
@@ -2457,6 +2524,9 @@ export class PiEngineAdapter {
2457
2524
  const index = this.#transcriptIndex.get(block.id);
2458
2525
  if (index !== undefined) {
2459
2526
  const existing = this.#transcript[index];
2527
+ // Invariant: an obsolete partial cannot revive finalized content after a completion barrier.
2528
+ if (existing?.status === "finalized" && block.status === "live")
2529
+ return;
2460
2530
  // Performance: nothing is emitted for a block that repeats itself, and keeping the
2461
2531
  // revision keeps the rows it already rendered.
2462
2532
  if (existing !== undefined && sameBlockContent(existing, block))
@@ -2540,52 +2610,118 @@ export class PiEngineAdapter {
2540
2610
  this.#enqueueEvent(event);
2541
2611
  }
2542
2612
  #enqueueEvent(event) {
2543
- const capacity = 1_024;
2544
- if (this.#eventQueue.length >= capacity) {
2545
- const coalescible = this.#eventQueue.findIndex(queued => queued.type === "session-view"
2546
- || queued.type === "status"
2547
- || queued.type === "editor-state"
2548
- || queued.type === "transcript-block");
2549
- if (coalescible >= 0)
2550
- this.#eventQueue.splice(coalescible, 1);
2551
- else
2552
- this.#eventQueue.shift();
2553
- this.#droppedEventCount += 1;
2554
- if (this.#droppedEventCount === 1 || this.#droppedEventCount % 128 === 0) {
2555
- this.#recordDiagnostic("warning", "event-backpressure", `owned UI coalesced ${this.#droppedEventCount} engine events under backpressure`, true);
2613
+ if (this.#overload !== undefined) {
2614
+ if (event.type === "command-outcome" && event.outcome !== "accepted" && this.#pendingCommands.has(event.correlationId)) {
2615
+ this.#reservedOutcomes.set(event.correlationId, { outcome: event.outcome, diagnostic: event.diagnostic });
2616
+ }
2617
+ return;
2618
+ }
2619
+ if (!this.#eventQueue.push(event, this.#sessionGeneration, event.type === "transcript-block"
2620
+ ? this.#blockReconciliation(event.block.id, event.sequence) : undefined)) {
2621
+ this.#beginOverload();
2622
+ if (event.type === "command-outcome" && event.outcome !== "accepted" && this.#pendingCommands.has(event.correlationId)
2623
+ && !this.#reservedOutcomes.has(event.correlationId)) {
2624
+ this.#reservedOutcomes.set(event.correlationId, { outcome: event.outcome, diagnostic: event.diagnostic });
2556
2625
  }
2557
2626
  }
2558
- this.#eventQueue.push(event);
2559
2627
  this.#eventQueueProcessing ??= Promise.resolve().then(() => this.#processEventQueue());
2560
2628
  }
2629
+ // Performance: capture only identity, not the complete block/event retained by an earlier revision.
2630
+ #blockReconciliation(id, sequence) {
2631
+ return () => ({ type: "transcript-block", sessionId: this.#sessionId, sequence, block: this.#transcriptBlock(id) });
2632
+ }
2633
+ #beginOverload() {
2634
+ if (this.#overload !== undefined)
2635
+ return;
2636
+ this.#overloads = Math.min(Number.MAX_SAFE_INTEGER, this.#overloads + 1);
2637
+ this.#deliveryFailed = true;
2638
+ // Concurrency: reserve before cancellation; outcomes produced reentrantly must not enter the saturated queue.
2639
+ this.#overload = Promise.resolve(false);
2640
+ for (const pending of this.#pendingCommands.values())
2641
+ pending.cancel();
2642
+ for (const pending of this.#pendingWorkflows)
2643
+ pending.cancel();
2644
+ const session = this.#session;
2645
+ this.#overload = new Promise(resolve => {
2646
+ const timer = setTimeout(() => resolve(false), 2000);
2647
+ void Promise.resolve().then(() => session?.abort()).then(() => {
2648
+ clearTimeout(timer);
2649
+ resolve(true);
2650
+ }, () => { clearTimeout(timer); resolve(false); });
2651
+ });
2652
+ }
2653
+ #deliver(event) {
2654
+ for (const [listener, subscribedAt] of this.#listeners) {
2655
+ if (event.sequence <= subscribedAt)
2656
+ continue;
2657
+ try {
2658
+ listener(event);
2659
+ }
2660
+ catch (error) {
2661
+ this.#deliveryFailed = true;
2662
+ this.#recordDiagnostic("warning", "event-listener", error instanceof Error ? error.message : String(error), true);
2663
+ }
2664
+ }
2665
+ }
2666
+ async #reconcileOverload() {
2667
+ const cancelled = await this.#overload;
2668
+ const canResume = cancelled === true && this.#runningCommands === 0;
2669
+ this.#admissionStopped = !canResume;
2670
+ this.#unsubscribe?.();
2671
+ this.#unsubscribe = undefined;
2672
+ ++this.#sessionGeneration;
2673
+ this.#agentRunActive = false;
2674
+ this.#statusKind = null;
2675
+ this.#status = { ...this.#status, workingMessage: null };
2676
+ this.#lifecycle = this.#disposed ? "stopped" : canResume ? "ready" : "failed";
2677
+ this.#editor = { ...this.#editor, submitEnabled: canResume && !this.#disposed };
2678
+ this.#viewRevision += 1;
2679
+ this.#setTranscript(this.#transcript.map(block => block.status === "live" ? { ...block, status: "finalized", revision: block.revision + 1 } : block));
2680
+ for (const [correlationId, result] of this.#reservedOutcomes) {
2681
+ this.#deliver(this.#event({ type: "command-outcome", correlationId, ...result }));
2682
+ await new Promise(resolve => setImmediate(resolve));
2683
+ }
2684
+ this.#reservedOutcomes.clear();
2685
+ // Invariant: one out-of-band authoritative reconciliation, not an emergency transcript/event log.
2686
+ this.#deliver(this.#event({ type: "session-view", view: this.view() }));
2687
+ if (this.#disposed)
2688
+ this.#deliver(this.#event({ type: "session-lifecycle", lifecycle: "stopped", reason: null }));
2689
+ if (canResume && !this.#disposed && this.#session !== undefined) {
2690
+ const generation = this.#sessionGeneration;
2691
+ this.#unsubscribe = this.#session.subscribe(event => {
2692
+ if (generation === this.#sessionGeneration && !this.#disposed)
2693
+ this.#handlePiEvent(event);
2694
+ });
2695
+ }
2696
+ this.#overload = undefined;
2697
+ }
2561
2698
  async #processEventQueue() {
2562
2699
  try {
2563
2700
  let deliveredSinceYield = 0;
2564
- while (this.#eventQueue.length > 0) {
2565
- const event = this.#eventQueue.shift();
2566
- if (!event)
2701
+ while (this.#eventQueue.size > 0) {
2702
+ const pending = this.#eventQueue.shift();
2703
+ if (pending === undefined)
2704
+ continue;
2705
+ if (pending.generation !== this.#sessionGeneration && pending.event.type !== "command-outcome") {
2706
+ this.#invalidatedEvents = Math.min(Number.MAX_SAFE_INTEGER, this.#invalidatedEvents + 1);
2567
2707
  continue;
2568
- for (const listener of this.#listeners) {
2569
- try {
2570
- listener(event);
2571
- }
2572
- catch (error) {
2573
- this.#recordDiagnostic("warning", "event-listener", error instanceof Error ? error.message : String(error), true);
2574
- }
2575
2708
  }
2709
+ this.#deliver(pending.event);
2576
2710
  deliveredSinceYield += 1;
2577
2711
  // Concurrency: a microtask chain runs to exhaustion before the loop turns, so a streaming
2578
2712
  // burst would hold typed input, pointer reports, and timed indicators until it
2579
2713
  // drained. Yielding on a macrotask hands those their turn between batches.
2580
- if (deliveredSinceYield >= EVENT_DELIVERY_BATCH && this.#eventQueue.length > 0) {
2714
+ if (deliveredSinceYield >= EVENT_DELIVERY_BATCH && this.#eventQueue.size > 0) {
2581
2715
  deliveredSinceYield = 0;
2582
2716
  await new Promise(resolve => { setImmediate(resolve); });
2583
2717
  }
2584
2718
  }
2719
+ if (this.#overload !== undefined)
2720
+ await this.#reconcileOverload();
2585
2721
  }
2586
2722
  finally {
2587
2723
  this.#eventQueueProcessing = undefined;
2588
- if (this.#eventQueue.length > 0) {
2724
+ if (this.#eventQueue.size > 0) {
2589
2725
  this.#eventQueueProcessing = Promise.resolve().then(() => this.#processEventQueue());
2590
2726
  }
2591
2727
  }
@@ -0,0 +1,26 @@
1
+ import type { OwnedUiEvent } from "../../../contracts/owned-ui/index.js";
2
+ export declare const MAX_PENDING_EVENTS = 1024;
3
+ export declare const MAX_PENDING_EVENT_BYTES: number;
4
+ /** Actual shell semantics: only live complete block state has no independent side effect. */
5
+ export declare function replaceableEventKey(event: OwnedUiEvent): string | null;
6
+ /** Intrusive FIFO + segment index: replacement unlinks in O(1), never changes source ordering. */
7
+ export declare class PendingEngineDelivery {
8
+ #private;
9
+ get size(): number;
10
+ diagnostics(): {
11
+ pending: number;
12
+ bytes: number;
13
+ protected: number;
14
+ superseded: number;
15
+ peakNodes: number;
16
+ peakBytes: number;
17
+ };
18
+ /** False means not admitted. The caller must enter its reserved recovery protocol, never evict. */
19
+ push(event: OwnedUiEvent, generation: number, reconcile?: () => OwnedUiEvent): boolean;
20
+ /** Session replacement invalidates old view state without resolving its lazy block references. */
21
+ discardObsolete(generation: number): number;
22
+ shift(): {
23
+ event: OwnedUiEvent;
24
+ generation: number;
25
+ } | undefined;
26
+ }
@@ -0,0 +1,149 @@
1
+ export const MAX_PENDING_EVENTS = 1024;
2
+ export const MAX_PENDING_EVENT_BYTES = 8 * 1024 * 1024;
3
+ /** Actual shell semantics: only live complete block state has no independent side effect. */
4
+ export function replaceableEventKey(event) {
5
+ switch (event.type) {
6
+ case "transcript-block": return event.block.status === "live" && !(typeof event.block.payload === "object" && event.block.payload !== null && "isError" in event.block.payload && event.block.payload.isError === true) ? event.block.id : null;
7
+ case "session-view": // Invariant: may clear compaction work, replace editor/session state, or preempt presentation.
8
+ case "status": // Invariant: retry/compaction transitions invalidate suggestion work.
9
+ case "editor-state":
10
+ case "terminal-surface":
11
+ case "session-lifecycle":
12
+ case "agent-run-started":
13
+ case "agent-run-settled":
14
+ case "assistant-message-completed":
15
+ case "command-outcome":
16
+ case "diagnostic":
17
+ case "dialog":
18
+ case "overlay":
19
+ case "customization": return null;
20
+ default: {
21
+ const exhaustive = event;
22
+ return exhaustive;
23
+ }
24
+ }
25
+ }
26
+ /** Intrusive FIFO + segment index: replacement unlinks in O(1), never changes source ordering. */
27
+ export class PendingEngineDelivery {
28
+ #replaceable = new Map();
29
+ #head;
30
+ #tail;
31
+ #size = 0;
32
+ #bytes = 0;
33
+ #protected = 0;
34
+ #generation = -1;
35
+ #superseded = 0;
36
+ #peakNodes = 0;
37
+ #peakBytes = 0;
38
+ get size() { return this.#size; }
39
+ diagnostics() {
40
+ return { pending: this.#size, bytes: this.#bytes, protected: this.#protected,
41
+ superseded: this.#superseded, peakNodes: this.#peakNodes, peakBytes: this.#peakBytes };
42
+ }
43
+ /** False means not admitted. The caller must enter its reserved recovery protocol, never evict. */
44
+ push(event, generation, reconcile) {
45
+ const key = replaceableEventKey(event);
46
+ if (generation !== this.#generation || key === null) {
47
+ if (!this.#seal())
48
+ return false;
49
+ this.#generation = generation;
50
+ }
51
+ const prior = key === null ? undefined : this.#replaceable.get(key);
52
+ let bytes = retainedEventBytes(event);
53
+ const marker = key !== null && bytes > 64 * 1024 && reconcile !== undefined;
54
+ if (marker)
55
+ bytes = 128;
56
+ if (this.#size - (prior === undefined ? 0 : 1) >= MAX_PENDING_EVENTS
57
+ || this.#bytes - (prior?.bytes ?? 0) + bytes > MAX_PENDING_EVENT_BYTES)
58
+ return false;
59
+ if (prior !== undefined) {
60
+ this.#unlink(prior);
61
+ this.#superseded = Math.min(Number.MAX_SAFE_INTEGER, this.#superseded + 1);
62
+ }
63
+ const node = { key, generation, bytes, ...(marker ? { resolve: reconcile } : { event }) };
64
+ node.previous = this.#tail;
65
+ if (this.#tail !== undefined)
66
+ this.#tail.next = node;
67
+ else
68
+ this.#head = node;
69
+ this.#tail = node;
70
+ this.#size++;
71
+ this.#bytes += bytes;
72
+ if (key === null)
73
+ this.#protected++;
74
+ else
75
+ this.#replaceable.set(key, node);
76
+ this.#peakNodes = Math.max(this.#peakNodes, this.#size);
77
+ this.#peakBytes = Math.max(this.#peakBytes, this.#bytes);
78
+ return true;
79
+ }
80
+ /** Session replacement invalidates old view state without resolving its lazy block references. */
81
+ discardObsolete(generation) {
82
+ let discarded = 0;
83
+ let node = this.#head;
84
+ while (node !== undefined) {
85
+ const next = node.next;
86
+ if (node.generation !== generation && node.event?.type !== "command-outcome") {
87
+ this.#unlink(node);
88
+ discarded++;
89
+ }
90
+ node = next;
91
+ }
92
+ this.#replaceable.clear();
93
+ this.#generation = generation;
94
+ return discarded;
95
+ }
96
+ shift() {
97
+ const node = this.#head;
98
+ if (node === undefined)
99
+ return undefined;
100
+ const event = node.event ?? node.resolve();
101
+ this.#unlink(node);
102
+ return { event, generation: node.generation };
103
+ }
104
+ // Invariant: a barrier freezes lazy authoritative markers before a later segment can change them.
105
+ #seal() {
106
+ for (const node of this.#replaceable.values()) {
107
+ if (node.resolve === undefined)
108
+ continue;
109
+ const event = node.resolve();
110
+ const bytes = retainedEventBytes(event);
111
+ if (this.#bytes - node.bytes + bytes > MAX_PENDING_EVENT_BYTES)
112
+ return false;
113
+ this.#bytes += bytes - node.bytes;
114
+ node.bytes = bytes;
115
+ node.event = event;
116
+ node.resolve = undefined;
117
+ this.#peakBytes = Math.max(this.#peakBytes, this.#bytes);
118
+ }
119
+ this.#replaceable.clear();
120
+ return true;
121
+ }
122
+ #unlink(node) {
123
+ if (node.previous !== undefined)
124
+ node.previous.next = node.next;
125
+ else
126
+ this.#head = node.next;
127
+ if (node.next !== undefined)
128
+ node.next.previous = node.previous;
129
+ else
130
+ this.#tail = node.previous;
131
+ this.#size--;
132
+ this.#bytes -= node.bytes;
133
+ if (node.key === null)
134
+ this.#protected--;
135
+ else if (this.#replaceable.get(node.key) === node)
136
+ this.#replaceable.delete(node.key);
137
+ }
138
+ }
139
+ /** Conservative UTF-16 payload bound. String length is O(1); never encode accumulated text per chunk. */
140
+ function retainedEventBytes(value) {
141
+ if (typeof value === "string")
142
+ return 16 + value.length * 2;
143
+ if (value === null || typeof value !== "object")
144
+ return 8;
145
+ let bytes = 32;
146
+ for (const [key, item] of Object.entries(value))
147
+ bytes += 16 + key.length * 2 + retainedEventBytes(item);
148
+ return bytes;
149
+ }