@mearl/client 2.20.6 → 2.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -2
- package/dist/cli.js +10 -5
- package/dist/commands.d.ts +14 -5
- package/dist/commands.js +21 -2
- package/dist/generated-browser-action-protocol.d.ts +11 -1
- package/dist/generated-browser-action-protocol.js +3 -0
- package/dist/generated-plugin-diagnostics.d.ts +110 -0
- package/dist/generated-plugin-diagnostics.js +1 -0
- package/dist/generated-plugin-execution.d.ts +27 -0
- package/dist/generated-plugin-execution.js +1 -0
- package/dist/generated-plugin-models.d.ts +64 -0
- package/dist/generated-plugin-models.js +1 -0
- package/dist/generated-plugin-pages.d.ts +76 -0
- package/dist/generated-plugin-pages.js +59 -0
- package/dist/generated-plugin-recovery.d.ts +32 -0
- package/dist/generated-plugin-recovery.js +1 -0
- package/dist/generated-plugin-state.d.ts +50 -0
- package/dist/generated-plugin-state.js +1 -0
- package/dist/generated-plugin-tasks.d.ts +74 -0
- package/dist/generated-plugin-tasks.js +7 -0
- package/dist/generated-toolbox-icon.d.ts +10 -0
- package/dist/generated-toolbox-icon.js +37 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +6 -0
- package/dist/plugin-task-host.d.ts +1 -0
- package/dist/plugin-task-host.js +14 -0
- package/dist/plugin-worker.d.ts +1 -0
- package/dist/plugin-worker.js +17 -0
- package/dist/plugin.d.ts +57 -4
- package/dist/plugin.js +296 -15
- package/dist/pluginCliOutput.d.ts +2 -0
- package/dist/pluginCliOutput.js +7 -0
- package/dist/pluginCommandWorker.d.ts +2 -0
- package/dist/pluginCommandWorker.js +174 -0
- package/dist/pluginDiagnostics.d.ts +26 -0
- package/dist/pluginDiagnostics.js +309 -0
- package/dist/pluginExecution.d.ts +21 -0
- package/dist/pluginExecution.js +43 -0
- package/dist/pluginExecutor.d.ts +29 -0
- package/dist/pluginExecutor.js +223 -0
- package/dist/pluginExecutorProtocol.d.ts +55 -0
- package/dist/pluginExecutorProtocol.js +47 -0
- package/dist/pluginExecutorSupervisor.d.ts +4 -0
- package/dist/pluginExecutorSupervisor.js +63 -0
- package/dist/pluginHost.d.ts +20 -0
- package/dist/pluginHost.js +53 -0
- package/dist/pluginHostConnection.d.ts +35 -0
- package/dist/pluginHostConnection.js +257 -0
- package/dist/pluginHostFiles.d.ts +1 -0
- package/dist/pluginHostFiles.js +21 -0
- package/dist/pluginHostPaths.d.ts +11 -0
- package/dist/pluginHostPaths.js +22 -0
- package/dist/pluginHostServer.d.ts +7 -0
- package/dist/pluginHostServer.js +252 -0
- package/dist/pluginModelCatalog.d.ts +14 -0
- package/dist/pluginModelCatalog.js +51 -0
- package/dist/pluginModelDefinition.d.ts +6 -0
- package/dist/pluginModelDefinition.js +75 -0
- package/dist/pluginModelSessionWorker.d.ts +23 -0
- package/dist/pluginModelSessionWorker.js +60 -0
- package/dist/pluginModelSessions.d.ts +16 -0
- package/dist/pluginModelSessions.js +234 -0
- package/dist/pluginModels.d.ts +17 -0
- package/dist/pluginModels.js +47 -0
- package/dist/pluginPageBundle.js +74 -17
- package/dist/pluginPageDefinition.d.ts +3 -0
- package/dist/pluginPageDefinition.js +62 -0
- package/dist/pluginPageManager.d.ts +57 -0
- package/dist/pluginPageManager.js +877 -0
- package/dist/pluginPages.d.ts +21 -0
- package/dist/pluginPages.js +46 -0
- package/dist/pluginProcessTree.d.ts +2 -0
- package/dist/pluginProcessTree.js +31 -0
- package/dist/pluginRecoveryDefinition.d.ts +3 -0
- package/dist/pluginRecoveryDefinition.js +56 -0
- package/dist/pluginRegistry.d.ts +2 -3
- package/dist/pluginRegistry.js +14 -13
- package/dist/pluginRevision.d.ts +14 -0
- package/dist/pluginRevision.js +227 -0
- package/dist/pluginRuntimeCleanup.d.ts +2 -0
- package/dist/pluginRuntimeCleanup.js +55 -0
- package/dist/pluginState.d.ts +26 -0
- package/dist/pluginState.js +28 -0
- package/dist/pluginStateDefinition.d.ts +4 -0
- package/dist/pluginStateDefinition.js +68 -0
- package/dist/pluginStateManager.d.ts +24 -0
- package/dist/pluginStateManager.js +132 -0
- package/dist/pluginStateStore.d.ts +57 -0
- package/dist/pluginStateStore.js +396 -0
- package/dist/pluginTaskManager.d.ts +92 -0
- package/dist/pluginTaskManager.js +1080 -0
- package/dist/pluginTaskProtocol.d.ts +47 -0
- package/dist/pluginTaskProtocol.js +1 -0
- package/dist/pluginTaskStore.d.ts +53 -0
- package/dist/pluginTaskStore.js +319 -0
- package/dist/pluginTaskWire.d.ts +4 -0
- package/dist/pluginTaskWire.js +42 -0
- package/dist/pluginTasks.d.ts +51 -0
- package/dist/pluginTasks.js +181 -0
- package/dist/pythonPlugin.d.ts +13 -2
- package/dist/pythonPlugin.js +235 -30
- package/dist/socket.js +5 -2
- package/dist/unified.d.ts +1 -0
- package/dist/unified.js +81 -0
- package/package.json +7 -3
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { PluginExecutionError, PluginInvocationSource } from './generated-plugin-execution.js';
|
|
2
|
+
import type { PluginOptions } from './plugin.js';
|
|
3
|
+
export interface PluginTaskScope {
|
|
4
|
+
access: 'public' | 'toolbox' | 'runtime';
|
|
5
|
+
plugin?: string;
|
|
6
|
+
source: PluginInvocationSource;
|
|
7
|
+
bridge: 'service' | 'caller';
|
|
8
|
+
scopeId?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface PluginTaskStartRequest {
|
|
11
|
+
plugin: string;
|
|
12
|
+
command: string;
|
|
13
|
+
kind?: 'command' | 'model';
|
|
14
|
+
input: Record<string, unknown>;
|
|
15
|
+
cwd: string;
|
|
16
|
+
environment: Record<string, string>;
|
|
17
|
+
options: PluginOptions;
|
|
18
|
+
executionTimeoutMs?: number;
|
|
19
|
+
idempotencyKey?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface PluginTaskResumeRequest {
|
|
22
|
+
taskId: string;
|
|
23
|
+
environment: Record<string, string>;
|
|
24
|
+
revision?: 'original' | 'current';
|
|
25
|
+
}
|
|
26
|
+
export type PluginTaskWireMessage = {
|
|
27
|
+
type: 'request';
|
|
28
|
+
id: number;
|
|
29
|
+
method: string;
|
|
30
|
+
input: Record<string, unknown>;
|
|
31
|
+
} | {
|
|
32
|
+
type: 'response';
|
|
33
|
+
id: number;
|
|
34
|
+
result?: unknown;
|
|
35
|
+
error?: PluginExecutionError;
|
|
36
|
+
} | {
|
|
37
|
+
type: 'invoke';
|
|
38
|
+
id: number;
|
|
39
|
+
action: string;
|
|
40
|
+
payload: Record<string, unknown>;
|
|
41
|
+
options: PluginOptions;
|
|
42
|
+
} | {
|
|
43
|
+
type: 'invoke-result';
|
|
44
|
+
id: number;
|
|
45
|
+
result?: unknown;
|
|
46
|
+
error?: PluginExecutionError;
|
|
47
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PluginTaskDiagnosticMetadata } from './generated-plugin-diagnostics.js';
|
|
2
|
+
import type { PluginTask, PluginTaskEvent, PluginTaskEvents, PluginTaskResultChunk } from './generated-plugin-tasks.js';
|
|
3
|
+
import type { PluginTaskCheckpoint } from './generated-plugin-recovery.js';
|
|
4
|
+
import type { PluginOptions } from './plugin.js';
|
|
5
|
+
export declare const PLUGIN_TASK_RESULT_CHUNK_BYTES: number;
|
|
6
|
+
export interface PluginTaskRecord {
|
|
7
|
+
task: PluginTask;
|
|
8
|
+
events: PluginTaskEvent[];
|
|
9
|
+
dedupeKey?: string;
|
|
10
|
+
requestHash?: string;
|
|
11
|
+
access: 'public' | 'toolbox' | 'runtime';
|
|
12
|
+
checkpoint?: PluginTaskCheckpoint;
|
|
13
|
+
diagnostics?: PluginTaskDiagnosticMetadata;
|
|
14
|
+
execution?: {
|
|
15
|
+
entry: string;
|
|
16
|
+
options: PluginOptions;
|
|
17
|
+
bridge: 'service' | 'caller';
|
|
18
|
+
executionTimeoutMs?: number;
|
|
19
|
+
};
|
|
20
|
+
executorOwner?: {
|
|
21
|
+
pid: number;
|
|
22
|
+
processIdentity?: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export declare function pluginTaskExecutionUnconfirmed(record: PluginTaskRecord): boolean;
|
|
26
|
+
export declare class PluginTaskStore {
|
|
27
|
+
readonly directory: string;
|
|
28
|
+
private activeTaskIds?;
|
|
29
|
+
private dedupeIndex?;
|
|
30
|
+
private readonly dedupeKeys;
|
|
31
|
+
constructor(directory?: string);
|
|
32
|
+
private forgetDedupe;
|
|
33
|
+
private indexDedupe;
|
|
34
|
+
/** The service owns writes; the index retains identities and reads the current matching record. */
|
|
35
|
+
findDedupe(key: string, activeOnly?: boolean): PluginTaskRecord | undefined;
|
|
36
|
+
private path;
|
|
37
|
+
read(taskId: string): PluginTaskRecord;
|
|
38
|
+
write(record: PluginTaskRecord): void;
|
|
39
|
+
private indexActive;
|
|
40
|
+
/** The service owns writes; only active identities remain indexed in memory. */
|
|
41
|
+
listActive(): PluginTaskRecord[];
|
|
42
|
+
update(taskId: string, update: Partial<PluginTask>, type?: PluginTaskEvent['type'], fields?: Pick<Partial<PluginTaskRecord>, 'checkpoint' | 'diagnostics'>): PluginTask;
|
|
43
|
+
list(): PluginTaskRecord[];
|
|
44
|
+
diagnosticRecords(taskId?: string): {
|
|
45
|
+
records: PluginTaskRecord[];
|
|
46
|
+
limited: boolean;
|
|
47
|
+
};
|
|
48
|
+
events(taskId: string, cursor: number): PluginTaskEvents;
|
|
49
|
+
saveResult(taskId: string, value: unknown): PluginTask['result'];
|
|
50
|
+
resultChunk(taskId: string, index: number): PluginTaskResultChunk;
|
|
51
|
+
recover(epoch: string): void;
|
|
52
|
+
prune(now?: number): void;
|
|
53
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { closeSync, openSync, opendirSync, readFileSync, readSync, readdirSync, rmSync, statSync, } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { isPluginTaskActive, isPluginTaskId } from './generated-plugin-tasks.js';
|
|
5
|
+
import { writePluginHostFile } from './pluginHostFiles.js';
|
|
6
|
+
import { pluginHostPaths } from './pluginHostPaths.js';
|
|
7
|
+
import { isProcessGroupAlive, isProcessOwnerAlive } from '@mearl/daemon-core';
|
|
8
|
+
export const PLUGIN_TASK_RESULT_CHUNK_BYTES = 192 * 1024;
|
|
9
|
+
const MAX_EVENT_BYTES = 512 * 1024;
|
|
10
|
+
const MAX_RESULT_BYTES = 64 * 1024 * 1024;
|
|
11
|
+
const RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
12
|
+
export function pluginTaskExecutionUnconfirmed(record) {
|
|
13
|
+
const owner = record.executorOwner;
|
|
14
|
+
if (owner && (!Number.isSafeInteger(owner.pid) || owner.pid < 1))
|
|
15
|
+
return true;
|
|
16
|
+
if ((owner && isProcessOwnerAlive(owner)) ||
|
|
17
|
+
record.task.error?.details?.executionStopped === false ||
|
|
18
|
+
(process.platform === 'win32' && record.task.error?.code === 'PLUGIN_TASK_HOST_RESTARTED'))
|
|
19
|
+
return true;
|
|
20
|
+
if (owner && process.platform !== 'win32')
|
|
21
|
+
return isProcessGroupAlive(owner.pid);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
export class PluginTaskStore {
|
|
25
|
+
directory;
|
|
26
|
+
activeTaskIds;
|
|
27
|
+
dedupeIndex;
|
|
28
|
+
dedupeKeys = new Map();
|
|
29
|
+
constructor(directory = pluginHostPaths().tasks) {
|
|
30
|
+
this.directory = directory;
|
|
31
|
+
}
|
|
32
|
+
forgetDedupe(taskId) {
|
|
33
|
+
const key = this.dedupeKeys.get(taskId);
|
|
34
|
+
if (!key)
|
|
35
|
+
return;
|
|
36
|
+
const ids = this.dedupeIndex?.get(key);
|
|
37
|
+
ids?.delete(taskId);
|
|
38
|
+
if (!ids?.size)
|
|
39
|
+
this.dedupeIndex?.delete(key);
|
|
40
|
+
this.dedupeKeys.delete(taskId);
|
|
41
|
+
}
|
|
42
|
+
indexDedupe(record) {
|
|
43
|
+
if (!this.dedupeIndex)
|
|
44
|
+
return;
|
|
45
|
+
const { dedupeKey: key, task } = record;
|
|
46
|
+
const keep = key && (!key.startsWith('active:') || isPluginTaskActive(task.state));
|
|
47
|
+
if (keep && this.dedupeKeys.get(task.taskId) === key)
|
|
48
|
+
return;
|
|
49
|
+
this.forgetDedupe(task.taskId);
|
|
50
|
+
if (!keep)
|
|
51
|
+
return;
|
|
52
|
+
const ids = this.dedupeIndex.get(key) ?? new Set();
|
|
53
|
+
ids.add(task.taskId);
|
|
54
|
+
this.dedupeIndex.set(key, ids);
|
|
55
|
+
this.dedupeKeys.set(task.taskId, key);
|
|
56
|
+
}
|
|
57
|
+
/** The service owns writes; the index retains identities and reads the current matching record. */
|
|
58
|
+
findDedupe(key, activeOnly = false) {
|
|
59
|
+
if (!this.dedupeIndex) {
|
|
60
|
+
this.dedupeIndex = new Map();
|
|
61
|
+
for (const record of this.list())
|
|
62
|
+
this.indexDedupe(record);
|
|
63
|
+
}
|
|
64
|
+
for (const taskId of this.dedupeIndex.get(key) ?? []) {
|
|
65
|
+
let record;
|
|
66
|
+
try {
|
|
67
|
+
record = this.read(taskId);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error.code !== 'PLUGIN_TASK_NOT_FOUND')
|
|
71
|
+
throw error;
|
|
72
|
+
this.forgetDedupe(taskId);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
this.indexDedupe(record);
|
|
76
|
+
if (record.dedupeKey === key && (!activeOnly || isPluginTaskActive(record.task.state)))
|
|
77
|
+
return record;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
path(taskId, file) {
|
|
82
|
+
if (!isPluginTaskId(taskId))
|
|
83
|
+
throw Object.assign(new Error('Invalid plugin task ID'), { code: 'PLUGIN_TASK_INVALID_ID' });
|
|
84
|
+
return join(this.directory, taskId, file);
|
|
85
|
+
}
|
|
86
|
+
read(taskId) {
|
|
87
|
+
let record;
|
|
88
|
+
try {
|
|
89
|
+
record = JSON.parse(readFileSync(this.path(taskId, 'task.json'), 'utf8'));
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error.code === 'ENOENT')
|
|
93
|
+
throw Object.assign(new Error('Plugin task was not found'), {
|
|
94
|
+
code: 'PLUGIN_TASK_NOT_FOUND',
|
|
95
|
+
});
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
if (record?.task?.taskId !== taskId || !Array.isArray(record.events))
|
|
99
|
+
throw new Error('Invalid persistent plugin task record');
|
|
100
|
+
return record;
|
|
101
|
+
}
|
|
102
|
+
write(record) {
|
|
103
|
+
writePluginHostFile(this.path(record.task.taskId, 'task.json'), JSON.stringify(record));
|
|
104
|
+
this.indexDedupe(record);
|
|
105
|
+
this.indexActive(record);
|
|
106
|
+
}
|
|
107
|
+
indexActive(record) {
|
|
108
|
+
if (isPluginTaskActive(record.task.state))
|
|
109
|
+
this.activeTaskIds?.add(record.task.taskId);
|
|
110
|
+
else
|
|
111
|
+
this.activeTaskIds?.delete(record.task.taskId);
|
|
112
|
+
}
|
|
113
|
+
/** The service owns writes; only active identities remain indexed in memory. */
|
|
114
|
+
listActive() {
|
|
115
|
+
if (!this.activeTaskIds) {
|
|
116
|
+
this.activeTaskIds = new Set();
|
|
117
|
+
for (const record of this.list())
|
|
118
|
+
this.indexActive(record);
|
|
119
|
+
}
|
|
120
|
+
const records = [];
|
|
121
|
+
for (const taskId of this.activeTaskIds) {
|
|
122
|
+
try {
|
|
123
|
+
const record = this.read(taskId);
|
|
124
|
+
this.indexActive(record);
|
|
125
|
+
if (isPluginTaskActive(record.task.state))
|
|
126
|
+
records.push(record);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (error.code !== 'PLUGIN_TASK_NOT_FOUND')
|
|
130
|
+
throw error;
|
|
131
|
+
this.activeTaskIds.delete(taskId);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return records;
|
|
135
|
+
}
|
|
136
|
+
update(taskId, update, type = 'state', fields = {}) {
|
|
137
|
+
const record = this.read(taskId);
|
|
138
|
+
Object.assign(record, fields);
|
|
139
|
+
record.task = {
|
|
140
|
+
...record.task,
|
|
141
|
+
...update,
|
|
142
|
+
sequence: record.task.sequence + 1,
|
|
143
|
+
updatedAt: Date.now(),
|
|
144
|
+
};
|
|
145
|
+
const event = {
|
|
146
|
+
sequence: record.task.sequence,
|
|
147
|
+
time: record.task.updatedAt,
|
|
148
|
+
type,
|
|
149
|
+
state: record.task.state,
|
|
150
|
+
...(type === 'progress' ? { progress: record.task.progress } : {}),
|
|
151
|
+
...(type === 'checkpoint' ? { checkpoint: record.task.checkpoint } : {}),
|
|
152
|
+
};
|
|
153
|
+
record.events.push(event);
|
|
154
|
+
let bytes = Buffer.byteLength(JSON.stringify(record.events));
|
|
155
|
+
while (record.events.length > 256 || (bytes > MAX_EVENT_BYTES && record.events.length > 1)) {
|
|
156
|
+
bytes -= Buffer.byteLength(JSON.stringify(record.events.shift())) + 1;
|
|
157
|
+
}
|
|
158
|
+
this.write(record);
|
|
159
|
+
return record.task;
|
|
160
|
+
}
|
|
161
|
+
list() {
|
|
162
|
+
let names;
|
|
163
|
+
try {
|
|
164
|
+
names = readdirSync(this.directory);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (error.code === 'ENOENT')
|
|
168
|
+
return [];
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
return names.filter(isPluginTaskId).map(name => this.read(name));
|
|
172
|
+
}
|
|
173
|
+
diagnosticRecords(taskId) {
|
|
174
|
+
const records = [];
|
|
175
|
+
let bytes = 0, scanned = 0, limited = false;
|
|
176
|
+
if (taskId) {
|
|
177
|
+
let size;
|
|
178
|
+
try {
|
|
179
|
+
size = statSync(this.path(taskId, 'task.json')).size;
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (error.code === 'ENOENT')
|
|
183
|
+
return { records, limited };
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
if (size > 16 * 1024 * 1024)
|
|
187
|
+
return { records, limited: true };
|
|
188
|
+
records.push(this.read(taskId));
|
|
189
|
+
bytes += size;
|
|
190
|
+
}
|
|
191
|
+
let directory;
|
|
192
|
+
try {
|
|
193
|
+
directory = opendirSync(this.directory);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
if (error.code === 'ENOENT')
|
|
197
|
+
return { records, limited };
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
let item;
|
|
202
|
+
while ((item = directory.readSync()) !== null) {
|
|
203
|
+
if (++scanned > 2048) {
|
|
204
|
+
limited = true;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
if (!isPluginTaskId(item.name) || item.name === taskId)
|
|
208
|
+
continue;
|
|
209
|
+
try {
|
|
210
|
+
const size = statSync(this.path(item.name, 'task.json')).size;
|
|
211
|
+
if (records.length >= 512 || bytes + size > 16 * 1024 * 1024) {
|
|
212
|
+
limited = true;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
bytes += size;
|
|
216
|
+
records.push(this.read(item.name));
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
limited = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
directory.closeSync();
|
|
225
|
+
}
|
|
226
|
+
return { records, limited };
|
|
227
|
+
}
|
|
228
|
+
events(taskId, cursor) {
|
|
229
|
+
const record = this.read(taskId);
|
|
230
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0 || cursor > record.task.sequence)
|
|
231
|
+
throw new Error('Invalid plugin task event cursor');
|
|
232
|
+
const first = record.events[0]?.sequence ?? record.task.sequence + 1;
|
|
233
|
+
const events = [];
|
|
234
|
+
let bytes = 0;
|
|
235
|
+
for (const event of record.events) {
|
|
236
|
+
if (event.sequence <= cursor)
|
|
237
|
+
continue;
|
|
238
|
+
const size = Buffer.byteLength(JSON.stringify(event));
|
|
239
|
+
if (bytes + size > 192 * 1024 && events.length)
|
|
240
|
+
break;
|
|
241
|
+
events.push(event);
|
|
242
|
+
bytes += size;
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
task: record.task,
|
|
246
|
+
events,
|
|
247
|
+
firstAvailableSequence: first,
|
|
248
|
+
nextCursor: events.at(-1)?.sequence ?? cursor,
|
|
249
|
+
gap: cursor < first - 1,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
saveResult(taskId, value) {
|
|
253
|
+
const bytes = Buffer.from(JSON.stringify({ value }));
|
|
254
|
+
if (bytes.length > MAX_RESULT_BYTES)
|
|
255
|
+
throw Object.assign(new Error('Plugin task result exceeds 64 MiB'), {
|
|
256
|
+
code: 'PLUGIN_TASK_RESULT_TOO_LARGE',
|
|
257
|
+
});
|
|
258
|
+
writePluginHostFile(this.path(taskId, 'result.json'), bytes);
|
|
259
|
+
return {
|
|
260
|
+
bytes: bytes.length,
|
|
261
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
262
|
+
chunks: Math.ceil(bytes.length / PLUGIN_TASK_RESULT_CHUNK_BYTES),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
resultChunk(taskId, index) {
|
|
266
|
+
const task = this.read(taskId).task;
|
|
267
|
+
if (task.state !== 'success' || !task.result)
|
|
268
|
+
throw new Error('Plugin task result is not available');
|
|
269
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= task.result.chunks)
|
|
270
|
+
throw new Error('Invalid plugin task result chunk');
|
|
271
|
+
const file = this.path(taskId, 'result.json');
|
|
272
|
+
if (statSync(file).size !== task.result.bytes)
|
|
273
|
+
throw new Error('Plugin task result size does not match its record');
|
|
274
|
+
const offset = index * PLUGIN_TASK_RESULT_CHUNK_BYTES;
|
|
275
|
+
const buffer = Buffer.alloc(Math.min(PLUGIN_TASK_RESULT_CHUNK_BYTES, task.result.bytes - offset));
|
|
276
|
+
const descriptor = openSync(file, 'r');
|
|
277
|
+
try {
|
|
278
|
+
if (readSync(descriptor, buffer, 0, buffer.length, offset) !== buffer.length)
|
|
279
|
+
throw new Error('Plugin task result is incomplete');
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
closeSync(descriptor);
|
|
283
|
+
}
|
|
284
|
+
return { taskId, index, data: buffer.toString('base64'), ...task.result };
|
|
285
|
+
}
|
|
286
|
+
recover(epoch) {
|
|
287
|
+
this.activeTaskIds = new Set();
|
|
288
|
+
for (const record of this.list()) {
|
|
289
|
+
this.indexActive(record);
|
|
290
|
+
const { task } = record;
|
|
291
|
+
if (isPluginTaskActive(task.state) && task.host.epoch !== epoch) {
|
|
292
|
+
this.update(task.taskId, {
|
|
293
|
+
state: 'interrupted',
|
|
294
|
+
error: {
|
|
295
|
+
code: 'PLUGIN_TASK_HOST_RESTARTED',
|
|
296
|
+
message: 'The plugin execution service restarted; external operations may need reconciliation',
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
else if (!isPluginTaskActive(task.state) &&
|
|
301
|
+
Date.now() - task.updatedAt > RETENTION_MS &&
|
|
302
|
+
!pluginTaskExecutionUnconfirmed(record)) {
|
|
303
|
+
rmSync(join(this.directory, task.taskId), { recursive: true, force: true });
|
|
304
|
+
this.forgetDedupe(task.taskId);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
prune(now = Date.now()) {
|
|
309
|
+
for (const record of this.list()) {
|
|
310
|
+
const { task } = record;
|
|
311
|
+
if (!isPluginTaskActive(task.state) &&
|
|
312
|
+
now - task.updatedAt > RETENTION_MS &&
|
|
313
|
+
!pluginTaskExecutionUnconfirmed(record)) {
|
|
314
|
+
rmSync(join(this.directory, task.taskId), { recursive: true, force: true });
|
|
315
|
+
this.forgetDedupe(task.taskId);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Socket } from 'node:net';
|
|
2
|
+
import type { PluginTaskWireMessage } from './pluginTaskProtocol.js';
|
|
3
|
+
export declare function readPluginTaskMessages(socket: Socket, receive: (message: PluginTaskWireMessage) => void): void;
|
|
4
|
+
export declare function writePluginTaskMessage(socket: Socket, message: PluginTaskWireMessage): Promise<void>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
2
|
+
const MAX_MESSAGE_BYTES = 96 * 1024 * 1024;
|
|
3
|
+
export function readPluginTaskMessages(socket, receive) {
|
|
4
|
+
const decoder = new StringDecoder('utf8');
|
|
5
|
+
let buffer = '';
|
|
6
|
+
socket.on('data', chunk => {
|
|
7
|
+
try {
|
|
8
|
+
buffer += decoder.write(chunk);
|
|
9
|
+
let end;
|
|
10
|
+
while ((end = buffer.indexOf('\n')) >= 0) {
|
|
11
|
+
const line = buffer.slice(0, end);
|
|
12
|
+
buffer = buffer.slice(end + 1);
|
|
13
|
+
if (Buffer.byteLength(line) > MAX_MESSAGE_BYTES)
|
|
14
|
+
throw new Error('Plugin host message is too large');
|
|
15
|
+
const message = JSON.parse(line);
|
|
16
|
+
if (!message || typeof message !== 'object' || !Number.isSafeInteger(message.id))
|
|
17
|
+
throw new Error('Invalid plugin host message');
|
|
18
|
+
receive(message);
|
|
19
|
+
}
|
|
20
|
+
if (Buffer.byteLength(buffer) > MAX_MESSAGE_BYTES)
|
|
21
|
+
throw new Error('Plugin host message buffer exceeded its limit');
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
socket.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function writePluginTaskMessage(socket, message) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
try {
|
|
31
|
+
const text = JSON.stringify(message) + '\n';
|
|
32
|
+
if (Buffer.byteLength(text) > MAX_MESSAGE_BYTES)
|
|
33
|
+
throw new Error('Plugin host message is too large');
|
|
34
|
+
if (socket.destroyed)
|
|
35
|
+
throw new Error('Plugin host connection is closed');
|
|
36
|
+
socket.write(text, error => (error ? reject(error) : resolve()));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
reject(error);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { PluginHostConnection } from './pluginHostConnection.js';
|
|
2
|
+
import type { PluginHostClientOptions, HostStatus as PluginHostStatus } from './pluginHostConnection.js';
|
|
3
|
+
import type { PluginTaskCheckpoint } from './generated-plugin-recovery.js';
|
|
4
|
+
import type { PluginDiagnosticOptions, PluginDiagnosticReport } from './generated-plugin-diagnostics.js';
|
|
5
|
+
import type { PluginTask, PluginTaskEvents, PluginTaskListResult, PluginTaskResultChunk, PluginTaskStartResult } from './generated-plugin-tasks.js';
|
|
6
|
+
import type { PluginInvocation, PluginOptions } from './plugin.js';
|
|
7
|
+
export type { PluginTask, PluginTaskEvents, PluginTaskListResult, PluginTaskStartResult };
|
|
8
|
+
export interface PluginTaskStartOptions extends PluginOptions {
|
|
9
|
+
cwd?: string;
|
|
10
|
+
environment?: NodeJS.ProcessEnv;
|
|
11
|
+
executionTimeoutMs?: number;
|
|
12
|
+
idempotencyKey?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface PluginTaskWaitOptions {
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
readResult?: boolean;
|
|
18
|
+
onProgress?: (progress: PluginTask['progress'], task: PluginTask) => void;
|
|
19
|
+
}
|
|
20
|
+
export type PluginTaskClientOptions = PluginHostClientOptions;
|
|
21
|
+
/** Background execution stays on this host; disconnecting a caller never cancels its tasks. */
|
|
22
|
+
export declare class PluginTaskClient {
|
|
23
|
+
private readonly configuration;
|
|
24
|
+
readonly connection: PluginHostConnection;
|
|
25
|
+
constructor(configuration?: PluginTaskClientOptions);
|
|
26
|
+
private request;
|
|
27
|
+
status(): Promise<PluginHostStatus>;
|
|
28
|
+
diagnose(options?: PluginDiagnosticOptions): Promise<PluginDiagnosticReport>;
|
|
29
|
+
checkpoint(taskId: string): Promise<PluginTaskCheckpoint | null>;
|
|
30
|
+
resume(taskId: string, options?: {
|
|
31
|
+
environment?: NodeJS.ProcessEnv;
|
|
32
|
+
revision?: 'original' | 'current';
|
|
33
|
+
}): Promise<PluginTaskStartResult>;
|
|
34
|
+
start(plugin: string, invocation: PluginInvocation, options?: PluginTaskStartOptions): Promise<PluginTaskStartResult>;
|
|
35
|
+
get(taskId: string): Promise<PluginTask>;
|
|
36
|
+
list(activeOnly?: boolean, page?: {
|
|
37
|
+
cursor?: string;
|
|
38
|
+
limit?: number;
|
|
39
|
+
}): Promise<PluginTaskListResult>;
|
|
40
|
+
cancel(taskId: string): Promise<PluginTask>;
|
|
41
|
+
events(taskId: string, cursor?: number, waitMs?: number): Promise<PluginTaskEvents>;
|
|
42
|
+
resultChunk(taskId: string, index: number): Promise<PluginTaskResultChunk>;
|
|
43
|
+
result(taskId: string): Promise<unknown>;
|
|
44
|
+
subscribe(taskId: string, options?: {
|
|
45
|
+
cursor?: number;
|
|
46
|
+
signal?: AbortSignal;
|
|
47
|
+
}): AsyncGenerator<PluginTaskEvents>;
|
|
48
|
+
wait(taskId: string, options?: PluginTaskWaitOptions): Promise<unknown>;
|
|
49
|
+
stopHost(): Promise<void>;
|
|
50
|
+
disconnect(): void;
|
|
51
|
+
}
|