poly-weaver 1.1.1 → 1.2.0
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/agents/implementors/index.d.ts +2 -2
- package/dist/agents/implementors/index.d.ts.map +1 -1
- package/dist/agents/implementors/index.js.map +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +7 -6
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/interviewers/index.d.ts +2 -2
- package/dist/agents/interviewers/index.d.ts.map +1 -1
- package/dist/agents/planners/index.d.ts +2 -2
- package/dist/agents/planners/index.d.ts.map +1 -1
- package/dist/agents/planners/index.js.map +1 -1
- package/dist/agents/registry.d.ts +2 -1
- package/dist/agents/registry.d.ts.map +1 -1
- package/dist/agents/registry.js +1 -1
- package/dist/agents/registry.js.map +1 -1
- package/dist/agents/reviewers/index.d.ts +2 -2
- package/dist/agents/reviewers/index.d.ts.map +1 -1
- package/dist/agents/reviewers/index.js.map +1 -1
- package/dist/agents/runner.d.ts +3 -1
- package/dist/agents/runner.d.ts.map +1 -1
- package/dist/agents/runner.js +106 -64
- package/dist/agents/runner.js.map +1 -1
- package/dist/agents/simplifiers/adapter.d.ts.map +1 -1
- package/dist/agents/simplifiers/adapter.js +1 -0
- package/dist/agents/simplifiers/adapter.js.map +1 -1
- package/dist/agents/simplifiers/dsl.d.ts +1 -0
- package/dist/agents/simplifiers/dsl.d.ts.map +1 -1
- package/dist/agents/simplifiers/dsl.js +1 -0
- package/dist/agents/simplifiers/dsl.js.map +1 -1
- package/dist/agents/simplifiers/handler.d.ts.map +1 -1
- package/dist/agents/simplifiers/handler.js +1 -0
- package/dist/agents/simplifiers/handler.js.map +1 -1
- package/dist/agents/simplifiers/index.d.ts +2 -2
- package/dist/agents/simplifiers/index.d.ts.map +1 -1
- package/dist/agents/simplifiers/index.js.map +1 -1
- package/dist/agents/simplifiers/prompts.d.ts +1 -0
- package/dist/agents/simplifiers/prompts.d.ts.map +1 -1
- package/dist/agents/simplifiers/prompts.js +1 -1
- package/dist/agents/simplifiers/prompts.js.map +1 -1
- package/dist/agents/simplifiers/types.d.ts +1 -0
- package/dist/agents/simplifiers/types.d.ts.map +1 -1
- package/dist/cleanup.d.ts +9 -0
- package/dist/cleanup.d.ts.map +1 -0
- package/dist/cleanup.js +35 -0
- package/dist/cleanup.js.map +1 -0
- package/dist/cli.js +30 -13
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +5 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/crosscheck/coordinator.d.ts.map +1 -1
- package/dist/crosscheck/coordinator.js +51 -36
- package/dist/crosscheck/coordinator.js.map +1 -1
- package/dist/crosscheck/live-agent-session.d.ts +5 -1
- package/dist/crosscheck/live-agent-session.d.ts.map +1 -1
- package/dist/crosscheck/live-agent-session.js +53 -32
- package/dist/crosscheck/live-agent-session.js.map +1 -1
- package/dist/dump/collector.d.ts.map +1 -1
- package/dist/dump/collector.js +1 -0
- package/dist/dump/collector.js.map +1 -1
- package/dist/dump/observer-diagnostics.d.ts +6 -0
- package/dist/dump/observer-diagnostics.d.ts.map +1 -0
- package/dist/dump/observer-diagnostics.js +19 -0
- package/dist/dump/observer-diagnostics.js.map +1 -0
- package/dist/dump/service.d.ts.map +1 -1
- package/dist/dump/service.js +2 -3
- package/dist/dump/service.js.map +1 -1
- package/dist/dump/types.d.ts +2 -0
- package/dist/dump/types.d.ts.map +1 -1
- package/dist/flow/built-in/simplify-factory.d.ts.map +1 -1
- package/dist/flow/built-in/simplify-factory.js +4 -1
- package/dist/flow/built-in/simplify-factory.js.map +1 -1
- package/dist/flow/built-in/simplify.d.ts +1 -0
- package/dist/flow/built-in/simplify.d.ts.map +1 -1
- package/dist/flow/built-in/simplify.js +3 -2
- package/dist/flow/built-in/simplify.js.map +1 -1
- package/dist/flow/dsl.d.ts +1 -0
- package/dist/flow/dsl.d.ts.map +1 -1
- package/dist/flow/dsl.js +1 -0
- package/dist/flow/dsl.js.map +1 -1
- package/dist/flow/executor.d.ts.map +1 -1
- package/dist/flow/executor.js +3 -3
- package/dist/flow/executor.js.map +1 -1
- package/dist/flow/params.d.ts +3 -1
- package/dist/flow/params.d.ts.map +1 -1
- package/dist/flow/params.js +85 -121
- package/dist/flow/params.js.map +1 -1
- package/dist/flow/types.d.ts +5 -2
- package/dist/flow/types.d.ts.map +1 -1
- package/dist/flow/types.js.map +1 -1
- package/dist/offscreen/controller.d.ts.map +1 -1
- package/dist/offscreen/controller.js +67 -45
- package/dist/offscreen/controller.js.map +1 -1
- package/dist/offscreen/fork.d.ts +4 -2
- package/dist/offscreen/fork.d.ts.map +1 -1
- package/dist/offscreen/fork.js +49 -34
- package/dist/offscreen/fork.js.map +1 -1
- package/dist/offscreen/inquiry-pane.d.ts.map +1 -1
- package/dist/offscreen/inquiry-pane.js +7 -2
- package/dist/offscreen/inquiry-pane.js.map +1 -1
- package/dist/offscreen/inquiry-sender.d.ts +1 -0
- package/dist/offscreen/inquiry-sender.d.ts.map +1 -1
- package/dist/offscreen/inquiry-sender.js +12 -2
- package/dist/offscreen/inquiry-sender.js.map +1 -1
- package/dist/offscreen/registry.d.ts.map +1 -1
- package/dist/offscreen/registry.js +14 -12
- package/dist/offscreen/registry.js.map +1 -1
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +20 -24
- package/dist/orchestrator.js.map +1 -1
- package/dist/preview-panel.d.ts +4 -1
- package/dist/preview-panel.d.ts.map +1 -1
- package/dist/preview-panel.js +114 -55
- package/dist/preview-panel.js.map +1 -1
- package/dist/providers/configuration-presentation.d.ts.map +1 -1
- package/dist/providers/configuration-presentation.js +5 -8
- package/dist/providers/configuration-presentation.js.map +1 -1
- package/dist/providers/copilot/capability-discovery.d.ts +16 -0
- package/dist/providers/copilot/capability-discovery.d.ts.map +1 -0
- package/dist/providers/copilot/capability-discovery.js +238 -0
- package/dist/providers/copilot/capability-discovery.js.map +1 -0
- package/dist/providers/copilot/capability-extension/extension.d.mts +2 -0
- package/dist/providers/copilot/capability-extension/extension.d.mts.map +1 -0
- package/dist/providers/copilot/capability-extension/extension.mjs +46 -0
- package/dist/providers/copilot/capability-extension/extension.mjs.map +1 -0
- package/dist/providers/copilot/completion.d.ts +2 -0
- package/dist/providers/copilot/completion.d.ts.map +1 -1
- package/dist/providers/copilot/completion.js +27 -0
- package/dist/providers/copilot/completion.js.map +1 -1
- package/dist/providers/copilot/model-capabilities.d.ts +21 -0
- package/dist/providers/copilot/model-capabilities.d.ts.map +1 -0
- package/dist/providers/copilot/model-capabilities.js +76 -0
- package/dist/providers/copilot/model-capabilities.js.map +1 -0
- package/dist/providers/copilot/observation.d.ts +123 -15
- package/dist/providers/copilot/observation.d.ts.map +1 -1
- package/dist/providers/copilot/observation.js +807 -179
- package/dist/providers/copilot/observation.js.map +1 -1
- package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/extension.mjs +332 -66
- package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/extension.mjs.map +1 -1
- package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/history-evidence.d.mts +17 -0
- package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/history-evidence.d.mts.map +1 -0
- package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/history-evidence.mjs +54 -0
- package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/history-evidence.mjs.map +1 -0
- package/dist/providers/copilot/strategy.d.ts +8 -8
- package/dist/providers/copilot/strategy.d.ts.map +1 -1
- package/dist/providers/copilot/strategy.js +61 -145
- package/dist/providers/copilot/strategy.js.map +1 -1
- package/dist/providers/fork-cleanup.d.ts.map +1 -1
- package/dist/providers/fork-cleanup.js +3 -19
- package/dist/providers/fork-cleanup.js.map +1 -1
- package/dist/providers/initialization.d.ts +14 -0
- package/dist/providers/initialization.d.ts.map +1 -0
- package/dist/providers/initialization.js +74 -0
- package/dist/providers/initialization.js.map +1 -0
- package/dist/providers/invoke.d.ts.map +1 -1
- package/dist/providers/invoke.js +54 -32
- package/dist/providers/invoke.js.map +1 -1
- package/dist/providers/observer-run.d.ts.map +1 -1
- package/dist/providers/observer-run.js +3 -2
- package/dist/providers/observer-run.js.map +1 -1
- package/dist/providers/option-catalog.d.ts +12 -8
- package/dist/providers/option-catalog.d.ts.map +1 -1
- package/dist/providers/option-catalog.js +25 -13
- package/dist/providers/option-catalog.js.map +1 -1
- package/dist/providers/registry.d.ts +3 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +20 -9
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/session-launch.d.ts.map +1 -1
- package/dist/providers/session-launch.js +2 -1
- package/dist/providers/session-launch.js.map +1 -1
- package/dist/providers/types.d.ts +35 -8
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/pty/exit.d.ts +3 -1
- package/dist/pty/exit.d.ts.map +1 -1
- package/dist/pty/exit.js +39 -6
- package/dist/pty/exit.js.map +1 -1
- package/dist/pty/spawn.d.ts.map +1 -1
- package/dist/pty/spawn.js +7 -3
- package/dist/pty/spawn.js.map +1 -1
- package/dist/pty/types.d.ts +1 -1
- package/dist/pty/types.d.ts.map +1 -1
- package/dist/session/resume.d.ts.map +1 -1
- package/dist/session/resume.js +7 -0
- package/dist/session/resume.js.map +1 -1
- package/dist/startup-tui.d.ts +1 -0
- package/dist/startup-tui.d.ts.map +1 -1
- package/dist/startup-tui.js +17 -6
- package/dist/startup-tui.js.map +1 -1
- package/dist/terminal/host.d.ts.map +1 -1
- package/dist/terminal/host.js +11 -10
- package/dist/terminal/host.js.map +1 -1
- package/dist/user/host-curate-prompt.d.ts.map +1 -1
- package/dist/user/host-curate-prompt.js +9 -5
- package/dist/user/host-curate-prompt.js.map +1 -1
- package/dist/user/host-prompt.d.ts.map +1 -1
- package/dist/user/host-prompt.js +9 -5
- package/dist/user/host-prompt.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { cleanupResources } from "../../cleanup.js";
|
|
2
3
|
import { COPILOT_PARENT_ENV_TO_CLEAR } from "./environment.js";
|
|
3
4
|
import { FileAndTurnCompletionGate, validateCompletionPolicy } from "../completion-gate.js";
|
|
4
5
|
import { existsSync } from "node:fs";
|
|
@@ -16,6 +17,7 @@ import { recordProp, stringProp, rawStringProp } from "./record-properties.js";
|
|
|
16
17
|
import { CopilotBackgroundWorkState, CopilotFinalBoundaryState, isCopilotTurnInitiatingUserLine as isLiveRootSubmit, } from "./completion.js";
|
|
17
18
|
import { getCopilotSessionDir } from "./session.js";
|
|
18
19
|
import { CopilotComposerInspector } from "./composer.js";
|
|
20
|
+
import { describeRecoveryEvent, matchesHistoryEvidence } from "./observer-extension/com.github.copilot/extensions/poly-weaver-observer/history-evidence.mjs";
|
|
19
21
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
22
|
const OBSERVER_PLUGIN_ASSET_DIR = join(__dirname, "observer-extension");
|
|
21
23
|
const OBSERVER_PLUGIN_TEMPLATE_NAME = "poly-weaver-copilot-observer";
|
|
@@ -24,7 +26,7 @@ const OBSERVER_PLUGIN_IDENTITY_PREFIX = "poly-weaver-observer";
|
|
|
24
26
|
const COPILOT_PLUGIN_NAME_MAX_LENGTH = 64;
|
|
25
27
|
const COPILOT_PLUGIN_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
26
28
|
const COPILOT_EXTENSIONS_CONTRACT = "com.github.copilot/extensions";
|
|
27
|
-
const READY_TIMEOUT_MS = 60_000;
|
|
29
|
+
const READY_TIMEOUT_MS = 5 * 60_000;
|
|
28
30
|
const PLAN_READ_TIMEOUT_MS = 5_000;
|
|
29
31
|
const PLAN_READ_RETRY_MS = 250;
|
|
30
32
|
const OBSERVER_DIAGNOSTIC_EVENT_TAIL_LIMIT = 80;
|
|
@@ -152,8 +154,8 @@ export class CopilotObservationBroker {
|
|
|
152
154
|
parts = [];
|
|
153
155
|
return;
|
|
154
156
|
}
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
+
// Committed live reductions backpressure the parser. Activation must
|
|
158
|
+
// instead consume already-received ingress before publishing its draft.
|
|
157
159
|
if (authed?.isConnectionDraining(socket)) {
|
|
158
160
|
scheduled = setImmediate(drain);
|
|
159
161
|
return;
|
|
@@ -204,7 +206,7 @@ export class CopilotObservationBroker {
|
|
|
204
206
|
}
|
|
205
207
|
let decision;
|
|
206
208
|
try {
|
|
207
|
-
decision = registration.authenticate(message, socket);
|
|
209
|
+
decision = registration.authenticate(message, socket, () => pendingChars > 0 || frameChars > 0);
|
|
208
210
|
sendAuthResult(socket, decision);
|
|
209
211
|
if (!decision.accepted) {
|
|
210
212
|
socket.end();
|
|
@@ -233,9 +235,16 @@ export class CopilotObservationBroker {
|
|
|
233
235
|
chunks = [];
|
|
234
236
|
cursor = 0;
|
|
235
237
|
if (ended) {
|
|
238
|
+
if (frameChars > 0) {
|
|
239
|
+
fail("Copilot observer disconnected with an incomplete frame");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
236
242
|
authed?.finishConnection(socket, closeCause);
|
|
237
243
|
stopped = true;
|
|
238
244
|
}
|
|
245
|
+
else if (frameChars === 0 && authed?.isConnectionBackpressured(socket)) {
|
|
246
|
+
scheduled = setImmediate(drain);
|
|
247
|
+
}
|
|
239
248
|
else {
|
|
240
249
|
socket.resume();
|
|
241
250
|
}
|
|
@@ -259,6 +268,15 @@ export class CopilotObservationBroker {
|
|
|
259
268
|
if (!stopped)
|
|
260
269
|
scheduled ??= setImmediate(drain);
|
|
261
270
|
});
|
|
271
|
+
socket.on("end", () => {
|
|
272
|
+
ended = true;
|
|
273
|
+
// EOF can precede the final admitted transaction. Record the close now
|
|
274
|
+
// when ingress is exhausted, rather than granting that epoch a new deadline.
|
|
275
|
+
if (cursor >= chunks.length && frameChars === 0)
|
|
276
|
+
authed?.finishConnection(socket, closeCause);
|
|
277
|
+
if (!stopped)
|
|
278
|
+
scheduled ??= setImmediate(drain);
|
|
279
|
+
});
|
|
262
280
|
socket.on("error", (err) => {
|
|
263
281
|
closeCause = err;
|
|
264
282
|
});
|
|
@@ -275,27 +293,51 @@ export class CopilotLaunchRegistration {
|
|
|
275
293
|
consumed = false;
|
|
276
294
|
connectionAttemptCount = 0;
|
|
277
295
|
rejectedDuplicateCount = 0;
|
|
278
|
-
lifecycleEventCount = 0;
|
|
279
296
|
epochCount = 0;
|
|
280
297
|
observerReadinessTimeoutMs = READY_TIMEOUT_MS;
|
|
281
298
|
workspacePath;
|
|
282
299
|
preferredExtensionDir;
|
|
283
300
|
readyIdentityValue;
|
|
284
301
|
recoveryCause;
|
|
302
|
+
continuityProblem;
|
|
303
|
+
startupVerificationProblem;
|
|
304
|
+
lastRecoveryMode;
|
|
305
|
+
recoveryBoundaryStatus = "established";
|
|
306
|
+
recoveryRejection;
|
|
285
307
|
recoveryDeadlineAt;
|
|
286
308
|
recoveryTimer;
|
|
287
309
|
failed;
|
|
288
310
|
disposed = false;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
311
|
+
state = new CopilotObservationState();
|
|
312
|
+
automatedRequest;
|
|
313
|
+
requestRisks = new Set();
|
|
292
314
|
excludeInheritedHistory = false;
|
|
293
315
|
inheritedEventIds;
|
|
294
316
|
eventHandlers = new Set();
|
|
295
317
|
errorHandlers = new Set();
|
|
296
318
|
readyWaiters = new Set();
|
|
297
319
|
pendingRpcs = new Map();
|
|
298
|
-
evidence
|
|
320
|
+
get evidence() { return this.state.evidence; }
|
|
321
|
+
get recoveryWatermark() { return this.state.watermark; }
|
|
322
|
+
get continuityAvailable() {
|
|
323
|
+
return !this.failed && !this.disposed && !this.recoveryCause && this.activeConnection?.committed === true;
|
|
324
|
+
}
|
|
325
|
+
get outcomesAvailable() {
|
|
326
|
+
return this.continuityAvailable && this.currentReadyIdentity() !== undefined;
|
|
327
|
+
}
|
|
328
|
+
prepareRequest(prompt) {
|
|
329
|
+
return this.automatedRequest = { prompt, written: false };
|
|
330
|
+
}
|
|
331
|
+
requestForDetector() { return this.automatedRequest; }
|
|
332
|
+
trackRequestRisk(risk) {
|
|
333
|
+
this.requestRisks.add(risk);
|
|
334
|
+
return () => this.requestRisks.delete(risk);
|
|
335
|
+
}
|
|
336
|
+
mayHaveExecutingRequest() {
|
|
337
|
+
return Boolean(this.automatedRequest?.written && this.evidence.request !== this.automatedRequest)
|
|
338
|
+
|| (this.evidence.submitIndex >= 0 && !this.evidence.settled && this.evidence.abortIndex < 0)
|
|
339
|
+
|| [...this.requestRisks].some((risk) => risk());
|
|
340
|
+
}
|
|
299
341
|
constructor(opts) {
|
|
300
342
|
this.launchId = opts.launchId;
|
|
301
343
|
this.sessionId = opts.sessionId;
|
|
@@ -305,16 +347,27 @@ export class CopilotLaunchRegistration {
|
|
|
305
347
|
this.broker = opts.broker;
|
|
306
348
|
}
|
|
307
349
|
get eventCount() {
|
|
308
|
-
return this.events.length;
|
|
350
|
+
return this.state.events.length;
|
|
309
351
|
}
|
|
310
352
|
ownsConnection(socket) {
|
|
311
353
|
return !this.disposed && !this.failed && this.activeConnection?.socket === socket;
|
|
312
354
|
}
|
|
313
355
|
isConnectionDraining(socket) {
|
|
314
|
-
return this.activeConnection?.socket === socket
|
|
356
|
+
return this.activeConnection?.socket === socket
|
|
357
|
+
&& this.activeConnection.committed === true && this.activeConnection.drain !== undefined;
|
|
358
|
+
}
|
|
359
|
+
isConnectionBackpressured(socket) {
|
|
360
|
+
const connection = this.activeConnection;
|
|
361
|
+
if (connection?.socket !== socket || !connection.drain)
|
|
362
|
+
return false;
|
|
363
|
+
// Parse received chunks even under pressure, but pause further reads until
|
|
364
|
+
// reductions catch up. A partial frame must remain able to receive its end.
|
|
365
|
+
return connection.committed === true
|
|
366
|
+
|| connection.bufferedEvents.length - Math.max(connection.bufferedCursor, connection.admissionEnd ?? 0)
|
|
367
|
+
>= COPILOT_OBSERVER_WORK_QUANTUM;
|
|
315
368
|
}
|
|
316
369
|
snapshotEvents(startIndex = 0) {
|
|
317
|
-
return this.events.slice(startIndex);
|
|
370
|
+
return this.state.events.slice(startIndex);
|
|
318
371
|
}
|
|
319
372
|
snapshotDiagnostics() {
|
|
320
373
|
const connection = this.activeConnection;
|
|
@@ -333,13 +386,22 @@ export class CopilotLaunchRegistration {
|
|
|
333
386
|
currentEpoch: connection?.epoch ?? 0,
|
|
334
387
|
epochCount: this.epochCount,
|
|
335
388
|
replacementCount: Math.max(0, this.epochCount - 1),
|
|
336
|
-
recovering: this.recoveryCause !== undefined,
|
|
389
|
+
recovering: this.recoveryCause !== undefined && !this.failed && !this.disposed,
|
|
390
|
+
...(this.lastRecoveryMode && { recoveryMode: this.lastRecoveryMode }),
|
|
391
|
+
recoveryBoundaryStatus: !this.state.watermark ? "absent"
|
|
392
|
+
: this.recoveryBoundaryStatus,
|
|
393
|
+
...(this.recoveryRejection && { recoveryRejection: {
|
|
394
|
+
...this.recoveryRejection,
|
|
395
|
+
...(this.recoveryRejection.event && { event: { ...this.recoveryRejection.event } }),
|
|
396
|
+
} }),
|
|
397
|
+
...(this.continuityProblem && { continuityProblem: this.continuityProblem }),
|
|
398
|
+
...(this.startupVerificationProblem && { startupVerificationProblem: this.startupVerificationProblem }),
|
|
337
399
|
connectionAttemptCount: this.connectionAttemptCount,
|
|
338
400
|
rejectedDuplicateCount: this.rejectedDuplicateCount,
|
|
339
|
-
lifecycleEventCount: this.lifecycleEventCount,
|
|
340
|
-
eventCount: this.
|
|
401
|
+
lifecycleEventCount: this.state.lifecycleEventCount,
|
|
402
|
+
eventCount: this.eventCount,
|
|
341
403
|
lastEventSeq: connection ? connection.nextSeq - 1 : 0,
|
|
342
|
-
eventTail: diagnosticEventTail(this.events),
|
|
404
|
+
eventTail: diagnosticEventTail(this.state.events),
|
|
343
405
|
disposed: this.disposed,
|
|
344
406
|
...(this.workspacePath && { workspacePath: this.workspacePath }),
|
|
345
407
|
...(this.recoveryDeadlineAt !== undefined && { recoveryDeadlineAt: this.recoveryDeadlineAt }),
|
|
@@ -347,7 +409,7 @@ export class CopilotLaunchRegistration {
|
|
|
347
409
|
...(this.failed && { failureReason: this.failed.message }),
|
|
348
410
|
};
|
|
349
411
|
}
|
|
350
|
-
authenticate(message, socket) {
|
|
412
|
+
authenticate(message, socket, hasPendingIngress = () => false) {
|
|
351
413
|
this.connectionAttemptCount++;
|
|
352
414
|
if (this.disposed) {
|
|
353
415
|
return {
|
|
@@ -382,8 +444,13 @@ export class CopilotLaunchRegistration {
|
|
|
382
444
|
};
|
|
383
445
|
}
|
|
384
446
|
this.consumed = true;
|
|
447
|
+
const recovery = this.state.watermark
|
|
448
|
+
? { mode: "suffix", afterEventId: this.state.watermark }
|
|
449
|
+
: this.mayHaveExecutingRequest() ? { mode: "uncertain" } : { mode: "live-only" };
|
|
450
|
+
this.lastRecoveryMode = recovery.mode;
|
|
385
451
|
this.activeConnection = {
|
|
386
452
|
socket,
|
|
453
|
+
hasPendingIngress,
|
|
387
454
|
epoch: ++this.epochCount,
|
|
388
455
|
nextSeq: 1,
|
|
389
456
|
subscriptionReady: false,
|
|
@@ -393,10 +460,11 @@ export class CopilotLaunchRegistration {
|
|
|
393
460
|
bufferedCursor: 0,
|
|
394
461
|
bufferedChars: 0,
|
|
395
462
|
extensionPath: message.extensionPath,
|
|
463
|
+
recovery,
|
|
396
464
|
};
|
|
397
465
|
return {
|
|
398
466
|
accepted: true,
|
|
399
|
-
|
|
467
|
+
recovery,
|
|
400
468
|
...(this.excludeInheritedHistory && !this.inheritedEventIds && { captureHistoryBaseline: true }),
|
|
401
469
|
};
|
|
402
470
|
}
|
|
@@ -558,7 +626,7 @@ export class CopilotLaunchRegistration {
|
|
|
558
626
|
if (this.failed)
|
|
559
627
|
return;
|
|
560
628
|
this.failed = error;
|
|
561
|
-
this.
|
|
629
|
+
this.stopRecoveryTimer();
|
|
562
630
|
const connection = this.activeConnection;
|
|
563
631
|
this.activeConnection = undefined;
|
|
564
632
|
if (connection?.drain)
|
|
@@ -585,7 +653,7 @@ export class CopilotLaunchRegistration {
|
|
|
585
653
|
this.disposed = true;
|
|
586
654
|
this.broker.unregisterLaunch(this);
|
|
587
655
|
const disposedError = new Error("Copilot observer disposed");
|
|
588
|
-
this.
|
|
656
|
+
this.stopRecoveryTimer();
|
|
589
657
|
for (const waiter of this.readyWaiters) {
|
|
590
658
|
if (waiter.timer)
|
|
591
659
|
clearTimeout(waiter.timer);
|
|
@@ -617,6 +685,21 @@ export class CopilotLaunchRegistration {
|
|
|
617
685
|
return;
|
|
618
686
|
}
|
|
619
687
|
connection.nextSeq++;
|
|
688
|
+
if (message.origin === "history") {
|
|
689
|
+
if (connection.recovery.mode !== "suffix" || !connection.recoveryStarted
|
|
690
|
+
|| connection.recoveryComplete || connection.liveSuffixStarted
|
|
691
|
+
|| !durableEventId(message)) {
|
|
692
|
+
this.fail(new Error("Copilot observer received history outside a verified recovery suffix"));
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (message.event.id === connection.recovery.afterEventId) {
|
|
696
|
+
this.fail(new Error("Copilot observer recovery included its excluded watermark"));
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
else if (connection.recoveryStarted) {
|
|
701
|
+
connection.liveSuffixStarted = true;
|
|
702
|
+
}
|
|
620
703
|
const eventId = stringProp(message.event, "id");
|
|
621
704
|
if (eventId && this.inheritedEventIds?.has(eventId))
|
|
622
705
|
return;
|
|
@@ -625,18 +708,59 @@ export class CopilotLaunchRegistration {
|
|
|
625
708
|
this.fail(new Error(historyPayloadError));
|
|
626
709
|
return;
|
|
627
710
|
}
|
|
628
|
-
connection.bufferedEvents.push(message
|
|
711
|
+
connection.bufferedEvents.push(message);
|
|
629
712
|
connection.bufferedChars += JSON.stringify(message.event).length;
|
|
630
713
|
if (connection.bufferedEvents.length - connection.bufferedCursor > OBSERVER_MAX_TENTATIVE_EVENTS
|
|
631
714
|
|| connection.bufferedChars > OBSERVER_MAX_PENDING_CHARS) {
|
|
632
715
|
this.fail(new Error("Copilot observer readiness buffer exceeds the transport resource limit"));
|
|
633
716
|
return;
|
|
634
717
|
}
|
|
635
|
-
if (!this.updateLifecycle(message.event, connection))
|
|
718
|
+
if (message.origin === "live" && !this.updateLifecycle(message.event, connection))
|
|
636
719
|
return;
|
|
637
720
|
this.resolveReadyIfPossible(connection);
|
|
638
721
|
}
|
|
639
722
|
acceptControl(message, connection) {
|
|
723
|
+
if (message.type === "lifecycle.unverified") {
|
|
724
|
+
this.startupVerificationProblem = message.error || "Startup lifecycle durability was not verified";
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (message.type === "recovery.error") {
|
|
728
|
+
this.continuityProblem = message.error || "SDK history boundary is unavailable";
|
|
729
|
+
if (message.recoveryRejection)
|
|
730
|
+
this.recordRecoveryRejection(message.recoveryRejection);
|
|
731
|
+
else
|
|
732
|
+
this.recoveryBoundaryStatus = connection.recoveryBoundary ? "established" : "unknown";
|
|
733
|
+
this.startRecovery(this.disconnectCause(connection, undefined));
|
|
734
|
+
this.disconnect(connection.socket);
|
|
735
|
+
connection.socket.destroy();
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (message.type === "recovery.begin" || message.type === "recovery.end") {
|
|
739
|
+
if (connection.recovery.mode !== "suffix"
|
|
740
|
+
|| message.afterEventId !== connection.recovery.afterEventId
|
|
741
|
+
|| connection.subscriptionReady
|
|
742
|
+
|| (message.type === "recovery.begin" ? connection.recoveryStarted
|
|
743
|
+
: !connection.recoveryStarted || connection.recoveryComplete)) {
|
|
744
|
+
this.fail(new Error("Copilot observer reported an invalid recovery boundary"));
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (message.type === "recovery.begin") {
|
|
748
|
+
connection.recoveryStarted = true;
|
|
749
|
+
connection.recoveryBoundary = message.event;
|
|
750
|
+
}
|
|
751
|
+
else
|
|
752
|
+
connection.recoveryComplete = true;
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (message.type === "session.lifecycle") {
|
|
756
|
+
if (!message.event || (message.event.type !== "session.start" && message.event.type !== "session.resume")) {
|
|
757
|
+
this.fail(new Error("Copilot observer reported invalid live lifecycle evidence"));
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
this.updateLifecycle(message.event, connection);
|
|
761
|
+
this.resolveReadyIfPossible(connection);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
640
764
|
if (message.type === "inquiry.history_baseline") {
|
|
641
765
|
if (!this.excludeInheritedHistory || this.inheritedEventIds || connection.inheritedEventIds
|
|
642
766
|
|| !connection.joinedSessionReady || connection.subscriptionReady
|
|
@@ -650,7 +774,13 @@ export class CopilotLaunchRegistration {
|
|
|
650
774
|
return;
|
|
651
775
|
}
|
|
652
776
|
if (message.type === "subscription.ready") {
|
|
777
|
+
if (connection.recovery.mode === "uncertain"
|
|
778
|
+
|| (connection.recovery.mode === "suffix" && !connection.recoveryComplete)) {
|
|
779
|
+
this.fail(new Error("Copilot observer subscription preceded recovery reconciliation"));
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
653
782
|
connection.subscriptionReady = true;
|
|
783
|
+
connection.admissionEnd ??= connection.bufferedEvents.length;
|
|
654
784
|
this.resolveReadyIfPossible(connection);
|
|
655
785
|
return;
|
|
656
786
|
}
|
|
@@ -705,7 +835,7 @@ export class CopilotLaunchRegistration {
|
|
|
705
835
|
resolveReadyIfPossible(connection) {
|
|
706
836
|
if (connection !== this.activeConnection || !this.isConnectionAdmitted(connection))
|
|
707
837
|
return;
|
|
708
|
-
if (connection.bufferedCursor < connection.bufferedEvents.length) {
|
|
838
|
+
if (!connection.committed || connection.bufferedCursor < connection.bufferedEvents.length) {
|
|
709
839
|
connection.drain ??= setImmediate(() => this.flushConnectionEventsIfReady(connection));
|
|
710
840
|
return;
|
|
711
841
|
}
|
|
@@ -714,11 +844,11 @@ export class CopilotLaunchRegistration {
|
|
|
714
844
|
this.inheritedEventIds = connection.inheritedEventIds;
|
|
715
845
|
}
|
|
716
846
|
this.readyIdentityValue = ready;
|
|
717
|
-
this.clearRecovery();
|
|
718
847
|
if (connection.closed) {
|
|
719
848
|
this.disconnect(connection.socket, connection.closed.cause);
|
|
720
849
|
return;
|
|
721
850
|
}
|
|
851
|
+
this.clearRecovery();
|
|
722
852
|
for (const waiter of this.readyWaiters) {
|
|
723
853
|
if (waiter.timer)
|
|
724
854
|
clearTimeout(waiter.timer);
|
|
@@ -744,49 +874,100 @@ export class CopilotLaunchRegistration {
|
|
|
744
874
|
}
|
|
745
875
|
isConnectionReady(connection) {
|
|
746
876
|
return this.isConnectionAdmitted(connection)
|
|
877
|
+
&& connection.committed === true
|
|
747
878
|
&& connection.bufferedCursor === connection.bufferedEvents.length && !connection.drain;
|
|
748
879
|
}
|
|
749
880
|
isConnectionAdmitted(connection) {
|
|
750
881
|
return connection.subscriptionReady && connection.joinedSessionReady && connection.lifecycleReady
|
|
882
|
+
&& connection.recovery.mode !== "uncertain"
|
|
883
|
+
&& (connection.recovery.mode !== "suffix" || connection.recoveryComplete === true)
|
|
751
884
|
&& (!this.excludeInheritedHistory || this.inheritedEventIds !== undefined || connection.inheritedEventIds !== undefined);
|
|
752
885
|
}
|
|
753
886
|
flushConnectionEventsIfReady(connection) {
|
|
754
887
|
connection.drain = undefined;
|
|
755
888
|
if (connection !== this.activeConnection || !this.isConnectionAdmitted(connection))
|
|
756
889
|
return;
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
890
|
+
if (!connection.committed) {
|
|
891
|
+
connection.transaction ??= this.prepareActivation(connection);
|
|
892
|
+
for (let i = 0; i < COPILOT_OBSERVER_WORK_QUANTUM; i++) {
|
|
893
|
+
let next;
|
|
894
|
+
try {
|
|
895
|
+
next = connection.transaction.next();
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
if (!(error instanceof CopilotRecoveryOrderError))
|
|
899
|
+
throw error;
|
|
900
|
+
this.continuityProblem = error.message;
|
|
901
|
+
this.recordRecoveryRejection(error.rejection);
|
|
902
|
+
this.startRecovery(this.disconnectCause(connection, undefined));
|
|
903
|
+
this.disconnect(connection.socket);
|
|
904
|
+
connection.socket.destroy();
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (!next.done)
|
|
908
|
+
continue;
|
|
909
|
+
// Host receipts are deliberately outside the draft. A concurrent write
|
|
910
|
+
// cannot make a previously safe live-only attachment safe to publish.
|
|
911
|
+
if (connection.recovery.mode === "live-only" && this.mayHaveExecutingRequest()) {
|
|
912
|
+
this.continuityProblem = "A request may have started without a durable recovery watermark";
|
|
913
|
+
this.startRecovery(this.disconnectCause(connection, undefined));
|
|
914
|
+
this.disconnect(connection.socket);
|
|
915
|
+
connection.socket.destroy();
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
this.state = next.value;
|
|
919
|
+
connection.transaction = undefined;
|
|
920
|
+
connection.committed = true;
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
const end = Math.min(connection.bufferedEvents.length, connection.bufferedCursor + COPILOT_OBSERVER_WORK_QUANTUM);
|
|
926
|
+
while (connection.bufferedCursor < end) {
|
|
927
|
+
this.state.accept(connection.bufferedEvents[connection.bufferedCursor++], this.sessionId, this.automatedRequest);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (connection.committed && connection.bufferedCursor === connection.bufferedEvents.length) {
|
|
766
931
|
connection.bufferedEvents = [];
|
|
767
932
|
connection.bufferedCursor = 0;
|
|
768
933
|
connection.bufferedChars = 0;
|
|
769
934
|
}
|
|
770
935
|
this.resolveReadyIfPossible(connection);
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
936
|
+
if (connection.committed && !connection.drain) {
|
|
937
|
+
const last = this.state.events.at(-1);
|
|
938
|
+
if (last)
|
|
939
|
+
for (const handler of this.eventHandlers)
|
|
940
|
+
handler(last);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
*prepareActivation(connection) {
|
|
944
|
+
const draft = yield* this.state.copy();
|
|
945
|
+
if (connection.recovery.mode === "suffix") {
|
|
946
|
+
const end = connection.admissionEnd;
|
|
947
|
+
const recovered = yield* reconcileRecoveryEvents(connection.bufferedEvents, end, connection.recovery.afterEventId, this.state.watermarkEvent, connection.recoveryBoundary);
|
|
948
|
+
for (const message of recovered) {
|
|
949
|
+
draft.accept(message, this.sessionId, this.automatedRequest);
|
|
950
|
+
yield;
|
|
951
|
+
}
|
|
952
|
+
// Ingress keeps appending to the original queue while the draft yields.
|
|
953
|
+
// Only the sealed admission prefix was reconciled; never replace that queue.
|
|
954
|
+
connection.bufferedCursor = end;
|
|
955
|
+
}
|
|
956
|
+
for (;;) {
|
|
957
|
+
while (connection.bufferedCursor < connection.bufferedEvents.length) {
|
|
958
|
+
const message = connection.bufferedEvents[connection.bufferedCursor++];
|
|
959
|
+
const id = stringProp(message.event, "id");
|
|
960
|
+
if (id)
|
|
961
|
+
connection.inheritedEventIds?.delete(id);
|
|
962
|
+
draft.accept(message, this.sessionId, this.automatedRequest);
|
|
963
|
+
yield;
|
|
964
|
+
}
|
|
965
|
+
// The transport parser also yields. An empty event queue is not a
|
|
966
|
+
// publication barrier while received frames still await parsing.
|
|
967
|
+
if (!connection.hasPendingIngress())
|
|
968
|
+
return draft;
|
|
969
|
+
yield;
|
|
775
970
|
}
|
|
776
|
-
const eventId = eventStableId(event);
|
|
777
|
-
const replayKey = eventReplayKey(event);
|
|
778
|
-
if (eventId && this.seenEventIds.has(eventId))
|
|
779
|
-
return;
|
|
780
|
-
if (replayKey && this.seenEventReplayKeys.has(replayKey))
|
|
781
|
-
return;
|
|
782
|
-
if (eventId)
|
|
783
|
-
this.seenEventIds.add(eventId);
|
|
784
|
-
if (replayKey)
|
|
785
|
-
this.seenEventReplayKeys.add(replayKey);
|
|
786
|
-
this.events.push(event);
|
|
787
|
-
this.evidence.accept(event, this.events.length - 1, this.sessionId);
|
|
788
|
-
for (const handler of this.eventHandlers)
|
|
789
|
-
handler(event);
|
|
790
971
|
}
|
|
791
972
|
flushPendingRpcs() {
|
|
792
973
|
for (const requestId of this.pendingRpcs.keys()) {
|
|
@@ -816,15 +997,29 @@ export class CopilotLaunchRegistration {
|
|
|
816
997
|
this.recoveryDeadlineAt = Date.now() + this.observerReadinessTimeoutMs;
|
|
817
998
|
this.recoveryTimer = setTimeout(() => {
|
|
818
999
|
this.recoveryTimer = undefined;
|
|
819
|
-
this.
|
|
1000
|
+
const original = this.recoveryCause ?? cause;
|
|
1001
|
+
this.fail(this.continuityProblem
|
|
1002
|
+
? new Error(`Copilot observation continuity uncertain for session ${this.sessionId} (launch ${this.launchId}): ${this.continuityProblem}; ${original.message}`, { cause: original })
|
|
1003
|
+
: original);
|
|
820
1004
|
}, Math.max(0, this.recoveryDeadlineAt - Date.now()));
|
|
821
1005
|
}
|
|
822
1006
|
clearRecovery() {
|
|
1007
|
+
this.stopRecoveryTimer();
|
|
1008
|
+
this.recoveryCause = undefined;
|
|
1009
|
+
this.continuityProblem = undefined;
|
|
1010
|
+
this.recoveryBoundaryStatus = "established";
|
|
1011
|
+
this.recoveryRejection = undefined;
|
|
1012
|
+
this.recoveryDeadlineAt = undefined;
|
|
1013
|
+
}
|
|
1014
|
+
stopRecoveryTimer() {
|
|
823
1015
|
if (this.recoveryTimer)
|
|
824
1016
|
clearTimeout(this.recoveryTimer);
|
|
825
1017
|
this.recoveryTimer = undefined;
|
|
826
|
-
|
|
827
|
-
|
|
1018
|
+
}
|
|
1019
|
+
recordRecoveryRejection(rejection) {
|
|
1020
|
+
this.recoveryRejection = rejection;
|
|
1021
|
+
this.recoveryBoundaryStatus = rejection.boundary === "located" ? "established"
|
|
1022
|
+
: rejection.boundary === "missing" || rejection.boundary === "untrustworthy" ? "unavailable" : "unknown";
|
|
828
1023
|
}
|
|
829
1024
|
disconnectCause(connection, cause) {
|
|
830
1025
|
if (cause)
|
|
@@ -839,6 +1034,147 @@ export class CopilotLaunchRegistration {
|
|
|
839
1034
|
return samePath(dirname(extensionPath), this.preferredExtensionDir);
|
|
840
1035
|
}
|
|
841
1036
|
}
|
|
1037
|
+
class CopilotRecoveryOrderError extends Error {
|
|
1038
|
+
rejection;
|
|
1039
|
+
constructor(message, rejection) {
|
|
1040
|
+
super(message);
|
|
1041
|
+
this.rejection = rejection;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
/** Canonical history orders overlap; parent evidence precedes surrounding-match fallback. */
|
|
1045
|
+
function* reconcileRecoveryEvents(events, end, watermark, watermarkEvent, boundaryRecord) {
|
|
1046
|
+
if (watermarkEvent && boundaryRecord && !matchesHistoryEvidence(watermarkEvent, boundaryRecord)) {
|
|
1047
|
+
throw new CopilotRecoveryOrderError("SDK history conflicts with the committed watermark record", {
|
|
1048
|
+
stage: "reconciliation", boundary: "untrustworthy",
|
|
1049
|
+
event: describeRecoveryEvent(boundaryRecord, "history", new Map([[watermark, -1]])),
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
const history = [];
|
|
1053
|
+
const live = [];
|
|
1054
|
+
const historyPositions = new Map([[watermark, -1]]);
|
|
1055
|
+
const callbackPositions = new Map();
|
|
1056
|
+
const records = new Map();
|
|
1057
|
+
if (watermarkEvent)
|
|
1058
|
+
records.set(watermark, watermarkEvent);
|
|
1059
|
+
const planTools = new Map();
|
|
1060
|
+
const recordPlanTools = (event, position) => {
|
|
1061
|
+
const tools = recordProp(event, "data")?.toolRequests;
|
|
1062
|
+
if (Array.isArray(tools))
|
|
1063
|
+
for (const tool of tools) {
|
|
1064
|
+
if (isRecord(tool) && tool.name === "exit_plan_mode") {
|
|
1065
|
+
const id = stringProp(tool, "toolCallId");
|
|
1066
|
+
if (id)
|
|
1067
|
+
planTools.set(id, position);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
if (watermarkEvent)
|
|
1072
|
+
recordPlanTools(watermarkEvent, -1);
|
|
1073
|
+
const reject = (reason, message, boundary = "located") => new CopilotRecoveryOrderError(reason, {
|
|
1074
|
+
stage: "reconciliation", boundary,
|
|
1075
|
+
event: describeRecoveryEvent(message.event, message.origin, historyPositions, callbackPositions),
|
|
1076
|
+
});
|
|
1077
|
+
for (let i = 0; i < end; i++) {
|
|
1078
|
+
const message = events[i];
|
|
1079
|
+
if (message.origin === "history") {
|
|
1080
|
+
const id = stringProp(message.event, "id");
|
|
1081
|
+
if (!id || historyPositions.has(id))
|
|
1082
|
+
throw reject("SDK history has ambiguous durable event identities", message, "untrustworthy");
|
|
1083
|
+
historyPositions.set(id, history.length);
|
|
1084
|
+
records.set(id, message.event);
|
|
1085
|
+
recordPlanTools(message.event, history.length);
|
|
1086
|
+
history.push(message);
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
live.push(message);
|
|
1090
|
+
}
|
|
1091
|
+
yield;
|
|
1092
|
+
}
|
|
1093
|
+
for (let i = 0; i < history.length; i++) {
|
|
1094
|
+
const parent = historyPositions.get(stringProp(history[i].event, "parentId") ?? "");
|
|
1095
|
+
if (parent !== undefined && parent >= i)
|
|
1096
|
+
throw reject("SDK history has an inconsistent event order", history[i], "untrustworthy");
|
|
1097
|
+
yield;
|
|
1098
|
+
}
|
|
1099
|
+
const followingMatches = new Array(live.length);
|
|
1100
|
+
for (const message of live) {
|
|
1101
|
+
const id = stringProp(message.event, "id");
|
|
1102
|
+
const record = id ? records.get(id) : undefined;
|
|
1103
|
+
if (record && !matchesHistoryEvidence(message.event, record)) {
|
|
1104
|
+
throw reject("SDK callback conflicts with its durable history record", message);
|
|
1105
|
+
}
|
|
1106
|
+
if (id)
|
|
1107
|
+
callbackPositions.set(id, 0);
|
|
1108
|
+
yield;
|
|
1109
|
+
}
|
|
1110
|
+
// Validation tracked availability only; placement rebuilds supported ancestry.
|
|
1111
|
+
callbackPositions.clear();
|
|
1112
|
+
let following;
|
|
1113
|
+
for (let i = live.length - 1; i >= 0; i--) {
|
|
1114
|
+
followingMatches[i] = following;
|
|
1115
|
+
const id = stringProp(live[i].event, "id");
|
|
1116
|
+
following = (id ? historyPositions.get(id) : undefined) ?? following;
|
|
1117
|
+
yield;
|
|
1118
|
+
}
|
|
1119
|
+
const slots = new Map();
|
|
1120
|
+
let preceding;
|
|
1121
|
+
for (let i = 0; i < live.length; i++) {
|
|
1122
|
+
const message = live[i];
|
|
1123
|
+
const event = message.event;
|
|
1124
|
+
const id = stringProp(event, "id");
|
|
1125
|
+
const historyPosition = id ? historyPositions.get(id) : undefined;
|
|
1126
|
+
if (historyPosition !== undefined) {
|
|
1127
|
+
preceding = historyPosition;
|
|
1128
|
+
yield;
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
const record = id ? records.get(id) : undefined;
|
|
1132
|
+
if (record) {
|
|
1133
|
+
if (!matchesHistoryEvidence(event, record)) {
|
|
1134
|
+
throw reject("SDK callback conflicts with an earlier callback record", message);
|
|
1135
|
+
}
|
|
1136
|
+
yield;
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
const parentId = stringProp(event, "parentId");
|
|
1140
|
+
const parent = message.origin === "bridge" && message.toolCallId
|
|
1141
|
+
? planTools.get(message.toolCallId)
|
|
1142
|
+
: historyPositions.get(parentId ?? "") ?? callbackPositions.get(parentId ?? "");
|
|
1143
|
+
const lifecycle = event.type === "session.start" || event.type === "session.resume";
|
|
1144
|
+
const telemetry = isModelTelemetry(event);
|
|
1145
|
+
const next = followingMatches[i];
|
|
1146
|
+
let slot = preceding !== undefined && next !== undefined ? Math.min(preceding, next) + 1
|
|
1147
|
+
: preceding !== undefined ? preceding + 1
|
|
1148
|
+
: next !== undefined ? Math.max(0, next) : history.length;
|
|
1149
|
+
const transient = event.ephemeral === true || message.origin === "bridge"
|
|
1150
|
+
|| event.type === "session.idle" || isAbortEvent(event);
|
|
1151
|
+
if (!lifecycle && !telemetry) {
|
|
1152
|
+
if (parent !== undefined)
|
|
1153
|
+
slot = parent + 1;
|
|
1154
|
+
}
|
|
1155
|
+
const bucket = slots.get(slot) ?? [];
|
|
1156
|
+
bucket.push(transient || lifecycle || telemetry ? { ...message, transient: true } : message);
|
|
1157
|
+
slots.set(slot, bucket);
|
|
1158
|
+
if (!lifecycle && !telemetry && id) {
|
|
1159
|
+
callbackPositions.set(id, slot - 1);
|
|
1160
|
+
records.set(id, event);
|
|
1161
|
+
recordPlanTools(event, slot - 1);
|
|
1162
|
+
}
|
|
1163
|
+
yield;
|
|
1164
|
+
}
|
|
1165
|
+
const reconciled = [];
|
|
1166
|
+
for (let i = 0; i <= history.length; i++) {
|
|
1167
|
+
for (const message of slots.get(i) ?? []) {
|
|
1168
|
+
reconciled.push(message);
|
|
1169
|
+
yield;
|
|
1170
|
+
}
|
|
1171
|
+
if (i < history.length) {
|
|
1172
|
+
reconciled.push(history[i]);
|
|
1173
|
+
yield;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return reconciled;
|
|
1177
|
+
}
|
|
842
1178
|
export function copilotObserverPluginIdentityForLaunchId(launchId) {
|
|
843
1179
|
const compactId = launchId.toLowerCase().replace(/-/g, "");
|
|
844
1180
|
const identity = `${OBSERVER_PLUGIN_IDENTITY_PREFIX}-${compactId}`;
|
|
@@ -861,6 +1197,7 @@ export class CopilotProcessObserver {
|
|
|
861
1197
|
args;
|
|
862
1198
|
env;
|
|
863
1199
|
disposed = false;
|
|
1200
|
+
disposePromise;
|
|
864
1201
|
composer = new CopilotComposerInspector();
|
|
865
1202
|
constructor(broker, registration, tempRoot, pluginDir, pluginIdentity, extensionDir, sessionExtensionDir, sdkPath, sdkEntryPath, ownsBroker, ownsTempRoot, onDispose) {
|
|
866
1203
|
this.broker = broker;
|
|
@@ -896,22 +1233,24 @@ export class CopilotProcessObserver {
|
|
|
896
1233
|
const broker = await CopilotObservationBroker.start();
|
|
897
1234
|
try {
|
|
898
1235
|
const sdkAssets = await resolveCopilotObserverSdkAssets();
|
|
899
|
-
return await CopilotProcessObserver.createWithResources(broker, sdkAssets, sessionId, true, opts.excludeInheritedHistory);
|
|
1236
|
+
return await CopilotProcessObserver.createWithResources(broker, sdkAssets, sessionId, true, opts.excludeInheritedHistory, undefined, opts.prompt);
|
|
900
1237
|
}
|
|
901
1238
|
catch (err) {
|
|
902
|
-
await broker.close();
|
|
1239
|
+
await cleanupResources([() => broker.close()], { error: err });
|
|
903
1240
|
throw err;
|
|
904
1241
|
}
|
|
905
1242
|
}
|
|
906
1243
|
static async createWithSharedBroker(broker, sdkAssets, opts, onDispose) {
|
|
907
1244
|
const sessionId = requiredCopilotObserverSessionId(opts);
|
|
908
|
-
return CopilotProcessObserver.createWithResources(broker, sdkAssets, sessionId, false, opts.excludeInheritedHistory, onDispose);
|
|
1245
|
+
return CopilotProcessObserver.createWithResources(broker, sdkAssets, sessionId, false, opts.excludeInheritedHistory, onDispose, opts.prompt);
|
|
909
1246
|
}
|
|
910
|
-
static async createWithResources(broker, sdkAssets, sessionId, ownsBroker, excludeInheritedHistory = false, onDispose) {
|
|
1247
|
+
static async createWithResources(broker, sdkAssets, sessionId, ownsBroker, excludeInheritedHistory = false, onDispose, prompt = "") {
|
|
911
1248
|
let pluginAssets;
|
|
912
1249
|
let registration;
|
|
913
1250
|
try {
|
|
914
1251
|
registration = broker.registerLaunch(sessionId);
|
|
1252
|
+
if (prompt)
|
|
1253
|
+
registration.prepareRequest(prompt);
|
|
915
1254
|
if (excludeInheritedHistory)
|
|
916
1255
|
registration.requireInheritedHistoryBoundary();
|
|
917
1256
|
const pluginIdentity = copilotObserverPluginIdentityForLaunchId(registration.launchId);
|
|
@@ -920,10 +1259,10 @@ export class CopilotProcessObserver {
|
|
|
920
1259
|
return new CopilotProcessObserver(broker, registration, pluginAssets.tempRoot, pluginAssets.pluginDir, pluginAssets.pluginIdentity, pluginAssets.extensionDir, pluginAssets.sessionExtensionDir, sdkAssets.sdkPath, sdkAssets.sdkEntryPath, ownsBroker, true, onDispose);
|
|
921
1260
|
}
|
|
922
1261
|
catch (err) {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
}
|
|
1262
|
+
await cleanupResources([
|
|
1263
|
+
() => registration?.dispose(),
|
|
1264
|
+
() => pluginAssets ? cleanupCopilotObserverPluginAssets(pluginAssets) : undefined,
|
|
1265
|
+
], { error: err });
|
|
927
1266
|
throw err;
|
|
928
1267
|
}
|
|
929
1268
|
}
|
|
@@ -937,6 +1276,10 @@ export class CopilotProcessObserver {
|
|
|
937
1276
|
return this.registration.waitForReady(opts);
|
|
938
1277
|
}
|
|
939
1278
|
async submitPrompt(target, prompt, opts = {}) {
|
|
1279
|
+
const existing = this.registration.requestForDetector();
|
|
1280
|
+
const request = existing && !existing.written
|
|
1281
|
+
? existing : this.registration.prepareRequest(prompt);
|
|
1282
|
+
request.prompt = prompt;
|
|
940
1283
|
await this.registration.waitForUsableConnection({ signal: opts.signal });
|
|
941
1284
|
await sendComposerText(target, prompt, this.composer, {
|
|
942
1285
|
signal: opts.signal,
|
|
@@ -944,6 +1287,7 @@ export class CopilotProcessObserver {
|
|
|
944
1287
|
beforeSubmit: async () => {
|
|
945
1288
|
await this.registration.waitForUsableConnection({ signal: opts.signal });
|
|
946
1289
|
},
|
|
1290
|
+
onSubmitWritten: () => { request.written = true; },
|
|
947
1291
|
});
|
|
948
1292
|
}
|
|
949
1293
|
createDetector(opts) {
|
|
@@ -965,25 +1309,24 @@ export class CopilotProcessObserver {
|
|
|
965
1309
|
sdkPath: this.sdkPath,
|
|
966
1310
|
};
|
|
967
1311
|
}
|
|
968
|
-
|
|
969
|
-
if (this.
|
|
970
|
-
return;
|
|
1312
|
+
dispose() {
|
|
1313
|
+
if (this.disposePromise)
|
|
1314
|
+
return this.disposePromise;
|
|
971
1315
|
this.disposed = true;
|
|
972
|
-
this.
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
this.
|
|
978
|
-
? rm(this.tempRoot, { recursive: true, force: true }).catch(() => { })
|
|
979
|
-
: Promise.resolve(),
|
|
980
|
-
rm(this.sessionExtensionDir, { recursive: true, force: true }).catch(() => { }),
|
|
1316
|
+
this.disposePromise = cleanupResources([
|
|
1317
|
+
() => this.registration.dispose(),
|
|
1318
|
+
() => this.ownsBroker ? this.broker.close() : undefined,
|
|
1319
|
+
() => this.ownsTempRoot ? rm(this.tempRoot, { recursive: true, force: true }) : undefined,
|
|
1320
|
+
() => rm(this.sessionExtensionDir, { recursive: true, force: true }),
|
|
1321
|
+
() => this.onDispose?.(this),
|
|
981
1322
|
]);
|
|
1323
|
+
return this.disposePromise;
|
|
982
1324
|
}
|
|
983
1325
|
}
|
|
984
1326
|
export class CopilotObserverRun {
|
|
985
1327
|
provider = "copilot";
|
|
986
1328
|
disposed = false;
|
|
1329
|
+
disposePromise;
|
|
987
1330
|
observers = new Set();
|
|
988
1331
|
sharedResourcesValue;
|
|
989
1332
|
sharedResourcesPromise;
|
|
@@ -1005,25 +1348,30 @@ export class CopilotObserverRun {
|
|
|
1005
1348
|
this.observers.add(observer);
|
|
1006
1349
|
return observer;
|
|
1007
1350
|
}
|
|
1008
|
-
|
|
1009
|
-
if (this.
|
|
1010
|
-
return;
|
|
1351
|
+
dispose() {
|
|
1352
|
+
if (this.disposePromise)
|
|
1353
|
+
return this.disposePromise;
|
|
1011
1354
|
this.disposed = true;
|
|
1012
1355
|
const observers = [...this.observers];
|
|
1013
1356
|
this.observers.clear();
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1357
|
+
this.disposePromise = cleanupResources([
|
|
1358
|
+
...observers.map((observer) => () => observer.dispose()),
|
|
1359
|
+
async () => {
|
|
1360
|
+
let resources = this.sharedResourcesValue;
|
|
1361
|
+
if (this.sharedResourcesPromise) {
|
|
1362
|
+
try {
|
|
1363
|
+
resources = await this.sharedResourcesPromise;
|
|
1364
|
+
}
|
|
1365
|
+
catch {
|
|
1366
|
+
// Initialization failure is reported to the launch caller.
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
this.sharedResourcesValue = undefined;
|
|
1370
|
+
this.sharedResourcesPromise = undefined;
|
|
1371
|
+
await resources?.broker.close();
|
|
1372
|
+
},
|
|
1373
|
+
]);
|
|
1374
|
+
return this.disposePromise;
|
|
1027
1375
|
}
|
|
1028
1376
|
sharedResources() {
|
|
1029
1377
|
if (this.disposed) {
|
|
@@ -1058,7 +1406,7 @@ export class CopilotObserverRun {
|
|
|
1058
1406
|
return resources;
|
|
1059
1407
|
}
|
|
1060
1408
|
catch (err) {
|
|
1061
|
-
await broker.close();
|
|
1409
|
+
await cleanupResources([() => broker.close()], { error: err });
|
|
1062
1410
|
throw err;
|
|
1063
1411
|
}
|
|
1064
1412
|
}
|
|
@@ -1092,10 +1440,10 @@ async function prepareCopilotObserverPluginAssets(pluginIdentity, launchId, sess
|
|
|
1092
1440
|
};
|
|
1093
1441
|
}
|
|
1094
1442
|
catch (err) {
|
|
1095
|
-
await
|
|
1096
|
-
rm(tempRoot, { recursive: true, force: true }),
|
|
1097
|
-
rm(sessionExtensionDir, { recursive: true, force: true }),
|
|
1098
|
-
]);
|
|
1443
|
+
await cleanupResources([
|
|
1444
|
+
() => rm(tempRoot, { recursive: true, force: true }),
|
|
1445
|
+
() => rm(sessionExtensionDir, { recursive: true, force: true }),
|
|
1446
|
+
], { error: err });
|
|
1099
1447
|
throw err;
|
|
1100
1448
|
}
|
|
1101
1449
|
}
|
|
@@ -1153,9 +1501,9 @@ function copilotObserverSessionExtensionDir(sessionId, pluginIdentity) {
|
|
|
1153
1501
|
return join(getCopilotSessionDir(), sessionId, "extensions", pluginIdentity);
|
|
1154
1502
|
}
|
|
1155
1503
|
async function cleanupCopilotObserverPluginAssets(assets) {
|
|
1156
|
-
await
|
|
1157
|
-
rm(assets.tempRoot, { recursive: true, force: true })
|
|
1158
|
-
rm(assets.sessionExtensionDir, { recursive: true, force: true })
|
|
1504
|
+
await cleanupResources([
|
|
1505
|
+
() => rm(assets.tempRoot, { recursive: true, force: true }),
|
|
1506
|
+
() => rm(assets.sessionExtensionDir, { recursive: true, force: true }),
|
|
1159
1507
|
]);
|
|
1160
1508
|
}
|
|
1161
1509
|
function assertValidCopilotObserverPluginIdentity(identity) {
|
|
@@ -1186,7 +1534,7 @@ function sendAuthResult(socket, decision) {
|
|
|
1186
1534
|
? {
|
|
1187
1535
|
kind: "auth.result",
|
|
1188
1536
|
accepted: true,
|
|
1189
|
-
|
|
1537
|
+
recovery: decision.recovery,
|
|
1190
1538
|
...(decision.captureHistoryBaseline && { captureHistoryBaseline: true }),
|
|
1191
1539
|
}
|
|
1192
1540
|
: { kind: "auth.result", accepted: false, reason: decision.reason };
|
|
@@ -1202,6 +1550,75 @@ function requiredCopilotObserverSessionId(opts) {
|
|
|
1202
1550
|
function disposedCopilotObserverRunError() {
|
|
1203
1551
|
return new Error("Copilot observer run has been disposed");
|
|
1204
1552
|
}
|
|
1553
|
+
/** State and durable progress are published together, never from a draft prefix. */
|
|
1554
|
+
class CopilotObservationState {
|
|
1555
|
+
events = [];
|
|
1556
|
+
seenEventIds = new Set();
|
|
1557
|
+
seenEventReplayKeys = new Set();
|
|
1558
|
+
evidence = new CopilotLiveEvidence();
|
|
1559
|
+
watermark;
|
|
1560
|
+
watermarkEvent;
|
|
1561
|
+
lifecycleEventCount = 0;
|
|
1562
|
+
*copy() {
|
|
1563
|
+
const copy = new CopilotObservationState();
|
|
1564
|
+
copy.watermark = this.watermark;
|
|
1565
|
+
copy.watermarkEvent = this.watermarkEvent;
|
|
1566
|
+
copy.lifecycleEventCount = this.lifecycleEventCount;
|
|
1567
|
+
for (const event of this.events) {
|
|
1568
|
+
copy.events.push(event);
|
|
1569
|
+
yield;
|
|
1570
|
+
}
|
|
1571
|
+
for (const id of this.seenEventIds) {
|
|
1572
|
+
copy.seenEventIds.add(id);
|
|
1573
|
+
yield;
|
|
1574
|
+
}
|
|
1575
|
+
for (const key of this.seenEventReplayKeys) {
|
|
1576
|
+
copy.seenEventReplayKeys.add(key);
|
|
1577
|
+
yield;
|
|
1578
|
+
}
|
|
1579
|
+
copy.evidence = yield* this.evidence.copy();
|
|
1580
|
+
return copy;
|
|
1581
|
+
}
|
|
1582
|
+
accept(message, sessionId, request) {
|
|
1583
|
+
const event = message.event;
|
|
1584
|
+
if (event.type === "session.start" || event.type === "session.resume")
|
|
1585
|
+
this.lifecycleEventCount++;
|
|
1586
|
+
const id = eventStableId(event);
|
|
1587
|
+
const key = eventReplayKey(event);
|
|
1588
|
+
if (id && this.seenEventIds.has(id))
|
|
1589
|
+
return;
|
|
1590
|
+
const duplicate = key !== undefined && this.seenEventReplayKeys.has(key);
|
|
1591
|
+
if (id)
|
|
1592
|
+
this.seenEventIds.add(id);
|
|
1593
|
+
if (key)
|
|
1594
|
+
this.seenEventReplayKeys.add(key);
|
|
1595
|
+
if (!duplicate) {
|
|
1596
|
+
this.events.push(event);
|
|
1597
|
+
this.evidence.accept(event, this.events.length - 1, sessionId, request, message);
|
|
1598
|
+
}
|
|
1599
|
+
// A semantic alias can be skipped, but only its genuine durable ID is progress.
|
|
1600
|
+
const durableId = durableEventId(message);
|
|
1601
|
+
if (durableId && (message.origin === "live" || this.watermark)) {
|
|
1602
|
+
this.watermark = durableId;
|
|
1603
|
+
this.watermarkEvent = event;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function durableEventId(message) {
|
|
1608
|
+
if (message.origin === "bridge" || message.transient || message.event.ephemeral === true
|
|
1609
|
+
|| isModelTelemetry(message.event))
|
|
1610
|
+
return undefined;
|
|
1611
|
+
if (message.origin === "live"
|
|
1612
|
+
&& (message.event.type === "session.start" || message.event.type === "session.resume")
|
|
1613
|
+
&& message.lifecycleDurability !== "sdk-history")
|
|
1614
|
+
return undefined;
|
|
1615
|
+
return stringProp(message.event, "id");
|
|
1616
|
+
}
|
|
1617
|
+
function isModelTelemetry(event) {
|
|
1618
|
+
// SDK model tracing is a separate, non-persisted graph, including unflagged
|
|
1619
|
+
// title-generation callbacks. It supplies neither progress nor turn evidence.
|
|
1620
|
+
return typeof event.type === "string" && event.type.startsWith("model.");
|
|
1621
|
+
}
|
|
1205
1622
|
/** Launch-local authority. Snapshots/materialized output are explicit operations. */
|
|
1206
1623
|
class CopilotLiveEvidence {
|
|
1207
1624
|
rewriteGeneration = 0;
|
|
@@ -1214,33 +1631,123 @@ class CopilotLiveEvidence {
|
|
|
1214
1631
|
background = new CopilotBackgroundWorkState();
|
|
1215
1632
|
boundary = new CopilotFinalBoundaryState();
|
|
1216
1633
|
boundarySettled = false;
|
|
1634
|
+
generation = 0;
|
|
1635
|
+
request;
|
|
1636
|
+
responseParts = [];
|
|
1637
|
+
planExit;
|
|
1638
|
+
turnId;
|
|
1639
|
+
interactionId;
|
|
1640
|
+
retiredTurnIds = new Set();
|
|
1641
|
+
retiredInteractionIds = new Set();
|
|
1642
|
+
exitTools = new Set();
|
|
1643
|
+
*copy() {
|
|
1644
|
+
const copy = Object.assign(new CopilotLiveEvidence(), this);
|
|
1645
|
+
copy.submitIdentities = [];
|
|
1646
|
+
for (const id of this.submitIdentities) {
|
|
1647
|
+
copy.submitIdentities.push(id);
|
|
1648
|
+
yield;
|
|
1649
|
+
}
|
|
1650
|
+
copy.responseParts = [];
|
|
1651
|
+
for (const part of this.responseParts) {
|
|
1652
|
+
copy.responseParts.push(part);
|
|
1653
|
+
yield;
|
|
1654
|
+
}
|
|
1655
|
+
copy.retiredTurnIds = new Set();
|
|
1656
|
+
for (const id of this.retiredTurnIds) {
|
|
1657
|
+
copy.retiredTurnIds.add(id);
|
|
1658
|
+
yield;
|
|
1659
|
+
}
|
|
1660
|
+
copy.retiredInteractionIds = new Set();
|
|
1661
|
+
for (const id of this.retiredInteractionIds) {
|
|
1662
|
+
copy.retiredInteractionIds.add(id);
|
|
1663
|
+
yield;
|
|
1664
|
+
}
|
|
1665
|
+
copy.exitTools = new Set(this.exitTools);
|
|
1666
|
+
copy.background = yield* this.background.copy();
|
|
1667
|
+
copy.boundary = this.boundary.copy();
|
|
1668
|
+
return copy;
|
|
1669
|
+
}
|
|
1217
1670
|
get submitCount() { return this.submitIdentities.length; }
|
|
1218
1671
|
get settled() {
|
|
1219
1672
|
return this.submitIndex >= 0 && ((this.boundary.latest !== undefined && this.boundarySettled)
|
|
1220
1673
|
|| (this.assistantIndex > this.submitIndex && this.idleIndex > this.assistantIndex));
|
|
1221
1674
|
}
|
|
1222
|
-
accept(event, index, sessionId) {
|
|
1675
|
+
accept(event, index, sessionId, request, message) {
|
|
1676
|
+
if (isModelTelemetry(event))
|
|
1677
|
+
return;
|
|
1223
1678
|
if (isLiveHistoryRewriteEvent(event)) {
|
|
1224
1679
|
this.rewriteGeneration++;
|
|
1225
1680
|
this.removedSubmitIdentities = this.submitIdentities;
|
|
1226
1681
|
this.submitIdentities = [];
|
|
1227
1682
|
this.resetTurn();
|
|
1683
|
+
this.retiredInteractionIds.clear();
|
|
1684
|
+
this.request = undefined;
|
|
1228
1685
|
return;
|
|
1229
1686
|
}
|
|
1687
|
+
const data = recordProp(event, "data");
|
|
1688
|
+
const turnId = stringProp(data, "turnId") ?? stringProp(event, "turnId")
|
|
1689
|
+
?? stringProp(recordProp(data, "input"), "turnId");
|
|
1690
|
+
const interactionId = stringProp(data, "interactionId");
|
|
1691
|
+
if (!isAgentScoped(event) && interactionId && this.retiredInteractionIds.has(interactionId))
|
|
1692
|
+
return;
|
|
1230
1693
|
if (isLiveRootSubmit(event)) {
|
|
1694
|
+
if (this.interactionId && this.interactionId !== interactionId) {
|
|
1695
|
+
this.retiredInteractionIds.add(this.interactionId);
|
|
1696
|
+
}
|
|
1231
1697
|
this.resetTurn();
|
|
1698
|
+
this.turnId = turnId;
|
|
1699
|
+
this.interactionId = interactionId;
|
|
1232
1700
|
this.submitIndex = index;
|
|
1233
1701
|
this.submitIdentities.push(liveSubmitIdentity(event, index, this.rewriteGeneration));
|
|
1702
|
+
if (request?.written && normalizePrompt(stringProp(data, "content") ?? "") === normalizePrompt(request.prompt)) {
|
|
1703
|
+
this.request = request;
|
|
1704
|
+
}
|
|
1705
|
+
else if (request !== this.request) {
|
|
1706
|
+
this.request = undefined;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
if (!isAgentScoped(event)) {
|
|
1710
|
+
if (event.type === "assistant.turn_start" && this.submitIndex >= 0 && turnId
|
|
1711
|
+
&& !this.retiredTurnIds.has(turnId)
|
|
1712
|
+
&& (!interactionId || !this.interactionId || interactionId === this.interactionId)) {
|
|
1713
|
+
if (this.turnId && this.turnId !== turnId)
|
|
1714
|
+
this.retiredTurnIds.add(this.turnId);
|
|
1715
|
+
this.turnId = turnId;
|
|
1716
|
+
this.interactionId ??= interactionId;
|
|
1717
|
+
}
|
|
1718
|
+
if ((turnId && this.retiredTurnIds.has(turnId))
|
|
1719
|
+
|| (turnId && this.turnId && turnId !== this.turnId)
|
|
1720
|
+
|| (interactionId && this.interactionId && interactionId !== this.interactionId))
|
|
1721
|
+
return;
|
|
1722
|
+
if (this.submitIndex >= 0) {
|
|
1723
|
+
if (isAbortEvent(event))
|
|
1724
|
+
this.abortIndex = index;
|
|
1725
|
+
if (event.type === "assistant.message") {
|
|
1726
|
+
const content = stringProp(data, "content");
|
|
1727
|
+
if (content) {
|
|
1728
|
+
this.assistantIndex = index;
|
|
1729
|
+
this.responseParts.push(content);
|
|
1730
|
+
}
|
|
1731
|
+
const tools = data?.toolRequests;
|
|
1732
|
+
if (Array.isArray(tools))
|
|
1733
|
+
for (const tool of tools) {
|
|
1734
|
+
if (isRecord(tool) && tool.name === "exit_plan_mode") {
|
|
1735
|
+
const id = stringProp(tool, "toolCallId");
|
|
1736
|
+
if (id)
|
|
1737
|
+
this.exitTools.add(id);
|
|
1738
|
+
this.planExit = { generation: this.generation };
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
if (message?.origin === "bridge" && message.toolCallId && this.exitTools.has(message.toolCallId)) {
|
|
1743
|
+
this.planExit = { generation: this.generation, inline: nativePlanFromExitEvent(event) };
|
|
1744
|
+
}
|
|
1745
|
+
if (isSessionIdle(event, sessionId))
|
|
1746
|
+
this.idleIndex = index;
|
|
1747
|
+
}
|
|
1234
1748
|
}
|
|
1235
|
-
if (isAbortEvent(event) && this.submitIndex >= 0)
|
|
1236
|
-
this.abortIndex = index;
|
|
1237
|
-
if (!isAgentScoped(event) && event.type === "assistant.message"
|
|
1238
|
-
&& stringProp(recordProp(event, "data"), "content"))
|
|
1239
|
-
this.assistantIndex = index;
|
|
1240
|
-
if (isSessionIdle(event, sessionId))
|
|
1241
|
-
this.idleIndex = index;
|
|
1242
1749
|
this.background.accept(event);
|
|
1243
|
-
if (this.boundary.accept(event, index, sessionId)) {
|
|
1750
|
+
if (this.submitIndex >= 0 && this.boundary.accept(event, index, sessionId)) {
|
|
1244
1751
|
// Later background completion cannot validate an earlier stop.
|
|
1245
1752
|
this.boundarySettled = this.background.size === 0;
|
|
1246
1753
|
}
|
|
@@ -1262,6 +1769,14 @@ class CopilotLiveEvidence {
|
|
|
1262
1769
|
};
|
|
1263
1770
|
}
|
|
1264
1771
|
resetTurn() {
|
|
1772
|
+
this.generation++;
|
|
1773
|
+
// A root submission owns its cycles even when the SDK omits interactionId.
|
|
1774
|
+
this.retiredTurnIds.clear();
|
|
1775
|
+
this.turnId = undefined;
|
|
1776
|
+
this.interactionId = undefined;
|
|
1777
|
+
this.responseParts = [];
|
|
1778
|
+
this.planExit = undefined;
|
|
1779
|
+
this.exitTools = new Set();
|
|
1265
1780
|
this.submitIndex = -1;
|
|
1266
1781
|
this.abortIndex = -1;
|
|
1267
1782
|
this.assistantIndex = -1;
|
|
@@ -1271,7 +1786,10 @@ class CopilotLiveEvidence {
|
|
|
1271
1786
|
this.boundarySettled = false;
|
|
1272
1787
|
}
|
|
1273
1788
|
}
|
|
1274
|
-
|
|
1789
|
+
function normalizePrompt(prompt) {
|
|
1790
|
+
return prompt.replace(/\r\n/g, "\n").trim().replace(/^\/plan\s+/, "");
|
|
1791
|
+
}
|
|
1792
|
+
export class CopilotLiveTurnMonitor {
|
|
1275
1793
|
registration;
|
|
1276
1794
|
opts;
|
|
1277
1795
|
unsubscribeEvent;
|
|
@@ -1290,12 +1808,15 @@ class CopilotLiveTurnMonitor {
|
|
|
1290
1808
|
historySnapshot;
|
|
1291
1809
|
historyRevision;
|
|
1292
1810
|
abort = new AbortController();
|
|
1811
|
+
untrackRequestRisk;
|
|
1812
|
+
unresolvedAttempt;
|
|
1293
1813
|
constructor(registration, opts = {}) {
|
|
1294
1814
|
this.registration = registration;
|
|
1295
1815
|
this.opts = opts;
|
|
1296
1816
|
this.attemptTimeoutMs = opts.attemptTimeoutMs ?? 5_000;
|
|
1297
1817
|
this.historySnapshot = opts.initialHistorySnapshot;
|
|
1298
1818
|
this.refreshCounts();
|
|
1819
|
+
this.untrackRequestRisk = registration.trackRequestRisk(() => this.hasUnconfirmedInput());
|
|
1299
1820
|
this.unsubscribeEvent = registration.onEvent(() => this.onEvent());
|
|
1300
1821
|
this.unsubscribeError = registration.onError((error) => {
|
|
1301
1822
|
if (this.stopped)
|
|
@@ -1306,7 +1827,14 @@ class CopilotLiveTurnMonitor {
|
|
|
1306
1827
|
});
|
|
1307
1828
|
}
|
|
1308
1829
|
get isIdle() {
|
|
1309
|
-
return !this.busy;
|
|
1830
|
+
return this.registration.continuityAvailable && !this.busy && !this.hasUnconfirmedInput();
|
|
1831
|
+
}
|
|
1832
|
+
hasUnconfirmedInput() {
|
|
1833
|
+
this.refreshAttemptRisk();
|
|
1834
|
+
const count = this.registration.evidence.submitCount;
|
|
1835
|
+
return this.unresolvedAttempt !== undefined
|
|
1836
|
+
|| [...this.entries.values()].some((entry) => entry.receipt.inputWritten
|
|
1837
|
+
&& (entry.ordinal === undefined || entry.ordinal > count));
|
|
1310
1838
|
}
|
|
1311
1839
|
get lineBaseline() {
|
|
1312
1840
|
return this.coveredSubmitCount;
|
|
@@ -1321,6 +1849,8 @@ class CopilotLiveTurnMonitor {
|
|
|
1321
1849
|
this.reconcileLiveHistory();
|
|
1322
1850
|
}
|
|
1323
1851
|
reserveTurn(boundary = {}) {
|
|
1852
|
+
this.reconcileLiveHistory();
|
|
1853
|
+
this.refreshCounts();
|
|
1324
1854
|
const receipt = new SessionTurnReceipt({
|
|
1325
1855
|
...boundary,
|
|
1326
1856
|
anchorSubmitCount: boundary.anchorSubmitCount ?? this.observedSubmitCount,
|
|
@@ -1340,10 +1870,22 @@ class CopilotLiveTurnMonitor {
|
|
|
1340
1870
|
}
|
|
1341
1871
|
}
|
|
1342
1872
|
attachTurn(receipt) {
|
|
1343
|
-
if (this.stopped || this.attached.has(receipt)
|
|
1873
|
+
if (this.stopped || this.attached.has(receipt))
|
|
1344
1874
|
return;
|
|
1345
1875
|
this.attached.add(receipt);
|
|
1346
|
-
|
|
1876
|
+
this.reconcileLiveHistory();
|
|
1877
|
+
this.refreshCounts();
|
|
1878
|
+
if (receipt.rawAttempt && receipt.inputWritten && (!receipt.settlement || receipt.settlement === "expired")) {
|
|
1879
|
+
this.unresolvedAttempt = {
|
|
1880
|
+
submitFloor: Math.max(this.unresolvedAttempt?.submitFloor ?? -1, receipt.anchorSubmitCount ?? this.observedSubmitCount),
|
|
1881
|
+
rewriteGeneration: this.registration.evidence.rewriteGeneration,
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
if (receipt.settlement)
|
|
1885
|
+
return;
|
|
1886
|
+
const entry = {
|
|
1887
|
+
receipt, anchorSubmitCount: receipt.anchorSubmitCount ?? this.observedSubmitCount,
|
|
1888
|
+
};
|
|
1347
1889
|
this.entries.set(receipt.id, entry);
|
|
1348
1890
|
entry.unsubscribe = receipt.onChange(() => {
|
|
1349
1891
|
if (receipt.settlement === "released" && entry.ordinal === undefined) {
|
|
@@ -1351,8 +1893,12 @@ class CopilotLiveTurnMonitor {
|
|
|
1351
1893
|
this.evaluateSettlement();
|
|
1352
1894
|
}
|
|
1353
1895
|
});
|
|
1354
|
-
if (receipt.inputWritten
|
|
1355
|
-
|
|
1896
|
+
if (receipt.inputWritten) {
|
|
1897
|
+
if (receipt.rawAttempt)
|
|
1898
|
+
this.trackRawAttempt(entry);
|
|
1899
|
+
else
|
|
1900
|
+
this.confirmTurn(entry);
|
|
1901
|
+
}
|
|
1356
1902
|
}
|
|
1357
1903
|
beginTurn(receipt) {
|
|
1358
1904
|
if (!receipt) {
|
|
@@ -1385,28 +1931,28 @@ class CopilotLiveTurnMonitor {
|
|
|
1385
1931
|
this.evaluateSettlement();
|
|
1386
1932
|
}
|
|
1387
1933
|
beginTurnAttempt(snapshot = {}) {
|
|
1934
|
+
this.reconcileLiveHistory();
|
|
1935
|
+
this.refreshCounts();
|
|
1388
1936
|
const receipt = new SessionTurnReceipt({
|
|
1389
1937
|
...snapshot,
|
|
1390
1938
|
anchorSubmitCount: snapshot.anchorSubmitCount ?? this.observedSubmitCount,
|
|
1391
1939
|
}, true);
|
|
1392
1940
|
receipt.markWritten();
|
|
1393
1941
|
this.attachTurn(receipt);
|
|
1394
|
-
|
|
1395
|
-
|
|
1942
|
+
return receipt;
|
|
1943
|
+
}
|
|
1944
|
+
trackRawAttempt(entry) {
|
|
1945
|
+
const { receipt, anchorSubmitCount } = entry;
|
|
1396
1946
|
this.refreshCounts();
|
|
1397
|
-
const
|
|
1398
|
-
const elapsed = Date.now() - startedAt;
|
|
1947
|
+
const elapsed = Date.now() - receipt.startedAt;
|
|
1399
1948
|
if (elapsed >= this.attemptTimeoutMs) {
|
|
1400
1949
|
receipt.settle("expired");
|
|
1401
|
-
|
|
1402
|
-
if (entry)
|
|
1403
|
-
this.removeEntry(entry);
|
|
1950
|
+
this.removeEntry(entry);
|
|
1404
1951
|
this.evaluateSettlement({ notifyIfAlreadyIdle: true });
|
|
1405
|
-
return
|
|
1952
|
+
return;
|
|
1406
1953
|
}
|
|
1407
1954
|
const attempt = {
|
|
1408
|
-
anchorSubmitCount
|
|
1409
|
-
receipt,
|
|
1955
|
+
anchorSubmitCount, receipt,
|
|
1410
1956
|
};
|
|
1411
1957
|
attempt.timer = setTimeout(() => {
|
|
1412
1958
|
const index = this.pendingAttempts.indexOf(attempt);
|
|
@@ -1421,7 +1967,10 @@ class CopilotLiveTurnMonitor {
|
|
|
1421
1967
|
}, Math.max(0, this.attemptTimeoutMs - elapsed));
|
|
1422
1968
|
this.pendingAttempts.push(attempt);
|
|
1423
1969
|
this.markBusy();
|
|
1424
|
-
|
|
1970
|
+
if (this.observedSubmitCount > anchorSubmitCount) {
|
|
1971
|
+
this.adoptObservedSubmits(anchorSubmitCount);
|
|
1972
|
+
this.evaluateSettlement();
|
|
1973
|
+
}
|
|
1425
1974
|
}
|
|
1426
1975
|
waitForPendingTurnAttempts() {
|
|
1427
1976
|
if (this.pendingAttempts.length === 0 || this.stopped)
|
|
@@ -1449,12 +1998,15 @@ class CopilotLiveTurnMonitor {
|
|
|
1449
1998
|
this.refreshCounts();
|
|
1450
1999
|
if (baseline < 0)
|
|
1451
2000
|
return "unknown";
|
|
1452
|
-
|
|
2001
|
+
if (this.observedSubmitCount > baseline)
|
|
2002
|
+
return "present";
|
|
2003
|
+
return this.registration.continuityAvailable && !this.hasUnconfirmedInput() ? "absent" : "unknown";
|
|
1453
2004
|
}
|
|
1454
2005
|
dispose() {
|
|
1455
2006
|
this.abort.abort(new Error("Copilot turn monitor disposed"));
|
|
1456
2007
|
this.unsubscribeEvent();
|
|
1457
2008
|
this.unsubscribeError();
|
|
2009
|
+
this.untrackRequestRisk();
|
|
1458
2010
|
for (const attempt of this.pendingAttempts) {
|
|
1459
2011
|
if (attempt.timer)
|
|
1460
2012
|
clearTimeout(attempt.timer);
|
|
@@ -1479,8 +2031,24 @@ class CopilotLiveTurnMonitor {
|
|
|
1479
2031
|
if (this.observedSubmitCount > previousObserved) {
|
|
1480
2032
|
this.adoptObservedSubmits(previousObserved);
|
|
1481
2033
|
}
|
|
2034
|
+
this.adoptWrittenSubmits();
|
|
1482
2035
|
this.evaluateSettlement();
|
|
1483
2036
|
}
|
|
2037
|
+
adoptWrittenSubmits() {
|
|
2038
|
+
let previousOrdinal = 0;
|
|
2039
|
+
for (const entry of this.entries.values()) {
|
|
2040
|
+
if (entry.ordinal === undefined && entry.receipt.inputWritten && !entry.receipt.rawAttempt) {
|
|
2041
|
+
const ordinal = Math.max(previousOrdinal + 1, entry.anchorSubmitCount + 1);
|
|
2042
|
+
entry.anchorSubmitCount = ordinal - 1;
|
|
2043
|
+
if (ordinal <= this.observedSubmitCount) {
|
|
2044
|
+
entry.ordinal = ordinal;
|
|
2045
|
+
this.highestOrdinal = Math.max(this.highestOrdinal, ordinal);
|
|
2046
|
+
this.expectedSubmitCount = Math.max(this.expectedSubmitCount, ordinal);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
previousOrdinal = Math.max(previousOrdinal, entry.ordinal ?? entry.anchorSubmitCount + 1);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
1484
2052
|
adoptObservedSubmits(previousObserved) {
|
|
1485
2053
|
for (let i = this.pendingAttempts.length - 1; i >= 0; i--) {
|
|
1486
2054
|
const attempt = this.pendingAttempts[i];
|
|
@@ -1505,8 +2073,26 @@ class CopilotLiveTurnMonitor {
|
|
|
1505
2073
|
}
|
|
1506
2074
|
refreshCounts() {
|
|
1507
2075
|
this.observedSubmitCount = this.registration.evidence.submitCount;
|
|
2076
|
+
this.refreshAttemptRisk();
|
|
2077
|
+
}
|
|
2078
|
+
refreshAttemptRisk() {
|
|
2079
|
+
const risk = this.unresolvedAttempt;
|
|
2080
|
+
if (!risk)
|
|
2081
|
+
return;
|
|
2082
|
+
const evidence = this.registration.evidence;
|
|
2083
|
+
if (risk.rewriteGeneration !== evidence.rewriteGeneration) {
|
|
2084
|
+
// A validated rewrite resets the submit namespace, not proof of nondelivery.
|
|
2085
|
+
// New input recorded in the committed generation must retain its own floor.
|
|
2086
|
+
risk.submitFloor = 0;
|
|
2087
|
+
risk.rewriteGeneration = evidence.rewriteGeneration;
|
|
2088
|
+
}
|
|
2089
|
+
if (evidence.submitCount > risk.submitFloor) {
|
|
2090
|
+
this.unresolvedAttempt = undefined;
|
|
2091
|
+
}
|
|
1508
2092
|
}
|
|
1509
2093
|
evaluateSettlement(options = {}) {
|
|
2094
|
+
if (!this.registration.outcomesAvailable)
|
|
2095
|
+
return;
|
|
1510
2096
|
const evidence = this.registration.evidence;
|
|
1511
2097
|
this.observedSubmitCount = evidence.submitCount;
|
|
1512
2098
|
if (this.observedSubmitCount > this.coveredSubmitCount
|
|
@@ -1523,7 +2109,8 @@ class CopilotLiveTurnMonitor {
|
|
|
1523
2109
|
}
|
|
1524
2110
|
}
|
|
1525
2111
|
if (this.coveredSubmitCount < this.expectedSubmitCount
|
|
1526
|
-
|| this.pendingAttempts.length > 0
|
|
2112
|
+
|| this.pendingAttempts.length > 0
|
|
2113
|
+
|| this.hasUnconfirmedInput()) {
|
|
1527
2114
|
return;
|
|
1528
2115
|
}
|
|
1529
2116
|
const wasBusy = this.busy;
|
|
@@ -1568,6 +2155,7 @@ class CopilotLiveTurnMonitor {
|
|
|
1568
2155
|
attempt.anchorSubmitCount = Math.min(attempt.anchorSubmitCount, regression.commonSubmitCount);
|
|
1569
2156
|
}
|
|
1570
2157
|
for (const entry of this.entries.values()) {
|
|
2158
|
+
entry.anchorSubmitCount = Math.min(entry.anchorSubmitCount, regression.commonSubmitCount);
|
|
1571
2159
|
if (entry.ordinal !== undefined && entry.ordinal > regression.commonSubmitCount) {
|
|
1572
2160
|
this.removeEntry(entry);
|
|
1573
2161
|
entry.receipt.settle("removed");
|
|
@@ -1607,14 +2195,15 @@ export class CopilotLiveCompletionDetector {
|
|
|
1607
2195
|
markerTimer;
|
|
1608
2196
|
resolve;
|
|
1609
2197
|
reject;
|
|
1610
|
-
|
|
2198
|
+
startGeneration = 0;
|
|
2199
|
+
initialResponsePartCount = 0;
|
|
2200
|
+
request;
|
|
1611
2201
|
completed = false;
|
|
1612
2202
|
activationFired = false;
|
|
1613
2203
|
nativePlan;
|
|
1614
2204
|
disposed = false;
|
|
1615
|
-
|
|
2205
|
+
readingPlanGeneration;
|
|
1616
2206
|
abort = new AbortController();
|
|
1617
|
-
responseParts = [];
|
|
1618
2207
|
constructor(registration, opts = {}) {
|
|
1619
2208
|
this.registration = registration;
|
|
1620
2209
|
this.opts = opts;
|
|
@@ -1622,13 +2211,16 @@ export class CopilotLiveCompletionDetector {
|
|
|
1622
2211
|
if (opts.completionPolicy)
|
|
1623
2212
|
this.completionGate = new FileAndTurnCompletionGate(opts.completionPolicy);
|
|
1624
2213
|
}
|
|
1625
|
-
start(_sessionFile,
|
|
1626
|
-
this.
|
|
2214
|
+
start(_sessionFile, _lineOffset = 0) {
|
|
2215
|
+
this.startGeneration = this.registration.evidence.generation;
|
|
2216
|
+
this.initialResponsePartCount = this.registration.evidence.responseParts.length;
|
|
2217
|
+
this.request = this.registration.requestForDetector();
|
|
1627
2218
|
return new Promise((resolve, reject) => {
|
|
1628
2219
|
this.resolve = resolve;
|
|
1629
2220
|
this.reject = reject;
|
|
1630
|
-
this.unsubscribeEvent = this.registration.onEvent((
|
|
1631
|
-
|
|
2221
|
+
this.unsubscribeEvent = this.registration.onEvent(() => {
|
|
2222
|
+
this.fireActivationIfNeeded();
|
|
2223
|
+
void this.evaluate();
|
|
1632
2224
|
});
|
|
1633
2225
|
this.unsubscribeError = this.registration.onError((error) => {
|
|
1634
2226
|
this.finishError(error);
|
|
@@ -1670,53 +2262,52 @@ export class CopilotLiveCompletionDetector {
|
|
|
1670
2262
|
getCompletionDisposition() {
|
|
1671
2263
|
return this.completionGate?.disposition;
|
|
1672
2264
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
2265
|
+
ownsCurrentRequest(generation = this.registration.evidence.generation) {
|
|
2266
|
+
const evidence = this.registration.evidence;
|
|
2267
|
+
return evidence.generation === generation && generation > this.startGeneration
|
|
2268
|
+
&& evidence.submitIndex >= 0 && (!this.request || evidence.request === this.request);
|
|
2269
|
+
}
|
|
2270
|
+
async evaluate() {
|
|
2271
|
+
if (this.completed || this.disposed || !this.registration.outcomesAvailable)
|
|
2272
|
+
return;
|
|
2273
|
+
const evidence = this.registration.evidence;
|
|
2274
|
+
const owned = this.ownsCurrentRequest();
|
|
2275
|
+
if (owned && evidence.abortIndex >= 0) {
|
|
2276
|
+
this.finishError(new Error("Copilot session aborted before completing the active turn"));
|
|
1675
2277
|
return;
|
|
1676
|
-
this.fireActivationIfNeeded();
|
|
1677
|
-
if (isLiveRootSubmit(event) || isLiveHistoryRewriteEvent(event))
|
|
1678
|
-
this.responseParts = [];
|
|
1679
|
-
const data = recordProp(event, "data");
|
|
1680
|
-
if (!isAgentScoped(event) && event.type === "assistant.message") {
|
|
1681
|
-
const content = stringProp(data, "content");
|
|
1682
|
-
if (content)
|
|
1683
|
-
this.responseParts.push(content);
|
|
1684
2278
|
}
|
|
1685
|
-
if (this.opts.planMode &&
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
const inlinePlan = nativePlanFromExitEvent(event);
|
|
2279
|
+
if (owned && this.opts.planMode && evidence.planExit) {
|
|
2280
|
+
const generation = evidence.generation;
|
|
2281
|
+
const inlinePlan = evidence.planExit.inline;
|
|
1689
2282
|
if (inlinePlan?.content || inlinePlan?.path) {
|
|
1690
2283
|
this.nativePlan = inlinePlan;
|
|
1691
2284
|
this.finishSuccess(inlinePlan.content ?? "");
|
|
1692
2285
|
return;
|
|
1693
2286
|
}
|
|
1694
|
-
this.
|
|
2287
|
+
if (this.readingPlanGeneration === generation)
|
|
2288
|
+
return;
|
|
2289
|
+
this.readingPlanGeneration = generation;
|
|
2290
|
+
const deadline = Date.now() + PLAN_READ_TIMEOUT_MS;
|
|
1695
2291
|
try {
|
|
1696
|
-
|
|
1697
|
-
|
|
2292
|
+
const plan = await readPlanWithRetry(this.registration, this.abort.signal, deadline);
|
|
2293
|
+
await this.registration.waitForUsableConnection({
|
|
2294
|
+
signal: this.abort.signal, timeoutMs: Math.max(1, deadline - Date.now()),
|
|
2295
|
+
});
|
|
2296
|
+
if (this.disposed || !this.registration.outcomesAvailable || !this.ownsCurrentRequest(generation))
|
|
1698
2297
|
return;
|
|
1699
|
-
this.
|
|
2298
|
+
this.nativePlan = plan;
|
|
2299
|
+
this.finishSuccess(plan.content ?? "");
|
|
1700
2300
|
}
|
|
1701
2301
|
catch (err) {
|
|
1702
|
-
this.
|
|
2302
|
+
if (this.ownsCurrentRequest(generation))
|
|
2303
|
+
this.finishError(err instanceof Error ? err : new Error(String(err)));
|
|
1703
2304
|
}
|
|
1704
2305
|
return;
|
|
1705
2306
|
}
|
|
1706
|
-
await this.evaluate();
|
|
1707
|
-
}
|
|
1708
|
-
async evaluate() {
|
|
1709
|
-
if (this.completed || this.disposed || this.readingPlan)
|
|
1710
|
-
return;
|
|
1711
2307
|
if (this.opts.completionMarkerFile) {
|
|
1712
2308
|
return;
|
|
1713
2309
|
}
|
|
1714
|
-
const
|
|
1715
|
-
if (evidence.submitIndex >= this.startIndex && evidence.abortIndex >= this.startIndex) {
|
|
1716
|
-
this.finishError(new Error("Copilot session aborted before completing the active turn"));
|
|
1717
|
-
return;
|
|
1718
|
-
}
|
|
1719
|
-
const complete = evidence.submitIndex >= this.startIndex && evidence.settled;
|
|
2310
|
+
const complete = owned && evidence.settled;
|
|
1720
2311
|
if (this.completionGate) {
|
|
1721
2312
|
this.completionGate.observeMarker();
|
|
1722
2313
|
if (!this.completionGate.evaluate(complete))
|
|
@@ -1724,10 +2315,10 @@ export class CopilotLiveCompletionDetector {
|
|
|
1724
2315
|
}
|
|
1725
2316
|
else if (!complete)
|
|
1726
2317
|
return;
|
|
1727
|
-
this.finishSuccess(
|
|
2318
|
+
this.finishSuccess(evidence.responseParts.join("\n"));
|
|
1728
2319
|
}
|
|
1729
2320
|
checkMarker() {
|
|
1730
|
-
if (this.completed || this.disposed || !this.opts.completionMarkerFile)
|
|
2321
|
+
if (this.completed || this.disposed || !this.opts.completionMarkerFile || !this.registration.outcomesAvailable)
|
|
1731
2322
|
return;
|
|
1732
2323
|
let markerPresent = false;
|
|
1733
2324
|
try {
|
|
@@ -1738,7 +2329,8 @@ export class CopilotLiveCompletionDetector {
|
|
|
1738
2329
|
}
|
|
1739
2330
|
if (!markerPresent)
|
|
1740
2331
|
return;
|
|
1741
|
-
this.
|
|
2332
|
+
const evidence = this.registration.evidence;
|
|
2333
|
+
this.finishSuccess(evidence.responseParts.slice(evidence.generation === this.startGeneration ? this.initialResponsePartCount : 0).join("\n"));
|
|
1742
2334
|
}
|
|
1743
2335
|
fireActivationIfNeeded() {
|
|
1744
2336
|
if (this.activationFired)
|
|
@@ -1761,8 +2353,7 @@ export class CopilotLiveCompletionDetector {
|
|
|
1761
2353
|
this.reject?.(error);
|
|
1762
2354
|
}
|
|
1763
2355
|
}
|
|
1764
|
-
async function readPlanWithRetry(registration, signal) {
|
|
1765
|
-
const deadline = Date.now() + PLAN_READ_TIMEOUT_MS;
|
|
2356
|
+
async function readPlanWithRetry(registration, signal, deadline = Date.now() + PLAN_READ_TIMEOUT_MS) {
|
|
1766
2357
|
let last;
|
|
1767
2358
|
while (Date.now() < deadline) {
|
|
1768
2359
|
throwIfAborted(signal);
|
|
@@ -1783,16 +2374,6 @@ function isSessionIdle(line, sessionId) {
|
|
|
1783
2374
|
function isAbortEvent(line) {
|
|
1784
2375
|
return line.type === "session.abort" || line.type === "abort";
|
|
1785
2376
|
}
|
|
1786
|
-
function isExitPlanRequested(line) {
|
|
1787
|
-
if (line.type === "exit_plan_mode.requested")
|
|
1788
|
-
return true;
|
|
1789
|
-
if (line.type !== "assistant.message")
|
|
1790
|
-
return false;
|
|
1791
|
-
const data = recordProp(line, "data");
|
|
1792
|
-
const requests = data?.toolRequests;
|
|
1793
|
-
return Array.isArray(requests)
|
|
1794
|
-
&& requests.some((request) => isRecord(request) && stringProp(request, "name") === "exit_plan_mode");
|
|
1795
|
-
}
|
|
1796
2377
|
function nativePlanFromExitEvent(line) {
|
|
1797
2378
|
if (line.type !== "exit_plan_mode.requested")
|
|
1798
2379
|
return undefined;
|
|
@@ -1841,6 +2422,9 @@ function parseInbound(value) {
|
|
|
1841
2422
|
workspacePath: stringProp(value, "workspacePath"),
|
|
1842
2423
|
error: stringProp(value, "error"),
|
|
1843
2424
|
eventIds: value.eventIds,
|
|
2425
|
+
afterEventId: stringProp(value, "afterEventId"),
|
|
2426
|
+
event: isRecord(value.event) ? value.event : undefined,
|
|
2427
|
+
recoveryRejection: parseRecoveryRejection(value.recoveryRejection),
|
|
1844
2428
|
};
|
|
1845
2429
|
}
|
|
1846
2430
|
if (value.kind === "event") {
|
|
@@ -1848,11 +2432,27 @@ function parseInbound(value) {
|
|
|
1848
2432
|
if (!Number.isInteger(seq) || Number(seq) <= 0 || !isRecord(value.event)) {
|
|
1849
2433
|
throw new Error("Copilot observer event message is malformed");
|
|
1850
2434
|
}
|
|
2435
|
+
if (value.origin !== "live" && value.origin !== "history" && value.origin !== "bridge") {
|
|
2436
|
+
throw new Error("Copilot observer event is missing a valid origin");
|
|
2437
|
+
}
|
|
2438
|
+
if (value.transient !== undefined && typeof value.transient !== "boolean") {
|
|
2439
|
+
throw new Error("Copilot observer event has invalid durability metadata");
|
|
2440
|
+
}
|
|
2441
|
+
if (value.lifecycleDurability !== undefined
|
|
2442
|
+
&& (value.lifecycleDurability !== "sdk-history" || value.origin !== "live"
|
|
2443
|
+
|| (value.event.type !== "session.start" && value.event.type !== "session.resume")
|
|
2444
|
+
|| !stringProp(value.event, "id") || value.event.ephemeral === true || value.transient === true)) {
|
|
2445
|
+
throw new Error("Copilot observer event has invalid lifecycle durability proof");
|
|
2446
|
+
}
|
|
1851
2447
|
return {
|
|
1852
2448
|
kind: "event",
|
|
1853
2449
|
seq: Number(seq),
|
|
1854
2450
|
event: value.event,
|
|
1855
2451
|
sessionId: stringProp(value, "sessionId"),
|
|
2452
|
+
origin: value.origin,
|
|
2453
|
+
transient: value.transient,
|
|
2454
|
+
lifecycleDurability: value.lifecycleDurability,
|
|
2455
|
+
toolCallId: stringProp(value, "toolCallId"),
|
|
1856
2456
|
};
|
|
1857
2457
|
}
|
|
1858
2458
|
if (value.kind === "rpc.response") {
|
|
@@ -1870,6 +2470,34 @@ function parseInbound(value) {
|
|
|
1870
2470
|
}
|
|
1871
2471
|
throw new Error("Copilot observer sent an unknown message kind");
|
|
1872
2472
|
}
|
|
2473
|
+
function parseRecoveryRejection(value) {
|
|
2474
|
+
if (value === undefined)
|
|
2475
|
+
return undefined;
|
|
2476
|
+
const invalid = () => new Error("Copilot observer recovery rejection metadata is malformed");
|
|
2477
|
+
if (!isRecord(value))
|
|
2478
|
+
throw invalid();
|
|
2479
|
+
const { stage, boundary } = value;
|
|
2480
|
+
if (stage !== "history-read" && stage !== "boundary-lookup" && stage !== "startup-verification"
|
|
2481
|
+
&& stage !== "admission" && stage !== "reconciliation")
|
|
2482
|
+
throw invalid();
|
|
2483
|
+
if (boundary !== "absent" && boundary !== "located" && boundary !== "missing"
|
|
2484
|
+
&& boundary !== "untrustworthy" && boundary !== "unknown")
|
|
2485
|
+
throw invalid();
|
|
2486
|
+
if (value.event === undefined)
|
|
2487
|
+
return { stage, boundary };
|
|
2488
|
+
if (!isRecord(value.event))
|
|
2489
|
+
throw invalid();
|
|
2490
|
+
const { type, source, ephemeral, historyMatch, parentReference, parentResolution } = value.event;
|
|
2491
|
+
if (typeof type !== "string" || !/^[a-zA-Z0-9_.-]{1,96}$/.test(type)
|
|
2492
|
+
|| (source !== "live" && source !== "history" && source !== "bridge")
|
|
2493
|
+
|| typeof ephemeral !== "boolean"
|
|
2494
|
+
|| (historyMatch !== "matched" && historyMatch !== "absent" && historyMatch !== "unknown")
|
|
2495
|
+
|| (parentReference !== "omitted" && parentReference !== "null" && parentReference !== "supplied")
|
|
2496
|
+
|| (parentResolution !== "history" && parentResolution !== "callback" && parentResolution !== "unresolved"
|
|
2497
|
+
&& parentResolution !== "unknown" && parentResolution !== "none"))
|
|
2498
|
+
throw invalid();
|
|
2499
|
+
return { stage, boundary, event: { type, source, ephemeral, historyMatch, parentReference, parentResolution } };
|
|
2500
|
+
}
|
|
1873
2501
|
function parsePlanReadResult(value) {
|
|
1874
2502
|
if (!isRecord(value))
|
|
1875
2503
|
return {};
|