@sovovs/bycli 2.1.39 → 2.1.41

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 (47) hide show
  1. package/cli-manifest.json +56 -3
  2. package/clis/ima/knowledge-list.js +48 -0
  3. package/clis/ima/knowledge.js +23 -0
  4. package/clis/ima/native-api.js +67 -10
  5. package/clis/ima/native-client.js +23 -7
  6. package/clis/ima/utils.js +1 -0
  7. package/clis/weixin/_wechat/article-artifact.js +55 -0
  8. package/clis/weixin/_wechat/article-identity.js +27 -0
  9. package/clis/weixin/_wechat/publish-analysis.js +8 -4
  10. package/clis/weixin/_wechat/publish-download.js +3 -1
  11. package/clis/weixin/download-publish-data.js +20 -0
  12. package/clis/weixin/download.js +15 -2
  13. package/dist/src/adapter-coordination.d.ts +26 -0
  14. package/dist/src/adapter-coordination.js +183 -0
  15. package/dist/src/adapter-coordination.test.d.ts +1 -0
  16. package/dist/src/adapter-execution-context.d.ts +6 -0
  17. package/dist/src/adapter-execution-context.js +8 -0
  18. package/dist/src/adapter-scheduler.d.ts +86 -0
  19. package/dist/src/adapter-scheduler.js +349 -0
  20. package/dist/src/adapter-scheduler.test.d.ts +1 -0
  21. package/dist/src/browser/daemon-client.d.ts +11 -0
  22. package/dist/src/browser/daemon-client.js +53 -1
  23. package/dist/src/browser/extension-capabilities.d.ts +1 -0
  24. package/dist/src/browser/extension-capabilities.js +18 -5
  25. package/dist/src/browser/page.d.ts +2 -1
  26. package/dist/src/browser/page.js +3 -0
  27. package/dist/src/build-manifest.js +1 -0
  28. package/dist/src/cli-argv-preprocess.d.ts +3 -0
  29. package/dist/src/cli-argv-preprocess.js +4 -0
  30. package/dist/src/commanderAdapter.js +11 -0
  31. package/dist/src/daemon.js +118 -0
  32. package/dist/src/discovery.js +1 -0
  33. package/dist/src/download/article-download.d.ts +2 -0
  34. package/dist/src/download/article-download.js +30 -4
  35. package/dist/src/errors.d.ts +3 -0
  36. package/dist/src/errors.js +5 -0
  37. package/dist/src/execution.d.ts +2 -0
  38. package/dist/src/execution.js +168 -105
  39. package/dist/src/help.d.ts +1 -0
  40. package/dist/src/help.js +40 -0
  41. package/dist/src/manifest-types.d.ts +4 -0
  42. package/dist/src/registry.d.ts +6 -0
  43. package/dist/src/registry.js +23 -0
  44. package/dist/src/serialization.d.ts +1 -0
  45. package/dist/src/serialization.js +1 -0
  46. package/dist/src/types.d.ts +2 -0
  47. package/package.json +3 -2
@@ -0,0 +1,183 @@
1
+ import { acquireAdapterLease, heartbeatAdapterLease, releaseAdapterLease, acquireAdapterResources, releaseAdapterResources, } from './browser/daemon-client.js';
2
+ import { log } from './logger.js';
3
+ import { AdapterCoordinationError } from './errors.js';
4
+ import { getAdapterExecutionContext, runWithAdapterExecutionContext, } from './adapter-execution-context.js';
5
+ export function getCurrentAdapterLease() {
6
+ return getAdapterExecutionContext()?.lease;
7
+ }
8
+ /**
9
+ * Renew the active lease immediately before an irreversible local publication.
10
+ * A restarted daemon or reclaimed lease rejects this fencing check.
11
+ */
12
+ export async function assertCurrentAdapterLease(dependencies = {}) {
13
+ const context = getAdapterExecutionContext();
14
+ if (!context)
15
+ return;
16
+ try {
17
+ context.lease = await (dependencies.heartbeat ?? heartbeatAdapterLease)(context.lease);
18
+ }
19
+ catch (error) {
20
+ if (error instanceof AdapterCoordinationError && error.code === 'ADAPTER_LEASE_LOST')
21
+ throw error;
22
+ throw new AdapterCoordinationError('ADAPTER_LEASE_LOST', 'Adapter lease fencing failed before artifact publication.', true);
23
+ }
24
+ }
25
+ export async function settleAdapterOperationAfterTimeout(operation, timeoutMs, timeoutError, stop) {
26
+ const outcome = operation.then(value => ({ kind: 'value', value }), error => ({ kind: 'error', error }));
27
+ let timeout;
28
+ const first = await Promise.race([
29
+ outcome,
30
+ new Promise(resolve => {
31
+ timeout = setTimeout(() => resolve({ kind: 'timeout' }), timeoutMs);
32
+ }),
33
+ ]);
34
+ if (first.kind !== 'timeout') {
35
+ if (timeout)
36
+ clearTimeout(timeout);
37
+ if (first.kind === 'error')
38
+ throw first.error;
39
+ return first.value;
40
+ }
41
+ try {
42
+ await stop();
43
+ }
44
+ finally {
45
+ await outcome;
46
+ }
47
+ throw timeoutError;
48
+ }
49
+ export async function withAdapterResourceLocks(keys, operation, dependencies = {}) {
50
+ const lease = getCurrentAdapterLease();
51
+ if (!lease)
52
+ return operation();
53
+ const acquire = dependencies.acquire ?? acquireAdapterResources;
54
+ const release = dependencies.release ?? releaseAdapterResources;
55
+ const warn = dependencies.warn ?? ((message) => log.warn(message));
56
+ const scopedKeys = keys.map(key => key.startsWith('article:') || key.startsWith('data:')
57
+ ? `profile:${lease.contextId}:${key}`
58
+ : key);
59
+ const grant = await acquire(lease, scopedKeys, dependencies.timeoutMs ?? 300_000);
60
+ try {
61
+ return await operation();
62
+ }
63
+ finally {
64
+ let released = false;
65
+ let lastError;
66
+ for (let attempt = 0; attempt < 2 && !released; attempt++) {
67
+ try {
68
+ await release(lease, grant.grantId);
69
+ released = true;
70
+ }
71
+ catch (error) {
72
+ lastError = error;
73
+ }
74
+ }
75
+ if (!released) {
76
+ warn(`Adapter resource release acknowledgement failed for ${grant.grantId}: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
77
+ }
78
+ }
79
+ }
80
+ export async function withAdapterCommandLease(request, operation, dependencies = {}) {
81
+ const acquire = dependencies.acquire ?? acquireAdapterLease;
82
+ const heartbeat = dependencies.heartbeat ?? heartbeatAdapterLease;
83
+ const release = dependencies.release ?? releaseAdapterLease;
84
+ const warn = dependencies.warn ?? ((message) => log.warn(message));
85
+ const heartbeatIntervalMs = dependencies.heartbeatIntervalMs ?? 10_000;
86
+ const context = { lease: await acquire(request) };
87
+ let releaseReason = 'error';
88
+ let leaseLost;
89
+ let stopAfterLeaseLoss;
90
+ let heartbeatTask;
91
+ const heartbeatTimer = setInterval(() => {
92
+ if (heartbeatTask || leaseLost)
93
+ return;
94
+ heartbeatTask = heartbeat(context.lease)
95
+ .then(next => { context.lease = next; })
96
+ .catch(error => {
97
+ const code = error && typeof error === 'object' ? error.code : undefined;
98
+ if (code === 'ADAPTER_LEASE_LOST') {
99
+ leaseLost = error;
100
+ stopAfterLeaseLoss = (dependencies.onLeaseLost?.() ?? Promise.resolve()).catch(stopError => {
101
+ warn(`Adapter operation stop failed after lease loss for ${context.lease.requestId}: ${stopError instanceof Error ? stopError.message : String(stopError)}`);
102
+ });
103
+ }
104
+ else {
105
+ warn(`Adapter lease heartbeat failed for ${context.lease.requestId}: ${error instanceof Error ? error.message : String(error)}`);
106
+ }
107
+ })
108
+ .finally(() => { heartbeatTask = undefined; });
109
+ }, heartbeatIntervalMs);
110
+ heartbeatTimer.unref?.();
111
+ try {
112
+ let result;
113
+ try {
114
+ result = await runWithAdapterExecutionContext(context, operation);
115
+ }
116
+ catch (error) {
117
+ if (heartbeatTask)
118
+ await heartbeatTask;
119
+ if (stopAfterLeaseLoss)
120
+ await stopAfterLeaseLoss;
121
+ if (leaseLost)
122
+ throw leaseLost;
123
+ throw error;
124
+ }
125
+ if (heartbeatTask)
126
+ await heartbeatTask;
127
+ if (stopAfterLeaseLoss)
128
+ await stopAfterLeaseLoss;
129
+ if (leaseLost)
130
+ throw leaseLost;
131
+ releaseReason = classifyAdapterResult(result);
132
+ return result;
133
+ }
134
+ catch (error) {
135
+ releaseReason = classifyAdapterError(error);
136
+ throw error;
137
+ }
138
+ finally {
139
+ clearInterval(heartbeatTimer);
140
+ const payload = { ...context.lease, reason: releaseReason };
141
+ let released = false;
142
+ let lastError;
143
+ for (let attempt = 0; attempt < 2 && !released; attempt++) {
144
+ try {
145
+ await release(payload);
146
+ released = true;
147
+ }
148
+ catch (error) {
149
+ lastError = error;
150
+ }
151
+ }
152
+ if (!released) {
153
+ warn(`Adapter lease release acknowledgement failed for ${payload.requestId}: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
154
+ }
155
+ }
156
+ }
157
+ function classifyAdapterError(error) {
158
+ const code = error && typeof error === 'object' && typeof error.code === 'string'
159
+ ? error.code
160
+ : '';
161
+ if (code === 'AUTH_REQUIRED' || /CAPTCHA|MFA|VERIFICATION/.test(code))
162
+ return 'auth_gate';
163
+ if (code === 'RATE_LIMITED')
164
+ return 'rate_limited';
165
+ if (code === 'TIMEOUT')
166
+ return 'timeout';
167
+ return 'error';
168
+ }
169
+ function classifyAdapterResult(result) {
170
+ const rows = Array.isArray(result) ? result : [result];
171
+ const statuses = rows
172
+ .filter(row => row && typeof row === 'object')
173
+ .map(row => String(row.status ?? '').toLowerCase());
174
+ if (statuses.some(status => /auth|login|captcha|verification|mfa/.test(status)))
175
+ return 'auth_gate';
176
+ if (statuses.some(status => /rate.?limit/.test(status)))
177
+ return 'rate_limited';
178
+ if (statuses.some(status => status === 'partial'))
179
+ return 'partial';
180
+ if (statuses.length > 0 && statuses.every(status => status.startsWith('failed') || status.startsWith('failure')))
181
+ return 'failed';
182
+ return 'success';
183
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { AdapterLease } from './adapter-scheduler.js';
2
+ export interface AdapterExecutionContext {
3
+ lease: AdapterLease;
4
+ }
5
+ export declare function getAdapterExecutionContext(): AdapterExecutionContext | undefined;
6
+ export declare function runWithAdapterExecutionContext<T>(context: AdapterExecutionContext, operation: () => Promise<T>): Promise<T>;
@@ -0,0 +1,8 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ const storage = new AsyncLocalStorage();
3
+ export function getAdapterExecutionContext() {
4
+ return storage.getStore();
5
+ }
6
+ export function runWithAdapterExecutionContext(context, operation) {
7
+ return storage.run(context, operation);
8
+ }
@@ -0,0 +1,86 @@
1
+ export type AdapterPoolCloseReason = 'auth_gate' | 'rate_limited';
2
+ export type AdapterReleaseReason = 'success' | 'partial' | 'failed' | AdapterPoolCloseReason | 'timeout' | 'error' | 'cancelled';
3
+ export interface AdapterLeaseRequest {
4
+ requestId: string;
5
+ contextId: string;
6
+ surface: 'adapter';
7
+ site: string;
8
+ adapterSession: string;
9
+ sessionKey: string;
10
+ queueTimeoutMs: number;
11
+ maxParallel: number;
12
+ }
13
+ export interface AdapterLease {
14
+ leaseId: string;
15
+ requestId: string;
16
+ poolKey: string;
17
+ contextId: string;
18
+ surface: 'adapter';
19
+ site: string;
20
+ adapterSession: string;
21
+ sessionKey: string;
22
+ generation: number;
23
+ grantedAt: number;
24
+ heartbeatDeadline: number;
25
+ }
26
+ export interface AdapterLeaseRelease extends AdapterLease {
27
+ reason: AdapterReleaseReason;
28
+ }
29
+ export declare class AdapterSchedulerError extends Error {
30
+ readonly code: string;
31
+ constructor(code: string, message: string);
32
+ }
33
+ export interface AdapterResourceGrant {
34
+ grantId: string;
35
+ leaseId: string;
36
+ keys: string[];
37
+ }
38
+ export interface AdapterSchedulerOptions {
39
+ now?: () => number;
40
+ leaseExpiryMs?: number;
41
+ runtimeCeiling?: number;
42
+ releasedLeaseRetentionMs?: number;
43
+ }
44
+ export declare class AdapterScheduler {
45
+ private readonly now;
46
+ private readonly leaseExpiryMs;
47
+ private readonly runtimeCeiling;
48
+ private readonly releasedLeaseRetentionMs;
49
+ private readonly pools;
50
+ private readonly releasedLeaseIds;
51
+ private readonly generations;
52
+ private readonly resourceOwners;
53
+ private readonly resourceGrants;
54
+ private readonly resourceQueue;
55
+ private sequence;
56
+ constructor(options?: AdapterSchedulerOptions);
57
+ acquire(request: AdapterLeaseRequest): Promise<AdapterLease>;
58
+ heartbeat(identity: AdapterLease): AdapterLease;
59
+ assertLease(identity: AdapterLease): AdapterLease;
60
+ release(release: AdapterLeaseRelease): boolean;
61
+ acquireResources(leaseIdentity: AdapterLease, rawKeys: string[], timeoutMs: number): Promise<AdapterResourceGrant>;
62
+ releaseResources(leaseIdentity: AdapterLease, grantId: string): boolean;
63
+ cancel(requestId: string, code?: string): boolean;
64
+ sweepExpired(): void;
65
+ reset(): void;
66
+ snapshot(): {
67
+ running: number;
68
+ queued: number;
69
+ pools: number;
70
+ };
71
+ resourceSnapshot(): {
72
+ locked: number;
73
+ queued: number;
74
+ grants: number;
75
+ };
76
+ private schedule;
77
+ private scheduleResources;
78
+ private releaseAllResourcesForLease;
79
+ private pruneReleasedLeaseIds;
80
+ private rejectResourceWaitersForLease;
81
+ private requireLease;
82
+ private getOrCreatePool;
83
+ private removeDrainedPool;
84
+ private poolClosedError;
85
+ private validateRequest;
86
+ }
@@ -0,0 +1,349 @@
1
+ import * as crypto from 'node:crypto';
2
+ export class AdapterSchedulerError extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.code = code;
7
+ this.name = 'AdapterSchedulerError';
8
+ }
9
+ }
10
+ export class AdapterScheduler {
11
+ now;
12
+ leaseExpiryMs;
13
+ runtimeCeiling;
14
+ releasedLeaseRetentionMs;
15
+ pools = new Map();
16
+ releasedLeaseIds = new Map();
17
+ generations = new Map();
18
+ resourceOwners = new Map();
19
+ resourceGrants = new Map();
20
+ resourceQueue = [];
21
+ sequence = 0;
22
+ constructor(options = {}) {
23
+ this.now = options.now ?? Date.now;
24
+ this.leaseExpiryMs = options.leaseExpiryMs ?? 45_000;
25
+ this.runtimeCeiling = options.runtimeCeiling ?? 3;
26
+ this.releasedLeaseRetentionMs = options.releasedLeaseRetentionMs ?? 300_000;
27
+ }
28
+ acquire(request) {
29
+ this.validateRequest(request);
30
+ const pool = this.getOrCreatePool(request);
31
+ if (pool.closed) {
32
+ return Promise.reject(this.poolClosedError(pool.closed));
33
+ }
34
+ pool.maxParallel = Math.min(pool.maxParallel, request.maxParallel, this.runtimeCeiling);
35
+ if ([...pool.running.values()].some(lease => lease.requestId === request.requestId)
36
+ || pool.queued.some(entry => entry.request.requestId === request.requestId)) {
37
+ return Promise.reject(new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Duplicate Adapter lease request id'));
38
+ }
39
+ return new Promise((resolve, reject) => {
40
+ const enqueuedAt = this.now();
41
+ const pending = {
42
+ request,
43
+ enqueuedAt,
44
+ deadline: enqueuedAt + request.queueTimeoutMs,
45
+ sequence: ++this.sequence,
46
+ resolve,
47
+ reject,
48
+ };
49
+ pending.timer = setTimeout(() => this.sweepExpired(), request.queueTimeoutMs);
50
+ pending.timer.unref?.();
51
+ pool.queued.push(pending);
52
+ this.schedule(pool);
53
+ });
54
+ }
55
+ heartbeat(identity) {
56
+ const lease = this.requireLease(identity);
57
+ lease.heartbeatDeadline = this.now() + this.leaseExpiryMs;
58
+ return { ...lease };
59
+ }
60
+ assertLease(identity) {
61
+ return { ...this.requireLease(identity) };
62
+ }
63
+ release(release) {
64
+ this.pruneReleasedLeaseIds();
65
+ if (this.releasedLeaseIds.has(release.leaseId))
66
+ return false;
67
+ const lease = this.requireLease(release);
68
+ const pool = this.pools.get(lease.poolKey);
69
+ this.releaseAllResourcesForLease(lease.leaseId);
70
+ this.rejectResourceWaitersForLease(lease.leaseId);
71
+ pool.running.delete(lease.leaseId);
72
+ pool.activeSessions.delete(lease.adapterSession);
73
+ this.releasedLeaseIds.set(lease.leaseId, this.now() + this.releasedLeaseRetentionMs);
74
+ if (release.reason === 'auth_gate' || release.reason === 'rate_limited') {
75
+ pool.closed = release.reason;
76
+ const error = this.poolClosedError(release.reason);
77
+ for (const pending of pool.queued.splice(0)) {
78
+ if (pending.timer)
79
+ clearTimeout(pending.timer);
80
+ pending.reject(error);
81
+ }
82
+ }
83
+ else {
84
+ this.schedule(pool);
85
+ }
86
+ this.scheduleResources();
87
+ this.removeDrainedPool(pool);
88
+ return true;
89
+ }
90
+ acquireResources(leaseIdentity, rawKeys, timeoutMs) {
91
+ const lease = this.requireLease(leaseIdentity);
92
+ const keys = [...new Set(rawKeys.map(key => key.trim()).filter(Boolean))].sort();
93
+ if (keys.length === 0) {
94
+ return Promise.reject(new AdapterSchedulerError('ADAPTER_RESOURCE_TIMEOUT', 'At least one resource key is required'));
95
+ }
96
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1) {
97
+ return Promise.reject(new AdapterSchedulerError('ADAPTER_RESOURCE_TIMEOUT', 'Invalid Adapter resource timeout'));
98
+ }
99
+ return new Promise((resolve, reject) => {
100
+ const pending = {
101
+ lease: { ...lease },
102
+ keys,
103
+ deadline: this.now() + timeoutMs,
104
+ sequence: ++this.sequence,
105
+ resolve,
106
+ reject,
107
+ };
108
+ pending.timer = setTimeout(() => this.sweepExpired(), timeoutMs);
109
+ pending.timer.unref?.();
110
+ this.resourceQueue.push(pending);
111
+ this.scheduleResources();
112
+ });
113
+ }
114
+ releaseResources(leaseIdentity, grantId) {
115
+ const lease = this.requireLease(leaseIdentity);
116
+ const grant = this.resourceGrants.get(grantId);
117
+ if (!grant || grant.leaseId !== lease.leaseId) {
118
+ throw new AdapterSchedulerError('ADAPTER_LEASE_LOST', 'Adapter resource grant is no longer valid');
119
+ }
120
+ this.resourceGrants.delete(grantId);
121
+ for (const key of grant.keys) {
122
+ const owner = this.resourceOwners.get(key);
123
+ if (owner?.grantId === grantId)
124
+ this.resourceOwners.delete(key);
125
+ }
126
+ this.scheduleResources();
127
+ return true;
128
+ }
129
+ cancel(requestId, code = 'ADAPTER_QUEUE_RESET') {
130
+ for (const pool of this.pools.values()) {
131
+ const index = pool.queued.findIndex(entry => entry.request.requestId === requestId);
132
+ if (index === -1)
133
+ continue;
134
+ const [pending] = pool.queued.splice(index, 1);
135
+ if (pending.timer)
136
+ clearTimeout(pending.timer);
137
+ pending.reject(new AdapterSchedulerError(code, 'Adapter lease request was cancelled'));
138
+ this.removeDrainedPool(pool);
139
+ return true;
140
+ }
141
+ return false;
142
+ }
143
+ sweepExpired() {
144
+ const now = this.now();
145
+ this.pruneReleasedLeaseIds(now);
146
+ for (const pool of [...this.pools.values()]) {
147
+ for (const pending of [...pool.queued]) {
148
+ if (pending.deadline > now)
149
+ continue;
150
+ pool.queued.splice(pool.queued.indexOf(pending), 1);
151
+ if (pending.timer)
152
+ clearTimeout(pending.timer);
153
+ pending.reject(new AdapterSchedulerError('ADAPTER_QUEUE_TIMEOUT', 'Timed out waiting for an Adapter command lease'));
154
+ }
155
+ for (const lease of [...pool.running.values()]) {
156
+ if (lease.heartbeatDeadline > now)
157
+ continue;
158
+ this.releaseAllResourcesForLease(lease.leaseId);
159
+ this.rejectResourceWaitersForLease(lease.leaseId);
160
+ pool.running.delete(lease.leaseId);
161
+ pool.activeSessions.delete(lease.adapterSession);
162
+ }
163
+ if (!pool.closed)
164
+ this.schedule(pool);
165
+ this.removeDrainedPool(pool);
166
+ }
167
+ for (const pending of [...this.resourceQueue]) {
168
+ if (pending.deadline > now)
169
+ continue;
170
+ this.resourceQueue.splice(this.resourceQueue.indexOf(pending), 1);
171
+ if (pending.timer)
172
+ clearTimeout(pending.timer);
173
+ pending.reject(new AdapterSchedulerError('ADAPTER_RESOURCE_TIMEOUT', 'Timed out waiting for Adapter resource locks'));
174
+ }
175
+ this.scheduleResources();
176
+ }
177
+ reset() {
178
+ for (const pool of this.pools.values()) {
179
+ for (const pending of pool.queued) {
180
+ if (pending.timer)
181
+ clearTimeout(pending.timer);
182
+ pending.reject(new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Adapter scheduler restarted'));
183
+ }
184
+ }
185
+ this.pools.clear();
186
+ for (const pending of this.resourceQueue.splice(0)) {
187
+ if (pending.timer)
188
+ clearTimeout(pending.timer);
189
+ pending.reject(new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Adapter scheduler restarted'));
190
+ }
191
+ this.resourceOwners.clear();
192
+ this.resourceGrants.clear();
193
+ this.releasedLeaseIds.clear();
194
+ }
195
+ snapshot() {
196
+ let running = 0;
197
+ let queued = 0;
198
+ for (const pool of this.pools.values()) {
199
+ running += pool.running.size;
200
+ queued += pool.queued.length;
201
+ }
202
+ return { running, queued, pools: this.pools.size };
203
+ }
204
+ resourceSnapshot() {
205
+ return {
206
+ locked: this.resourceOwners.size,
207
+ queued: this.resourceQueue.length,
208
+ grants: this.resourceGrants.size,
209
+ };
210
+ }
211
+ schedule(pool) {
212
+ while (!pool.closed && pool.running.size < pool.maxParallel) {
213
+ const eligible = pool.queued
214
+ .filter(entry => !pool.activeSessions.has(entry.request.adapterSession)
215
+ && pool.running.size < pool.maxParallel)
216
+ .sort((a, b) => a.enqueuedAt - b.enqueuedAt || a.sequence - b.sequence)[0];
217
+ if (!eligible)
218
+ return;
219
+ pool.queued.splice(pool.queued.indexOf(eligible), 1);
220
+ if (eligible.timer)
221
+ clearTimeout(eligible.timer);
222
+ const grantedAt = this.now();
223
+ const lease = {
224
+ leaseId: crypto.randomUUID(),
225
+ requestId: eligible.request.requestId,
226
+ poolKey: pool.key,
227
+ contextId: eligible.request.contextId,
228
+ surface: 'adapter',
229
+ site: eligible.request.site,
230
+ adapterSession: eligible.request.adapterSession,
231
+ sessionKey: eligible.request.sessionKey,
232
+ generation: pool.generation,
233
+ grantedAt,
234
+ heartbeatDeadline: grantedAt + this.leaseExpiryMs,
235
+ };
236
+ pool.running.set(lease.leaseId, lease);
237
+ pool.activeSessions.add(lease.adapterSession);
238
+ eligible.resolve({ ...lease });
239
+ }
240
+ }
241
+ scheduleResources() {
242
+ for (const pending of [...this.resourceQueue].sort((a, b) => a.sequence - b.sequence)) {
243
+ try {
244
+ this.requireLease(pending.lease);
245
+ }
246
+ catch {
247
+ this.resourceQueue.splice(this.resourceQueue.indexOf(pending), 1);
248
+ if (pending.timer)
249
+ clearTimeout(pending.timer);
250
+ pending.reject(new AdapterSchedulerError('ADAPTER_LEASE_LOST', 'Adapter command lease was lost while waiting for resources'));
251
+ continue;
252
+ }
253
+ if (!pending.keys.every(key => !this.resourceOwners.has(key)))
254
+ continue;
255
+ this.resourceQueue.splice(this.resourceQueue.indexOf(pending), 1);
256
+ if (pending.timer)
257
+ clearTimeout(pending.timer);
258
+ const grant = {
259
+ grantId: crypto.randomUUID(),
260
+ leaseId: pending.lease.leaseId,
261
+ keys: pending.keys,
262
+ };
263
+ this.resourceGrants.set(grant.grantId, grant);
264
+ for (const key of grant.keys)
265
+ this.resourceOwners.set(key, { leaseId: grant.leaseId, grantId: grant.grantId });
266
+ pending.resolve({ ...grant, keys: [...grant.keys] });
267
+ }
268
+ }
269
+ releaseAllResourcesForLease(leaseId) {
270
+ for (const grant of [...this.resourceGrants.values()]) {
271
+ if (grant.leaseId !== leaseId)
272
+ continue;
273
+ this.resourceGrants.delete(grant.grantId);
274
+ for (const key of grant.keys) {
275
+ const owner = this.resourceOwners.get(key);
276
+ if (owner?.grantId === grant.grantId)
277
+ this.resourceOwners.delete(key);
278
+ }
279
+ }
280
+ }
281
+ pruneReleasedLeaseIds(now = this.now()) {
282
+ for (const [leaseId, expiresAt] of this.releasedLeaseIds) {
283
+ if (expiresAt <= now)
284
+ this.releasedLeaseIds.delete(leaseId);
285
+ }
286
+ }
287
+ rejectResourceWaitersForLease(leaseId) {
288
+ for (const pending of [...this.resourceQueue]) {
289
+ if (pending.lease.leaseId !== leaseId)
290
+ continue;
291
+ this.resourceQueue.splice(this.resourceQueue.indexOf(pending), 1);
292
+ if (pending.timer)
293
+ clearTimeout(pending.timer);
294
+ pending.reject(new AdapterSchedulerError('ADAPTER_LEASE_LOST', 'Adapter command lease ended while waiting for resources'));
295
+ }
296
+ }
297
+ requireLease(identity) {
298
+ const pool = this.pools.get(identity.poolKey);
299
+ const lease = pool?.running.get(identity.leaseId);
300
+ if (!lease
301
+ || lease.requestId !== identity.requestId
302
+ || lease.contextId !== identity.contextId
303
+ || lease.site !== identity.site
304
+ || lease.adapterSession !== identity.adapterSession
305
+ || lease.sessionKey !== identity.sessionKey
306
+ || lease.generation !== identity.generation) {
307
+ throw new AdapterSchedulerError('ADAPTER_LEASE_LOST', 'Adapter command lease is no longer valid');
308
+ }
309
+ return lease;
310
+ }
311
+ getOrCreatePool(request) {
312
+ const key = `${request.contextId}\u0000${request.surface}\u0000${request.site}`;
313
+ const existing = this.pools.get(key);
314
+ if (existing)
315
+ return existing;
316
+ const generation = (this.generations.get(key) ?? 0) + 1;
317
+ this.generations.set(key, generation);
318
+ const pool = {
319
+ key,
320
+ generation,
321
+ maxParallel: Math.min(request.maxParallel, this.runtimeCeiling),
322
+ running: new Map(),
323
+ activeSessions: new Set(),
324
+ queued: [],
325
+ };
326
+ this.pools.set(key, pool);
327
+ return pool;
328
+ }
329
+ removeDrainedPool(pool) {
330
+ if (pool.running.size === 0 && pool.queued.length === 0)
331
+ this.pools.delete(pool.key);
332
+ }
333
+ poolClosedError(reason) {
334
+ return reason === 'auth_gate'
335
+ ? new AdapterSchedulerError('ADAPTER_POOL_AUTH_GATE', 'Adapter pool stopped at an authentication or verification gate')
336
+ : new AdapterSchedulerError('ADAPTER_POOL_RATE_LIMITED', 'Adapter pool stopped after account rate limiting');
337
+ }
338
+ validateRequest(request) {
339
+ if (!request.requestId || !request.contextId || !request.site || !request.adapterSession || !request.sessionKey) {
340
+ throw new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Invalid Adapter lease request');
341
+ }
342
+ if (!Number.isInteger(request.queueTimeoutMs) || request.queueTimeoutMs < 1) {
343
+ throw new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Invalid Adapter queue timeout');
344
+ }
345
+ if (!Number.isInteger(request.maxParallel) || request.maxParallel < 1 || request.maxParallel > this.runtimeCeiling) {
346
+ throw new AdapterSchedulerError('ADAPTER_QUEUE_RESET', 'Invalid Adapter concurrency limit');
347
+ }
348
+ }
349
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Provides a typed send() function that posts a Command and returns a Result.
5
5
  */
6
+ import type { AdapterLease, AdapterLeaseRelease, AdapterLeaseRequest, AdapterResourceGrant } from '../adapter-scheduler.js';
6
7
  export interface DaemonCommand {
7
8
  id: string;
8
9
  action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'set-file-input' | 'insert-text' | 'bind' | 'network-capture-start' | 'network-capture-read' | 'ima-auth-start' | 'ima-auth-read' | 'ima-reader-request' | 'ima-auth-release' | 'wait-download' | 'cdp' | 'frames';
@@ -47,6 +48,8 @@ export interface DaemonCommand {
47
48
  frameIndex?: number;
48
49
  /** Browser profile/context to route the command to. */
49
50
  contextId?: string;
51
+ /** Active Adapter lease used by the daemon to fence every browser action. */
52
+ adapterLease?: AdapterLease;
50
53
  }
51
54
  export interface DaemonResult {
52
55
  id: string;
@@ -107,6 +110,12 @@ export type DaemonStatusProbe = {
107
110
  } | {
108
111
  kind: 'network_error';
109
112
  };
113
+ export declare function acquireAdapterLease(request: AdapterLeaseRequest): Promise<AdapterLease>;
114
+ export declare function heartbeatAdapterLease(lease: AdapterLease): Promise<AdapterLease>;
115
+ export declare function releaseAdapterLease(release: AdapterLeaseRelease): Promise<boolean>;
116
+ export declare function cancelAdapterLease(requestId: string): Promise<boolean>;
117
+ export declare function acquireAdapterResources(lease: AdapterLease, keys: string[], timeoutMs: number): Promise<AdapterResourceGrant>;
118
+ export declare function releaseAdapterResources(lease: AdapterLease, grantId: string): Promise<boolean>;
110
119
  export declare function probeDaemonStatus(opts?: {
111
120
  timeout?: number;
112
121
  contextId?: string;
@@ -139,6 +148,8 @@ export declare function getDaemonHealth(opts?: {
139
148
  timeout?: number;
140
149
  contextId?: string;
141
150
  }): Promise<DaemonHealth>;
151
+ /** Resolve the concrete daemon profile used to key Adapter scheduler pools. */
152
+ export declare function resolveAdapterLeaseContextId(requestedContextId?: string): Promise<string>;
142
153
  export declare function requestDaemonShutdown(opts?: {
143
154
  timeout?: number;
144
155
  }): Promise<boolean>;