@timurproko/a1 0.1.8-dev.299 → 0.1.8-dev.332

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 (95) hide show
  1. package/README.md +16 -4
  2. package/bin/guardian.js +4 -3
  3. package/bin/ui.js +4 -2
  4. package/bin/update-recovery.js +3 -1
  5. package/dist/cli/dispatch.js +1 -1
  6. package/dist/composition/owned-ui.js +2 -1
  7. package/dist/features/launch/profile-paths.d.ts +2 -0
  8. package/dist/features/launch/profile-paths.js +11 -0
  9. package/dist/features/owned-ui/project-trust-prompt.js +1 -1
  10. package/dist/features/owned-ui/settings-app.js +1 -1
  11. package/dist/features/prompt-history/service.d.ts +31 -1
  12. package/dist/features/prompt-history/service.js +294 -129
  13. package/dist/features/prompt-history/store.d.ts +2 -1
  14. package/dist/features/prompt-history/store.js +17 -4
  15. package/dist/features/prompt-history/worker.js +6 -3
  16. package/dist/foundation/launch-context/index.d.ts +59 -0
  17. package/dist/foundation/launch-context/index.js +136 -0
  18. package/dist/foundation/launch-guardian/main.js +6 -4
  19. package/dist/foundation/release/bootstrap.d.ts +6 -1
  20. package/dist/foundation/release/bootstrap.js +59 -13
  21. package/dist/foundation/release/cohort-state.d.ts +1 -0
  22. package/dist/foundation/release/cohort-state.js +1 -0
  23. package/dist/foundation/release/dependency-certification-retention.d.ts +7 -0
  24. package/dist/foundation/release/dependency-certification-retention.js +88 -0
  25. package/dist/foundation/release/dependency-certification.d.ts +24 -0
  26. package/dist/foundation/release/dependency-certification.js +208 -0
  27. package/dist/foundation/release/dependency-layer.d.ts +3 -4
  28. package/dist/foundation/release/dependency-layer.js +6 -37
  29. package/dist/foundation/release/release-gc.js +74 -46
  30. package/dist/foundation/release/release-store.d.ts +2 -0
  31. package/dist/foundation/release/release-store.js +11 -3
  32. package/dist/foundation/release/release.d.ts +3 -1
  33. package/dist/foundation/release/release.js +7 -3
  34. package/dist/foundation/release/restart-certification.d.ts +1 -0
  35. package/dist/foundation/release/restart-certification.js +17 -1
  36. package/dist/foundation/release/update-recovery.d.ts +2 -0
  37. package/dist/foundation/release/update-recovery.js +4 -0
  38. package/dist/foundation/release/update.js +1 -1
  39. package/dist/foundation/release/warmup.d.ts +1 -1
  40. package/dist/foundation/release/warmup.js +18 -6
  41. package/dist/foundation/startup/startup-runtime.js +13 -11
  42. package/dist/foundation/supervision/main.js +8 -7
  43. package/dist/integrations/pi/components/owned-editor-ux.d.ts +2 -1
  44. package/dist/integrations/pi/components/owned-editor-ux.js +105 -30
  45. package/dist/integrations/pi/components/shell-editor-autocomplete.js +3 -0
  46. package/dist/integrations/pi/components/shell-presenters-info.d.ts +9 -2
  47. package/dist/integrations/pi/components/shell-presenters-info.js +35 -4
  48. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +3 -2
  49. package/dist/integrations/pi/components/shell-selectors-dialogs.js +8 -5
  50. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
  51. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +12 -0
  52. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +25 -2
  53. package/dist/integrations/pi/engine/adapter.d.ts +33 -1
  54. package/dist/integrations/pi/engine/adapter.js +309 -112
  55. package/dist/integrations/pi/engine/changelog.d.ts +2 -0
  56. package/dist/integrations/pi/engine/changelog.js +63 -0
  57. package/dist/integrations/pi/engine/pending-delivery.d.ts +26 -0
  58. package/dist/integrations/pi/engine/pending-delivery.js +149 -0
  59. package/dist/integrations/pi/engine/resources/changelog.json +12 -0
  60. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
  61. package/dist/integrations/pi/engine/workflows.d.ts +1 -1
  62. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +3 -1
  63. package/dist/integrations/pi/session-ui/prompt-chips.js +74 -37
  64. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +0 -2
  65. package/dist/integrations/pi/session-ui/prompt-history-controller.js +3 -8
  66. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +26 -1
  67. package/dist/integrations/pi/session-ui/session-shell-root.js +70 -75
  68. package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
  69. package/dist/integrations/pi/session-ui/session-shell.js +67 -13
  70. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -2
  71. package/dist/integrations/pi/session-ui/session-viewport-controller.js +25 -20
  72. package/dist/integrations/pi/session-ui/text-paste.d.ts +5 -0
  73. package/dist/integrations/pi/session-ui/text-paste.js +16 -0
  74. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +9 -2
  75. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +87 -43
  76. package/dist/native/darwin-arm64/manifest.json +1 -1
  77. package/dist/native/linux-x64/manifest.json +1 -1
  78. package/dist/native/win32-x64/manifest.json +2 -2
  79. package/dist/native/win32-x64/process-guardian.exe +0 -0
  80. package/dist/product-identity.d.ts +1 -1
  81. package/dist/product-identity.js +2 -35
  82. package/dist/product-identity.json +1 -23
  83. package/dist/ui/components/transcript-viewport.d.ts +8 -0
  84. package/dist/ui/components/transcript-viewport.js +23 -2
  85. package/dist/ui/components/visible-hyperlinks.d.ts +7 -0
  86. package/dist/ui/components/visible-hyperlinks.js +1 -0
  87. package/dist/ui/settings/declarations.js +1 -0
  88. package/dist/ui/settings/sections.js +11 -1
  89. package/docs/architecture/boundaries.md +2 -1
  90. package/docs/architecture/internal-naming.md +91 -0
  91. package/docs/architecture/resource-and-data-policy.md +9 -4
  92. package/docs/architecture/toolchain.md +15 -1
  93. package/docs/features/prompt-history.md +39 -11
  94. package/docs/manual-code-streaming-cleanup.md +88 -0
  95. package/package.json +8 -3
@@ -7,12 +7,18 @@ import { dirname, join, resolve } from "node:path";
7
7
  import { promisify } from "node:util";
8
8
  import { PRODUCT_IDENTITY } from "../../../product-identity.js";
9
9
  import { configureOwnedHttpDispatcher } from "./http-dispatcher.js";
10
+ import { readPinnedCommandChangelog } from "./changelog.js";
10
11
  import { copyToClipboard, CredentialSynchronizationError, DefaultPackageManager, getAgentDir, ProjectTrustStore, SessionManager, VERSION, } from "@earendil-works/pi-coding-agent";
11
12
  import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION, assertOwnedUiCommand, assertOwnedUiExtensionUiPort, assertOwnedUiPromptSuggestionRequest, assertOwnedUiPromptSuggestionResult, assertOwnedUiSnapshot, normalizePromptSuggestionCandidate, } from "../../../contracts/owned-ui/index.js";
12
13
  import { PINNED_PI_SETTINGS_CALLBACKS, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "./workflows.js";
13
14
  import { createPiRuntimeIntegration } from "./runtime-integration.js";
14
15
  import { PiSessionCommandIntegration } from "./session-integration.js";
15
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
+ }
16
22
  const execFileAsync = promisify(execFile);
17
23
  /**
18
24
  * Engine events delivered before the queue hands the event loop a turn. Small enough
@@ -23,7 +29,6 @@ const execFileAsync = promisify(execFile);
23
29
  // be expensive in long sessions; a larger synchronous batch starves terminal
24
30
  // input and makes an in-progress mouse selection appear frozen.
25
31
  const EVENT_DELIVERY_BATCH = 1;
26
- const TOOL_UPDATE_COALESCE_MS = 50;
27
32
  const AUTH_REFRESH_TIMEOUT_MS = 15_000;
28
33
  // Provenance: Pi 0.84.2 core/model-resolver.ts defaultModelPerProvider.
29
34
  const PINNED_DEFAULT_MODEL_BY_PROVIDER = Object.freeze({
@@ -86,7 +91,7 @@ export class PiEngineAdapter {
86
91
  #sessionForkPrompt;
87
92
  #workflowHost;
88
93
  #workflowInteraction;
89
- #listeners = new Set();
94
+ #listeners = new Map();
90
95
  #runtime;
91
96
  #session;
92
97
  #unsubscribe;
@@ -102,6 +107,7 @@ export class PiEngineAdapter {
102
107
  submitEnabled: false,
103
108
  };
104
109
  #sessionGeneration = 0;
110
+ #sessionBindingGeneration = 0;
105
111
  #status = {
106
112
  title: "Pi",
107
113
  workingMessage: null,
@@ -120,14 +126,20 @@ export class PiEngineAdapter {
120
126
  #messageFallbackIds = new Map();
121
127
  #toolBlockIds = new Map();
122
128
  #transcriptImageAssets = new Map();
123
- #pendingToolUpdates = new Map();
124
- #toolUpdateFlush = null;
125
129
  #usageCache;
126
130
  #nextBlockSequence = 0;
127
131
  #diagnostics = [];
128
- #eventQueue = [];
132
+ #eventQueue = new PendingEngineDelivery();
129
133
  #eventQueueProcessing;
130
- #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();
131
143
  #agentRunActive = false;
132
144
  #agentRunSequence = 0;
133
145
  #assistantResponseSequence = 0;
@@ -170,6 +182,8 @@ export class PiEngineAdapter {
170
182
  get sessionGeneration() {
171
183
  return this.#sessionGeneration;
172
184
  }
185
+ /** Actual session replacements, excluding delivery-only invalidation of callbacks. */
186
+ get sessionBindingGeneration() { return this.#sessionBindingGeneration; }
173
187
  get cwd() {
174
188
  return this.#runtime?.cwd ?? this.#cwd;
175
189
  }
@@ -208,7 +222,7 @@ export class PiEngineAdapter {
208
222
  const session = this.#session;
209
223
  const runtime = this.#runtime;
210
224
  const activeModel = this.#activeModel;
211
- 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
212
226
  || identity.sessionId !== this.#sessionId
213
227
  || identity.sessionGeneration !== this.#sessionGeneration
214
228
  || identity.runSequence !== this.#agentRunSequence
@@ -289,6 +303,8 @@ export class PiEngineAdapter {
289
303
  hardwareCursor: runtime.services.settingsManager?.getShowHardwareCursor?.() ?? this.#terminal.hardwareCursor,
290
304
  };
291
305
  runtime.setRebindSession(async (session) => {
306
+ if (this.#overload !== undefined || this.#admissionStopped || this.#disposed)
307
+ return;
292
308
  this.#bindSession(session);
293
309
  this.#emitView();
294
310
  });
@@ -308,14 +324,22 @@ export class PiEngineAdapter {
308
324
  if (!settingsManager || typeof settingsManager.getLastChangelogVersion !== "function"
309
325
  || typeof settingsManager.setLastChangelogVersion !== "function")
310
326
  return;
311
- if (settingsManager.getLastChangelogVersion() === VERSION)
327
+ if ((this.#session?.messages.length ?? 0) > 0)
328
+ return;
329
+ const lastVersion = settingsManager.getLastChangelogVersion();
330
+ if (lastVersion === VERSION)
312
331
  return;
313
- const markdown = await this.#workflowHost.readChangelog().catch(() => "");
332
+ if (!lastVersion) {
333
+ settingsManager.setLastChangelogVersion(VERSION);
334
+ await settingsManager.flush();
335
+ return;
336
+ }
337
+ const markdown = await this.#workflowHost.readChangelog(lastVersion).catch(() => "");
314
338
  if (markdown.trim().length > 0) {
315
339
  this.#addDiagnostic("info", settingsManager.getCollapseChangelog() ? "changelog-collapsed" : "changelog-expanded", markdown, true);
340
+ settingsManager.setLastChangelogVersion(VERSION);
341
+ await settingsManager.flush();
316
342
  }
317
- settingsManager.setLastChangelogVersion(VERSION);
318
- await settingsManager.flush();
319
343
  }
320
344
  async #announcePackageUpdates(settingsManager) {
321
345
  if (process.env.PI_OFFLINE)
@@ -334,14 +358,24 @@ export class PiEngineAdapter {
334
358
  this.#emitView();
335
359
  }
336
360
  onEvent(listener) {
337
- this.#listeners.add(listener);
338
- 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);
339
364
  return () => this.#listeners.delete(listener);
340
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
+ }
341
371
  async flushEvents() {
342
- this.#flushPendingToolUpdates();
372
+ const failed = this.#deliveryFailed;
343
373
  while (this.#eventQueueProcessing)
344
374
  await this.#eventQueueProcessing;
375
+ if (failed || this.#deliveryFailed || this.#disposed && this.#lifecycle !== "stopped") {
376
+ this.#deliveryFailed = false;
377
+ throw new EngineDeliveryError();
378
+ }
345
379
  }
346
380
  nonVisualResources() {
347
381
  const loader = this.#runtime?.services.resourceLoader;
@@ -877,6 +911,17 @@ export class PiEngineAdapter {
877
911
  }));
878
912
  return { title: `Select authentication method for ${providerName}:`, options };
879
913
  }
914
+ pinnedAmbientAuthentication(selection) {
915
+ if (!selection.startsWith("api_key:"))
916
+ return null;
917
+ const providerId = selection.slice("api_key:".length);
918
+ const provider = this.#runtime?.services.modelRuntime.getProvider?.(providerId);
919
+ const method = dynamicObject(dynamicObject(provider, "auth"), "apiKey");
920
+ if (!method || typeof method.login === "function")
921
+ return null;
922
+ const providerName = stringProperty(provider, "name") ?? providerId;
923
+ return { providerId, providerName, title: `${providerName} setup`, message: `${stringProperty(method, "name") ?? "Authentication"} is configured outside pi.` };
924
+ }
880
925
  pinnedLogoutOptions() {
881
926
  return this.#logoutOptions();
882
927
  }
@@ -957,9 +1002,16 @@ export class PiEngineAdapter {
957
1002
  pinnedMessageRenderer(customType) {
958
1003
  return this.#requireWorkflowSession().extensionRunner?.getMessageRenderer?.(customType);
959
1004
  }
1005
+ pinnedShortcutDescriptions(bindings) {
1006
+ const shortcuts = this.#requireWorkflowSession().extensionRunner?.getShortcuts?.(bindings);
1007
+ return shortcuts === undefined ? [] : [...shortcuts].map(([key, shortcut]) => ({ key, description: shortcut.description ?? shortcut.extensionPath }));
1008
+ }
960
1009
  pinnedToolDefinition(toolName) {
961
1010
  return this.#requireWorkflowSession().extensionRunner?.getToolDefinition?.(toolName);
962
1011
  }
1012
+ async copyWorkflowText(text) {
1013
+ await this.#workflowHost.copyText(text);
1014
+ }
963
1015
  clearQueuedWorkflows() {
964
1016
  const session = this.#requireWorkflowSession();
965
1017
  const result = session.clearQueue?.();
@@ -992,14 +1044,40 @@ export class PiEngineAdapter {
992
1044
  return workflowResult("settings", "failed", error instanceof Error ? error.message : String(error));
993
1045
  }
994
1046
  }
1047
+ reloadBlockedResult() {
1048
+ const session = this.#requireWorkflowSession();
1049
+ if (session.isStreaming)
1050
+ return workflowResult("reload", "failed", "Wait for the current response to finish before reloading.", undefined, "warning");
1051
+ if (session.isCompacting)
1052
+ return workflowResult("reload", "failed", "Wait for compaction to finish before reloading.", undefined, "warning");
1053
+ return null;
1054
+ }
995
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) {
996
1074
  try {
997
1075
  return await this.#performWorkflow(request);
998
1076
  }
999
1077
  catch (error) {
1000
1078
  const message = error instanceof Error ? error.message : String(error);
1001
1079
  const contextualMessage = request.command === "export"
1002
- ? `Failed to export session: ${message}`
1080
+ ? `Failed to export session: ${errorMessage(error, "Unknown error")}`
1003
1081
  : request.command === "import"
1004
1082
  ? `Failed to import session: ${message}`
1005
1083
  : request.command === "new"
@@ -1059,6 +1137,9 @@ export class PiEngineAdapter {
1059
1137
  if (this.#disposed || !this.#runtime || !this.#session) {
1060
1138
  return this.#finishCommand(command, "rejected", "engine adapter is not running");
1061
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 };
1062
1143
  const existing = this.#completedCommands.get(command.correlationId);
1063
1144
  if (existing)
1064
1145
  return existing;
@@ -1066,33 +1147,64 @@ export class PiEngineAdapter {
1066
1147
  return this.#finishCommand(command, "rejected", "duplicate engine command correlation id");
1067
1148
  }
1068
1149
  this.#activeCommandIds.push(command.correlationId);
1069
- this.#emitEvent({
1070
- type: "command-outcome",
1071
- correlationId: command.correlationId,
1072
- outcome: "accepted",
1073
- 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
+ } });
1074
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
+ };
1075
1190
  try {
1076
- await this.#perform(command);
1077
- this.#emitView();
1078
- return this.#recordCommand(command, "completed", null);
1191
+ return await Promise.race([operation(), cancellation]);
1079
1192
  }
1080
- catch (error) {
1081
- const diagnostic = error instanceof Error ? error.message : String(error);
1082
- this.#addDiagnostic("error", "engine-command", diagnostic, true);
1083
- this.#emitView();
1084
- return this.#recordCommand(command, "failed", diagnostic);
1193
+ finally {
1194
+ this.#pendingCommands.delete(command.correlationId);
1085
1195
  }
1086
1196
  }
1087
1197
  async dispose() {
1088
1198
  if (this.#disposed)
1089
1199
  return;
1090
1200
  this.#disposed = true;
1091
- if (this.#toolUpdateFlush !== null) {
1092
- clearTimeout(this.#toolUpdateFlush);
1093
- this.#toolUpdateFlush = null;
1094
- }
1095
- 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();
1096
1208
  this.#transcriptImageAssets.clear();
1097
1209
  this.#extensionBound = false;
1098
1210
  this.#extensionUi = undefined;
@@ -1105,7 +1217,13 @@ export class PiEngineAdapter {
1105
1217
  this.#lifecycle = "stopped";
1106
1218
  this.#emitEvent({ type: "session-lifecycle", lifecycle: "stopped", reason: null });
1107
1219
  this.#emitView();
1108
- await this.flushEvents();
1220
+ try {
1221
+ await this.flushEvents();
1222
+ }
1223
+ catch (error) {
1224
+ if (!(error instanceof EngineDeliveryError))
1225
+ throw error;
1226
+ }
1109
1227
  }
1110
1228
  async #performWorkflow(request) {
1111
1229
  const session = this.#requireWorkflowSession();
@@ -1133,8 +1251,18 @@ export class PiEngineAdapter {
1133
1251
  : [...(runtime.services.modelRuntime.getAvailableSnapshot?.() ?? [])];
1134
1252
  let model = findExactWorkflowModel(reference, availableModels);
1135
1253
  const messages = [];
1254
+ const generation = this.#sessionGeneration;
1255
+ const current = () => !this.#disposed && generation === this.#sessionGeneration;
1256
+ const publish = (message) => {
1257
+ if (!current())
1258
+ return;
1259
+ if (this.#workflowInteraction.publish)
1260
+ this.#workflowInteraction.publish(message);
1261
+ else
1262
+ messages.push(message);
1263
+ };
1136
1264
  if (model === undefined && scopedModels.length === 0) {
1137
- messages.push({ kind: "status", message: "Refreshing model catalogs…" });
1265
+ publish({ kind: "status", message: "Refreshing model catalogs…" });
1138
1266
  const controller = new AbortController();
1139
1267
  let timedOut = false;
1140
1268
  const timeout = setTimeout(() => {
@@ -1144,14 +1272,14 @@ export class PiEngineAdapter {
1144
1272
  try {
1145
1273
  const refreshed = await runtime.services.modelRuntime.refresh?.({ signal: controller.signal });
1146
1274
  if (isRecord(refreshed) && refreshed.aborted === true && timedOut) {
1147
- messages.push({ kind: "warning", message: "Model refresh timed out; searching cached models." });
1275
+ publish({ kind: "warning", message: "Model refresh timed out; searching cached models." });
1148
1276
  }
1149
1277
  else if (isRecord(refreshed) && refreshed.errors instanceof Map && refreshed.errors.size > 0) {
1150
- messages.push({ kind: "warning", message: `Could not refresh ${[...refreshed.errors.keys()].join(", ")}; searching cached models.` });
1278
+ publish({ kind: "warning", message: `Could not refresh ${[...refreshed.errors.keys()].join(", ")}; searching cached models.` });
1151
1279
  }
1152
1280
  }
1153
1281
  catch (error) {
1154
- messages.push({
1282
+ publish({
1155
1283
  kind: "warning",
1156
1284
  message: timedOut
1157
1285
  ? "Model refresh timed out; searching cached models."
@@ -1164,13 +1292,25 @@ export class PiEngineAdapter {
1164
1292
  availableModels = [...(runtime.services.modelRuntime.getAvailableSnapshot?.() ?? [])];
1165
1293
  model = findExactWorkflowModel(reference, availableModels);
1166
1294
  }
1295
+ if (!current())
1296
+ return workflowResult(request.command, "cancelled", "Model selection cancelled", undefined, "silent");
1167
1297
  if (model === undefined) {
1168
1298
  return {
1169
1299
  ...workflowResult(request.command, "requires-selection", "Select a model", reference, "silent"),
1170
1300
  ...(messages.length === 0 ? {} : { messages: Object.freeze(messages) }),
1171
1301
  };
1172
1302
  }
1173
- await session.setModel(model);
1303
+ try {
1304
+ await session.setModel(model);
1305
+ }
1306
+ catch (error) {
1307
+ if (!current())
1308
+ return workflowResult(request.command, "cancelled", "Model selection cancelled", undefined, "silent");
1309
+ const failure = { kind: "error", message: error instanceof Error ? error.message : String(error) };
1310
+ return workflowResult(request.command, "failed", failure.message, undefined, "error", messages.length === 0 ? undefined : [...messages, failure]);
1311
+ }
1312
+ if (!current())
1313
+ return workflowResult(request.command, "cancelled", "Model selection cancelled", undefined, "silent");
1174
1314
  const providerId = stringProperty(model, "provider") ?? "unknown";
1175
1315
  const modelId = stringProperty(model, "id") ?? reference;
1176
1316
  this.#activeModel = { providerId, modelId, displayName: stringProperty(model, "name") ?? modelId };
@@ -1226,9 +1366,8 @@ export class PiEngineAdapter {
1226
1366
  const signal = request.signal;
1227
1367
  if (signal?.aborted)
1228
1368
  return workflowResult(request.command, "cancelled", "Share cancelled", undefined, "status");
1229
- let auth;
1230
1369
  try {
1231
- auth = await this.#workflowHost.runCommand("gh", ["auth", "status"], signal === undefined ? undefined : { signal });
1370
+ await this.#workflowHost.runCommand("gh", ["auth", "status"], signal === undefined ? undefined : { signal });
1232
1371
  }
1233
1372
  catch (error) {
1234
1373
  if (signal?.aborted || isAbortError(error))
@@ -1238,9 +1377,6 @@ export class PiEngineAdapter {
1238
1377
  : "GitHub CLI is not logged in. Run 'gh auth login' first.";
1239
1378
  return workflowResult(request.command, "failed", message);
1240
1379
  }
1241
- if (auth.stderr && !auth.stdout) {
1242
- return workflowResult(request.command, "failed", "GitHub CLI is not logged in. Run 'gh auth login' first.");
1243
- }
1244
1380
  const temporary = join(tmpdir(), `${PRODUCT_IDENTITY.filesystem.temporaryPrefix}pi-session-${process.pid}.html`);
1245
1381
  try {
1246
1382
  try {
@@ -1258,13 +1394,10 @@ export class PiEngineAdapter {
1258
1394
  catch (error) {
1259
1395
  if (signal?.aborted || isAbortError(error))
1260
1396
  return workflowResult(request.command, "cancelled", "Share cancelled", undefined, "status");
1261
- return workflowResult(request.command, "failed", `Failed to create gist: ${errorMessage(error, "Unknown error")}`);
1397
+ return workflowResult(request.command, "failed", `Failed to create gist: ${shareCommandFailureDetail(error)}`);
1262
1398
  }
1263
1399
  if (signal?.aborted)
1264
1400
  return workflowResult(request.command, "cancelled", "Share cancelled", undefined, "status");
1265
- if (gist.stderr && !gist.stdout) {
1266
- return workflowResult(request.command, "failed", `Failed to create gist: ${gist.stderr.trim() || "Unknown error"}`);
1267
- }
1268
1401
  const gistUrl = gist.stdout.trim();
1269
1402
  const gistId = gistUrl.split("/").at(-1);
1270
1403
  if (!gistId)
@@ -1497,13 +1630,17 @@ export class PiEngineAdapter {
1497
1630
  }
1498
1631
  }
1499
1632
  case "reload": {
1500
- if (session.isStreaming)
1501
- return workflowResult(request.command, "failed", "Wait for the current response to finish before reloading.", undefined, "warning");
1502
- if (session.isCompacting)
1503
- return workflowResult(request.command, "failed", "Wait for compaction to finish before reloading.", undefined, "warning");
1633
+ const blocked = this.reloadBlockedResult();
1634
+ if (blocked)
1635
+ return blocked;
1504
1636
  await requireCapability(session.reload, "reload").call(session);
1505
1637
  await this.#bindExtensionUiToSession();
1506
- return workflowResult(request.command, "completed", "Reloaded keybindings, extensions, skills, prompts, themes, and context files");
1638
+ const message = "Reloaded keybindings, extensions, skills, prompts, themes, and context files";
1639
+ const modelError = runtime.services.modelRuntime.getError?.();
1640
+ return workflowResult(request.command, "completed", message, undefined, "status", modelError ? [
1641
+ { kind: "error", message: `models.json error: ${modelError}` },
1642
+ { kind: "status", message },
1643
+ ] : undefined);
1507
1644
  }
1508
1645
  case "quit": {
1509
1646
  await this.dispose();
@@ -1767,6 +1904,7 @@ export class PiEngineAdapter {
1767
1904
  async #perform(command) {
1768
1905
  const runtime = this.#runtime;
1769
1906
  const session = this.#session;
1907
+ const generation = this.#sessionGeneration;
1770
1908
  if (!runtime || !session)
1771
1909
  throw new Error("engine session is unavailable");
1772
1910
  switch (command.type) {
@@ -1794,7 +1932,8 @@ export class PiEngineAdapter {
1794
1932
  throw new Error(`model is unavailable: ${command.model.providerId}/${command.model.modelId}`);
1795
1933
  }
1796
1934
  await session.setModel(model);
1797
- this.#activeModel = { ...command.model };
1935
+ if (generation === this.#sessionGeneration && this.#overload === undefined && !this.#admissionStopped)
1936
+ this.#activeModel = { ...command.model };
1798
1937
  return;
1799
1938
  }
1800
1939
  case "set-thinking-level":
@@ -1822,7 +1961,13 @@ export class PiEngineAdapter {
1822
1961
  }
1823
1962
  #bindSession(session) {
1824
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
+ }
1825
1968
  this.#sessionGeneration += 1;
1969
+ this.#sessionBindingGeneration += 1;
1970
+ this.#invalidatedEvents += this.#eventQueue.discardObsolete(this.#sessionGeneration);
1826
1971
  this.#session = session;
1827
1972
  this.#activeCommandIds = [];
1828
1973
  this.#completedCommands.clear();
@@ -1845,7 +1990,11 @@ export class PiEngineAdapter {
1845
1990
  this.#thinkingLevel = readThinkingLevel(session.thinkingLevel);
1846
1991
  this.#transcriptImageAssets.clear();
1847
1992
  this.#rebuildTranscript(session.messages, "finalized");
1848
- 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
+ });
1849
1998
  if (this.#extensionUi !== undefined)
1850
1999
  void this.#bindExtensionUiToSession();
1851
2000
  }
@@ -2005,15 +2154,11 @@ export class PiEngineAdapter {
2005
2154
  return;
2006
2155
  case "tool_execution_start":
2007
2156
  case "tool_execution_end": {
2008
- // Concurrency: the end supersedes any update still waiting on the coalescing timer.
2009
- const toolCallId = stringValue(event.toolCallId);
2010
- if (toolCallId !== undefined)
2011
- this.#pendingToolUpdates.delete(toolCallId);
2012
2157
  this.#upsertToolExecutionBlock(event);
2013
2158
  return;
2014
2159
  }
2015
2160
  case "tool_execution_update":
2016
- this.#coalesceToolExecutionUpdate(event);
2161
+ this.#upsertToolExecutionBlock(event);
2017
2162
  return;
2018
2163
  case "agent_settled":
2019
2164
  case "agent_end": {
@@ -2025,7 +2170,7 @@ export class PiEngineAdapter {
2025
2170
  if (finalMessages.length > 0)
2026
2171
  this.#rebuildTranscript(finalMessages, "finalized");
2027
2172
  else
2028
- 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));
2029
2174
  // Compatibility: ending a turn leaves the working state, as the recorded pinned baseline does, but
2030
2175
  // it leaves only that state: a compaction or retry being shown outlives the turn
2031
2176
  // that ended under it. Settlement ends the run, and with it every state — the
@@ -2347,30 +2492,6 @@ export class PiEngineAdapter {
2347
2492
  }
2348
2493
  return references;
2349
2494
  }
2350
- // Performance: coalescing each tool to its newest chunk bounds work by frames, not stream events.
2351
- #coalesceToolExecutionUpdate(event) {
2352
- const toolCallId = stringValue(event.toolCallId);
2353
- if (!toolCallId)
2354
- return;
2355
- this.#pendingToolUpdates.set(toolCallId, event);
2356
- this.#toolUpdateFlush ??= setTimeout(() => {
2357
- this.#toolUpdateFlush = null;
2358
- if (!this.#disposed)
2359
- this.#flushPendingToolUpdates();
2360
- }, TOOL_UPDATE_COALESCE_MS);
2361
- }
2362
- #flushPendingToolUpdates() {
2363
- if (this.#toolUpdateFlush !== null) {
2364
- clearTimeout(this.#toolUpdateFlush);
2365
- this.#toolUpdateFlush = null;
2366
- }
2367
- if (this.#pendingToolUpdates.size === 0)
2368
- return;
2369
- const pending = [...this.#pendingToolUpdates.values()];
2370
- this.#pendingToolUpdates.clear();
2371
- for (const update of pending)
2372
- this.#upsertToolExecutionBlock(update);
2373
- }
2374
2495
  #upsertToolExecutionBlock(event) {
2375
2496
  const toolCallId = stringValue(event.toolCallId);
2376
2497
  if (!toolCallId)
@@ -2403,6 +2524,9 @@ export class PiEngineAdapter {
2403
2524
  const index = this.#transcriptIndex.get(block.id);
2404
2525
  if (index !== undefined) {
2405
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;
2406
2530
  // Performance: nothing is emitted for a block that repeats itself, and keeping the
2407
2531
  // revision keeps the rows it already rendered.
2408
2532
  if (existing !== undefined && sameBlockContent(existing, block))
@@ -2486,52 +2610,118 @@ export class PiEngineAdapter {
2486
2610
  this.#enqueueEvent(event);
2487
2611
  }
2488
2612
  #enqueueEvent(event) {
2489
- const capacity = 1_024;
2490
- if (this.#eventQueue.length >= capacity) {
2491
- const coalescible = this.#eventQueue.findIndex(queued => queued.type === "session-view"
2492
- || queued.type === "status"
2493
- || queued.type === "editor-state"
2494
- || queued.type === "transcript-block");
2495
- if (coalescible >= 0)
2496
- this.#eventQueue.splice(coalescible, 1);
2497
- else
2498
- this.#eventQueue.shift();
2499
- this.#droppedEventCount += 1;
2500
- if (this.#droppedEventCount === 1 || this.#droppedEventCount % 128 === 0) {
2501
- 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 });
2502
2625
  }
2503
2626
  }
2504
- this.#eventQueue.push(event);
2505
2627
  this.#eventQueueProcessing ??= Promise.resolve().then(() => this.#processEventQueue());
2506
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
+ }
2507
2698
  async #processEventQueue() {
2508
2699
  try {
2509
2700
  let deliveredSinceYield = 0;
2510
- while (this.#eventQueue.length > 0) {
2511
- const event = this.#eventQueue.shift();
2512
- 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);
2513
2707
  continue;
2514
- for (const listener of this.#listeners) {
2515
- try {
2516
- listener(event);
2517
- }
2518
- catch (error) {
2519
- this.#recordDiagnostic("warning", "event-listener", error instanceof Error ? error.message : String(error), true);
2520
- }
2521
2708
  }
2709
+ this.#deliver(pending.event);
2522
2710
  deliveredSinceYield += 1;
2523
2711
  // Concurrency: a microtask chain runs to exhaustion before the loop turns, so a streaming
2524
2712
  // burst would hold typed input, pointer reports, and timed indicators until it
2525
2713
  // drained. Yielding on a macrotask hands those their turn between batches.
2526
- if (deliveredSinceYield >= EVENT_DELIVERY_BATCH && this.#eventQueue.length > 0) {
2714
+ if (deliveredSinceYield >= EVENT_DELIVERY_BATCH && this.#eventQueue.size > 0) {
2527
2715
  deliveredSinceYield = 0;
2528
2716
  await new Promise(resolve => { setImmediate(resolve); });
2529
2717
  }
2530
2718
  }
2719
+ if (this.#overload !== undefined)
2720
+ await this.#reconcileOverload();
2531
2721
  }
2532
2722
  finally {
2533
2723
  this.#eventQueueProcessing = undefined;
2534
- if (this.#eventQueue.length > 0) {
2724
+ if (this.#eventQueue.size > 0) {
2535
2725
  this.#eventQueueProcessing = Promise.resolve().then(() => this.#processEventQueue());
2536
2726
  }
2537
2727
  }
@@ -2698,7 +2888,7 @@ function defaultWorkflowHost() {
2698
2888
  const result = await execFileAsync(command, [...arguments_], { encoding: "utf8", signal: options?.signal });
2699
2889
  return { stdout: result.stdout, stderr: result.stderr };
2700
2890
  },
2701
- readChangelog: async () => "No changelog entries found.",
2891
+ readChangelog: readPinnedCommandChangelog,
2702
2892
  };
2703
2893
  }
2704
2894
  function workflowLoginNotification(event) {
@@ -2757,6 +2947,13 @@ function workflowConfirmation(command, message, detail) {
2757
2947
  function errorMessage(error, fallback) {
2758
2948
  return error instanceof Error ? error.message : fallback;
2759
2949
  }
2950
+ function shareCommandFailureDetail(error) {
2951
+ // Compatibility: Pi reports a failed child's stderr, not execFile's command wrapper.
2952
+ if (isRecord(error) && (typeof error.code === "number" || typeof error.signal === "string") && typeof error.stderr === "string") {
2953
+ return error.stderr.trim() || "Unknown error";
2954
+ }
2955
+ return errorMessage(error, "Unknown error");
2956
+ }
2760
2957
  function commandMissing(error) {
2761
2958
  return isRecord(error) && error.code === "ENOENT";
2762
2959
  }