@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.
- package/README.md +16 -4
- package/bin/guardian.js +4 -3
- package/bin/ui.js +4 -2
- package/bin/update-recovery.js +3 -1
- package/dist/cli/dispatch.js +1 -1
- package/dist/composition/owned-ui.js +2 -1
- package/dist/features/launch/profile-paths.d.ts +2 -0
- package/dist/features/launch/profile-paths.js +11 -0
- package/dist/features/owned-ui/project-trust-prompt.js +1 -1
- package/dist/features/owned-ui/settings-app.js +1 -1
- package/dist/features/prompt-history/service.d.ts +31 -1
- package/dist/features/prompt-history/service.js +294 -129
- package/dist/features/prompt-history/store.d.ts +2 -1
- package/dist/features/prompt-history/store.js +17 -4
- package/dist/features/prompt-history/worker.js +6 -3
- package/dist/foundation/launch-context/index.d.ts +59 -0
- package/dist/foundation/launch-context/index.js +136 -0
- package/dist/foundation/launch-guardian/main.js +6 -4
- package/dist/foundation/release/bootstrap.d.ts +6 -1
- package/dist/foundation/release/bootstrap.js +59 -13
- package/dist/foundation/release/cohort-state.d.ts +1 -0
- package/dist/foundation/release/cohort-state.js +1 -0
- package/dist/foundation/release/dependency-certification-retention.d.ts +7 -0
- package/dist/foundation/release/dependency-certification-retention.js +88 -0
- package/dist/foundation/release/dependency-certification.d.ts +24 -0
- package/dist/foundation/release/dependency-certification.js +208 -0
- package/dist/foundation/release/dependency-layer.d.ts +3 -4
- package/dist/foundation/release/dependency-layer.js +6 -37
- package/dist/foundation/release/release-gc.js +74 -46
- package/dist/foundation/release/release-store.d.ts +2 -0
- package/dist/foundation/release/release-store.js +11 -3
- package/dist/foundation/release/release.d.ts +3 -1
- package/dist/foundation/release/release.js +7 -3
- package/dist/foundation/release/restart-certification.d.ts +1 -0
- package/dist/foundation/release/restart-certification.js +17 -1
- package/dist/foundation/release/update-recovery.d.ts +2 -0
- package/dist/foundation/release/update-recovery.js +4 -0
- package/dist/foundation/release/update.js +1 -1
- package/dist/foundation/release/warmup.d.ts +1 -1
- package/dist/foundation/release/warmup.js +18 -6
- package/dist/foundation/startup/startup-runtime.js +13 -11
- package/dist/foundation/supervision/main.js +8 -7
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +2 -1
- package/dist/integrations/pi/components/owned-editor-ux.js +105 -30
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +3 -0
- package/dist/integrations/pi/components/shell-presenters-info.d.ts +9 -2
- package/dist/integrations/pi/components/shell-presenters-info.js +35 -4
- package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +3 -2
- package/dist/integrations/pi/components/shell-selectors-dialogs.js +8 -5
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
- package/dist/integrations/pi/components/upstream/assets/earendil-image.json +12 -0
- package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +25 -2
- package/dist/integrations/pi/engine/adapter.d.ts +33 -1
- package/dist/integrations/pi/engine/adapter.js +309 -112
- package/dist/integrations/pi/engine/changelog.d.ts +2 -0
- package/dist/integrations/pi/engine/changelog.js +63 -0
- package/dist/integrations/pi/engine/pending-delivery.d.ts +26 -0
- package/dist/integrations/pi/engine/pending-delivery.js +149 -0
- package/dist/integrations/pi/engine/resources/changelog.json +12 -0
- package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
- package/dist/integrations/pi/engine/workflows.d.ts +1 -1
- package/dist/integrations/pi/session-ui/prompt-chips.d.ts +3 -1
- package/dist/integrations/pi/session-ui/prompt-chips.js +74 -37
- package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +0 -2
- package/dist/integrations/pi/session-ui/prompt-history-controller.js +3 -8
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +26 -1
- package/dist/integrations/pi/session-ui/session-shell-root.js +70 -75
- package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
- package/dist/integrations/pi/session-ui/session-shell.js +67 -13
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -2
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +25 -20
- package/dist/integrations/pi/session-ui/text-paste.d.ts +5 -0
- package/dist/integrations/pi/session-ui/text-paste.js +16 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +9 -2
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +87 -43
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/product-identity.d.ts +1 -1
- package/dist/product-identity.js +2 -35
- package/dist/product-identity.json +1 -23
- package/dist/ui/components/transcript-viewport.d.ts +8 -0
- package/dist/ui/components/transcript-viewport.js +23 -2
- package/dist/ui/components/visible-hyperlinks.d.ts +7 -0
- package/dist/ui/components/visible-hyperlinks.js +1 -0
- package/dist/ui/settings/declarations.js +1 -0
- package/dist/ui/settings/sections.js +11 -1
- package/docs/architecture/boundaries.md +2 -1
- package/docs/architecture/internal-naming.md +91 -0
- package/docs/architecture/resource-and-data-policy.md +9 -4
- package/docs/architecture/toolchain.md +15 -1
- package/docs/features/prompt-history.md +39 -11
- package/docs/manual-code-streaming-cleanup.md +88 -0
- 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
|
|
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
|
-
#
|
|
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 (
|
|
327
|
+
if ((this.#session?.messages.length ?? 0) > 0)
|
|
328
|
+
return;
|
|
329
|
+
const lastVersion = settingsManager.getLastChangelogVersion();
|
|
330
|
+
if (lastVersion === VERSION)
|
|
312
331
|
return;
|
|
313
|
-
|
|
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.#
|
|
338
|
-
|
|
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.#
|
|
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: ${
|
|
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.#
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
|
1077
|
-
this.#emitView();
|
|
1078
|
-
return this.#recordCommand(command, "completed", null);
|
|
1191
|
+
return await Promise.race([operation(), cancellation]);
|
|
1079
1192
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
this.#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1278
|
+
publish({ kind: "warning", message: `Could not refresh ${[...refreshed.errors.keys()].join(", ")}; searching cached models.` });
|
|
1151
1279
|
}
|
|
1152
1280
|
}
|
|
1153
1281
|
catch (error) {
|
|
1154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: ${
|
|
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
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
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
|
-
|
|
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.#
|
|
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
|
-
|
|
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.#
|
|
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
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
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.
|
|
2511
|
-
const
|
|
2512
|
-
if (
|
|
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.
|
|
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.
|
|
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:
|
|
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
|
}
|