taskplane 0.20.5 → 0.20.6

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,18 @@
1
+ /**
2
+ * Fork entry point for the engine child process.
3
+ *
4
+ * Node v25 blocks .ts files inside node_modules regardless of flags.
5
+ * This .mjs file loads cleanly (no TypeScript processing needed), then
6
+ * uses jiti to import engine-worker.ts — bypassing Node's restriction.
7
+ *
8
+ * jiti is the same TypeScript runtime loader that Pi uses to load
9
+ * extensions. It transforms .ts files itself, independent of Node's
10
+ * --experimental-strip-types support.
11
+ */
12
+ import { createJiti } from "jiti";
13
+ import { fileURLToPath } from "node:url";
14
+ import { dirname, join } from "node:path";
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const jiti = createJiti(import.meta.url);
18
+ await jiti.import(join(__dirname, "engine-worker.ts"));
@@ -5,8 +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
- // child_process.fork() disabled Node v25 blocks .ts in node_modules.
9
- // Re-enable when engine-worker ships as pre-compiled .js bundle.
8
+ import { fork, type ChildProcess } from "child_process";
10
9
 
11
10
  // Direct imports — avoid barrel (index.ts) to prevent loading the entire module graph.
12
11
  // Each import targets the specific module where the symbol is defined.
@@ -898,7 +897,7 @@ function resolveEngineWorkerPath(): string {
898
897
  } catch {
899
898
  thisDir = __dirname;
900
899
  }
901
- return join(thisDir, "engine-worker.ts");
900
+ return join(thisDir, "engine-worker-entry.mjs");
902
901
  }
903
902
 
904
903
  /**
@@ -936,14 +935,24 @@ export function startBatchInWorker(
936
935
  updateWidget: () => void,
937
936
  onMonitorUpdate?: (state: import("./types.ts").MonitorState) => void,
938
937
  onTerminal?: () => void,
939
- ): null {
940
- // ── Main-thread execution (TP-071 fork disabled) ─────────────
941
- // Node v25 blocks .ts files inside node_modules regardless of
942
- // --experimental-strip-types or --experimental-transform-types.
943
- // Until we ship a pre-compiled engine bundle, the engine runs on
944
- // the main thread via startBatchAsync(). The supervisor stays
945
- // responsive because engine work is async I/O (tmux, git, fs).
946
- {
938
+ ): ChildProcess | null {
939
+ const workerPath = resolveEngineWorkerPath();
940
+
941
+ let child: ChildProcess;
942
+ try {
943
+ // Fork a child process to run the engine in a separate isolate.
944
+ // The entry point is a .mjs file that uses jiti to load .ts files,
945
+ // bypassing Node v25's restriction on .ts in node_modules.
946
+ child = fork(workerPath, [], {
947
+ env: { ...process.env, TASKPLANE_ENGINE_FORK: "1" },
948
+ serialization: "advanced",
949
+ });
950
+ } catch (spawnErr: unknown) {
951
+ const errMsg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
952
+ ctx.ui.notify(
953
+ `⚠️ Engine process spawn failed: ${errMsg}\n Falling back to main-thread execution.`,
954
+ "warning",
955
+ );
947
956
  // Construct fallback engine function from workerData and run on main thread
948
957
  const wsConfig = wkData.workspaceConfig
949
958
  ? deserializeWorkspaceConfig(wkData.workspaceConfig)
@@ -976,6 +985,91 @@ export function startBatchInWorker(
976
985
  startBatchAsync(fallbackFn, batchState, ctx, updateWidget, onTerminal);
977
986
  return null;
978
987
  }
988
+
989
+ // Send workerData as first IPC message
990
+ child.send({ type: "init", data: wkData });
991
+
992
+ // Terminal settlement guard (R001 §3): ensures onTerminal fires at most once.
993
+ let settled = false;
994
+ const settle = () => {
995
+ if (settled) return;
996
+ settled = true;
997
+ onTerminal?.();
998
+ };
999
+
1000
+ child.on("message", (msg: WorkerToMainMessage) => {
1001
+ switch (msg.type) {
1002
+ case "notify":
1003
+ ctx.ui.notify(msg.msg, msg.level);
1004
+ updateWidget();
1005
+ break;
1006
+
1007
+ case "monitor-update":
1008
+ onMonitorUpdate?.(msg.state);
1009
+ break;
1010
+
1011
+ case "engine-event":
1012
+ break;
1013
+
1014
+ case "state-sync":
1015
+ applySerializedState(batchState, msg.state);
1016
+ updateWidget();
1017
+ break;
1018
+
1019
+ case "complete":
1020
+ applySerializedState(batchState, msg.state);
1021
+ updateWidget();
1022
+ settle();
1023
+ break;
1024
+
1025
+ case "error":
1026
+ if (batchState.phase !== "completed" && batchState.phase !== "failed") {
1027
+ batchState.phase = "failed";
1028
+ batchState.endedAt = Date.now();
1029
+ batchState.errors.push(`Unhandled engine error: ${msg.message}`);
1030
+ }
1031
+ ctx.ui.notify(
1032
+ `❌ Engine crashed with unhandled error: ${msg.message}\n` +
1033
+ ` Batch ${batchState.batchId} marked as failed.`,
1034
+ "error",
1035
+ );
1036
+ updateWidget();
1037
+ break;
1038
+ }
1039
+ });
1040
+
1041
+ child.on("error", (err: Error) => {
1042
+ if (batchState.phase !== "completed" && batchState.phase !== "failed") {
1043
+ batchState.phase = "failed";
1044
+ batchState.endedAt = Date.now();
1045
+ batchState.errors.push(`Engine process error: ${err.message}`);
1046
+ }
1047
+ ctx.ui.notify(
1048
+ `❌ Engine process error: ${err.message}\n` +
1049
+ ` Batch ${batchState.batchId} marked as failed.`,
1050
+ "error",
1051
+ );
1052
+ updateWidget();
1053
+ settle();
1054
+ });
1055
+
1056
+ child.on("exit", (code: number | null) => {
1057
+ if (code !== 0 && !settled) {
1058
+ if (batchState.phase !== "completed" && batchState.phase !== "failed") {
1059
+ batchState.phase = "failed";
1060
+ batchState.endedAt = Date.now();
1061
+ batchState.errors.push(`Engine process exited with code ${code}`);
1062
+ }
1063
+ ctx.ui.notify(
1064
+ `❌ Engine process exited unexpectedly (code ${code}).`,
1065
+ "error",
1066
+ );
1067
+ updateWidget();
1068
+ }
1069
+ settle();
1070
+ });
1071
+
1072
+ return child;
979
1073
  }
980
1074
 
981
1075
  // ── TP-043 R002-2: Integration Executor Builder ─────────────────────
@@ -1292,9 +1386,9 @@ export default function (pi: ExtensionAPI) {
1292
1386
  let orchWidgetCtx: ExtensionContext | undefined;
1293
1387
  let latestMonitorState: MonitorState | null = null;
1294
1388
 
1295
- // ── TP-071: Active engine handle (currently unused — fork disabled) ──
1296
- // Will be restored when engine-worker ships as pre-compiled .js bundle.
1297
- let activeWorker: null = null;
1389
+ // ── TP-071: Active engine child process ──────────────────────────
1390
+ // Tracked so pause/abort can send control messages to the engine.
1391
+ let activeWorker: ChildProcess | null = null;
1298
1392
 
1299
1393
  // ── Supervisor State (TP-041) ────────────────────────────────────
1300
1394
  let supervisorState = freshSupervisorState();
@@ -1911,8 +2005,8 @@ export default function (pi: ExtensionAPI) {
1911
2005
  return ORCH_MESSAGES.pauseAlreadyPaused(orchBatchState.batchId);
1912
2006
  }
1913
2007
  orchBatchState.pauseSignal.paused = true;
1914
- // TP-071: Forward pause to engine process (disabled fork not active)
1915
- // activeWorker?.send({ type: "pause" });
2008
+ // TP-071: Forward pause to engine process (its pauseSignal is separate)
2009
+ activeWorker?.send({ type: "pause" });
1916
2010
  updateOrchWidget();
1917
2011
  return ORCH_MESSAGES.pauseActivated(orchBatchState.batchId);
1918
2012
  }
@@ -2102,10 +2196,16 @@ export default function (pi: ExtensionAPI) {
2102
2196
  orchBatchState.pauseSignal.paused = true;
2103
2197
  messages.push(" ✓ Pause signal set on in-memory batch state");
2104
2198
  }
2105
- // TP-071: Forward pause to engine (disabled fork not active)
2106
- if (false as boolean) {
2107
- // Will be restored when engine-worker ships as pre-compiled .js bundle
2108
- messages.push(" ✓ Pause signal forwarded to engine process");
2199
+ // TP-071: Forward pause to engine and kill on hard abort
2200
+ if (activeWorker) {
2201
+ activeWorker.send({ type: "pause" });
2202
+ if (hard) {
2203
+ activeWorker.kill();
2204
+ activeWorker = null;
2205
+ messages.push(" ✓ Engine process killed (hard abort)");
2206
+ } else {
2207
+ messages.push(" ✓ Pause signal forwarded to engine process");
2208
+ }
2109
2209
  }
2110
2210
 
2111
2211
  // Step 3: Check what we're aborting
@@ -3182,8 +3282,15 @@ export default function (pi: ExtensionAPI) {
3182
3282
  // Ensure supervisor lockfile/heartbeat are cleaned up on normal session exit.
3183
3283
  // This avoids leaving a live-looking lock when the process exits cleanly.
3184
3284
  pi.on("session_end", async () => {
3185
- // TP-071: Kill engine process on session exit (disabled — fork not active)
3186
- // Will be restored when engine-worker ships as pre-compiled .js bundle
3285
+ // TP-071: Kill engine process on session exit
3286
+ if (activeWorker) {
3287
+ try {
3288
+ activeWorker.kill();
3289
+ activeWorker = null;
3290
+ } catch {
3291
+ // Best effort — process may already be dead
3292
+ }
3293
+ }
3187
3294
  try {
3188
3295
  await deactivateSupervisor(pi, supervisorState);
3189
3296
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.20.5",
3
+ "version": "0.20.6",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -43,6 +43,7 @@
43
43
  "@sinclair/typebox": "*"
44
44
  },
45
45
  "dependencies": {
46
+ "jiti": "^2.6.1",
46
47
  "yaml": "^2.4.0"
47
48
  },
48
49
  "license": "MIT",