opencode-supertask 0.1.26 → 0.1.27

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.
@@ -0,0 +1,9 @@
1
+ interface ProcessGroupDrainOptions {
2
+ probe?: () => Promise<boolean | null>;
3
+ delay?: (milliseconds: number) => Promise<void>;
4
+ initialDelayMs?: number;
5
+ maxDelayMs?: number;
6
+ }
7
+ declare function waitForProcessGroupDrain(options?: ProcessGroupDrainOptions): Promise<void>;
8
+
9
+ export { type ProcessGroupDrainOptions, waitForProcessGroupDrain };
@@ -0,0 +1,164 @@
1
+ // src/worker/launcher.ts
2
+ import { spawn } from "child_process";
3
+
4
+ // src/core/launch-protocol.ts
5
+ var LAUNCH_IDENTITY_ARGUMENT = "--supertask-launch-identity";
6
+ var DRAIN_PROOF_MESSAGE_TYPE = "supertask-drained";
7
+ function isLaunchIdentity(value) {
8
+ return value != null && /^gateway-[1-9]\d*:launch:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
9
+ }
10
+ function drainProofForIdentity(launchIdentity) {
11
+ return { type: DRAIN_PROOF_MESSAGE_TYPE, identity: launchIdentity };
12
+ }
13
+
14
+ // src/worker/launcher.ts
15
+ var RELEASE_MESSAGE = "START";
16
+ var INITIAL_GROUP_PROBE_DELAY_MS = 1e3;
17
+ var MAX_GROUP_PROBE_DELAY_MS = 5e3;
18
+ var GROUP_PROBE_TIMEOUT_MS = 2e3;
19
+ var MAX_PS_OUTPUT_CHARS = 4 * 1024 * 1024;
20
+ function waitForRelease() {
21
+ return new Promise((resolve) => {
22
+ let settled = false;
23
+ let input = "";
24
+ const finish = (released) => {
25
+ if (settled) return;
26
+ settled = true;
27
+ process.stdin.removeAllListeners();
28
+ resolve(released);
29
+ };
30
+ process.stdin.setEncoding("utf8");
31
+ process.stdin.on("data", (chunk) => {
32
+ input += chunk;
33
+ const newline = input.indexOf("\n");
34
+ if (newline >= 0) finish(input.slice(0, newline).trim() === RELEASE_MESSAGE);
35
+ });
36
+ process.stdin.once("end", () => finish(false));
37
+ process.stdin.once("error", () => finish(false));
38
+ process.stdin.resume();
39
+ });
40
+ }
41
+ async function hasOtherProcessGroupMembers() {
42
+ return new Promise((resolve) => {
43
+ const inspection = spawn("ps", ["-axo", "pid=,pgid="], {
44
+ stdio: ["ignore", "pipe", "ignore"]
45
+ });
46
+ let output = "";
47
+ let settled = false;
48
+ const finish = (result) => {
49
+ if (settled) return;
50
+ settled = true;
51
+ clearTimeout(timeout);
52
+ resolve(result);
53
+ };
54
+ const timeout = setTimeout(() => {
55
+ inspection.kill("SIGKILL");
56
+ finish(null);
57
+ }, GROUP_PROBE_TIMEOUT_MS);
58
+ inspection.stdout.setEncoding("utf8");
59
+ inspection.stdout.on("data", (chunk) => {
60
+ output += chunk;
61
+ if (output.length <= MAX_PS_OUTPUT_CHARS) return;
62
+ inspection.kill("SIGKILL");
63
+ finish(null);
64
+ });
65
+ inspection.once("error", () => finish(null));
66
+ inspection.once("close", (code) => {
67
+ if (code !== 0) {
68
+ finish(null);
69
+ return;
70
+ }
71
+ const hasMembers = output.split("\n").some((line) => {
72
+ const match = line.trim().match(/^(\d+)\s+(\d+)$/);
73
+ if (!match) return false;
74
+ const pid = Number(match[1]);
75
+ const processGroupId = Number(match[2]);
76
+ return pid !== process.pid && pid !== inspection.pid && processGroupId === process.pid;
77
+ });
78
+ finish(hasMembers);
79
+ });
80
+ });
81
+ }
82
+ async function waitForProcessGroupDrain(options = {}) {
83
+ if (process.platform === "win32") return;
84
+ const probe = options.probe ?? hasOtherProcessGroupMembers;
85
+ const delay = options.delay ?? Bun.sleep;
86
+ const maxDelayMs = options.maxDelayMs ?? MAX_GROUP_PROBE_DELAY_MS;
87
+ let probeDelayMs = options.initialDelayMs ?? INITIAL_GROUP_PROBE_DELAY_MS;
88
+ while (true) {
89
+ const hasMembers = await probe();
90
+ if (hasMembers === false) return;
91
+ await delay(probeDelayMs);
92
+ probeDelayMs = Math.min(maxDelayMs, probeDelayMs * 2);
93
+ }
94
+ }
95
+ async function main() {
96
+ const launcherArgs = Bun.argv.slice(2);
97
+ let launchIdentity = null;
98
+ if (launcherArgs[0] === LAUNCH_IDENTITY_ARGUMENT) {
99
+ if (!isLaunchIdentity(launcherArgs[1])) {
100
+ console.error("supertask launcher: missing launch identity");
101
+ process.exitCode = 127;
102
+ return;
103
+ }
104
+ launchIdentity = launcherArgs[1];
105
+ launcherArgs.splice(0, 2);
106
+ }
107
+ const [executable, ...args] = launcherArgs;
108
+ if (!executable) {
109
+ console.error("supertask launcher: missing OpenCode executable");
110
+ process.exitCode = 127;
111
+ return;
112
+ }
113
+ if (!await waitForRelease()) {
114
+ process.exitCode = 125;
115
+ return;
116
+ }
117
+ const preserveGroupLeader = () => {
118
+ };
119
+ process.on("SIGTERM", preserveGroupLeader);
120
+ process.on("SIGINT", preserveGroupLeader);
121
+ let result;
122
+ try {
123
+ const child = spawn(executable, args, {
124
+ stdio: ["ignore", "pipe", "pipe"]
125
+ });
126
+ child.stdout?.pipe(process.stdout, { end: false });
127
+ child.stderr?.pipe(process.stderr, { end: false });
128
+ result = await new Promise((resolve) => {
129
+ child.once("error", (error) => {
130
+ console.error(`supertask launcher: ${error.message}`);
131
+ resolve({ code: 127, signal: null });
132
+ });
133
+ child.once("exit", (code, signal) => resolve({ code, signal }));
134
+ });
135
+ await waitForProcessGroupDrain();
136
+ if (launchIdentity) {
137
+ if (!process.send) throw new Error("supertask launcher: drain proof IPC unavailable");
138
+ await new Promise((resolve, reject) => {
139
+ process.send(drainProofForIdentity(launchIdentity), (error) => {
140
+ if (error) reject(error);
141
+ else resolve();
142
+ });
143
+ });
144
+ }
145
+ } finally {
146
+ process.off("SIGTERM", preserveGroupLeader);
147
+ process.off("SIGINT", preserveGroupLeader);
148
+ }
149
+ if (result.signal) {
150
+ try {
151
+ process.kill(process.pid, result.signal);
152
+ return;
153
+ } catch {
154
+ process.exitCode = 128;
155
+ return;
156
+ }
157
+ }
158
+ process.exitCode = result.code ?? 1;
159
+ }
160
+ if (import.meta.main) await main();
161
+ export {
162
+ waitForProcessGroupDrain
163
+ };
164
+ //# sourceMappingURL=launcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/worker/launcher.ts","../../src/core/launch-protocol.ts"],"sourcesContent":["import { spawn } from 'child_process';\nimport {\n drainProofForIdentity,\n isLaunchIdentity,\n LAUNCH_IDENTITY_ARGUMENT,\n} from '@core/launch-protocol';\n\nconst RELEASE_MESSAGE = 'START';\nconst INITIAL_GROUP_PROBE_DELAY_MS = 1_000;\nconst MAX_GROUP_PROBE_DELAY_MS = 5_000;\nconst GROUP_PROBE_TIMEOUT_MS = 2_000;\nconst MAX_PS_OUTPUT_CHARS = 4 * 1024 * 1024;\n\nfunction waitForRelease(): Promise<boolean> {\n return new Promise((resolve) => {\n let settled = false;\n let input = '';\n const finish = (released: boolean) => {\n if (settled) return;\n settled = true;\n process.stdin.removeAllListeners();\n resolve(released);\n };\n\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk: string) => {\n input += chunk;\n const newline = input.indexOf('\\n');\n if (newline >= 0) finish(input.slice(0, newline).trim() === RELEASE_MESSAGE);\n });\n process.stdin.once('end', () => finish(false));\n process.stdin.once('error', () => finish(false));\n process.stdin.resume();\n });\n}\n\nasync function hasOtherProcessGroupMembers(): Promise<boolean | null> {\n return new Promise((resolve) => {\n const inspection = spawn('ps', ['-axo', 'pid=,pgid='], {\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n let output = '';\n let settled = false;\n const finish = (result: boolean | null) => {\n if (settled) return;\n settled = true;\n clearTimeout(timeout);\n resolve(result);\n };\n const timeout = setTimeout(() => {\n inspection.kill('SIGKILL');\n finish(null);\n }, GROUP_PROBE_TIMEOUT_MS);\n\n inspection.stdout.setEncoding('utf8');\n inspection.stdout.on('data', (chunk: string) => {\n output += chunk;\n if (output.length <= MAX_PS_OUTPUT_CHARS) return;\n inspection.kill('SIGKILL');\n finish(null);\n });\n inspection.once('error', () => finish(null));\n inspection.once('close', (code) => {\n if (code !== 0) {\n finish(null);\n return;\n }\n const hasMembers = output.split('\\n').some((line) => {\n const match = line.trim().match(/^(\\d+)\\s+(\\d+)$/);\n if (!match) return false;\n const pid = Number(match[1]);\n const processGroupId = Number(match[2]);\n return pid !== process.pid\n && pid !== inspection.pid\n && processGroupId === process.pid;\n });\n finish(hasMembers);\n });\n });\n}\n\nexport interface ProcessGroupDrainOptions {\n probe?: () => Promise<boolean | null>;\n delay?: (milliseconds: number) => Promise<void>;\n initialDelayMs?: number;\n maxDelayMs?: number;\n}\n\nexport async function waitForProcessGroupDrain(\n options: ProcessGroupDrainOptions = {},\n): Promise<void> {\n if (process.platform === 'win32') return;\n\n const probe = options.probe ?? hasOtherProcessGroupMembers;\n const delay = options.delay ?? Bun.sleep;\n const maxDelayMs = options.maxDelayMs ?? MAX_GROUP_PROBE_DELAY_MS;\n let probeDelayMs = options.initialDelayMs ?? INITIAL_GROUP_PROBE_DELAY_MS;\n while (true) {\n const hasMembers = await probe();\n if (hasMembers === false) return;\n await delay(probeDelayMs);\n probeDelayMs = Math.min(maxDelayMs, probeDelayMs * 2);\n }\n}\n\nasync function main(): Promise<void> {\n const launcherArgs = Bun.argv.slice(2);\n let launchIdentity: string | null = null;\n if (launcherArgs[0] === LAUNCH_IDENTITY_ARGUMENT) {\n if (!isLaunchIdentity(launcherArgs[1])) {\n console.error('supertask launcher: missing launch identity');\n process.exitCode = 127;\n return;\n }\n launchIdentity = launcherArgs[1];\n launcherArgs.splice(0, 2);\n }\n const [executable, ...args] = launcherArgs;\n if (!executable) {\n console.error('supertask launcher: missing OpenCode executable');\n process.exitCode = 127;\n return;\n }\n\n if (!await waitForRelease()) {\n process.exitCode = 125;\n return;\n }\n\n const preserveGroupLeader = () => {};\n process.on('SIGTERM', preserveGroupLeader);\n process.on('SIGINT', preserveGroupLeader);\n let result: { code: number | null; signal: NodeJS.Signals | null };\n try {\n const child = spawn(executable, args, {\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n child.stdout?.pipe(process.stdout, { end: false });\n child.stderr?.pipe(process.stderr, { end: false });\n result = await new Promise((resolve) => {\n child.once('error', (error) => {\n console.error(`supertask launcher: ${error.message}`);\n resolve({ code: 127, signal: null });\n });\n // 使用独立 pipe 转发输出;exit 只等 OpenCode 主进程结束,\n // 后续的进程组排空才是 guardian 退出的最终条件。\n child.once('exit', (code, signal) => resolve({ code, signal }));\n });\n\n // 保持进程组长存活,直到 OpenCode 派生的整棵进程树都退出。\n // Watchdog 因而始终可以通过 launcher 命令验证进程组归属,避免 PID/PGID 复用误杀。\n await waitForProcessGroupDrain();\n if (launchIdentity) {\n // IPC 不传递给 OpenCode;只有持有该次随机身份的 guardian\n // 可在整个进程组排空后向 Worker 发出绑定证明。\n if (!process.send) throw new Error('supertask launcher: drain proof IPC unavailable');\n await new Promise<void>((resolve, reject) => {\n process.send!(drainProofForIdentity(launchIdentity), (error) => {\n if (error) reject(error);\n else resolve();\n });\n });\n }\n } finally {\n process.off('SIGTERM', preserveGroupLeader);\n process.off('SIGINT', preserveGroupLeader);\n }\n\n if (result.signal) {\n try {\n process.kill(process.pid, result.signal);\n return;\n } catch {\n process.exitCode = 128;\n return;\n }\n }\n process.exitCode = result.code ?? 1;\n}\n\nif (import.meta.main) await main();\n","export const LEGACY_GUARDIAN_LAUNCH_PROTOCOL = 'gated-v2-guardian';\nexport const TOKEN_GUARDIAN_LAUNCH_PROTOCOL = 'gated-v3-token-guardian';\nexport const LAUNCH_IDENTITY_ARGUMENT = '--supertask-launch-identity';\nexport const DRAIN_PROOF_MESSAGE_TYPE = 'supertask-drained';\nexport const MANAGED_RUN_ENV = 'SUPERTASK_MANAGED_RUN';\nexport const MANAGED_RUN_ENV_VALUE = '1';\n\nexport function isLaunchIdentity(value: string | null | undefined): value is string {\n return value != null\n && /^gateway-[1-9]\\d*:launch:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);\n}\n\nexport function drainProofForIdentity(launchIdentity: string) {\n return { type: DRAIN_PROOF_MESSAGE_TYPE, identity: launchIdentity } as const;\n}\n\nexport function isMatchingDrainProof(message: unknown, launchIdentity: string): boolean {\n if (typeof message !== 'object' || message == null) return false;\n const candidate = message as Record<string, unknown>;\n return candidate.type === DRAIN_PROOF_MESSAGE_TYPE\n && candidate.identity === launchIdentity;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1 @@
1
+ CREATE INDEX `tasks_depends_on_status_idx` ON `tasks` (`depends_on`,`status`);
@@ -0,0 +1,2 @@
1
+ ALTER TABLE `task_runs` ADD `launch_protocol` text;--> statement-breakpoint
2
+ CREATE INDEX `tasks_cleanup_idx` ON `tasks` (`finished_at`,`id`,`status`);
@@ -0,0 +1 @@
1
+ CREATE INDEX `task_templates_retention_idx` ON `task_templates` (`schedule_type`,`enabled`,`last_run_at`,`id`);