poly-weaver 0.15.2 → 1.0.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 (194) hide show
  1. package/dist/agents/implementors/handler.d.ts.map +1 -1
  2. package/dist/agents/implementors/handler.js +7 -2
  3. package/dist/agents/implementors/handler.js.map +1 -1
  4. package/dist/agents/interviewers/handler.d.ts.map +1 -1
  5. package/dist/agents/interviewers/handler.js +3 -1
  6. package/dist/agents/interviewers/handler.js.map +1 -1
  7. package/dist/agents/planners/adapter.d.ts.map +1 -1
  8. package/dist/agents/planners/adapter.js +30 -11
  9. package/dist/agents/planners/adapter.js.map +1 -1
  10. package/dist/agents/planners/handler.d.ts.map +1 -1
  11. package/dist/agents/planners/handler.js +7 -2
  12. package/dist/agents/planners/handler.js.map +1 -1
  13. package/dist/agents/reviewers/adapter.d.ts.map +1 -1
  14. package/dist/agents/reviewers/adapter.js +1 -0
  15. package/dist/agents/reviewers/adapter.js.map +1 -1
  16. package/dist/agents/reviewers/handler.d.ts.map +1 -1
  17. package/dist/agents/reviewers/handler.js +23 -4
  18. package/dist/agents/reviewers/handler.js.map +1 -1
  19. package/dist/agents/reviewers/types.d.ts +2 -0
  20. package/dist/agents/reviewers/types.d.ts.map +1 -1
  21. package/dist/agents/runner.d.ts +18 -1
  22. package/dist/agents/runner.d.ts.map +1 -1
  23. package/dist/agents/runner.js +136 -20
  24. package/dist/agents/runner.js.map +1 -1
  25. package/dist/agents/simplifiers/handler.d.ts.map +1 -1
  26. package/dist/agents/simplifiers/handler.js +7 -2
  27. package/dist/agents/simplifiers/handler.js.map +1 -1
  28. package/dist/auto-update/detect-agents.d.ts +1 -0
  29. package/dist/auto-update/detect-agents.d.ts.map +1 -1
  30. package/dist/auto-update/detect-agents.js +8 -7
  31. package/dist/auto-update/detect-agents.js.map +1 -1
  32. package/dist/cli.js +16 -4
  33. package/dist/cli.js.map +1 -1
  34. package/dist/crosscheck/coordinator.d.ts +5 -0
  35. package/dist/crosscheck/coordinator.d.ts.map +1 -1
  36. package/dist/crosscheck/coordinator.js +216 -96
  37. package/dist/crosscheck/coordinator.js.map +1 -1
  38. package/dist/crosscheck/live-agent-session.d.ts +10 -1
  39. package/dist/crosscheck/live-agent-session.d.ts.map +1 -1
  40. package/dist/crosscheck/live-agent-session.js +68 -7
  41. package/dist/crosscheck/live-agent-session.js.map +1 -1
  42. package/dist/crosscheck/state.d.ts +3 -0
  43. package/dist/crosscheck/state.d.ts.map +1 -1
  44. package/dist/crosscheck/state.js +17 -2
  45. package/dist/crosscheck/state.js.map +1 -1
  46. package/dist/dump/collector.d.ts.map +1 -1
  47. package/dist/dump/collector.js +3 -0
  48. package/dist/dump/collector.js.map +1 -1
  49. package/dist/dump/service.d.ts.map +1 -1
  50. package/dist/dump/service.js +3 -0
  51. package/dist/dump/service.js.map +1 -1
  52. package/dist/dump/types.d.ts +3 -0
  53. package/dist/dump/types.d.ts.map +1 -1
  54. package/dist/dump/types.js.map +1 -1
  55. package/dist/flow/executor.d.ts.map +1 -1
  56. package/dist/flow/executor.js +5 -0
  57. package/dist/flow/executor.js.map +1 -1
  58. package/dist/flow/types.d.ts +5 -0
  59. package/dist/flow/types.d.ts.map +1 -1
  60. package/dist/flow/types.js.map +1 -1
  61. package/dist/offscreen/controller.d.ts +19 -5
  62. package/dist/offscreen/controller.d.ts.map +1 -1
  63. package/dist/offscreen/controller.js +336 -159
  64. package/dist/offscreen/controller.js.map +1 -1
  65. package/dist/offscreen/fork-launch.d.ts +2 -2
  66. package/dist/offscreen/fork-launch.d.ts.map +1 -1
  67. package/dist/offscreen/fork-launch.js +2 -2
  68. package/dist/offscreen/fork-launch.js.map +1 -1
  69. package/dist/offscreen/fork.d.ts +16 -2
  70. package/dist/offscreen/fork.d.ts.map +1 -1
  71. package/dist/offscreen/fork.js +79 -40
  72. package/dist/offscreen/fork.js.map +1 -1
  73. package/dist/offscreen/inquiry-pane.d.ts +2 -1
  74. package/dist/offscreen/inquiry-pane.d.ts.map +1 -1
  75. package/dist/offscreen/inquiry-pane.js +8 -2
  76. package/dist/offscreen/inquiry-pane.js.map +1 -1
  77. package/dist/offscreen/inquiry-sender.d.ts +1 -0
  78. package/dist/offscreen/inquiry-sender.d.ts.map +1 -1
  79. package/dist/offscreen/inquiry-sender.js +1 -0
  80. package/dist/offscreen/inquiry-sender.js.map +1 -1
  81. package/dist/offscreen/panes.d.ts +1 -0
  82. package/dist/offscreen/panes.d.ts.map +1 -1
  83. package/dist/offscreen/registry.d.ts +2 -2
  84. package/dist/offscreen/registry.d.ts.map +1 -1
  85. package/dist/offscreen/registry.js +5 -1
  86. package/dist/offscreen/registry.js.map +1 -1
  87. package/dist/offscreen/resolve.d.ts +2 -0
  88. package/dist/offscreen/resolve.d.ts.map +1 -1
  89. package/dist/offscreen/resolve.js +5 -1
  90. package/dist/offscreen/resolve.js.map +1 -1
  91. package/dist/offscreen/session-turn-monitor.d.ts +34 -33
  92. package/dist/offscreen/session-turn-monitor.d.ts.map +1 -1
  93. package/dist/offscreen/session-turn-monitor.js +327 -281
  94. package/dist/offscreen/session-turn-monitor.js.map +1 -1
  95. package/dist/offscreen/session-turn-receipt.d.ts +28 -0
  96. package/dist/offscreen/session-turn-receipt.d.ts.map +1 -0
  97. package/dist/offscreen/session-turn-receipt.js +53 -0
  98. package/dist/offscreen/session-turn-receipt.js.map +1 -0
  99. package/dist/offscreen/split-inquiry.d.ts.map +1 -1
  100. package/dist/offscreen/split-inquiry.js +5 -0
  101. package/dist/offscreen/split-inquiry.js.map +1 -1
  102. package/dist/offscreen/starting-pane.d.ts +13 -0
  103. package/dist/offscreen/starting-pane.d.ts.map +1 -0
  104. package/dist/offscreen/starting-pane.js +24 -0
  105. package/dist/offscreen/starting-pane.js.map +1 -0
  106. package/dist/offscreen/surface.d.ts +1 -0
  107. package/dist/offscreen/surface.d.ts.map +1 -1
  108. package/dist/offscreen/surface.js +5 -3
  109. package/dist/offscreen/surface.js.map +1 -1
  110. package/dist/orchestrator.d.ts.map +1 -1
  111. package/dist/orchestrator.js +7 -1
  112. package/dist/orchestrator.js.map +1 -1
  113. package/dist/providers/base-completion.d.ts +1 -0
  114. package/dist/providers/base-completion.d.ts.map +1 -1
  115. package/dist/providers/base-completion.js +4 -2
  116. package/dist/providers/base-completion.js.map +1 -1
  117. package/dist/providers/codex/completion.d.ts +4 -2
  118. package/dist/providers/codex/completion.d.ts.map +1 -1
  119. package/dist/providers/codex/completion.js +99 -14
  120. package/dist/providers/codex/completion.js.map +1 -1
  121. package/dist/providers/codex/strategy.d.ts.map +1 -1
  122. package/dist/providers/codex/strategy.js +3 -2
  123. package/dist/providers/codex/strategy.js.map +1 -1
  124. package/dist/providers/copilot/completion.d.ts +32 -21
  125. package/dist/providers/copilot/completion.d.ts.map +1 -1
  126. package/dist/providers/copilot/completion.js +110 -166
  127. package/dist/providers/copilot/completion.js.map +1 -1
  128. package/dist/providers/copilot/environment.d.ts +2 -0
  129. package/dist/providers/copilot/environment.d.ts.map +1 -0
  130. package/dist/providers/copilot/environment.js +8 -0
  131. package/dist/providers/copilot/environment.js.map +1 -0
  132. package/dist/providers/copilot/fork-state.d.ts +7 -0
  133. package/dist/providers/copilot/fork-state.d.ts.map +1 -0
  134. package/dist/providers/copilot/fork-state.js +150 -0
  135. package/dist/providers/copilot/fork-state.js.map +1 -0
  136. package/dist/providers/copilot/observation.d.ts +275 -0
  137. package/dist/providers/copilot/observation.d.ts.map +1 -0
  138. package/dist/providers/copilot/observation.js +2072 -0
  139. package/dist/providers/copilot/observation.js.map +1 -0
  140. package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/extension.d.mts +2 -0
  141. package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/extension.d.mts.map +1 -0
  142. package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/extension.mjs +426 -0
  143. package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/extension.mjs.map +1 -0
  144. package/dist/providers/copilot/observer-extension/plugin.json +6 -0
  145. package/dist/providers/copilot/record-properties.d.ts +4 -0
  146. package/dist/providers/copilot/record-properties.d.ts.map +1 -0
  147. package/dist/providers/copilot/record-properties.js +14 -0
  148. package/dist/providers/copilot/record-properties.js.map +1 -0
  149. package/dist/providers/copilot/session.d.ts +8 -33
  150. package/dist/providers/copilot/session.d.ts.map +1 -1
  151. package/dist/providers/copilot/session.js +10 -176
  152. package/dist/providers/copilot/session.js.map +1 -1
  153. package/dist/providers/copilot/strategy.d.ts +4 -5
  154. package/dist/providers/copilot/strategy.d.ts.map +1 -1
  155. package/dist/providers/copilot/strategy.js +73 -67
  156. package/dist/providers/copilot/strategy.js.map +1 -1
  157. package/dist/providers/fork-cleanup.d.ts +4 -0
  158. package/dist/providers/fork-cleanup.d.ts.map +1 -0
  159. package/dist/providers/fork-cleanup.js +21 -0
  160. package/dist/providers/fork-cleanup.js.map +1 -0
  161. package/dist/providers/fork.d.ts +24 -3
  162. package/dist/providers/fork.d.ts.map +1 -1
  163. package/dist/providers/fork.js.map +1 -1
  164. package/dist/providers/invoke.d.ts.map +1 -1
  165. package/dist/providers/invoke.js +85 -34
  166. package/dist/providers/invoke.js.map +1 -1
  167. package/dist/providers/live.d.ts.map +1 -1
  168. package/dist/providers/live.js +62 -17
  169. package/dist/providers/live.js.map +1 -1
  170. package/dist/providers/observer-run.d.ts +4 -0
  171. package/dist/providers/observer-run.d.ts.map +1 -0
  172. package/dist/providers/observer-run.js +23 -0
  173. package/dist/providers/observer-run.js.map +1 -0
  174. package/dist/providers/session-launch.d.ts +5 -1
  175. package/dist/providers/session-launch.d.ts.map +1 -1
  176. package/dist/providers/session-launch.js +42 -16
  177. package/dist/providers/session-launch.js.map +1 -1
  178. package/dist/providers/types.d.ts +156 -4
  179. package/dist/providers/types.d.ts.map +1 -1
  180. package/dist/providers/types.js.map +1 -1
  181. package/dist/session/resume.d.ts.map +1 -1
  182. package/dist/session/resume.js +16 -2
  183. package/dist/session/resume.js.map +1 -1
  184. package/dist/terminal/paste-submit.d.ts +1 -0
  185. package/dist/terminal/paste-submit.d.ts.map +1 -1
  186. package/dist/terminal/paste-submit.js +45 -32
  187. package/dist/terminal/paste-submit.js.map +1 -1
  188. package/dist/user/host-curate-prompt.d.ts.map +1 -1
  189. package/dist/user/host-curate-prompt.js +9 -6
  190. package/dist/user/host-curate-prompt.js.map +1 -1
  191. package/dist/user/host-prompt.d.ts.map +1 -1
  192. package/dist/user/host-prompt.js +9 -6
  193. package/dist/user/host-prompt.js.map +1 -1
  194. package/package.json +4 -2
@@ -3,14 +3,15 @@ import { builtinStrategies, resolveProvider } from "../providers/registry.js";
3
3
  import { Fork } from "./fork.js";
4
4
  import { foldAltMetaKeys, isPrimaryIssueKey, isPrimaryShowKey, isSecondaryIssueKey, isSecondaryShowKey, isSummarizeKey, } from "./keys.js";
5
5
  import { InquiryPaneController } from "./inquiry-pane.js";
6
+ import { StartingInquiryPane } from "./starting-pane.js";
6
7
  import { ArtifactPaneController } from "./artifact-pane.js";
7
8
  import { registerOffscreenDisposer } from "./registry.js";
8
9
  import { SplitInquiryCoordinator } from "./split-inquiry.js";
9
10
  import { SPLIT_COLLAPSE_AT_COLS, } from "../user/split-surface.js";
10
11
  import { sendInquiryQuestion } from "./inquiry-sender.js";
11
- import { readSessionFile, readSessionFileWithStatus, } from "../session/reader.js";
12
+ import { readSessionFileWithStatus, } from "../session/reader.js";
12
13
  import { forkFileSubmitFloor, locateForkSessionFile, submitCount, } from "./fork-busy.js";
13
- import { SessionTurnMonitor, } from "./session-turn-monitor.js";
14
+ import { SessionTurnMonitor, SessionTurnReceipt, DEFAULT_TURN_ATTEMPT_TIMEOUT_MS, } from "./session-turn-monitor.js";
14
15
  import { abortableSleep } from "../util/abortable-sleep.js";
15
16
  const OFFSCREEN_MOUSE_SOURCE = "offscreen";
16
17
  const DEFAULT_TURN_POLL_MS = 500;
@@ -19,6 +20,7 @@ export class OffscreenController {
19
20
  host;
20
21
  workdir;
21
22
  inquiry;
23
+ observerRuns;
22
24
  slots = new Map();
23
25
  artifactConfigs;
24
26
  turnPollMs;
@@ -26,6 +28,9 @@ export class OffscreenController {
26
28
  tracksTurns;
27
29
  forks = new Map();
28
30
  pending = new Map();
31
+ pendingForks = new Map();
32
+ pendingQuestions = new Map();
33
+ launchAbort = new AbortController();
29
34
  sendPending = new Set();
30
35
  turnInProgress = new Set();
31
36
  turnMonitors = new Map();
@@ -36,36 +41,40 @@ export class OffscreenController {
36
41
  forkBaselines = new Map();
37
42
  forkLocateAborts = new Map();
38
43
  forkSubmitFloors = new Map();
39
- pendingTurnGenerations = new Map();
40
- pendingTurnAttempts = new Map();
44
+ pendingTurns = new Map();
45
+ pendingAttemptTimers = new Map();
41
46
  forkSubmitFloorReads = new Map();
42
47
  forkLocateWarned = new Set();
43
48
  activeArtifactIndexes = new Map();
49
+ artifactPanes = new Map();
44
50
  unregister;
45
51
  split;
46
52
  shown;
47
53
  disposed = false;
48
- constructor(host, workdir, slots, artifactConfigs = {}, inquiry) {
54
+ disposal;
55
+ constructor(host, workdir, slots, artifactConfigs = {}, inquiry, observerRuns) {
49
56
  this.host = host;
50
57
  this.workdir = workdir;
51
58
  this.inquiry = inquiry;
59
+ this.observerRuns = observerRuns;
52
60
  for (const slot of slots) {
53
61
  this.slots.set(slot.binding, slot);
54
62
  }
55
63
  // Snapshot the configs so later caller mutation can't reach the controller.
56
64
  this.artifactConfigs = { ...artifactConfigs };
57
65
  this.turnPollMs = inquiry?.turnPollMs ?? DEFAULT_TURN_POLL_MS;
58
- this.turnAttemptTimeoutMs = inquiry?.turnAttemptTimeoutMs;
66
+ this.turnAttemptTimeoutMs = inquiry?.turnAttemptTimeoutMs ?? DEFAULT_TURN_ATTEMPT_TIMEOUT_MS;
59
67
  this.tracksTurns =
60
68
  inquiry !== undefined || slots.some((slot) => slot.summarizePrompt !== undefined);
61
69
  this.unregister = registerOffscreenDisposer(() => this.dispose());
62
70
  }
63
71
  isIssueInquiryReady(binding) {
64
- return !this.sendPending.has(binding) && !this.turnInProgress.has(binding);
72
+ return !this.sendPending.has(binding) && !this.turnInProgress.has(binding)
73
+ && !this.sendLocks.has(binding);
65
74
  }
66
75
  selectionDelegate() {
67
76
  return {
68
- fork: this.shown ? this.forks.get(this.shown) : undefined,
77
+ fork: this.shown ? this.presentedFork(this.shown) : undefined,
69
78
  layout: this.host.getLayout(),
70
79
  coordinator: this.activeCoordinator(),
71
80
  };
@@ -73,15 +82,16 @@ export class OffscreenController {
73
82
  wrap(base) {
74
83
  return {
75
84
  getSurface: () => {
76
- const fork = this.shown ? this.forks.get(this.shown) : undefined;
77
- if (!fork)
85
+ const fork = this.shown ? this.presentedFork(this.shown) : undefined;
86
+ if (!this.shown)
78
87
  return base.getSurface();
79
88
  const layout = this.host.getLayout();
80
89
  const coordinator = this.activeCoordinator();
81
90
  if (coordinator) {
82
91
  return coordinator.getSurface(layout.columns, layout.childRows);
83
92
  }
84
- return new InquiryPaneController(fork).render(layout.columns, layout.childRows);
93
+ return (fork ? new InquiryPaneController(fork, this.forks.has(this.shown)) : this.startingPane(this.shown))
94
+ .render(layout.columns, layout.childRows);
85
95
  },
86
96
  selectionRegionAt: (column, row) => {
87
97
  const { fork, layout, coordinator } = this.selectionDelegate();
@@ -89,7 +99,7 @@ export class OffscreenController {
89
99
  return coordinator.selectionRegionAt(column, row, layout.columns, layout.childRows);
90
100
  }
91
101
  if (!fork)
92
- return base.selectionRegionAt?.(column, row);
102
+ return this.shown ? null : base.selectionRegionAt?.(column, row);
93
103
  if (row < 2 || row > layout.childRows)
94
104
  return null;
95
105
  return fork.vt.isAlternate === true
@@ -102,7 +112,7 @@ export class OffscreenController {
102
112
  return coordinator.selectionTargetAt(column, row, layout.columns, layout.childRows);
103
113
  }
104
114
  if (!fork)
105
- return base.selectionTargetAt?.(column, row) ?? null;
115
+ return this.shown ? null : base.selectionTargetAt?.(column, row) ?? null;
106
116
  if (fork.vt.isAlternate === true)
107
117
  return null;
108
118
  if (column < 1 || column > layout.columns || row < 2 || row > layout.childRows)
@@ -115,7 +125,7 @@ export class OffscreenController {
115
125
  return coordinator.scrollSelectableSurfaceAt(column, row, direction, layout.columns, layout.childRows);
116
126
  }
117
127
  if (!fork || fork.vt.isAlternate === true) {
118
- return base.scrollSelectableSurfaceAt?.(column, row, direction) ?? 0;
128
+ return this.shown ? 0 : base.scrollSelectableSurfaceAt?.(column, row, direction) ?? 0;
119
129
  }
120
130
  if (column < 1 || column > layout.columns)
121
131
  return 0;
@@ -133,7 +143,7 @@ export class OffscreenController {
133
143
  return placement ? { region: placement.region, vt: fork.vt } : undefined;
134
144
  }
135
145
  if (!fork || fork.vt.isAlternate === true || layout.childRows < 2) {
136
- return base.forkSelectionPlacement?.();
146
+ return this.shown ? undefined : base.forkSelectionPlacement?.();
137
147
  }
138
148
  return {
139
149
  region: { rowStart: 2, rowEnd: layout.childRows, colStart: 1, colEnd: layout.columns },
@@ -145,7 +155,7 @@ export class OffscreenController {
145
155
  if (coordinator) {
146
156
  return coordinator.artifactPlacementAt(column, row, layout.columns, layout.childRows);
147
157
  }
148
- if (fork)
158
+ if (this.shown)
149
159
  return undefined;
150
160
  return base.docSurfaceSelectionPlacementAt?.(column, row);
151
161
  },
@@ -155,7 +165,7 @@ export class OffscreenController {
155
165
  const placement = coordinator.artifactPlacement(layout.columns, layout.childRows);
156
166
  return placement ? [placement] : [];
157
167
  }
158
- if (fork)
168
+ if (this.shown)
159
169
  return [];
160
170
  return base.docSurfaceSelectionPlacements?.() ?? [];
161
171
  },
@@ -164,11 +174,13 @@ export class OffscreenController {
164
174
  if (coordinator) {
165
175
  return coordinator.artifactPlacement(layout.columns, layout.childRows);
166
176
  }
167
- if (fork)
177
+ if (this.shown)
168
178
  return undefined;
169
179
  return base.artifactSelectionPlacement?.();
170
180
  },
171
181
  onEvents: (keys) => {
182
+ if (this.disposed)
183
+ return true;
172
184
  const events = this.inquiry ? foldAltMetaKeys(keys) : keys;
173
185
  if (keys.some(isSummarizeKey) && this.canSummarize("primary")) {
174
186
  return this.summarize("primary");
@@ -200,41 +212,60 @@ export class OffscreenController {
200
212
  onResize: (cols, rows) => {
201
213
  if (this.disposed)
202
214
  return;
203
- const fork = this.shown ? this.forks.get(this.shown) : undefined;
204
- if (fork && this.shown)
215
+ const fork = this.shown ? this.presentedFork(this.shown) : undefined;
216
+ if (this.shown)
205
217
  this.configurePresentation(this.shown, fork, cols, rows);
206
218
  this.updateForkState();
207
219
  },
208
220
  };
209
221
  }
210
222
  dispose() {
211
- if (this.disposed)
212
- return;
223
+ if (this.disposal)
224
+ return this.disposal;
213
225
  this.disposed = true;
226
+ this.launchAbort.abort();
214
227
  this.unregister();
215
228
  for (const binding of Array.from(this.sendControllers.keys())) {
216
229
  this.abortIssueSend(binding);
217
230
  }
218
231
  this.clearForkSessionWatchers();
219
- for (const fork of this.forks.values()) {
220
- fork.kill();
221
- }
232
+ const releases = [...this.forks.values(), ...this.pendingForks.values()].map((fork) => fork.kill());
233
+ const pending = Array.from(this.pending.values());
222
234
  this.forks.clear();
223
235
  this.pending.clear();
236
+ this.pendingForks.clear();
237
+ this.pendingQuestions.clear();
238
+ this.artifactPanes.clear();
224
239
  this.sendPending.clear();
225
240
  this.sendLocks.clear();
226
241
  this.shown = undefined;
227
242
  this.clearSplitCoordinator();
228
243
  this.host.clearMouseModeSource(OFFSCREEN_MOUSE_SOURCE);
229
244
  this.updateForkState();
245
+ return this.disposal = Promise.all([...releases, ...pending]).then(() => { });
230
246
  }
231
247
  handleForkEvents(keys) {
232
248
  const binding = this.shown;
233
249
  const fork = binding ? this.forks.get(binding) : undefined;
234
250
  if (!fork) {
235
- this.shown = undefined;
236
- this.clearSplitCoordinator();
237
- this.updateForkState();
251
+ if (!binding)
252
+ return true;
253
+ if (keys.some((key) => key.kind === "control" && key.byte === 0x03)) {
254
+ this.hideFork(this.pendingForks.get(binding));
255
+ return true;
256
+ }
257
+ for (const key of keys) {
258
+ if (isArtifactViewerKey(key) && this.cycleCompanionArtifact(binding))
259
+ continue;
260
+ const shortcut = this.inquiry ? inquiryShortcutForKey(key) : undefined;
261
+ if (shortcut?.kind === "issue" && shortcut.binding === binding) {
262
+ this.openInquiry(shortcut);
263
+ }
264
+ }
265
+ const layout = this.host.getLayout();
266
+ const result = this.activeCoordinator()?.handleArtifactScroll(keys, layout.columns, layout.childRows);
267
+ if (result)
268
+ this.applyPaneResult(undefined, result);
238
269
  return true;
239
270
  }
240
271
  if (binding && this.sendLocks.has(binding)) {
@@ -285,16 +316,16 @@ export class OffscreenController {
285
316
  return true;
286
317
  if (routed.length === 0)
287
318
  return true;
288
- const coordinator = this.activeCoordinator();
289
- const layout = this.host.getLayout();
290
- const result = coordinator
291
- ? coordinator.handleEvents(routed, layout.columns, layout.childRows)
292
- : new InquiryPaneController(fork).handleKeys(routed);
293
319
  if (this.tracksTurns && binding) {
294
320
  for (let submission = 0; submission < manualSubmissions; submission++) {
295
321
  this.beginTurnAttempt(binding);
296
322
  }
297
323
  }
324
+ const coordinator = this.activeCoordinator();
325
+ const layout = this.host.getLayout();
326
+ const result = coordinator
327
+ ? coordinator.handleEvents(routed, layout.columns, layout.childRows)
328
+ : new InquiryPaneController(fork).handleKeys(routed);
298
329
  this.applyPaneResult(fork, result);
299
330
  return true;
300
331
  }
@@ -334,14 +365,17 @@ export class OffscreenController {
334
365
  this.hideFork(fork);
335
366
  }
336
367
  }
368
+ presentedFork(binding) {
369
+ return this.forks.get(binding) ?? this.pendingForks.get(binding);
370
+ }
337
371
  hideFork(fork) {
338
372
  if (this.shown)
339
373
  this.abortIssueSend(this.shown);
340
374
  if (this.activeCoordinator()) {
341
375
  const layout = this.host.getLayout();
342
- fork.resize(layout.columns, layout.childRows);
376
+ fork?.resize(layout.columns, layout.childRows);
343
377
  }
344
- fork.setVisible(false);
378
+ fork?.setVisible(false);
345
379
  this.shown = undefined;
346
380
  this.clearSplitCoordinator();
347
381
  this.host.clearMouseModeSource(OFFSCREEN_MOUSE_SOURCE);
@@ -374,7 +408,7 @@ export class OffscreenController {
374
408
  ];
375
409
  }
376
410
  nextCompanionArtifactHint(binding) {
377
- if (this.shown !== binding || !this.forks.has(binding))
411
+ if (this.shown !== binding)
378
412
  return undefined;
379
413
  const config = this.artifactConfigs[binding];
380
414
  if (!config)
@@ -387,8 +421,8 @@ export class OffscreenController {
387
421
  }
388
422
  cycleCompanionArtifact(binding) {
389
423
  const primary = this.artifactConfigs[binding];
390
- const fork = this.forks.get(binding);
391
- if (!primary || !fork)
424
+ const fork = this.presentedFork(binding);
425
+ if (!primary)
392
426
  return false;
393
427
  const count = this.companionArtifacts(primary).length;
394
428
  if (count <= 1)
@@ -404,21 +438,33 @@ export class OffscreenController {
404
438
  configurePresentation(binding, fork, cols, rows) {
405
439
  const artifact = this.activeArtifactConfig(binding);
406
440
  if (artifact && (cols >= SPLIT_COLLAPSE_AT_COLS || artifact.collapseWhenNarrow)) {
441
+ const index = this.activeArtifactIndexes.get(binding) ?? 0;
442
+ let panes = this.artifactPanes.get(binding);
443
+ if (!panes)
444
+ this.artifactPanes.set(binding, panes = new Map());
445
+ let artifactPane = panes.get(index);
446
+ if (!artifactPane) {
447
+ artifactPane = new ArtifactPaneController(artifact.renderArtifact, artifact.styleVersion);
448
+ panes.set(index, artifactPane);
449
+ }
407
450
  this.split = {
408
451
  binding,
409
452
  coordinator: new SplitInquiryCoordinator({
410
453
  inquirySide: artifact.inquirySide,
411
- inquiryPane: new InquiryPaneController(fork),
412
- artifactPane: new ArtifactPaneController(artifact.renderArtifact, artifact.styleVersion),
454
+ inquiryPane: fork ? new InquiryPaneController(fork, this.forks.has(binding)) : this.startingPane(binding),
455
+ artifactPane,
413
456
  collapseAt: SPLIT_COLLAPSE_AT_COLS,
414
457
  collapseArtifact: artifact.collapseWhenNarrow,
415
458
  }),
416
459
  };
417
- fork.resize(this.split.coordinator.inquiryWidth(cols), this.split.coordinator.inquiryHeight(cols, rows));
460
+ fork?.resize(this.split.coordinator.inquiryWidth(cols), this.split.coordinator.inquiryHeight(cols, rows));
418
461
  return;
419
462
  }
420
463
  this.clearSplitCoordinator();
421
- fork.resize(cols, rows);
464
+ fork?.resize(cols, rows);
465
+ }
466
+ startingPane(binding) {
467
+ return new StartingInquiryPane(this.slots.get(binding).roleLabel);
422
468
  }
423
469
  focusArtifactForCurrentIssue(binding) {
424
470
  if (this.shown !== binding)
@@ -444,60 +490,98 @@ export class OffscreenController {
444
490
  this.host.scheduleParentUiRender();
445
491
  return;
446
492
  }
447
- void this.ensureFork(binding, slot)
448
- .then((fork) => {
449
- if (this.disposed) {
450
- fork.kill();
451
- return;
452
- }
453
- for (const [key, other] of this.forks) {
454
- if (key !== binding)
455
- other.setVisible(false);
456
- }
457
- const layout = this.host.getLayout();
458
- this.shown = binding;
459
- this.configurePresentation(binding, fork, layout.columns, layout.childRows);
460
- if (focusIssueArtifact)
461
- this.focusArtifactForCurrentIssue(binding);
462
- fork.setVisible(true);
463
- this.updateForkState();
464
- this.host.scheduleRender();
493
+ if (this.shown && this.shown !== binding)
494
+ this.abortIssueSend(this.shown);
495
+ for (const [key, other] of [...this.forks, ...this.pendingForks]) {
496
+ if (key !== binding)
497
+ other.setVisible(false);
498
+ }
499
+ const fork = this.forks.get(binding);
500
+ const presented = this.presentedFork(binding);
501
+ const layout = this.host.getLayout();
502
+ this.shown = binding;
503
+ this.configurePresentation(binding, presented, layout.columns, layout.childRows);
504
+ if (focusIssueArtifact)
505
+ this.focusArtifactForCurrentIssue(binding);
506
+ presented?.setVisible(true);
507
+ this.updateForkState();
508
+ this.host.scheduleRender();
509
+ if (fork) {
465
510
  if (question)
466
511
  this.startInquirySend(binding, fork, question);
467
- })
468
- .catch((err) => {
469
- this.clearSendPending(binding);
470
- const reason = err instanceof Error ? err.message : String(err);
471
- this.host.output.warn(`offscreen ${slot.roleLabel} fork failed: ${reason}`);
472
- this.updateForkState();
473
- this.host.scheduleParentUiRender();
474
- });
512
+ }
513
+ else {
514
+ if (question)
515
+ this.pendingQuestions.set(binding, question);
516
+ this.ensureFork(binding, slot);
517
+ }
475
518
  }
476
519
  ensureFork(binding, slot) {
477
- const existing = this.forks.get(binding);
478
- if (existing)
479
- return Promise.resolve(existing);
480
- const pending = this.pending.get(binding);
481
- if (pending)
482
- return pending;
483
- const layout = this.host.getLayout();
484
- const created = Fork.spawn(slot, this.workdir, this.host, layout.columns, layout.childRows, () => this.updateForkState()).then((fork) => {
485
- this.pending.delete(binding);
486
- if (this.disposed) {
487
- fork.kill();
488
- return fork;
520
+ if (this.pending.has(binding))
521
+ return;
522
+ let constructed;
523
+ // The host's coalesced first frame must run before any provider preparation.
524
+ const created = new Promise((resolve) => setImmediate(resolve)).then(async () => {
525
+ this.launchAbort.signal.throwIfAborted();
526
+ const layout = this.host.getLayout();
527
+ return Fork.spawn(slot, this.workdir, this.host, layout.columns, layout.childRows, () => {
528
+ if (!this.disposed && constructed && this.presentedFork(binding) === constructed) {
529
+ this.updateForkState();
530
+ }
531
+ }, this.observerRuns, this.launchAbort.signal, (fork) => {
532
+ constructed = fork;
533
+ if (this.disposed || this.pending.get(binding) !== created) {
534
+ void fork.kill();
535
+ return;
536
+ }
537
+ this.pendingForks.set(binding, fork);
538
+ if (this.shown !== binding)
539
+ return;
540
+ const layout = this.host.getLayout();
541
+ this.configurePresentation(binding, fork, layout.columns, layout.childRows);
542
+ fork.setVisible(true);
543
+ this.updateForkState();
544
+ this.host.scheduleParentUiRender();
545
+ });
546
+ }).then(async (fork) => {
547
+ if (this.disposed || this.pending.get(binding) !== created) {
548
+ await fork.kill();
549
+ return;
489
550
  }
551
+ constructed = fork;
552
+ this.pendingForks.delete(binding);
490
553
  this.forks.set(binding, fork);
491
554
  if (this.tracksTurns) {
492
555
  void this.ensureForkSession(binding, fork);
493
556
  }
494
- return fork;
557
+ if (this.shown === binding) {
558
+ const layout = this.host.getLayout();
559
+ this.configurePresentation(binding, fork, layout.columns, layout.childRows);
560
+ fork.setVisible(true);
561
+ this.updateForkState();
562
+ this.host.scheduleRender();
563
+ }
564
+ const question = this.pendingQuestions.get(binding);
565
+ this.pendingQuestions.delete(binding);
566
+ if (question)
567
+ this.startInquirySend(binding, fork, question);
495
568
  }).catch((err) => {
496
- this.pending.delete(binding);
497
- throw err;
569
+ if (this.disposed && err === this.launchAbort.signal.reason)
570
+ return;
571
+ const reason = err instanceof Error ? err.message : String(err);
572
+ this.host.output.warn(`offscreen ${slot.roleLabel} fork failed: ${reason}`);
573
+ if (this.disposed || this.pending.get(binding) !== created)
574
+ return;
575
+ this.pendingQuestions.delete(binding);
576
+ this.clearSendPending(binding);
577
+ this.pendingForks.delete(binding);
578
+ if (this.shown === binding)
579
+ this.hideFork();
580
+ }).finally(() => {
581
+ if (this.pending.get(binding) === created)
582
+ this.pending.delete(binding);
498
583
  });
499
584
  this.pending.set(binding, created);
500
- return created;
501
585
  }
502
586
  startInquirySend(binding, fork, question) {
503
587
  if (this.disposed || this.sendControllers.has(binding))
@@ -508,12 +592,22 @@ export class OffscreenController {
508
592
  this.sendLocks.add(binding);
509
593
  this.host.scheduleParentUiRender();
510
594
  const composer = resolveProvider(fork.driver).composer;
595
+ let receipt;
511
596
  void sendInquiryQuestion(fork, question, {
512
597
  signal: controller.signal,
513
598
  composer,
599
+ beforeSubmit: async () => {
600
+ await this.turnMonitors.get(binding)?.refreshExpiredAttempts();
601
+ controller.signal.throwIfAborted();
602
+ this.launchAbort.signal.throwIfAborted();
603
+ receipt = this.reserveTurn(binding);
604
+ },
605
+ onSubmitWritten: () => receipt?.markWritten(),
514
606
  onSubmitted: () => {
515
607
  submitted = true;
516
- this.beginTurn(binding);
608
+ receipt?.confirm();
609
+ if (!this.disposed && this.sendControllers.get(binding) === controller)
610
+ this.clearSendPending(binding);
517
611
  },
518
612
  }).catch((err) => {
519
613
  if (!controller.signal.aborted) {
@@ -521,9 +615,20 @@ export class OffscreenController {
521
615
  this.host.output.warn(`offscreen inquiry send failed: ${reason}`);
522
616
  }
523
617
  }).finally(() => {
524
- if (this.sendControllers.get(binding) === controller) {
525
- this.sendControllers.delete(binding);
618
+ receipt?.release();
619
+ if (this.disposed)
620
+ return;
621
+ if (receipt?.settlement === "released") {
622
+ const remaining = (this.pendingTurns.get(binding) ?? []).filter((pending) => pending !== receipt);
623
+ if (remaining.length > 0)
624
+ this.pendingTurns.set(binding, remaining);
625
+ else
626
+ this.pendingTurns.delete(binding);
627
+ this.refreshStagedActivity(binding);
526
628
  }
629
+ if (this.sendControllers.get(binding) !== controller)
630
+ return;
631
+ this.sendControllers.delete(binding);
527
632
  this.sendLocks.delete(binding);
528
633
  if (!submitted)
529
634
  this.clearSendPending(binding);
@@ -539,44 +644,73 @@ export class OffscreenController {
539
644
  clearSendPending(binding) {
540
645
  if (!this.sendPending.delete(binding))
541
646
  return;
542
- this.host.scheduleParentUiRender();
647
+ if (!this.disposed)
648
+ this.host.scheduleParentUiRender();
543
649
  }
544
- beginTurn(binding) {
650
+ reserveTurn(binding, rawAttempt = false) {
545
651
  const pendingChanged = this.sendPending.delete(binding);
546
652
  const turnChanged = this.prepareTurn(binding);
547
653
  const monitor = this.turnMonitors.get(binding);
548
- if (monitor)
549
- monitor.beginTurn();
654
+ let receipt;
655
+ if (monitor) {
656
+ receipt = rawAttempt ? monitor.beginTurnAttempt() : monitor.reserveTurn();
657
+ }
550
658
  else {
551
- this.pendingTurnGenerations.set(binding, (this.pendingTurnGenerations.get(binding) ?? 0) + 1);
659
+ receipt = new SessionTurnReceipt({
660
+ anchorSubmitCount: this.forkSubmitFloors.get(binding),
661
+ }, rawAttempt);
662
+ if (rawAttempt)
663
+ receipt.markWritten();
664
+ const pending = this.pendingTurns.get(binding) ?? [];
665
+ pending.push(receipt);
666
+ this.pendingTurns.set(binding, pending);
667
+ if (rawAttempt) {
668
+ this.pendingAttemptTimers.set(receipt.id, setTimeout(() => {
669
+ this.pendingAttemptTimers.delete(receipt.id);
670
+ if (this.disposed)
671
+ return;
672
+ receipt.settle("expired");
673
+ this.refreshStagedActivity(binding);
674
+ }, Math.max(0, receipt.startedAt + this.turnAttemptTimeoutMs - Date.now())));
675
+ }
552
676
  this.ensureTurnPoll(binding);
553
677
  }
554
678
  if (pendingChanged || turnChanged)
555
679
  this.host.scheduleParentUiRender();
680
+ return receipt;
556
681
  }
557
682
  beginTurnAttempt(binding) {
558
- const turnChanged = this.prepareTurn(binding);
559
- const monitor = this.turnMonitors.get(binding);
560
- if (monitor)
561
- monitor.beginTurnAttempt();
562
- else {
563
- const attempts = this.pendingTurnAttempts.get(binding) ?? [];
564
- attempts.push({
565
- startedAt: Date.now(),
566
- anchorSubmitCount: this.forkSubmitFloors.get(binding),
567
- });
568
- this.pendingTurnAttempts.set(binding, attempts);
569
- this.ensureTurnPoll(binding);
683
+ this.reserveTurn(binding, true);
684
+ }
685
+ refreshStagedActivity(binding) {
686
+ const pending = this.pendingTurns.get(binding) ?? [];
687
+ if (pending.some((receipt) => !receipt.settlement)
688
+ || this.turnMonitors.get(binding)?.isIdle === false)
689
+ return;
690
+ if (pending.length === 0) {
691
+ this.releaseTurn(binding);
570
692
  }
571
- if (turnChanged)
693
+ else if (this.turnInProgress.delete(binding)) {
694
+ // Retain expired receipts until discovery so late genuine work is still observed.
572
695
  this.host.scheduleParentUiRender();
696
+ }
697
+ }
698
+ needsTurnObservation(binding) {
699
+ return this.turnInProgress.has(binding) || this.pendingTurns.has(binding);
700
+ }
701
+ clearStagedAttemptTimers(binding) {
702
+ for (const receipt of this.pendingTurns.get(binding) ?? []) {
703
+ clearTimeout(this.pendingAttemptTimers.get(receipt.id));
704
+ this.pendingAttemptTimers.delete(receipt.id);
705
+ }
573
706
  }
574
707
  prepareTurn(binding) {
575
708
  const turnChanged = !this.turnInProgress.has(binding);
576
709
  this.turnInProgress.add(binding);
577
710
  const fork = this.forks.get(binding);
578
- if (!this.forkSessionFiles.has(binding)
579
- || (fork && this.needsSubmitFloor(binding, fork))) {
711
+ if (!this.turnMonitors.has(binding)
712
+ && (!this.forkSessionFiles.has(binding)
713
+ || (fork && this.needsSubmitFloor(binding, fork)))) {
580
714
  if (fork) {
581
715
  this.forkLocateAborts.get(binding)?.abort();
582
716
  this.forkSession.delete(binding);
@@ -587,8 +721,8 @@ export class OffscreenController {
587
721
  }
588
722
  releaseTurn(binding, lines) {
589
723
  const changed = this.turnInProgress.delete(binding);
590
- this.pendingTurnGenerations.delete(binding);
591
- this.pendingTurnAttempts.delete(binding);
724
+ this.clearStagedAttemptTimers(binding);
725
+ this.pendingTurns.delete(binding);
592
726
  this.clearTurnPoll(binding);
593
727
  this.forkLocateWarned.delete(binding);
594
728
  if (lines)
@@ -605,6 +739,17 @@ export class OffscreenController {
605
739
  const abort = new AbortController();
606
740
  this.forkLocateAborts.set(binding, abort);
607
741
  const promise = (async () => {
742
+ const liveMonitor = fork.createTurnMonitor?.(this.forkMonitorOptions(binding));
743
+ if (liveMonitor) {
744
+ this.installForkTurnMonitor(binding, liveMonitor);
745
+ return;
746
+ }
747
+ const policy = resolveProvider(fork.driver).forkSubmitFloor;
748
+ if (!policy) {
749
+ if (this.turnInProgress.has(binding))
750
+ this.warnForkSessionUnavailable(binding);
751
+ return;
752
+ }
608
753
  await this.captureForkSubmitFloor(binding, fork, abort.signal);
609
754
  if (this.disposed || abort.signal.aborted)
610
755
  return;
@@ -617,10 +762,10 @@ export class OffscreenController {
617
762
  return;
618
763
  }
619
764
  this.forkSessionFiles.set(binding, file);
620
- if (!this.turnInProgress.has(binding)) {
765
+ if (!this.needsTurnObservation(binding)) {
621
766
  try {
622
- const lines = await readSessionFile(file);
623
- if (this.disposed || abort.signal.aborted || this.turnInProgress.has(binding))
767
+ const lines = await readTurnHistory(fork.driver, file);
768
+ if (this.disposed || abort.signal.aborted || this.needsTurnObservation(binding))
624
769
  return;
625
770
  this.refreshIdleBaseline(binding, lines);
626
771
  }
@@ -629,29 +774,31 @@ export class OffscreenController {
629
774
  }
630
775
  }
631
776
  else {
632
- const policy = resolveProvider(fork.driver).forkSubmitFloor;
633
- if (policy.from !== "fork-file"
634
- || this.forkSubmitFloors.has(binding)) {
635
- if (this.turnInProgress.has(binding))
636
- this.ensureTurnPoll(binding);
637
- return;
638
- }
639
- try {
640
- const lines = await readSessionFile(file);
641
- if (this.disposed || abort.signal.aborted || !this.turnInProgress.has(binding))
777
+ while (!this.disposed && !abort.signal.aborted && this.needsTurnObservation(binding)) {
778
+ let lines;
779
+ try {
780
+ lines = await readTurnHistory(fork.driver, file);
781
+ }
782
+ catch {
783
+ if (this.turnInProgress.has(binding))
784
+ this.warnForkSessionUnavailable(binding);
785
+ await waitForRetry(this.turnPollMs, abort.signal);
786
+ continue;
787
+ }
788
+ if (this.disposed || abort.signal.aborted || !this.needsTurnObservation(binding))
642
789
  return;
643
- const providerFloor = forkFileSubmitFloor(fork.driver, fork.lineage, lines);
644
- this.forkSubmitFloors.set(binding, providerFloor !== undefined
645
- ? 0
646
- :
647
- Math.max(0, submitCount(fork.driver, lines) + policy.offset));
648
- this.forkBaselines.set(binding, 0);
649
- }
650
- catch {
790
+ if (policy.from === "fork-file" && !this.forkSubmitFloors.has(binding)) {
791
+ const providerFloor = forkFileSubmitFloor(fork.driver, fork.lineage, lines);
792
+ this.forkSubmitFloors.set(binding, providerFloor !== undefined
793
+ ? 0
794
+ : Math.max(0, submitCount(fork.driver, lines) + policy.offset));
795
+ this.forkBaselines.set(binding, 0);
796
+ }
797
+ this.ensureTurnPoll(binding, lines);
651
798
  return;
652
799
  }
653
800
  }
654
- if (this.turnInProgress.has(binding))
801
+ if (this.needsTurnObservation(binding))
655
802
  this.ensureTurnPoll(binding);
656
803
  })().catch(() => {
657
804
  // Fail closed: leave the binding busy when session discovery/read fails.
@@ -664,7 +811,8 @@ export class OffscreenController {
664
811
  return promise;
665
812
  }
666
813
  async captureForkSubmitFloor(binding, fork, signal) {
667
- if (resolveProvider(fork.driver).forkSubmitFloor.from !== "source")
814
+ const policy = resolveProvider(fork.driver).forkSubmitFloor;
815
+ if (!policy || policy.from !== "source")
668
816
  return;
669
817
  if (this.forkSubmitFloors.has(binding))
670
818
  return;
@@ -682,16 +830,17 @@ export class OffscreenController {
682
830
  const promise = (async () => {
683
831
  while (!this.disposed && !signal.aborted && slot) {
684
832
  try {
685
- const lines = await readSessionFile(slot.sourceSessionPath);
833
+ const lines = await readTurnHistory(fork.driver, slot.sourceSessionPath);
686
834
  if (!this.disposed && !signal.aborted) {
687
835
  this.forkSubmitFloors.set(binding, submitCount(fork.driver, lines));
688
836
  }
689
837
  return;
690
838
  }
691
839
  catch {
692
- if (!this.turnInProgress.has(binding))
840
+ if (!this.needsTurnObservation(binding))
693
841
  return;
694
- this.warnForkSessionUnavailable(binding);
842
+ if (this.turnInProgress.has(binding))
843
+ this.warnForkSessionUnavailable(binding);
695
844
  await waitForRetry(this.turnPollMs, signal);
696
845
  }
697
846
  }
@@ -706,8 +855,8 @@ export class OffscreenController {
706
855
  refreshIdleBaseline(binding, lines) {
707
856
  this.forkBaselines.set(binding, lines.length);
708
857
  }
709
- ensureTurnPoll(binding) {
710
- if (!this.turnInProgress.has(binding))
858
+ ensureTurnPoll(binding, initialLines) {
859
+ if (!this.needsTurnObservation(binding))
711
860
  return;
712
861
  if (this.turnMonitors.has(binding))
713
862
  return;
@@ -727,6 +876,8 @@ export class OffscreenController {
727
876
  void this.ensureForkSession(binding, fork);
728
877
  return;
729
878
  }
879
+ if (!resolveProvider(fork.driver).forkSubmitFloor)
880
+ return;
730
881
  const providerFork = resolveProvider(fork.driver).fork;
731
882
  const effectiveSubmitCount = providerFork.forkFileSubmitFloor
732
883
  ? (lines) => Math.max(0, submitCount(fork.driver, lines) -
@@ -738,6 +889,14 @@ export class OffscreenController {
738
889
  submitFloor: this.forkSubmitFloors.get(binding),
739
890
  submitCount: effectiveSubmitCount,
740
891
  readResult: readSessionFileWithStatus,
892
+ ...this.forkMonitorOptions(binding),
893
+ });
894
+ this.installForkTurnMonitor(binding, monitor);
895
+ }
896
+ forkMonitorOptions(binding) {
897
+ return {
898
+ pollMs: this.turnPollMs,
899
+ attemptTimeoutMs: this.turnAttemptTimeoutMs,
741
900
  onReadFailure: () => this.warnForkSessionUnavailable(binding),
742
901
  onHistorySnapshot: (snapshot) => {
743
902
  this.forkBaselines.set(binding, snapshot.lineCount);
@@ -747,35 +906,43 @@ export class OffscreenController {
747
906
  this.host.scheduleParentUiRender();
748
907
  },
749
908
  onChange: (idle) => {
750
- if (!idle)
909
+ if (!idle) {
910
+ if (!this.turnInProgress.has(binding)) {
911
+ this.turnInProgress.add(binding);
912
+ this.host.scheduleParentUiRender();
913
+ }
751
914
  return;
752
- this.forkBaselines.set(binding, monitor.lineBaseline);
753
- const submitFloor = monitor.currentSubmitFloor;
754
- if (submitFloor !== undefined) {
755
- this.forkSubmitFloors.set(binding, submitFloor);
915
+ }
916
+ const monitor = this.turnMonitors.get(binding);
917
+ if (monitor) {
918
+ this.forkBaselines.set(binding, monitor.lineBaseline);
919
+ const submitFloor = monitor.currentSubmitFloor;
920
+ if (submitFloor !== undefined) {
921
+ this.forkSubmitFloors.set(binding, submitFloor);
922
+ }
756
923
  }
757
924
  this.releaseTurn(binding);
758
925
  },
759
- });
926
+ };
927
+ }
928
+ installForkTurnMonitor(binding, monitor, initialLines) {
760
929
  this.turnMonitors.set(binding, monitor);
761
- const generations = this.pendingTurnGenerations.get(binding) ?? 0;
762
- for (let generation = 0; generation < generations; generation++) {
763
- monitor.beginTurn();
764
- }
765
- const attempts = this.pendingTurnAttempts.get(binding) ?? [];
766
- for (const attempt of attempts) {
767
- monitor.beginTurnAttempt(attempt);
768
- }
769
- this.pendingTurnGenerations.delete(binding);
770
- this.pendingTurnAttempts.delete(binding);
930
+ const pending = this.pendingTurns.get(binding) ?? [];
931
+ this.clearStagedAttemptTimers(binding);
932
+ this.pendingTurns.delete(binding);
933
+ monitor.attachTurns(pending, initialLines);
771
934
  }
772
- needsSubmitFloor(binding, _fork) {
935
+ needsSubmitFloor(binding, fork) {
936
+ if (!resolveProvider(fork.driver).forkSubmitFloor)
937
+ return false;
773
938
  return !this.forkSubmitFloors.has(binding);
774
939
  }
775
940
  clearTurnPoll(binding) {
776
941
  const monitor = this.turnMonitors.get(binding);
777
942
  if (monitor) {
778
- if (monitor.supportsHistoryRegression)
943
+ if (monitor.supportsHistoryRegression
944
+ || monitor.hasUnobservedExpiredAttempts
945
+ || monitor.classifyDeliverySince)
779
946
  return;
780
947
  monitor.dispose();
781
948
  this.turnMonitors.delete(binding);
@@ -789,8 +956,10 @@ export class OffscreenController {
789
956
  monitor.dispose();
790
957
  this.turnMonitors.clear();
791
958
  this.turnInProgress.clear();
792
- this.pendingTurnGenerations.clear();
793
- this.pendingTurnAttempts.clear();
959
+ for (const timer of this.pendingAttemptTimers.values())
960
+ clearTimeout(timer);
961
+ this.pendingAttemptTimers.clear();
962
+ this.pendingTurns.clear();
794
963
  this.forkSession.clear();
795
964
  this.forkSessionFiles.clear();
796
965
  this.forkBaselines.clear();
@@ -806,6 +975,7 @@ export class OffscreenController {
806
975
  this.host.output.warn(`offscreen inquiry fork session unavailable for ${binding}; inquiry remains busy`);
807
976
  }
808
977
  abortIssueSend(binding) {
978
+ this.pendingQuestions.delete(binding);
809
979
  const controller = this.sendControllers.get(binding);
810
980
  if (controller) {
811
981
  controller.abort(new Error("Inquiry send aborted"));
@@ -840,7 +1010,7 @@ export class OffscreenController {
840
1010
  }
841
1011
  updateVisibleMouseModes() {
842
1012
  const fork = this.shown ? this.forks.get(this.shown) : undefined;
843
- if (!fork) {
1013
+ if (!this.shown) {
844
1014
  this.host.clearMouseModeSource(OFFSCREEN_MOUSE_SOURCE);
845
1015
  return;
846
1016
  }
@@ -852,7 +1022,7 @@ export class OffscreenController {
852
1022
  this.updateVisibleMouseModes();
853
1023
  }
854
1024
  visibleMouseModes(fork) {
855
- const modes = fork.mouseModes();
1025
+ const modes = fork?.mouseModes() ?? [];
856
1026
  const coordinator = this.activeCoordinator();
857
1027
  if (!coordinator)
858
1028
  return modes;
@@ -884,6 +1054,13 @@ function inquiryShortcutForKey(key) {
884
1054
  function waitForRetry(ms, signal) {
885
1055
  return abortableSleep(ms, signal, { onAbort: "resolve" });
886
1056
  }
1057
+ async function readTurnHistory(driver, path) {
1058
+ const { lines, malformed } = await readSessionFileWithStatus(path);
1059
+ if (malformed || resolveProvider(driver).fork.submissionEvidence?.(lines).indeterminate) {
1060
+ throw new Error("Inquiry submission history is indeterminate");
1061
+ }
1062
+ return lines;
1063
+ }
887
1064
  function isSubmitKey(key) {
888
1065
  return key.kind === "enter"
889
1066
  || key.kind === "shift-enter"