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.
Files changed (201) hide show
  1. package/dist/agents/implementors/index.d.ts +2 -2
  2. package/dist/agents/implementors/index.d.ts.map +1 -1
  3. package/dist/agents/implementors/index.js.map +1 -1
  4. package/dist/agents/index.d.ts.map +1 -1
  5. package/dist/agents/index.js +7 -6
  6. package/dist/agents/index.js.map +1 -1
  7. package/dist/agents/interviewers/index.d.ts +2 -2
  8. package/dist/agents/interviewers/index.d.ts.map +1 -1
  9. package/dist/agents/planners/index.d.ts +2 -2
  10. package/dist/agents/planners/index.d.ts.map +1 -1
  11. package/dist/agents/planners/index.js.map +1 -1
  12. package/dist/agents/registry.d.ts +2 -1
  13. package/dist/agents/registry.d.ts.map +1 -1
  14. package/dist/agents/registry.js +1 -1
  15. package/dist/agents/registry.js.map +1 -1
  16. package/dist/agents/reviewers/index.d.ts +2 -2
  17. package/dist/agents/reviewers/index.d.ts.map +1 -1
  18. package/dist/agents/reviewers/index.js.map +1 -1
  19. package/dist/agents/runner.d.ts +3 -1
  20. package/dist/agents/runner.d.ts.map +1 -1
  21. package/dist/agents/runner.js +106 -64
  22. package/dist/agents/runner.js.map +1 -1
  23. package/dist/agents/simplifiers/adapter.d.ts.map +1 -1
  24. package/dist/agents/simplifiers/adapter.js +1 -0
  25. package/dist/agents/simplifiers/adapter.js.map +1 -1
  26. package/dist/agents/simplifiers/dsl.d.ts +1 -0
  27. package/dist/agents/simplifiers/dsl.d.ts.map +1 -1
  28. package/dist/agents/simplifiers/dsl.js +1 -0
  29. package/dist/agents/simplifiers/dsl.js.map +1 -1
  30. package/dist/agents/simplifiers/handler.d.ts.map +1 -1
  31. package/dist/agents/simplifiers/handler.js +1 -0
  32. package/dist/agents/simplifiers/handler.js.map +1 -1
  33. package/dist/agents/simplifiers/index.d.ts +2 -2
  34. package/dist/agents/simplifiers/index.d.ts.map +1 -1
  35. package/dist/agents/simplifiers/index.js.map +1 -1
  36. package/dist/agents/simplifiers/prompts.d.ts +1 -0
  37. package/dist/agents/simplifiers/prompts.d.ts.map +1 -1
  38. package/dist/agents/simplifiers/prompts.js +1 -1
  39. package/dist/agents/simplifiers/prompts.js.map +1 -1
  40. package/dist/agents/simplifiers/types.d.ts +1 -0
  41. package/dist/agents/simplifiers/types.d.ts.map +1 -1
  42. package/dist/cleanup.d.ts +9 -0
  43. package/dist/cleanup.d.ts.map +1 -0
  44. package/dist/cleanup.js +35 -0
  45. package/dist/cleanup.js.map +1 -0
  46. package/dist/cli.js +30 -13
  47. package/dist/cli.js.map +1 -1
  48. package/dist/config.d.ts +5 -3
  49. package/dist/config.d.ts.map +1 -1
  50. package/dist/config.js.map +1 -1
  51. package/dist/crosscheck/coordinator.d.ts.map +1 -1
  52. package/dist/crosscheck/coordinator.js +51 -36
  53. package/dist/crosscheck/coordinator.js.map +1 -1
  54. package/dist/crosscheck/live-agent-session.d.ts +5 -1
  55. package/dist/crosscheck/live-agent-session.d.ts.map +1 -1
  56. package/dist/crosscheck/live-agent-session.js +53 -32
  57. package/dist/crosscheck/live-agent-session.js.map +1 -1
  58. package/dist/dump/collector.d.ts.map +1 -1
  59. package/dist/dump/collector.js +1 -0
  60. package/dist/dump/collector.js.map +1 -1
  61. package/dist/dump/observer-diagnostics.d.ts +6 -0
  62. package/dist/dump/observer-diagnostics.d.ts.map +1 -0
  63. package/dist/dump/observer-diagnostics.js +19 -0
  64. package/dist/dump/observer-diagnostics.js.map +1 -0
  65. package/dist/dump/service.d.ts.map +1 -1
  66. package/dist/dump/service.js +2 -3
  67. package/dist/dump/service.js.map +1 -1
  68. package/dist/dump/types.d.ts +2 -0
  69. package/dist/dump/types.d.ts.map +1 -1
  70. package/dist/flow/built-in/simplify-factory.d.ts.map +1 -1
  71. package/dist/flow/built-in/simplify-factory.js +4 -1
  72. package/dist/flow/built-in/simplify-factory.js.map +1 -1
  73. package/dist/flow/built-in/simplify.d.ts +1 -0
  74. package/dist/flow/built-in/simplify.d.ts.map +1 -1
  75. package/dist/flow/built-in/simplify.js +3 -2
  76. package/dist/flow/built-in/simplify.js.map +1 -1
  77. package/dist/flow/dsl.d.ts +1 -0
  78. package/dist/flow/dsl.d.ts.map +1 -1
  79. package/dist/flow/dsl.js +1 -0
  80. package/dist/flow/dsl.js.map +1 -1
  81. package/dist/flow/executor.d.ts.map +1 -1
  82. package/dist/flow/executor.js +3 -3
  83. package/dist/flow/executor.js.map +1 -1
  84. package/dist/flow/params.d.ts +3 -1
  85. package/dist/flow/params.d.ts.map +1 -1
  86. package/dist/flow/params.js +85 -121
  87. package/dist/flow/params.js.map +1 -1
  88. package/dist/flow/types.d.ts +5 -2
  89. package/dist/flow/types.d.ts.map +1 -1
  90. package/dist/flow/types.js.map +1 -1
  91. package/dist/offscreen/controller.d.ts.map +1 -1
  92. package/dist/offscreen/controller.js +67 -45
  93. package/dist/offscreen/controller.js.map +1 -1
  94. package/dist/offscreen/fork.d.ts +4 -2
  95. package/dist/offscreen/fork.d.ts.map +1 -1
  96. package/dist/offscreen/fork.js +49 -34
  97. package/dist/offscreen/fork.js.map +1 -1
  98. package/dist/offscreen/inquiry-pane.d.ts.map +1 -1
  99. package/dist/offscreen/inquiry-pane.js +7 -2
  100. package/dist/offscreen/inquiry-pane.js.map +1 -1
  101. package/dist/offscreen/inquiry-sender.d.ts +1 -0
  102. package/dist/offscreen/inquiry-sender.d.ts.map +1 -1
  103. package/dist/offscreen/inquiry-sender.js +12 -2
  104. package/dist/offscreen/inquiry-sender.js.map +1 -1
  105. package/dist/offscreen/registry.d.ts.map +1 -1
  106. package/dist/offscreen/registry.js +14 -12
  107. package/dist/offscreen/registry.js.map +1 -1
  108. package/dist/orchestrator.d.ts.map +1 -1
  109. package/dist/orchestrator.js +20 -24
  110. package/dist/orchestrator.js.map +1 -1
  111. package/dist/preview-panel.d.ts +4 -1
  112. package/dist/preview-panel.d.ts.map +1 -1
  113. package/dist/preview-panel.js +114 -55
  114. package/dist/preview-panel.js.map +1 -1
  115. package/dist/providers/configuration-presentation.d.ts.map +1 -1
  116. package/dist/providers/configuration-presentation.js +5 -8
  117. package/dist/providers/configuration-presentation.js.map +1 -1
  118. package/dist/providers/copilot/capability-discovery.d.ts +16 -0
  119. package/dist/providers/copilot/capability-discovery.d.ts.map +1 -0
  120. package/dist/providers/copilot/capability-discovery.js +238 -0
  121. package/dist/providers/copilot/capability-discovery.js.map +1 -0
  122. package/dist/providers/copilot/capability-extension/extension.d.mts +2 -0
  123. package/dist/providers/copilot/capability-extension/extension.d.mts.map +1 -0
  124. package/dist/providers/copilot/capability-extension/extension.mjs +46 -0
  125. package/dist/providers/copilot/capability-extension/extension.mjs.map +1 -0
  126. package/dist/providers/copilot/completion.d.ts +2 -0
  127. package/dist/providers/copilot/completion.d.ts.map +1 -1
  128. package/dist/providers/copilot/completion.js +27 -0
  129. package/dist/providers/copilot/completion.js.map +1 -1
  130. package/dist/providers/copilot/model-capabilities.d.ts +21 -0
  131. package/dist/providers/copilot/model-capabilities.d.ts.map +1 -0
  132. package/dist/providers/copilot/model-capabilities.js +76 -0
  133. package/dist/providers/copilot/model-capabilities.js.map +1 -0
  134. package/dist/providers/copilot/observation.d.ts +123 -15
  135. package/dist/providers/copilot/observation.d.ts.map +1 -1
  136. package/dist/providers/copilot/observation.js +807 -179
  137. package/dist/providers/copilot/observation.js.map +1 -1
  138. package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/extension.mjs +332 -66
  139. package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/extension.mjs.map +1 -1
  140. package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/history-evidence.d.mts +17 -0
  141. package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/history-evidence.d.mts.map +1 -0
  142. package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/history-evidence.mjs +54 -0
  143. package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/history-evidence.mjs.map +1 -0
  144. package/dist/providers/copilot/strategy.d.ts +8 -8
  145. package/dist/providers/copilot/strategy.d.ts.map +1 -1
  146. package/dist/providers/copilot/strategy.js +61 -145
  147. package/dist/providers/copilot/strategy.js.map +1 -1
  148. package/dist/providers/fork-cleanup.d.ts.map +1 -1
  149. package/dist/providers/fork-cleanup.js +3 -19
  150. package/dist/providers/fork-cleanup.js.map +1 -1
  151. package/dist/providers/initialization.d.ts +14 -0
  152. package/dist/providers/initialization.d.ts.map +1 -0
  153. package/dist/providers/initialization.js +74 -0
  154. package/dist/providers/initialization.js.map +1 -0
  155. package/dist/providers/invoke.d.ts.map +1 -1
  156. package/dist/providers/invoke.js +54 -32
  157. package/dist/providers/invoke.js.map +1 -1
  158. package/dist/providers/observer-run.d.ts.map +1 -1
  159. package/dist/providers/observer-run.js +3 -2
  160. package/dist/providers/observer-run.js.map +1 -1
  161. package/dist/providers/option-catalog.d.ts +12 -8
  162. package/dist/providers/option-catalog.d.ts.map +1 -1
  163. package/dist/providers/option-catalog.js +25 -13
  164. package/dist/providers/option-catalog.js.map +1 -1
  165. package/dist/providers/registry.d.ts +3 -1
  166. package/dist/providers/registry.d.ts.map +1 -1
  167. package/dist/providers/registry.js +20 -9
  168. package/dist/providers/registry.js.map +1 -1
  169. package/dist/providers/session-launch.d.ts.map +1 -1
  170. package/dist/providers/session-launch.js +2 -1
  171. package/dist/providers/session-launch.js.map +1 -1
  172. package/dist/providers/types.d.ts +35 -8
  173. package/dist/providers/types.d.ts.map +1 -1
  174. package/dist/providers/types.js +1 -1
  175. package/dist/providers/types.js.map +1 -1
  176. package/dist/pty/exit.d.ts +3 -1
  177. package/dist/pty/exit.d.ts.map +1 -1
  178. package/dist/pty/exit.js +39 -6
  179. package/dist/pty/exit.js.map +1 -1
  180. package/dist/pty/spawn.d.ts.map +1 -1
  181. package/dist/pty/spawn.js +7 -3
  182. package/dist/pty/spawn.js.map +1 -1
  183. package/dist/pty/types.d.ts +1 -1
  184. package/dist/pty/types.d.ts.map +1 -1
  185. package/dist/session/resume.d.ts.map +1 -1
  186. package/dist/session/resume.js +7 -0
  187. package/dist/session/resume.js.map +1 -1
  188. package/dist/startup-tui.d.ts +1 -0
  189. package/dist/startup-tui.d.ts.map +1 -1
  190. package/dist/startup-tui.js +17 -6
  191. package/dist/startup-tui.js.map +1 -1
  192. package/dist/terminal/host.d.ts.map +1 -1
  193. package/dist/terminal/host.js +11 -10
  194. package/dist/terminal/host.js.map +1 -1
  195. package/dist/user/host-curate-prompt.d.ts.map +1 -1
  196. package/dist/user/host-curate-prompt.js +9 -5
  197. package/dist/user/host-curate-prompt.js.map +1 -1
  198. package/dist/user/host-prompt.d.ts.map +1 -1
  199. package/dist/user/host-prompt.js +9 -5
  200. package/dist/user/host-prompt.js.map +1 -1
  201. 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
- // Let admitted replay catch up before accepting more live frames. The
156
- // paused socket carries this pressure back to the extension's writer.
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
- events = [];
290
- seenEventIds = new Set();
291
- seenEventReplayKeys = new Set();
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 = new CopilotLiveEvidence();
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 && this.activeConnection.drain !== undefined;
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.events.length,
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
- replayHistory: this.events.length > 0,
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.clearRecovery();
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.clearRecovery();
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.event);
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
- const end = Math.min(connection.bufferedEvents.length, connection.bufferedCursor + COPILOT_OBSERVER_WORK_QUANTUM);
758
- while (connection.bufferedCursor < end && connection === this.activeConnection) {
759
- const event = connection.bufferedEvents[connection.bufferedCursor++];
760
- const id = stringProp(event, "id");
761
- if (id)
762
- connection.inheritedEventIds?.delete(id);
763
- this.commitEvent(event);
764
- }
765
- if (connection.bufferedCursor === connection.bufferedEvents.length) {
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
- commitEvent(event) {
773
- if (event.type === "session.start" || event.type === "session.resume") {
774
- this.lifecycleEventCount++;
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.fail(this.recoveryCause ?? cause);
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
- this.recoveryCause = undefined;
827
- this.recoveryDeadlineAt = undefined;
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
- registration?.dispose();
924
- if (pluginAssets) {
925
- await cleanupCopilotObserverPluginAssets(pluginAssets);
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
- async dispose() {
969
- if (this.disposed)
970
- return;
1312
+ dispose() {
1313
+ if (this.disposePromise)
1314
+ return this.disposePromise;
971
1315
  this.disposed = true;
972
- this.onDispose?.(this);
973
- this.registration.dispose();
974
- if (this.ownsBroker)
975
- await this.broker.close();
976
- await Promise.all([
977
- this.ownsTempRoot
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
- async dispose() {
1009
- if (this.disposed)
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
- await Promise.all(observers.map((observer) => observer.dispose()));
1015
- let resources = this.sharedResourcesValue;
1016
- if (this.sharedResourcesPromise) {
1017
- try {
1018
- resources = await this.sharedResourcesPromise;
1019
- }
1020
- catch {
1021
- // Initialization failure is reported to the launch caller.
1022
- }
1023
- }
1024
- this.sharedResourcesValue = undefined;
1025
- this.sharedResourcesPromise = undefined;
1026
- await resources?.broker.close();
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 Promise.allSettled([
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 Promise.all([
1157
- rm(assets.tempRoot, { recursive: true, force: true }).catch(() => { }),
1158
- rm(assets.sessionExtensionDir, { recursive: true, force: true }).catch(() => { }),
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
- ...(decision.replayHistory && { replayHistory: true }),
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
- class CopilotLiveTurnMonitor {
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) || receipt.settlement)
1873
+ if (this.stopped || this.attached.has(receipt))
1344
1874
  return;
1345
1875
  this.attached.add(receipt);
1346
- const entry = { receipt };
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 && !receipt.rawAttempt)
1355
- this.confirmTurn(entry);
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
- if (this.stopped)
1395
- return receipt;
1942
+ return receipt;
1943
+ }
1944
+ trackRawAttempt(entry) {
1945
+ const { receipt, anchorSubmitCount } = entry;
1396
1946
  this.refreshCounts();
1397
- const startedAt = snapshot.startedAt ?? Date.now();
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
- const entry = this.entries.get(receipt.id);
1402
- if (entry)
1403
- this.removeEntry(entry);
1950
+ this.removeEntry(entry);
1404
1951
  this.evaluateSettlement({ notifyIfAlreadyIdle: true });
1405
- return receipt;
1952
+ return;
1406
1953
  }
1407
1954
  const attempt = {
1408
- anchorSubmitCount: snapshot.anchorSubmitCount ?? this.observedSubmitCount,
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
- return receipt;
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
- return this.observedSubmitCount > baseline ? "present" : "absent";
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
- startIndex = 0;
2198
+ startGeneration = 0;
2199
+ initialResponsePartCount = 0;
2200
+ request;
1611
2201
  completed = false;
1612
2202
  activationFired = false;
1613
2203
  nativePlan;
1614
2204
  disposed = false;
1615
- readingPlan = false;
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, lineOffset = 0) {
1626
- this.startIndex = Math.max(this.registration.eventCount, lineOffset);
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((event) => {
1631
- void this.onEvent(event);
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
- async onEvent(event) {
1674
- if (this.completed || this.disposed || this.registration.eventCount <= this.startIndex)
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 && isExitPlanRequested(event)) {
1686
- if (this.readingPlan)
1687
- return;
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.readingPlan = true;
2287
+ if (this.readingPlanGeneration === generation)
2288
+ return;
2289
+ this.readingPlanGeneration = generation;
2290
+ const deadline = Date.now() + PLAN_READ_TIMEOUT_MS;
1695
2291
  try {
1696
- this.nativePlan = await readPlanWithRetry(this.registration, this.abort.signal);
1697
- if (this.disposed)
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.finishSuccess(this.nativePlan.content ?? "");
2298
+ this.nativePlan = plan;
2299
+ this.finishSuccess(plan.content ?? "");
1700
2300
  }
1701
2301
  catch (err) {
1702
- this.finishError(err instanceof Error ? err : new Error(String(err)));
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 evidence = this.registration.evidence;
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(this.responseParts.join("\n"));
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.finishSuccess(this.responseParts.join("\n"));
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 {};