poly-weaver 0.15.2 → 0.15.3

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 (55) hide show
  1. package/dist/crosscheck/coordinator.d.ts.map +1 -1
  2. package/dist/crosscheck/coordinator.js +43 -9
  3. package/dist/crosscheck/coordinator.js.map +1 -1
  4. package/dist/crosscheck/live-agent-session.d.ts +3 -0
  5. package/dist/crosscheck/live-agent-session.d.ts.map +1 -1
  6. package/dist/crosscheck/live-agent-session.js +9 -2
  7. package/dist/crosscheck/live-agent-session.js.map +1 -1
  8. package/dist/offscreen/controller.d.ts +6 -3
  9. package/dist/offscreen/controller.d.ts.map +1 -1
  10. package/dist/offscreen/controller.js +127 -71
  11. package/dist/offscreen/controller.js.map +1 -1
  12. package/dist/offscreen/inquiry-sender.d.ts +1 -0
  13. package/dist/offscreen/inquiry-sender.d.ts.map +1 -1
  14. package/dist/offscreen/inquiry-sender.js +1 -0
  15. package/dist/offscreen/inquiry-sender.js.map +1 -1
  16. package/dist/offscreen/session-turn-monitor.d.ts +31 -32
  17. package/dist/offscreen/session-turn-monitor.d.ts.map +1 -1
  18. package/dist/offscreen/session-turn-monitor.js +318 -280
  19. package/dist/offscreen/session-turn-monitor.js.map +1 -1
  20. package/dist/offscreen/session-turn-receipt.d.ts +28 -0
  21. package/dist/offscreen/session-turn-receipt.d.ts.map +1 -0
  22. package/dist/offscreen/session-turn-receipt.js +53 -0
  23. package/dist/offscreen/session-turn-receipt.js.map +1 -0
  24. package/dist/providers/base-completion.d.ts +1 -0
  25. package/dist/providers/base-completion.d.ts.map +1 -1
  26. package/dist/providers/base-completion.js +4 -2
  27. package/dist/providers/base-completion.js.map +1 -1
  28. package/dist/providers/codex/completion.d.ts +4 -2
  29. package/dist/providers/codex/completion.d.ts.map +1 -1
  30. package/dist/providers/codex/completion.js +99 -14
  31. package/dist/providers/codex/completion.js.map +1 -1
  32. package/dist/providers/codex/strategy.d.ts.map +1 -1
  33. package/dist/providers/codex/strategy.js +3 -2
  34. package/dist/providers/codex/strategy.js.map +1 -1
  35. package/dist/providers/copilot/completion.js +10 -1
  36. package/dist/providers/copilot/completion.js.map +1 -1
  37. package/dist/providers/copilot/strategy.d.ts +1 -0
  38. package/dist/providers/copilot/strategy.d.ts.map +1 -1
  39. package/dist/providers/copilot/strategy.js +14 -1
  40. package/dist/providers/copilot/strategy.js.map +1 -1
  41. package/dist/providers/fork.d.ts +16 -0
  42. package/dist/providers/fork.d.ts.map +1 -1
  43. package/dist/providers/fork.js.map +1 -1
  44. package/dist/providers/live.d.ts.map +1 -1
  45. package/dist/providers/live.js +19 -0
  46. package/dist/providers/live.js.map +1 -1
  47. package/dist/providers/session-launch.d.ts +2 -0
  48. package/dist/providers/session-launch.d.ts.map +1 -1
  49. package/dist/providers/session-launch.js +9 -2
  50. package/dist/providers/session-launch.js.map +1 -1
  51. package/dist/terminal/paste-submit.d.ts +1 -0
  52. package/dist/terminal/paste-submit.d.ts.map +1 -1
  53. package/dist/terminal/paste-submit.js +7 -0
  54. package/dist/terminal/paste-submit.js.map +1 -1
  55. package/package.json +1 -1
@@ -1,10 +1,9 @@
1
1
  import { resolveProvider } from "../providers/registry.js";
2
2
  import { readSessionFileWithStatus, } from "../session/reader.js";
3
- const DEFAULT_ATTEMPT_TIMEOUT_MS = 5_000;
4
- /**
5
- * Tracks one provider session from a settled line baseline to the completion
6
- * of each subsequently submitted turn. Read failures fail closed as busy.
7
- */
3
+ import { SessionTurnReceipt } from "./session-turn-receipt.js";
4
+ export { SessionTurnReceipt } from "./session-turn-receipt.js";
5
+ export const DEFAULT_TURN_ATTEMPT_TIMEOUT_MS = 5_000;
6
+ /** Ordered request ownership; provider observations remain work even without a host receipt. */
8
7
  export class SessionTurnMonitor {
9
8
  driver;
10
9
  sessionFilePath;
@@ -12,136 +11,172 @@ export class SessionTurnMonitor {
12
11
  baseline;
13
12
  busy = false;
14
13
  timer;
15
- polling = false;
16
14
  activePoll;
17
15
  stopped = false;
18
- turnGeneration = 0;
19
- mappedGeneration = 0;
20
- coveredGeneration = 0;
21
- nextReservationSequence = 0;
22
- pendingConfirmedTurns = [];
23
- pendingTurnAttempts = [];
24
- unreservedObservedGenerations = [];
25
- pendingAttemptWaiters = new Set();
16
+ attachingBatch = false;
17
+ ownershipWarned = false;
18
+ entries = [];
19
+ attached = new Set();
20
+ attemptWaiters = new Set();
26
21
  observedSubmitCount;
27
22
  pollMs;
28
23
  attemptTimeoutMs;
29
24
  submitFloor;
30
25
  historySnapshot;
31
- mappedSubmitIdentities = [];
32
26
  constructor(driver, sessionFilePath, baseline, options = {}) {
33
27
  this.driver = driver;
34
28
  this.sessionFilePath = sessionFilePath;
35
29
  this.options = options;
36
30
  this.baseline = Math.max(0, baseline);
37
31
  this.pollMs = options.pollMs ?? 500;
38
- this.attemptTimeoutMs =
39
- options.attemptTimeoutMs ?? DEFAULT_ATTEMPT_TIMEOUT_MS;
32
+ this.attemptTimeoutMs = options.attemptTimeoutMs ?? DEFAULT_TURN_ATTEMPT_TIMEOUT_MS;
40
33
  this.submitFloor = options.submitFloor;
41
34
  this.historySnapshot = options.initialHistorySnapshot;
42
35
  }
43
- get isIdle() {
44
- return !this.busy;
45
- }
46
- get lineBaseline() {
47
- return this.baseline;
48
- }
49
- get currentSubmitFloor() {
50
- return this.submitFloor;
36
+ get isIdle() { return !this.busy; }
37
+ get lineBaseline() { return this.baseline; }
38
+ get currentSubmitFloor() { return this.submitFloor; }
39
+ get hasUnobservedExpiredAttempts() {
40
+ return this.entries.some((entry) => entry.ordinal === undefined
41
+ && entry.receipt?.settlement === "expired");
51
42
  }
52
43
  get supportsHistoryRegression() {
53
44
  return resolveProvider(this.driver).fork.sessionHistorySnapshot !== undefined;
54
45
  }
55
- expectedSubmitCount(lines) {
56
- const snapshot = this.reconcileHistory(lines);
57
- this.observeSubmissions(lines, undefined, snapshot);
58
- return (this.observedSubmitCount ?? 0)
59
- + this.pendingConfirmedTurns.length
60
- + this.pendingTurnAttempts.length;
46
+ reserveTurn(boundary = {}) {
47
+ const receipt = new SessionTurnReceipt({
48
+ ...boundary,
49
+ anchorSubmitCount: boundary.anchorSubmitCount ?? this.observedSubmitCount ?? this.submitFloor,
50
+ });
51
+ this.attachTurn(receipt);
52
+ return receipt;
61
53
  }
62
- startHistoryPolling() {
63
- if (this.stopped || !this.supportsHistoryRegression)
64
- return;
65
- this.ensurePolling();
54
+ async reserveTurnFromSession() {
55
+ await this.expectedSubmitCountFromSession();
56
+ return this.reserveTurn();
66
57
  }
67
- waitForPendingTurnAttempts() {
68
- if (this.pendingTurnAttempts.length === 0 || this.stopped) {
69
- return Promise.resolve();
58
+ /** Attach the same pre-input receipt after discovery, never an anonymous later registration. */
59
+ attachTurns(receipts, initialLines) {
60
+ this.attachingBatch = true;
61
+ try {
62
+ for (const receipt of [...receipts].sort((a, b) => a.sequence - b.sequence))
63
+ this.attachTurn(receipt);
64
+ if (initialLines)
65
+ this.expectedSubmitCount(initialLines);
70
66
  }
71
- return new Promise((resolve) => {
72
- this.pendingAttemptWaiters.add(resolve);
73
- });
67
+ finally {
68
+ this.attachingBatch = false;
69
+ }
70
+ this.updateActivity(true);
71
+ if (this.busy || this.hasUnobservedExpiredAttempts)
72
+ this.ensurePolling();
74
73
  }
75
- beginTurn() {
76
- if (this.stopped)
74
+ attachTurn(receipt) {
75
+ if (this.stopped || this.attached.has(receipt.id))
77
76
  return;
78
- if (this.adoptUnreservedObservedSubmit())
77
+ this.attached.add(receipt.id);
78
+ if (receipt.settlement && receipt.settlement !== "expired")
79
79
  return;
80
- this.turnGeneration++;
81
- this.pendingConfirmedTurns.push({
82
- sequence: this.nextReservationSequence++,
83
- anchorSubmitCount: this.observedSubmitCount ?? this.submitFloor,
80
+ const anchor = receipt.anchorSubmitCount ?? this.observedSubmitCount ?? this.submitFloor;
81
+ const observed = anchor === undefined ? undefined : this.entries.find((entry) => !entry.receipt && entry.ordinal !== undefined && entry.ordinal > anchor);
82
+ const entry = observed ?? { anchor };
83
+ entry.receipt = receipt;
84
+ if (!observed)
85
+ this.entries.push(entry);
86
+ entry.unsubscribe = receipt.onChange(() => {
87
+ if (this.stopped)
88
+ return;
89
+ if (receipt.settlement === "released" && entry.ordinal === undefined) {
90
+ this.removeEntry(entry);
91
+ this.updateActivity();
92
+ }
84
93
  });
85
- if (!this.busy) {
86
- this.busy = true;
87
- this.options.onChange?.(false);
94
+ if (receipt.rawAttempt && !receipt.settlement && entry.ordinal === undefined) {
95
+ const remainingMs = receipt.startedAt + this.attemptTimeoutMs - Date.now();
96
+ if (remainingMs <= 0) {
97
+ receipt.settle("expired");
98
+ }
99
+ else {
100
+ entry.timer = setTimeout(() => {
101
+ if (this.stopped || entry.ordinal !== undefined)
102
+ return;
103
+ entry.timer = undefined;
104
+ // Expiry releases speculative busy state, not its place ahead of later input.
105
+ receipt.settle("expired");
106
+ this.updateActivity();
107
+ }, remainingMs);
108
+ }
88
109
  }
89
- this.ensurePolling();
110
+ this.updateActivity(receipt.settlement === "expired");
111
+ if (entry.settlement)
112
+ receipt.settle(entry.settlement);
113
+ if (!this.attachingBatch && (this.busy || this.hasUnobservedExpiredAttempts))
114
+ this.ensurePolling();
115
+ }
116
+ beginTurn(receipt = this.reserveTurn()) {
117
+ this.attachTurn(receipt);
118
+ receipt.confirm();
119
+ return receipt;
120
+ }
121
+ beginTurnAttempt(boundary = {}) {
122
+ const receipt = new SessionTurnReceipt({
123
+ ...boundary,
124
+ anchorSubmitCount: boundary.anchorSubmitCount ?? this.observedSubmitCount ?? this.submitFloor,
125
+ }, true);
126
+ receipt.markWritten();
127
+ this.attachTurn(receipt);
128
+ return receipt;
90
129
  }
91
- beginTurnAttempt(snapshot = {}) {
130
+ expectedSubmitCount(lines) {
92
131
  if (this.stopped)
93
- return;
94
- if (this.adoptUnreservedObservedSubmit())
95
- return;
96
- const startedAt = snapshot.startedAt ?? Date.now();
97
- const expired = Date.now() - startedAt >= this.attemptTimeoutMs;
98
- const attempt = {
99
- sequence: this.nextReservationSequence++,
100
- anchorSubmitCount: snapshot.anchorSubmitCount
101
- ?? this.observedSubmitCount
102
- ?? this.submitFloor,
103
- expired,
104
- };
105
- if (attempt.expired) {
106
- this.releaseIfSettled({ notifyIfAlreadyIdle: true });
107
- return;
108
- }
109
- attempt.timer = setTimeout(() => {
110
- if (this.stopped || !this.pendingTurnAttempts.includes(attempt))
111
- return;
112
- attempt.expired = true;
113
- this.discardExpiredAttempts();
114
- this.releaseIfSettled();
115
- }, Math.max(0, startedAt + this.attemptTimeoutMs - Date.now()));
116
- this.pendingTurnAttempts.push(attempt);
117
- if (!this.busy) {
118
- this.busy = true;
119
- this.options.onChange?.(false);
120
- }
121
- this.ensurePolling();
132
+ throw new Error("Session turn monitor is shut down");
133
+ const evidence = this.validateEvidence(lines);
134
+ const snapshot = this.reconcileHistory(lines);
135
+ this.observeSubmissions(lines, snapshot, evidence);
136
+ if (!this.attachingBatch && (this.busy || this.hasUnobservedExpiredAttempts))
137
+ this.ensurePolling();
138
+ return (this.observedSubmitCount ?? 0)
139
+ + this.entries.filter((entry) => entry.ordinal === undefined && !entry.receipt?.settlement).length;
140
+ }
141
+ startHistoryPolling() {
142
+ if (!this.stopped && this.supportsHistoryRegression)
143
+ this.ensurePolling();
144
+ }
145
+ waitForPendingTurnAttempts() {
146
+ if (!this.hasPendingAttempts() || this.stopped)
147
+ return Promise.resolve();
148
+ return new Promise((resolve) => this.attemptWaiters.add(resolve));
122
149
  }
123
150
  async refreshIdleBaseline() {
124
- const result = await this.readLineResult();
125
- if (result.malformed)
126
- throw new Error("Session log contains malformed JSONL");
127
- this.reconcileHistory(result.lines);
128
- this.baseline = result.lines.length;
151
+ const result = await this.readValidResult();
152
+ this.expectedSubmitCount(result.lines);
153
+ if (this.isIdle)
154
+ this.baseline = result.lines.length;
155
+ this.retireNonSubmittingAttempts(result);
129
156
  }
130
157
  async expectedSubmitCountFromSession() {
131
- const result = await this.readLineResult();
132
- if (result.malformed)
133
- throw new Error("Session log contains malformed JSONL");
134
- return this.expectedSubmitCount(result.lines);
158
+ const result = await this.readValidResult();
159
+ const count = this.expectedSubmitCount(result.lines);
160
+ this.retireNonSubmittingAttempts(result);
161
+ return count;
162
+ }
163
+ async refreshExpiredAttempts() {
164
+ if (!this.hasUnobservedExpiredAttempts)
165
+ return;
166
+ await this.activePoll;
167
+ if (this.hasUnobservedExpiredAttempts)
168
+ await this.pollNow();
135
169
  }
136
170
  dispose() {
137
171
  if (this.timer)
138
172
  clearInterval(this.timer);
139
173
  this.timer = undefined;
140
- for (const attempt of this.pendingTurnAttempts) {
141
- if (attempt.timer)
142
- clearTimeout(attempt.timer);
174
+ for (const entry of this.entries) {
175
+ if (entry.timer)
176
+ clearTimeout(entry.timer);
177
+ entry.unsubscribe?.();
143
178
  }
144
- this.resolvePendingAttemptWaiters();
179
+ this.resolveAttemptWaiters();
145
180
  }
146
181
  async shutdown() {
147
182
  this.stopped = true;
@@ -149,234 +184,239 @@ export class SessionTurnMonitor {
149
184
  await this.activePoll;
150
185
  }
151
186
  pollNow() {
152
- if ((!this.busy && !this.supportsHistoryRegression) || this.stopped) {
187
+ if ((!this.busy && !this.supportsHistoryRegression && !this.hasUnobservedExpiredAttempts) || this.stopped) {
153
188
  return Promise.resolve();
154
189
  }
155
- if (this.polling)
156
- return this.activePoll ?? Promise.resolve();
157
- this.polling = true;
158
- let active;
159
- active = this.pollOnce()
160
- .catch((err) => {
190
+ if (this.activePoll)
191
+ return this.activePoll;
192
+ const active = this.pollOnce()
193
+ .catch((error) => {
161
194
  if (!this.stopped)
162
- this.options.onReadFailure?.(err);
195
+ this.options.onReadFailure?.(error);
163
196
  })
164
197
  .finally(() => {
165
198
  if (this.activePoll === active)
166
199
  this.activePoll = undefined;
167
- this.polling = false;
168
200
  });
169
201
  this.activePoll = active;
170
202
  return active;
171
203
  }
172
204
  async pollOnce() {
173
- const result = await this.readLineResult();
174
- if (result.malformed)
175
- throw new Error("Session log contains malformed JSONL");
176
- const lines = result.lines;
205
+ const result = await this.readValidResult();
206
+ const { lines } = result;
177
207
  if (this.stopped)
178
208
  return;
179
- const provider = resolveProvider(this.driver);
180
- const snapshot = this.reconcileHistory(lines);
181
- const priorSubmitCount = this.submitFloor
182
- ?? this.countSubmits(lines.slice(0, this.baseline));
183
- const currentSubmitCount = this.observeSubmissions(lines, priorSubmitCount, snapshot);
184
- this.discardExpiredAttempts();
185
- const unsettledSubmits = currentSubmitCount - priorSubmitCount;
186
- if (unsettledSubmits > 0) {
187
- const tail = lines.slice(this.baseline);
188
- const latestSubmit = provider.fork.latestSubmitIndex(tail);
189
- if (latestSubmit >= 0
190
- && provider.fork.turnComplete(lines, this.baseline + latestSubmit)) {
191
- this.submitFloor = currentSubmitCount;
192
- this.coveredGeneration = Math.min(this.mappedGeneration, this.coveredGeneration + unsettledSubmits);
193
- this.baseline = lines.length;
209
+ const evidence = this.validateEvidence(lines, this.options.pendingEvidencePolicy === "wait");
210
+ if (evidence?.indeterminate) {
211
+ const { prior, assignments } = this.prepareObservation(lines);
212
+ if (this.inspectHistory(lines).regression) {
213
+ throw new Error("Session submission history regressed during pending evidence");
194
214
  }
215
+ // Check projected ownership without committing any part of an incomplete observation.
216
+ this.warnAmbiguousOwnership(this.entries.map((entry) => ({
217
+ ...entry,
218
+ anchor: entry.anchor ?? prior,
219
+ ordinal: entry.ordinal ?? assignments.get(entry),
220
+ })));
221
+ return;
195
222
  }
196
- this.releaseIfSettled();
197
- }
198
- observeSubmissions(lines, priorSubmitCount = this.submitFloor
199
- ?? this.countSubmits(lines.slice(0, this.baseline)), snapshot) {
200
- const currentSubmitCount = this.countSubmits(lines);
201
- const observedSubmitCount = this.observedSubmitCount ?? priorSubmitCount;
202
- for (const attempt of this.pendingTurnAttempts) {
203
- attempt.anchorSubmitCount ??= observedSubmitCount;
204
- }
205
- for (const turn of this.pendingConfirmedTurns) {
206
- turn.anchorSubmitCount ??= observedSubmitCount;
223
+ const snapshot = this.reconcileHistory(lines);
224
+ const current = this.observeSubmissions(lines, snapshot, evidence);
225
+ this.retireNonSubmittingAttempts(result);
226
+ let aggregateComplete = false;
227
+ if (!evidence) {
228
+ const fork = resolveProvider(this.driver).fork;
229
+ const latest = fork.latestSubmitIndex(lines);
230
+ aggregateComplete = latest >= this.baseline && fork.turnComplete(lines, latest);
207
231
  }
208
- this.mapNewSubmits(observedSubmitCount, currentSubmitCount, snapshot);
209
- this.observedSubmitCount = Math.max(observedSubmitCount, currentSubmitCount);
210
- return currentSubmitCount;
211
- }
212
- mapNewSubmits(observedSubmitCount, currentSubmitCount, snapshot) {
213
- for (let submitOrdinal = observedSubmitCount + 1; submitOrdinal <= currentSubmitCount; submitOrdinal++) {
214
- const turnIndex = this.pendingConfirmedTurns.findIndex((turn) => turn.anchorSubmitCount !== undefined
215
- && submitOrdinal > turn.anchorSubmitCount);
216
- const attemptIndex = this.pendingTurnAttempts.findIndex((attempt) => attempt.anchorSubmitCount !== undefined
217
- && submitOrdinal > attempt.anchorSubmitCount);
218
- const turn = turnIndex >= 0
219
- ? this.pendingConfirmedTurns[turnIndex]
220
- : undefined;
221
- const attempt = attemptIndex >= 0
222
- ? this.pendingTurnAttempts[attemptIndex]
223
- : undefined;
224
- if (turn && (!attempt || turn.sequence < attempt.sequence)) {
225
- this.pendingConfirmedTurns.splice(turnIndex, 1);
226
- this.mappedGeneration++;
227
- this.rememberMappedSubmitIdentity(snapshot, submitOrdinal);
228
- continue;
229
- }
230
- if (!attempt) {
231
- this.mapUnreservedObservedSubmit(snapshot, submitOrdinal);
232
+ const offset = evidence ? evidence.submissions.length - current : 0;
233
+ for (const entry of this.entries) {
234
+ if (entry.ordinal === undefined || entry.settlement)
232
235
  continue;
233
- }
234
- this.pendingTurnAttempts.splice(attemptIndex, 1);
235
- if (attempt.timer)
236
- clearTimeout(attempt.timer);
237
- this.turnGeneration++;
238
- this.mappedGeneration++;
239
- this.rememberMappedSubmitIdentity(snapshot, submitOrdinal);
240
- continue;
236
+ entry.settlement = evidence
237
+ ? evidence.submissions[entry.ordinal - 1 + offset]?.settlement
238
+ : aggregateComplete ? "completed" : undefined;
241
239
  }
242
- this.resolvePendingAttemptWaitersIfSettled();
243
- }
244
- mapUnreservedObservedSubmit(snapshot, submitOrdinal) {
245
- this.turnGeneration++;
246
- this.mappedGeneration++;
247
- this.unreservedObservedGenerations.push(this.mappedGeneration);
248
- this.rememberMappedSubmitIdentity(snapshot, submitOrdinal);
249
- if (!this.busy) {
250
- this.busy = true;
251
- this.options.onChange?.(false);
240
+ if (this.entries.some((entry) => entry.ordinal !== undefined)
241
+ && this.entries.every((entry) => entry.ordinal === undefined || entry.settlement)) {
242
+ this.submitFloor = current;
243
+ this.baseline = lines.length;
252
244
  }
253
- }
254
- adoptUnreservedObservedSubmit() {
255
- const generation = this.unreservedObservedGenerations.shift();
256
- if (generation === undefined)
257
- return false;
258
- if (generation > this.coveredGeneration && !this.busy) {
259
- this.busy = true;
260
- this.options.onChange?.(false);
245
+ this.updateActivity();
246
+ if (this.busy)
261
247
  this.ensurePolling();
262
- return true;
248
+ for (const entry of this.entries) {
249
+ if (this.stopped)
250
+ break;
251
+ if (entry.settlement)
252
+ entry.receipt?.settle(entry.settlement);
263
253
  }
264
- this.releaseIfSettled();
265
- return true;
266
254
  }
267
- rememberMappedSubmitIdentity(snapshot, submitOrdinal) {
268
- const identity = snapshot?.submitIdentities[submitOrdinal - 1];
269
- if (identity)
270
- this.mappedSubmitIdentities.push(identity);
255
+ observeSubmissions(lines, snapshot, evidence) {
256
+ const { current, prior, assignments } = this.prepareObservation(lines);
257
+ for (const entry of this.entries)
258
+ entry.anchor ??= prior;
259
+ for (const [entry, ordinal] of assignments) {
260
+ entry.ordinal = ordinal;
261
+ entry.identity = evidence?.submissions[ordinal - 1 + evidence.submissions.length - current]?.identity
262
+ ?? snapshot?.submitIdentities[ordinal - 1 + snapshot.submitCount - current];
263
+ if (entry.timer)
264
+ clearTimeout(entry.timer);
265
+ if (!this.entries.includes(entry))
266
+ this.entries.push(entry);
267
+ }
268
+ this.observedSubmitCount = current;
269
+ this.updateActivity();
270
+ this.warnAmbiguousOwnership();
271
+ return current;
271
272
  }
272
- reconcileHistory(lines) {
273
- const fork = resolveProvider(this.driver).fork;
274
- const buildSnapshot = fork.sessionHistorySnapshot;
275
- if (!buildSnapshot)
276
- return undefined;
277
- const current = buildSnapshot(lines);
278
- if (!current) {
279
- throw new Error("Session history snapshot is unbuildable");
273
+ prepareObservation(lines) {
274
+ const current = this.countSubmits(lines);
275
+ const prior = this.observedSubmitCount ?? this.submitFloor
276
+ ?? this.countSubmits(lines.slice(0, this.baseline));
277
+ if (current < prior || this.baseline > lines.length) {
278
+ throw new Error("Session submission history regressed without a provider history boundary");
280
279
  }
281
- const previous = this.historySnapshot;
282
- if (previous && fork.compareSessionHistory) {
283
- const regression = fork.compareSessionHistory(previous, current);
284
- if (regression) {
285
- this.applyHistoryRegression(regression, current);
286
- this.historySnapshot = current;
287
- this.options.onHistoryRegression?.(regression, current);
288
- return current;
289
- }
280
+ const pending = this.entries
281
+ .filter((entry) => entry.ordinal === undefined)
282
+ .sort((a, b) => a.receipt.sequence - b.receipt.sequence);
283
+ const assignments = new Map();
284
+ for (let ordinal = prior + 1; ordinal <= current; ordinal++) {
285
+ const index = pending.findIndex((entry) => ordinal > (entry.anchor ?? prior));
286
+ const entry = index < 0 ? {} : pending.splice(index, 1)[0];
287
+ assignments.set(entry, ordinal);
290
288
  }
291
- const changed = !sameHistorySnapshot(previous, current);
292
- this.historySnapshot = current;
293
- if (changed)
294
- this.options.onHistorySnapshot?.(current);
295
- return current;
289
+ return { current, prior, assignments };
290
+ }
291
+ warnAmbiguousOwnership(entries = this.entries) {
292
+ const ambiguous = entries.some(({ receipt, ordinal }) => {
293
+ if (receipt?.settlement !== "expired" || ordinal === undefined)
294
+ return false;
295
+ return entries.some((newer) => newer.ordinal === undefined && newer.receipt && !newer.receipt.settlement
296
+ && newer.receipt.sequence > receipt.sequence
297
+ && newer.anchor !== undefined && newer.anchor < ordinal);
298
+ });
299
+ if (ambiguous && !this.ownershipWarned && !this.stopped) {
300
+ this.options.onReadFailure?.(new Error("Session submission ownership is ambiguous after an expired input attempt"));
301
+ }
302
+ this.ownershipWarned = ambiguous;
296
303
  }
297
- applyHistoryRegression(regression, snapshot) {
298
- const currentIdentities = new Set(snapshot.submitIdentities);
299
- let removedMapped = 0;
300
- for (let index = this.mappedSubmitIdentities.length - 1; index >= 0; index--) {
301
- const identity = this.mappedSubmitIdentities[index];
302
- if (currentIdentities.has(identity))
304
+ retireNonSubmittingAttempts(read) {
305
+ if (this.stopped || read.registrationCount !== this.attached.size)
306
+ return;
307
+ let removed = false;
308
+ for (const entry of read.expiredAttempts) {
309
+ if (entry.ordinal !== undefined || !this.entries.includes(entry))
303
310
  continue;
304
- this.mappedSubmitIdentities.splice(index, 1);
305
- removedMapped++;
311
+ this.removeEntry(entry);
312
+ removed = true;
306
313
  }
307
- if (removedMapped > 0) {
308
- this.mappedGeneration = Math.max(0, this.mappedGeneration - removedMapped);
309
- this.coveredGeneration = Math.min(this.mappedGeneration, Math.max(0, this.coveredGeneration - removedMapped));
310
- this.turnGeneration = Math.max(this.pendingTurnAttempts.length + this.pendingConfirmedTurns.length, this.turnGeneration - removedMapped);
314
+ if (removed)
315
+ this.updateActivity(true);
316
+ }
317
+ validateEvidence(lines, allowPending = false) {
318
+ const evidence = resolveProvider(this.driver).fork.submissionEvidence?.(lines);
319
+ if (evidence?.indeterminate && !(allowPending && evidence.indeterminateReason === "pending-submission")) {
320
+ throw new Error("Session submission evidence is indeterminate");
311
321
  }
312
- this.baseline = 0;
313
- this.submitFloor = Math.min(this.submitFloor ?? regression.commonSubmitCount, regression.commonSubmitCount);
314
- this.observedSubmitCount = Math.min(this.observedSubmitCount ?? regression.commonSubmitCount, regression.commonSubmitCount);
315
- for (const turn of this.pendingConfirmedTurns) {
316
- if (turn.anchorSubmitCount !== undefined) {
317
- turn.anchorSubmitCount = Math.min(turn.anchorSubmitCount, regression.commonSubmitCount);
322
+ return evidence;
323
+ }
324
+ inspectHistory(lines) {
325
+ const fork = resolveProvider(this.driver).fork;
326
+ if (!fork.sessionHistorySnapshot)
327
+ return {};
328
+ const current = fork.sessionHistorySnapshot(lines);
329
+ if (!current)
330
+ throw new Error("Session history snapshot is unbuildable");
331
+ const previous = this.historySnapshot;
332
+ const regression = previous && fork.compareSessionHistory?.(previous, current);
333
+ return { snapshot: current, regression };
334
+ }
335
+ reconcileHistory(lines) {
336
+ const { snapshot: current, regression } = this.inspectHistory(lines);
337
+ if (!current)
338
+ return undefined;
339
+ const previous = this.historySnapshot;
340
+ if (regression) {
341
+ const identities = new Set(current.submitIdentities);
342
+ for (const entry of [...this.entries]) {
343
+ if (entry.identity && !identities.has(entry.identity)) {
344
+ this.removeEntry(entry);
345
+ entry.receipt?.settle("removed");
346
+ }
347
+ else if (entry.ordinal === undefined && entry.anchor !== undefined) {
348
+ entry.anchor = Math.min(entry.anchor, regression.commonSubmitCount);
349
+ }
318
350
  }
351
+ this.baseline = 0;
352
+ this.submitFloor = Math.min(this.submitFloor ?? regression.commonSubmitCount, regression.commonSubmitCount);
353
+ this.observedSubmitCount = Math.min(this.observedSubmitCount ?? regression.commonSubmitCount, regression.commonSubmitCount);
354
+ this.historySnapshot = current;
355
+ if (!this.stopped)
356
+ this.options.onHistoryRegression?.(regression, current);
319
357
  }
320
- for (const attempt of this.pendingTurnAttempts) {
321
- if (attempt.anchorSubmitCount !== undefined) {
322
- attempt.anchorSubmitCount = Math.min(attempt.anchorSubmitCount, regression.commonSubmitCount);
323
- }
358
+ else {
359
+ this.historySnapshot = current;
360
+ if (!this.stopped && !sameHistorySnapshot(previous, current))
361
+ this.options.onHistorySnapshot?.(current);
324
362
  }
325
- this.unreservedObservedGenerations.length = 0;
363
+ return current;
326
364
  }
327
- discardExpiredAttempts() {
328
- for (let index = this.pendingTurnAttempts.length - 1; index >= 0; index--) {
329
- const attempt = this.pendingTurnAttempts[index];
330
- if (!attempt.expired)
331
- continue;
332
- if (attempt.timer)
333
- clearTimeout(attempt.timer);
334
- this.pendingTurnAttempts.splice(index, 1);
335
- }
336
- this.resolvePendingAttemptWaitersIfSettled();
365
+ removeEntry(entry) {
366
+ if (entry.timer)
367
+ clearTimeout(entry.timer);
368
+ entry.unsubscribe?.();
369
+ const index = this.entries.indexOf(entry);
370
+ if (index >= 0)
371
+ this.entries.splice(index, 1);
337
372
  }
338
- resolvePendingAttemptWaitersIfSettled() {
339
- if (this.pendingTurnAttempts.length === 0) {
340
- this.resolvePendingAttemptWaiters();
341
- }
373
+ hasPendingAttempts() {
374
+ return this.entries.some((entry) => entry.receipt?.rawAttempt && !entry.receipt.settlement
375
+ && entry.ordinal === undefined);
342
376
  }
343
- resolvePendingAttemptWaiters() {
344
- for (const resolve of this.pendingAttemptWaiters)
377
+ resolveAttemptWaiters() {
378
+ for (const resolve of this.attemptWaiters)
345
379
  resolve();
346
- this.pendingAttemptWaiters.clear();
380
+ this.attemptWaiters.clear();
347
381
  }
348
- releaseIfSettled(options = {}) {
349
- if (this.coveredGeneration !== this.turnGeneration
350
- || this.pendingTurnAttempts.length > 0) {
382
+ updateActivity(notifyIfAlreadyIdle = false) {
383
+ if (this.attachingBatch)
351
384
  return;
385
+ if (!this.hasPendingAttempts())
386
+ this.resolveAttemptWaiters();
387
+ const busy = this.entries.some((entry) => entry.ordinal === undefined
388
+ ? !entry.receipt?.settlement : !entry.settlement);
389
+ const changed = busy !== this.busy;
390
+ this.busy = busy;
391
+ if (!busy && !this.supportsHistoryRegression && !this.hasUnobservedExpiredAttempts && this.timer) {
392
+ clearInterval(this.timer);
393
+ this.timer = undefined;
352
394
  }
353
- const wasBusy = this.busy;
354
- this.busy = false;
355
- if (!this.supportsHistoryRegression)
356
- this.dispose();
357
- if ((wasBusy || options.notifyIfAlreadyIdle === true) && !this.stopped) {
358
- this.options.onChange?.(true);
395
+ if (!this.stopped && (changed || (!busy && notifyIfAlreadyIdle))) {
396
+ this.options.onChange?.(!busy);
359
397
  }
360
398
  }
361
399
  ensurePolling() {
362
- if (this.timer)
400
+ if (this.stopped || this.timer)
363
401
  return;
364
- this.timer = setInterval(() => {
402
+ this.timer = setInterval(() => { void this.pollNow(); }, this.pollMs);
403
+ if (this.busy || this.supportsHistoryRegression)
365
404
  void this.pollNow();
366
- }, this.pollMs);
367
- void this.pollNow();
368
405
  }
369
- async readLineResult() {
370
- if (this.options.readResult) {
371
- return this.options.readResult(this.sessionFilePath);
372
- }
373
- if (this.options.readLines) {
374
- return {
375
- lines: await this.options.readLines(this.sessionFilePath),
376
- malformed: false,
377
- };
378
- }
379
- return readSessionFileWithStatus(this.sessionFilePath);
406
+ async readValidResult() {
407
+ // Only a post-expiry read, uninterrupted by newer input, can release ownership.
408
+ const expiredAttempts = this.entries.filter(({ receipt, ordinal }) => ordinal === undefined && receipt?.settlement === "expired"
409
+ && !this.entries.some((newer) => newer.receipt && !newer.receipt.settlement
410
+ && newer.receipt.sequence > receipt.sequence));
411
+ const registrationCount = this.attached.size;
412
+ const result = this.options.readResult
413
+ ? await this.options.readResult(this.sessionFilePath)
414
+ : this.options.readLines
415
+ ? { lines: await this.options.readLines(this.sessionFilePath), malformed: false }
416
+ : await readSessionFileWithStatus(this.sessionFilePath);
417
+ if (result.malformed)
418
+ throw new Error("Session log contains malformed JSONL");
419
+ return { ...result, expiredAttempts, registrationCount };
380
420
  }
381
421
  countSubmits(lines) {
382
422
  return this.options.submitCount
@@ -385,9 +425,7 @@ export class SessionTurnMonitor {
385
425
  }
386
426
  }
387
427
  function sameHistorySnapshot(left, right) {
388
- if (!left)
389
- return false;
390
- return left.lineCount === right.lineCount
428
+ return left !== undefined && left.lineCount === right.lineCount
391
429
  && left.submitCount === right.submitCount
392
430
  && left.submitIdentities.length === right.submitIdentities.length
393
431
  && left.submitIdentities.every((identity, index) => identity === right.submitIdentities[index]);