@timurproko/a1 0.1.8-dev.322 → 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/dist/cli/dispatch.js +1 -1
- 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 +36 -6
- package/dist/foundation/launch-context/index.js +64 -11
- package/dist/foundation/launch-guardian/main.js +2 -1
- package/dist/foundation/release/bootstrap.d.ts +5 -0
- package/dist/foundation/release/bootstrap.js +49 -9
- package/dist/foundation/release/cohort-state.js +0 -3
- 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 +72 -44
- package/dist/foundation/release/release-store.d.ts +2 -0
- package/dist/foundation/release/release-store.js +4 -3
- package/dist/foundation/release/restart-certification.js +16 -5
- package/dist/foundation/release/update.js +1 -6
- package/dist/foundation/release/warmup.js +16 -5
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
- package/dist/integrations/pi/components/owned-editor-ux.js +51 -27
- package/dist/integrations/pi/engine/adapter.d.ts +20 -0
- package/dist/integrations/pi/engine/adapter.js +223 -87
- 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/session-ui/prompt-chips.js +38 -25
- 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 +1 -1
- package/dist/integrations/pi/session-ui/session-shell-root.js +5 -1
- package/dist/integrations/pi/session-ui/session-shell.js +9 -6
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -2
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +4 -5
- 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 +2 -2
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +82 -39
- 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.json +1 -1
- package/docs/architecture/internal-naming.md +5 -3
- package/docs/manual-code-streaming-cleanup.md +88 -0
- 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
|
|
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
|
-
#
|
|
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.#
|
|
347
|
-
|
|
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.#
|
|
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.#
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
|
1112
|
-
this.#emitView();
|
|
1113
|
-
return this.#recordCommand(command, "completed", null);
|
|
1191
|
+
return await Promise.race([operation(), cancellation]);
|
|
1114
1192
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
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();
|
|
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
|
-
|
|
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.#
|
|
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
|
-
|
|
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.#
|
|
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
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
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.
|
|
2565
|
-
const
|
|
2566
|
-
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);
|
|
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.
|
|
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.
|
|
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
|
+
}
|