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.
- package/dist/crosscheck/coordinator.d.ts.map +1 -1
- package/dist/crosscheck/coordinator.js +43 -9
- package/dist/crosscheck/coordinator.js.map +1 -1
- package/dist/crosscheck/live-agent-session.d.ts +3 -0
- package/dist/crosscheck/live-agent-session.d.ts.map +1 -1
- package/dist/crosscheck/live-agent-session.js +9 -2
- package/dist/crosscheck/live-agent-session.js.map +1 -1
- package/dist/offscreen/controller.d.ts +6 -3
- package/dist/offscreen/controller.d.ts.map +1 -1
- package/dist/offscreen/controller.js +127 -71
- package/dist/offscreen/controller.js.map +1 -1
- package/dist/offscreen/inquiry-sender.d.ts +1 -0
- package/dist/offscreen/inquiry-sender.d.ts.map +1 -1
- package/dist/offscreen/inquiry-sender.js +1 -0
- package/dist/offscreen/inquiry-sender.js.map +1 -1
- package/dist/offscreen/session-turn-monitor.d.ts +31 -32
- package/dist/offscreen/session-turn-monitor.d.ts.map +1 -1
- package/dist/offscreen/session-turn-monitor.js +318 -280
- package/dist/offscreen/session-turn-monitor.js.map +1 -1
- package/dist/offscreen/session-turn-receipt.d.ts +28 -0
- package/dist/offscreen/session-turn-receipt.d.ts.map +1 -0
- package/dist/offscreen/session-turn-receipt.js +53 -0
- package/dist/offscreen/session-turn-receipt.js.map +1 -0
- package/dist/providers/base-completion.d.ts +1 -0
- package/dist/providers/base-completion.d.ts.map +1 -1
- package/dist/providers/base-completion.js +4 -2
- package/dist/providers/base-completion.js.map +1 -1
- package/dist/providers/codex/completion.d.ts +4 -2
- package/dist/providers/codex/completion.d.ts.map +1 -1
- package/dist/providers/codex/completion.js +99 -14
- package/dist/providers/codex/completion.js.map +1 -1
- package/dist/providers/codex/strategy.d.ts.map +1 -1
- package/dist/providers/codex/strategy.js +3 -2
- package/dist/providers/codex/strategy.js.map +1 -1
- package/dist/providers/copilot/completion.js +10 -1
- package/dist/providers/copilot/completion.js.map +1 -1
- package/dist/providers/copilot/strategy.d.ts +1 -0
- package/dist/providers/copilot/strategy.d.ts.map +1 -1
- package/dist/providers/copilot/strategy.js +14 -1
- package/dist/providers/copilot/strategy.js.map +1 -1
- package/dist/providers/fork.d.ts +16 -0
- package/dist/providers/fork.d.ts.map +1 -1
- package/dist/providers/fork.js.map +1 -1
- package/dist/providers/live.d.ts.map +1 -1
- package/dist/providers/live.js +19 -0
- package/dist/providers/live.js.map +1 -1
- package/dist/providers/session-launch.d.ts +2 -0
- package/dist/providers/session-launch.d.ts.map +1 -1
- package/dist/providers/session-launch.js +9 -2
- package/dist/providers/session-launch.js.map +1 -1
- package/dist/terminal/paste-submit.d.ts +1 -0
- package/dist/terminal/paste-submit.d.ts.map +1 -1
- package/dist/terminal/paste-submit.js +7 -0
- package/dist/terminal/paste-submit.js.map +1 -1
- 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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
45
|
-
}
|
|
46
|
-
get
|
|
47
|
-
return this.
|
|
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
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
this.ensurePolling();
|
|
54
|
+
async reserveTurnFromSession() {
|
|
55
|
+
await this.expectedSubmitCountFromSession();
|
|
56
|
+
return this.reserveTurn();
|
|
66
57
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
this.
|
|
73
|
-
}
|
|
67
|
+
finally {
|
|
68
|
+
this.attachingBatch = false;
|
|
69
|
+
}
|
|
70
|
+
this.updateActivity(true);
|
|
71
|
+
if (this.busy || this.hasUnobservedExpiredAttempts)
|
|
72
|
+
this.ensurePolling();
|
|
74
73
|
}
|
|
75
|
-
|
|
76
|
-
if (this.stopped)
|
|
74
|
+
attachTurn(receipt) {
|
|
75
|
+
if (this.stopped || this.attached.has(receipt.id))
|
|
77
76
|
return;
|
|
78
|
-
|
|
77
|
+
this.attached.add(receipt.id);
|
|
78
|
+
if (receipt.settlement && receipt.settlement !== "expired")
|
|
79
79
|
return;
|
|
80
|
-
this.
|
|
81
|
-
this.
|
|
82
|
-
|
|
83
|
-
|
|
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 (!
|
|
86
|
-
this.
|
|
87
|
-
|
|
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.
|
|
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
|
-
|
|
130
|
+
expectedSubmitCount(lines) {
|
|
92
131
|
if (this.stopped)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
this.
|
|
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.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return
|
|
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
|
|
141
|
-
if (
|
|
142
|
-
clearTimeout(
|
|
174
|
+
for (const entry of this.entries) {
|
|
175
|
+
if (entry.timer)
|
|
176
|
+
clearTimeout(entry.timer);
|
|
177
|
+
entry.unsubscribe?.();
|
|
143
178
|
}
|
|
144
|
-
this.
|
|
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.
|
|
156
|
-
return this.activePoll
|
|
157
|
-
|
|
158
|
-
|
|
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?.(
|
|
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.
|
|
174
|
-
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (
|
|
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.
|
|
305
|
-
|
|
311
|
+
this.removeEntry(entry);
|
|
312
|
+
removed = true;
|
|
306
313
|
}
|
|
307
|
-
if (
|
|
308
|
-
this.
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
358
|
+
else {
|
|
359
|
+
this.historySnapshot = current;
|
|
360
|
+
if (!this.stopped && !sameHistorySnapshot(previous, current))
|
|
361
|
+
this.options.onHistorySnapshot?.(current);
|
|
324
362
|
}
|
|
325
|
-
|
|
363
|
+
return current;
|
|
326
364
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
373
|
+
hasPendingAttempts() {
|
|
374
|
+
return this.entries.some((entry) => entry.receipt?.rawAttempt && !entry.receipt.settlement
|
|
375
|
+
&& entry.ordinal === undefined);
|
|
342
376
|
}
|
|
343
|
-
|
|
344
|
-
for (const resolve of this.
|
|
377
|
+
resolveAttemptWaiters() {
|
|
378
|
+
for (const resolve of this.attemptWaiters)
|
|
345
379
|
resolve();
|
|
346
|
-
this.
|
|
380
|
+
this.attemptWaiters.clear();
|
|
347
381
|
}
|
|
348
|
-
|
|
349
|
-
if (this.
|
|
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
|
-
|
|
354
|
-
|
|
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
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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]);
|