@proagentstore/cli 0.4.98 → 0.4.99
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.
|
@@ -15,9 +15,14 @@ export class LocalApplyReconciliationRuntime {
|
|
|
15
15
|
deps;
|
|
16
16
|
sessions = new Map();
|
|
17
17
|
now;
|
|
18
|
+
schedule;
|
|
18
19
|
constructor(deps) {
|
|
19
20
|
this.deps = deps;
|
|
20
21
|
this.now = deps.now ?? (() => Date.now());
|
|
22
|
+
this.schedule = deps.schedule ?? ((task, delayMs) => {
|
|
23
|
+
const timer = setTimeout(task, delayMs);
|
|
24
|
+
return () => clearTimeout(timer);
|
|
25
|
+
});
|
|
21
26
|
}
|
|
22
27
|
async start(raw) {
|
|
23
28
|
const envelope = parseEnvelope(raw);
|
|
@@ -30,6 +35,9 @@ export class LocalApplyReconciliationRuntime {
|
|
|
30
35
|
const browser = await this.deps.browserFor(envelope.browserProfile, join(this.deps.dataDir, "reconciliation", envelope.reconciliationId));
|
|
31
36
|
const session = { envelope, browser, state: "running", expiresAt: this.now() + TTL_MS };
|
|
32
37
|
this.sessions.set(envelope.reconciliationId, session);
|
|
38
|
+
// The deadline belongs to the Runner, not to Console polling. It is installed before
|
|
39
|
+
// navigation so an abandoned client cannot retain an owned browser/liveWork indefinitely.
|
|
40
|
+
session.cancelDeadline = this.schedule(() => { void this.expire(session); }, TTL_MS);
|
|
33
41
|
try {
|
|
34
42
|
const page = this.page(session);
|
|
35
43
|
if (!page)
|
|
@@ -49,8 +57,10 @@ export class LocalApplyReconciliationRuntime {
|
|
|
49
57
|
return this.finish(session, { state: "ambiguous", proofKind: "ambiguous_site_history" });
|
|
50
58
|
if (!this.page(session))
|
|
51
59
|
return this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" });
|
|
52
|
-
if (session.handoff?.state === "ready" && (session.handoff.expiresAt <= this.now() || !this.deps.takeover || await this.deps.takeover.state(session.handoff.id) !== "ready"))
|
|
60
|
+
if (session.handoff?.state === "ready" && (session.handoff.expiresAt <= this.now() || !this.deps.takeover || await this.deps.takeover.state(session.handoff.id) !== "ready")) {
|
|
53
61
|
await this.closeHandoff(session, session.handoff.expiresAt <= this.now() ? "expired" : "page_lost");
|
|
62
|
+
return this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" });
|
|
63
|
+
}
|
|
54
64
|
return this.view(session);
|
|
55
65
|
}
|
|
56
66
|
async handoff(raw) {
|
|
@@ -80,6 +90,7 @@ export class LocalApplyReconciliationRuntime {
|
|
|
80
90
|
}
|
|
81
91
|
catch {
|
|
82
92
|
await this.closeHandoff(session, "page_lost");
|
|
93
|
+
await this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" });
|
|
83
94
|
throw new RunnerInputError("The reconciliation page is no longer available", 409);
|
|
84
95
|
}
|
|
85
96
|
}
|
|
@@ -98,6 +109,7 @@ export class LocalApplyReconciliationRuntime {
|
|
|
98
109
|
}
|
|
99
110
|
catch {
|
|
100
111
|
await this.closeHandoff(session, "page_lost");
|
|
112
|
+
await this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" });
|
|
101
113
|
throw new RunnerInputError("The reconciliation page is no longer available", 409);
|
|
102
114
|
}
|
|
103
115
|
}
|
|
@@ -106,12 +118,14 @@ export class LocalApplyReconciliationRuntime {
|
|
|
106
118
|
return this.inspect(session);
|
|
107
119
|
}
|
|
108
120
|
async end(raw) {
|
|
109
|
-
const
|
|
121
|
+
const request = this.handoffRequest(raw);
|
|
122
|
+
const session = this.require(request);
|
|
123
|
+
this.assertHandoffBinding(session, request);
|
|
124
|
+
if (session.handoff?.id !== request.handoffId)
|
|
125
|
+
throw new RunnerInputError("The reconciliation handoff is closed", 409);
|
|
110
126
|
if (session.handoff?.state === "ready")
|
|
111
127
|
await this.closeHandoff(session, "unavailable");
|
|
112
|
-
session
|
|
113
|
-
session.pauseReason = undefined;
|
|
114
|
-
return this.view(session);
|
|
128
|
+
return this.finish(session, { state: "ambiguous", proofKind: "ambiguous_site_history" });
|
|
115
129
|
}
|
|
116
130
|
liveWork() { return [...this.sessions.values()].filter((s) => s.state !== "ended").map((s) => `reconciliation:${s.envelope.reconciliationId}`); }
|
|
117
131
|
closeAll() { for (const session of this.sessions.values())
|
|
@@ -141,16 +155,29 @@ export class LocalApplyReconciliationRuntime {
|
|
|
141
155
|
// prose, receipt-like URLs, unauthenticated pages and absent receipts prove nothing.
|
|
142
156
|
return this.finish(session, { state: "ambiguous", proofKind: "ambiguous_site_history" });
|
|
143
157
|
}
|
|
158
|
+
async expire(session) {
|
|
159
|
+
if (session.state !== "ended" && session.expiresAt <= this.now())
|
|
160
|
+
await this.finish(session, { state: "ambiguous", proofKind: "ambiguous_site_history" });
|
|
161
|
+
}
|
|
144
162
|
async finish(session, result) {
|
|
163
|
+
if (session.finishing)
|
|
164
|
+
return session.finishing;
|
|
145
165
|
if (session.state === "ended")
|
|
146
166
|
return this.view(session);
|
|
147
|
-
|
|
148
|
-
|
|
167
|
+
// Set terminal state synchronously, before any async teardown. Concurrent End/timeout/page
|
|
168
|
+
// loss callers then converge on the same result and liveWork stops advertising ownership.
|
|
149
169
|
session.state = "ended";
|
|
150
170
|
session.pauseReason = undefined;
|
|
151
171
|
session.result = result;
|
|
152
|
-
|
|
153
|
-
|
|
172
|
+
session.cancelDeadline?.();
|
|
173
|
+
session.cancelDeadline = undefined;
|
|
174
|
+
session.finishing = (async () => {
|
|
175
|
+
if (session.handoff?.state === "ready")
|
|
176
|
+
await this.closeHandoff(session, "unavailable");
|
|
177
|
+
await session.browser.stop().catch(() => undefined);
|
|
178
|
+
return this.view(session);
|
|
179
|
+
})();
|
|
180
|
+
return session.finishing;
|
|
154
181
|
}
|
|
155
182
|
async closeHandoff(session, reason) {
|
|
156
183
|
if (!session.handoff || session.handoff.state === "closed")
|
|
@@ -159,7 +186,10 @@ export class LocalApplyReconciliationRuntime {
|
|
|
159
186
|
session.handoff.reason = reason;
|
|
160
187
|
await this.deps.takeover?.end(session.handoff.id).catch(() => undefined);
|
|
161
188
|
}
|
|
162
|
-
closedHandoff(session, id, reason) {
|
|
189
|
+
async closedHandoff(session, id, reason) {
|
|
190
|
+
session.handoff = { id, expiresAt: this.now(), state: "closed", reason };
|
|
191
|
+
return this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" });
|
|
192
|
+
}
|
|
163
193
|
page(session) { const page = session.browser.handoffPage?.(); return page && !page.isClosed() ? page : null; }
|
|
164
194
|
permitted(session, raw) { try {
|
|
165
195
|
const host = new URL(raw).hostname.toLowerCase();
|
|
@@ -191,6 +221,7 @@ export class LocalApplyReconciliationRuntime {
|
|
|
191
221
|
async liveHandoff(raw) { const request = this.handoffRequest(raw); const session = this.require(request); this.assertHandoffBinding(session, request); if ((session.state !== "paused" && session.state !== "running") || session.handoff?.id !== request.handoffId || session.handoff.state !== "ready")
|
|
192
222
|
throw new RunnerInputError("The reconciliation handoff is closed", 409); if (session.handoff.expiresAt <= this.now()) {
|
|
193
223
|
await this.closeHandoff(session, "expired");
|
|
224
|
+
await this.finish(session, { state: "ambiguous", proofKind: "ambiguous_site_history" });
|
|
194
225
|
throw new RunnerInputError("The reconciliation handoff is closed", 409);
|
|
195
226
|
} return session; }
|
|
196
227
|
view(session) { const e = session.envelope; return { reconciliationId: e.reconciliationId, runId: e.runId, applicationId: e.applicationId, browserProfile: e.browserProfile, context: "separate_read_only_context", state: session.state, ...(session.pauseReason ? { pauseReason: session.pauseReason } : {}), ...(session.result ? { result: session.result } : {}) }; }
|