taskplane 0.20.2 → 0.20.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.
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Engine
|
|
2
|
+
* Engine Child Process Entry Point (TP-071)
|
|
3
3
|
*
|
|
4
4
|
* This module serves two purposes:
|
|
5
5
|
* 1. Exports types and helpers used by extension.ts (main thread)
|
|
6
|
-
* 2. When
|
|
6
|
+
* 2. When forked as a child process, runs the engine in a separate Node.js process
|
|
7
|
+
*
|
|
8
|
+
* Uses child_process.fork() instead of worker_threads because Node v25's
|
|
9
|
+
* default --experimental-strip-types rejects .ts files inside node_modules.
|
|
10
|
+
* Fork creates a new process where --experimental-transform-types takes effect.
|
|
7
11
|
*
|
|
8
12
|
* Communication:
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
13
|
+
* - Child → Parent: process.send() for notify, monitor-update, engine-event, state-sync, complete, error
|
|
14
|
+
* - Parent → Child: child.send() for init, pause, resume, abort
|
|
11
15
|
*
|
|
12
16
|
* @module orch/engine-worker
|
|
13
17
|
*/
|
|
14
|
-
import { parentPort, workerData, isMainThread } from "worker_threads";
|
|
15
|
-
|
|
16
18
|
import type {
|
|
17
19
|
EngineEvent,
|
|
18
20
|
MonitorState,
|
|
@@ -186,104 +188,111 @@ export function applySerializedState(
|
|
|
186
188
|
batchState.errors = [...serialized.errors];
|
|
187
189
|
}
|
|
188
190
|
|
|
189
|
-
// ──
|
|
191
|
+
// ── Engine main (runs when launched as a forked child process) ───────
|
|
190
192
|
|
|
191
|
-
// Guard: only run
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (!isMainThread && parentPort && workerData?.engineWorker === true) {
|
|
195
|
-
// Dynamic imports — only loaded in worker context to avoid circular
|
|
196
|
-
// dependencies when this module is imported from extension.ts
|
|
197
|
-
const { executeOrchBatch } = await import("./engine.ts");
|
|
198
|
-
const { resumeOrchBatch } = await import("./resume.ts");
|
|
199
|
-
const { freshOrchBatchState } = await import("./types.ts");
|
|
193
|
+
// Guard: only run engine main when launched via fork() with the sentinel env var.
|
|
194
|
+
if (process.env.TASKPLANE_ENGINE_FORK === "1" && typeof process.send === "function") {
|
|
195
|
+
const send = (msg: WorkerToMainMessage) => process.send!(msg);
|
|
200
196
|
|
|
201
|
-
|
|
202
|
-
|
|
197
|
+
// Wait for the init message carrying workerData, then start the engine.
|
|
198
|
+
process.once("message", async (initMsg: { type: string; data: EngineWorkerData }) => {
|
|
199
|
+
if (initMsg?.type !== "init") return;
|
|
203
200
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
201
|
+
// Dynamic imports — only loaded in engine context to avoid circular
|
|
202
|
+
// dependencies when this module is imported from extension.ts
|
|
203
|
+
const { executeOrchBatch } = await import("./engine.ts");
|
|
204
|
+
const { resumeOrchBatch } = await import("./resume.ts");
|
|
205
|
+
const { freshOrchBatchState } = await import("./types.ts");
|
|
208
206
|
|
|
209
|
-
|
|
210
|
-
const wsConfig = deserializeWorkspaceConfig(data.workspaceConfig);
|
|
207
|
+
const data = initMsg.data;
|
|
211
208
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
switch (msg.type) {
|
|
217
|
-
case "pause":
|
|
218
|
-
batchState.pauseSignal.paused = true;
|
|
219
|
-
break;
|
|
220
|
-
case "resume":
|
|
221
|
-
batchState.pauseSignal.paused = false;
|
|
222
|
-
break;
|
|
223
|
-
case "abort":
|
|
224
|
-
batchState.pauseSignal.paused = true;
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
});
|
|
209
|
+
// Create a fresh batch state for this process
|
|
210
|
+
const batchState: OrchBatchRuntimeState = freshOrchBatchState();
|
|
211
|
+
batchState.phase = "launching";
|
|
212
|
+
batchState.startedAt = Date.now();
|
|
228
213
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
port.postMessage({ type: "notify", msg: message, level } satisfies WorkerToMainMessage);
|
|
232
|
-
// Sync batch state on every notify (lightweight — just the summary fields)
|
|
233
|
-
port.postMessage({ type: "state-sync", state: serializeBatchState(batchState) } satisfies WorkerToMainMessage);
|
|
234
|
-
};
|
|
214
|
+
// Deserialize workspace config
|
|
215
|
+
const wsConfig = deserializeWorkspaceConfig(data.workspaceConfig);
|
|
235
216
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
217
|
+
// ── Control signal listener ──────────────────────────────────
|
|
218
|
+
// Main process sends pause/resume/abort signals via IPC.
|
|
219
|
+
// We apply them to the in-process batchState.pauseSignal.
|
|
220
|
+
process.on("message", (msg: WorkerInMessage) => {
|
|
221
|
+
switch (msg.type) {
|
|
222
|
+
case "pause":
|
|
223
|
+
batchState.pauseSignal.paused = true;
|
|
224
|
+
break;
|
|
225
|
+
case "resume":
|
|
226
|
+
batchState.pauseSignal.paused = false;
|
|
227
|
+
break;
|
|
228
|
+
case "abort":
|
|
229
|
+
batchState.pauseSignal.paused = true;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
239
233
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
234
|
+
// ── Callback factories (replace ctx-dependent callbacks) ─────
|
|
235
|
+
const onNotify = (message: string, level: "info" | "warning" | "error") => {
|
|
236
|
+
send({ type: "notify", msg: message, level });
|
|
237
|
+
// Sync batch state on every notify (lightweight — just the summary fields)
|
|
238
|
+
send({ type: "state-sync", state: serializeBatchState(batchState) });
|
|
239
|
+
};
|
|
243
240
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
data.orchConfig,
|
|
248
|
-
data.runnerConfig,
|
|
249
|
-
data.cwd,
|
|
250
|
-
batchState,
|
|
251
|
-
onNotify,
|
|
252
|
-
onMonitorUpdate,
|
|
253
|
-
wsConfig,
|
|
254
|
-
data.workspaceRoot,
|
|
255
|
-
data.agentRoot,
|
|
256
|
-
data.force ?? false,
|
|
257
|
-
)
|
|
258
|
-
: executeOrchBatch(
|
|
259
|
-
data.args ?? "",
|
|
260
|
-
data.orchConfig,
|
|
261
|
-
data.runnerConfig,
|
|
262
|
-
data.cwd,
|
|
263
|
-
batchState,
|
|
264
|
-
onNotify,
|
|
265
|
-
onMonitorUpdate,
|
|
266
|
-
wsConfig,
|
|
267
|
-
data.workspaceRoot,
|
|
268
|
-
data.agentRoot,
|
|
269
|
-
onEngineEvent,
|
|
270
|
-
);
|
|
241
|
+
const onMonitorUpdate = (state: MonitorState) => {
|
|
242
|
+
send({ type: "monitor-update", state });
|
|
243
|
+
};
|
|
271
244
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
batchState
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
245
|
+
const onEngineEvent = (event: EngineEvent) => {
|
|
246
|
+
send({ type: "engine-event", event });
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// ── Execute engine ───────────────────────────────────────────
|
|
250
|
+
const enginePromise = data.mode === "resume"
|
|
251
|
+
? resumeOrchBatch(
|
|
252
|
+
data.orchConfig,
|
|
253
|
+
data.runnerConfig,
|
|
254
|
+
data.cwd,
|
|
255
|
+
batchState,
|
|
256
|
+
onNotify,
|
|
257
|
+
onMonitorUpdate,
|
|
258
|
+
wsConfig,
|
|
259
|
+
data.workspaceRoot,
|
|
260
|
+
data.agentRoot,
|
|
261
|
+
data.force ?? false,
|
|
262
|
+
)
|
|
263
|
+
: executeOrchBatch(
|
|
264
|
+
data.args ?? "",
|
|
265
|
+
data.orchConfig,
|
|
266
|
+
data.runnerConfig,
|
|
267
|
+
data.cwd,
|
|
268
|
+
batchState,
|
|
269
|
+
onNotify,
|
|
270
|
+
onMonitorUpdate,
|
|
271
|
+
wsConfig,
|
|
272
|
+
data.workspaceRoot,
|
|
273
|
+
data.agentRoot,
|
|
274
|
+
onEngineEvent,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
enginePromise
|
|
278
|
+
.then(() => {
|
|
279
|
+
// Final state sync + completion signal
|
|
280
|
+
const finalState = serializeBatchState(batchState);
|
|
281
|
+
send({ type: "complete", state: finalState });
|
|
282
|
+
// Disconnect IPC so the child process can exit cleanly
|
|
283
|
+
process.disconnect?.();
|
|
284
|
+
})
|
|
285
|
+
.catch((err: unknown) => {
|
|
286
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
287
|
+
// Ensure batch state reflects the failure
|
|
288
|
+
if (batchState.phase !== "completed" && batchState.phase !== "failed") {
|
|
289
|
+
batchState.phase = "failed";
|
|
290
|
+
batchState.endedAt = Date.now();
|
|
291
|
+
batchState.errors.push(`Unhandled engine error: ${errMsg}`);
|
|
292
|
+
}
|
|
293
|
+
send({ type: "state-sync", state: serializeBatchState(batchState) });
|
|
294
|
+
send({ type: "error", message: errMsg });
|
|
295
|
+
process.disconnect?.();
|
|
296
|
+
});
|
|
297
|
+
});
|
|
289
298
|
}
|
|
@@ -5,7 +5,7 @@ import { execSync, execFileSync } from "child_process";
|
|
|
5
5
|
import { writeFileSync, unlinkSync, mkdirSync, existsSync, readdirSync } from "fs";
|
|
6
6
|
import { join, dirname } from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
|
-
import {
|
|
8
|
+
import { fork, type ChildProcess } from "child_process";
|
|
9
9
|
|
|
10
10
|
// Direct imports — avoid barrel (index.ts) to prevent loading the entire module graph.
|
|
11
11
|
// Each import targets the specific module where the symbol is defined.
|
|
@@ -897,7 +897,7 @@ function resolveEngineWorkerPath(): string {
|
|
|
897
897
|
} catch {
|
|
898
898
|
thisDir = __dirname;
|
|
899
899
|
}
|
|
900
|
-
return join(thisDir, "engine-worker
|
|
900
|
+
return join(thisDir, "engine-worker.ts");
|
|
901
901
|
}
|
|
902
902
|
|
|
903
903
|
/**
|
|
@@ -935,19 +935,20 @@ export function startBatchInWorker(
|
|
|
935
935
|
updateWidget: () => void,
|
|
936
936
|
onMonitorUpdate?: (state: import("./types.ts").MonitorState) => void,
|
|
937
937
|
onTerminal?: () => void,
|
|
938
|
-
):
|
|
938
|
+
): ChildProcess | null {
|
|
939
939
|
const workerPath = resolveEngineWorkerPath();
|
|
940
940
|
|
|
941
|
-
let
|
|
941
|
+
let child: ChildProcess;
|
|
942
942
|
try {
|
|
943
|
-
|
|
944
|
-
workerData: wkData,
|
|
943
|
+
child = fork(workerPath, [], {
|
|
945
944
|
execArgv: ["--experimental-transform-types", "--no-warnings"],
|
|
945
|
+
env: { ...process.env, TASKPLANE_ENGINE_FORK: "1" },
|
|
946
|
+
serialization: "advanced",
|
|
946
947
|
});
|
|
947
948
|
} catch (spawnErr: unknown) {
|
|
948
949
|
const errMsg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
|
|
949
950
|
ctx.ui.notify(
|
|
950
|
-
`⚠️
|
|
951
|
+
`⚠️ Engine process spawn failed: ${errMsg}\n Falling back to main-thread execution.`,
|
|
951
952
|
"warning",
|
|
952
953
|
);
|
|
953
954
|
// Construct fallback engine function from workerData and run on main thread
|
|
@@ -983,8 +984,11 @@ export function startBatchInWorker(
|
|
|
983
984
|
return null;
|
|
984
985
|
}
|
|
985
986
|
|
|
987
|
+
// Send workerData as first IPC message (fork doesn't have workerData)
|
|
988
|
+
child.send({ type: "init", data: wkData });
|
|
989
|
+
|
|
986
990
|
// Terminal settlement guard (R001 §3): ensures onTerminal fires at most once.
|
|
987
|
-
// The
|
|
991
|
+
// The child can emit error + complete messages, followed by exit event.
|
|
988
992
|
// Without this guard, summary/integration/supervisor flows would fire multiple times.
|
|
989
993
|
let settled = false;
|
|
990
994
|
const settle = () => {
|
|
@@ -993,7 +997,7 @@ export function startBatchInWorker(
|
|
|
993
997
|
onTerminal?.();
|
|
994
998
|
};
|
|
995
999
|
|
|
996
|
-
|
|
1000
|
+
child.on("message", (msg: WorkerToMainMessage) => {
|
|
997
1001
|
switch (msg.type) {
|
|
998
1002
|
case "notify":
|
|
999
1003
|
ctx.ui.notify(msg.msg, msg.level);
|
|
@@ -1005,7 +1009,7 @@ export function startBatchInWorker(
|
|
|
1005
1009
|
break;
|
|
1006
1010
|
|
|
1007
1011
|
case "engine-event":
|
|
1008
|
-
// Engine events are already persisted to events.jsonl by the
|
|
1012
|
+
// Engine events are already persisted to events.jsonl by the child.
|
|
1009
1013
|
// No additional handling needed on the main thread.
|
|
1010
1014
|
break;
|
|
1011
1015
|
|
|
@@ -1036,15 +1040,15 @@ export function startBatchInWorker(
|
|
|
1036
1040
|
}
|
|
1037
1041
|
});
|
|
1038
1042
|
|
|
1039
|
-
|
|
1040
|
-
//
|
|
1043
|
+
child.on("error", (err: Error) => {
|
|
1044
|
+
// Child process encountered an error
|
|
1041
1045
|
if (batchState.phase !== "completed" && batchState.phase !== "failed") {
|
|
1042
1046
|
batchState.phase = "failed";
|
|
1043
1047
|
batchState.endedAt = Date.now();
|
|
1044
|
-
batchState.errors.push(`
|
|
1048
|
+
batchState.errors.push(`Engine process error: ${err.message}`);
|
|
1045
1049
|
}
|
|
1046
1050
|
ctx.ui.notify(
|
|
1047
|
-
`❌ Engine
|
|
1051
|
+
`❌ Engine process error: ${err.message}\n` +
|
|
1048
1052
|
` Batch ${batchState.batchId} marked as failed.`,
|
|
1049
1053
|
"error",
|
|
1050
1054
|
);
|
|
@@ -1052,16 +1056,16 @@ export function startBatchInWorker(
|
|
|
1052
1056
|
settle();
|
|
1053
1057
|
});
|
|
1054
1058
|
|
|
1055
|
-
|
|
1059
|
+
child.on("exit", (code: number | null) => {
|
|
1056
1060
|
if (code !== 0 && !settled) {
|
|
1057
1061
|
// Non-zero exit that wasn't handled by 'error' or 'complete'
|
|
1058
1062
|
if (batchState.phase !== "completed" && batchState.phase !== "failed") {
|
|
1059
1063
|
batchState.phase = "failed";
|
|
1060
1064
|
batchState.endedAt = Date.now();
|
|
1061
|
-
batchState.errors.push(`
|
|
1065
|
+
batchState.errors.push(`Engine process exited with code ${code}`);
|
|
1062
1066
|
}
|
|
1063
1067
|
ctx.ui.notify(
|
|
1064
|
-
`❌ Engine
|
|
1068
|
+
`❌ Engine process exited unexpectedly (code ${code}).`,
|
|
1065
1069
|
"error",
|
|
1066
1070
|
);
|
|
1067
1071
|
updateWidget();
|
|
@@ -1387,9 +1391,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1387
1391
|
let orchWidgetCtx: ExtensionContext | undefined;
|
|
1388
1392
|
let latestMonitorState: MonitorState | null = null;
|
|
1389
1393
|
|
|
1390
|
-
// ── TP-071: Active engine
|
|
1391
|
-
// Tracked so pause/abort can
|
|
1392
|
-
let activeWorker:
|
|
1394
|
+
// ── TP-071: Active engine child process ──────────────────────────
|
|
1395
|
+
// Tracked so pause/abort can send control messages to the engine.
|
|
1396
|
+
let activeWorker: ChildProcess | null = null;
|
|
1393
1397
|
|
|
1394
1398
|
// ── Supervisor State (TP-041) ────────────────────────────────────
|
|
1395
1399
|
let supervisorState = freshSupervisorState();
|
|
@@ -2006,8 +2010,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
2006
2010
|
return ORCH_MESSAGES.pauseAlreadyPaused(orchBatchState.batchId);
|
|
2007
2011
|
}
|
|
2008
2012
|
orchBatchState.pauseSignal.paused = true;
|
|
2009
|
-
// TP-071: Forward pause to
|
|
2010
|
-
activeWorker?.
|
|
2013
|
+
// TP-071: Forward pause to engine process (its pauseSignal is separate)
|
|
2014
|
+
activeWorker?.send({ type: "pause" });
|
|
2011
2015
|
updateOrchWidget();
|
|
2012
2016
|
return ORCH_MESSAGES.pauseActivated(orchBatchState.batchId);
|
|
2013
2017
|
}
|
|
@@ -2197,15 +2201,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
2197
2201
|
orchBatchState.pauseSignal.paused = true;
|
|
2198
2202
|
messages.push(" ✓ Pause signal set on in-memory batch state");
|
|
2199
2203
|
}
|
|
2200
|
-
// TP-071: Forward pause to
|
|
2204
|
+
// TP-071: Forward pause to engine and kill on hard abort
|
|
2201
2205
|
if (activeWorker) {
|
|
2202
|
-
activeWorker.
|
|
2206
|
+
activeWorker.send({ type: "pause" });
|
|
2203
2207
|
if (hard) {
|
|
2204
|
-
activeWorker.
|
|
2208
|
+
activeWorker.kill();
|
|
2205
2209
|
activeWorker = null;
|
|
2206
|
-
messages.push(" ✓ Engine
|
|
2210
|
+
messages.push(" ✓ Engine process killed (hard abort)");
|
|
2207
2211
|
} else {
|
|
2208
|
-
messages.push(" ✓ Pause signal forwarded to engine
|
|
2212
|
+
messages.push(" ✓ Pause signal forwarded to engine process");
|
|
2209
2213
|
}
|
|
2210
2214
|
}
|
|
2211
2215
|
|
|
@@ -3283,13 +3287,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
3283
3287
|
// Ensure supervisor lockfile/heartbeat are cleaned up on normal session exit.
|
|
3284
3288
|
// This avoids leaving a live-looking lock when the process exits cleanly.
|
|
3285
3289
|
pi.on("session_end", async () => {
|
|
3286
|
-
// TP-071:
|
|
3290
|
+
// TP-071: Kill engine process on session exit
|
|
3287
3291
|
if (activeWorker) {
|
|
3288
3292
|
try {
|
|
3289
|
-
activeWorker.
|
|
3293
|
+
activeWorker.kill();
|
|
3290
3294
|
activeWorker = null;
|
|
3291
3295
|
} catch {
|
|
3292
|
-
// Best effort —
|
|
3296
|
+
// Best effort — process may already be dead
|
|
3293
3297
|
}
|
|
3294
3298
|
}
|
|
3295
3299
|
try {
|
package/package.json
CHANGED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thin entry point for the engine worker thread.
|
|
3
|
-
*
|
|
4
|
-
* Node's Worker constructor rejects .ts files inside node_modules when
|
|
5
|
-
* --experimental-strip-types is active (the default in Node v25+).
|
|
6
|
-
* This .mjs wrapper avoids the restriction — Node loads .mjs files
|
|
7
|
-
* without TypeScript processing, then --experimental-transform-types
|
|
8
|
-
* (passed via execArgv) handles subsequent .ts imports.
|
|
9
|
-
*/
|
|
10
|
-
import "./engine-worker.ts";
|