@kontextmind/kxm 0.6.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.
Files changed (227) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.kxm/README.md +14 -0
  3. package/.kxm/assets/README.md +5 -0
  4. package/.kxm/assets/retrospectives/README.md +5 -0
  5. package/.kxm/config/README.md +5 -0
  6. package/.kxm/config/agents.json +43 -0
  7. package/.kxm/config/env.example +56 -0
  8. package/.kxm/config/update.example.yaml +9 -0
  9. package/.kxm/config/workflows/fix.json +160 -0
  10. package/.kxm/config/workflows/jira-development.json +116 -0
  11. package/.kxm/config/workflows/provenance-quorum.json +150 -0
  12. package/.kxm/config/workflows/v04-dogfood.json +72 -0
  13. package/CHANGELOG.md +465 -0
  14. package/LICENSE +21 -0
  15. package/README.md +306 -0
  16. package/SECURITY.md +72 -0
  17. package/docs/README.md +48 -0
  18. package/docs/agent-communication-envelopes-and-gates.md +553 -0
  19. package/docs/architecture.md +242 -0
  20. package/docs/assignment-runner.md +241 -0
  21. package/docs/configuration.md +361 -0
  22. package/docs/continuous-improvement.md +114 -0
  23. package/docs/getting-started.md +253 -0
  24. package/docs/kxm-handbook.md +1090 -0
  25. package/docs/operations.md +205 -0
  26. package/docs/provenance-gates.md +291 -0
  27. package/docs/skills.md +45 -0
  28. package/docs/templates/README.md +95 -0
  29. package/docs/templates/adr.md +88 -0
  30. package/docs/templates/architecture.md +120 -0
  31. package/docs/templates/bug-fix.md +109 -0
  32. package/docs/templates/feature.md +108 -0
  33. package/docs/templates/handoff.md +72 -0
  34. package/docs/templates/postmortem.md +77 -0
  35. package/docs/templates/research.md +100 -0
  36. package/docs/templates/review.md +85 -0
  37. package/docs/templates/runbook.md +73 -0
  38. package/docs/templates/test-plan.md +87 -0
  39. package/docs/templates/test-report.md +72 -0
  40. package/docs/test-matrix.md +121 -0
  41. package/docs/troubleshooting.md +249 -0
  42. package/docs/vnext/README.md +62 -0
  43. package/docs/vnext/architecture.md +185 -0
  44. package/docs/vnext/effects-and-recovery.md +172 -0
  45. package/docs/vnext/lifecycles.md +235 -0
  46. package/docs/vnext/migration.md +220 -0
  47. package/docs/vnext/routing.md +184 -0
  48. package/docs/vnext/synchronization.md +172 -0
  49. package/docs/vnext/terminology.md +240 -0
  50. package/docs/vnext/validation.md +335 -0
  51. package/docs/webhook-workflows.md +240 -0
  52. package/docs/workflow-guide.md +1150 -0
  53. package/examples/README.md +102 -0
  54. package/examples/provenance-workflow.json +40 -0
  55. package/examples/requester.ts +30 -0
  56. package/examples/reviewer-agent.ts +29 -0
  57. package/examples/roundtrip.ts +46 -0
  58. package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
  59. package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
  60. package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
  61. package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
  62. package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
  63. package/examples/vnext/.kxm/agents/planner.yaml +13 -0
  64. package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
  65. package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
  66. package/examples/vnext/.kxm/gates.yaml +8 -0
  67. package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
  68. package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
  69. package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
  70. package/examples/vnext/.kxm/models/implementation.yaml +14 -0
  71. package/examples/vnext/.kxm/models/primary.yaml +17 -0
  72. package/examples/vnext/.kxm/prices.yaml +111 -0
  73. package/examples/vnext/.kxm/project/env.yaml +7 -0
  74. package/examples/vnext/.kxm/project.yaml +32 -0
  75. package/examples/vnext/.kxm/repo/repo.yaml +8 -0
  76. package/examples/vnext/.kxm/workflows/default.yaml +92 -0
  77. package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
  78. package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
  79. package/examples/vnext/README.md +53 -0
  80. package/examples/vnext/records/assignment-result-recorded.json +63 -0
  81. package/examples/vnext/records/assignment-result.json +46 -0
  82. package/examples/vnext/records/context-candidate.json +42 -0
  83. package/examples/vnext/records/delivery-manifest.json +66 -0
  84. package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
  85. package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
  86. package/examples/vnext/records/run-created.json +54 -0
  87. package/examples/vnext/records/sync-event.json +65 -0
  88. package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
  89. package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
  90. package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
  91. package/examples/workflow-signal.ts +63 -0
  92. package/package.json +129 -0
  93. package/plugins/kxm/.claude-plugin/plugin.json +73 -0
  94. package/plugins/kxm/.mcp.json +19 -0
  95. package/plugins/kxm/README.md +93 -0
  96. package/plugins/kxm/dist/cli.js +42853 -0
  97. package/plugins/kxm/dist/client.js +416 -0
  98. package/plugins/kxm/dist/core.js +1823 -0
  99. package/plugins/kxm/dist/extension.js +3797 -0
  100. package/plugins/kxm/dist/mcp-server.js +17104 -0
  101. package/plugins/kxm/dist/runtime.js +23361 -0
  102. package/plugins/kxm/dist/server.js +13640 -0
  103. package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
  104. package/plugins/kxm/package.json +12 -0
  105. package/plugins/kxm/skills/kxm/SKILL.md +97 -0
  106. package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
  107. package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
  108. package/plugins/kxm/src/arbiter.ts +355 -0
  109. package/plugins/kxm/src/artifacts-exist.ts +62 -0
  110. package/plugins/kxm/src/autocomplete.ts +236 -0
  111. package/plugins/kxm/src/cli.ts +3707 -0
  112. package/plugins/kxm/src/client.ts +614 -0
  113. package/plugins/kxm/src/commands.ts +1063 -0
  114. package/plugins/kxm/src/config.ts +290 -0
  115. package/plugins/kxm/src/context/providers.ts +101 -0
  116. package/plugins/kxm/src/context-packet.ts +332 -0
  117. package/plugins/kxm/src/context.ts +499 -0
  118. package/plugins/kxm/src/core.ts +6 -0
  119. package/plugins/kxm/src/database.ts +563 -0
  120. package/plugins/kxm/src/diagnostics.ts +184 -0
  121. package/plugins/kxm/src/envelope.ts +118 -0
  122. package/plugins/kxm/src/extension.ts +895 -0
  123. package/plugins/kxm/src/external-effects.ts +299 -0
  124. package/plugins/kxm/src/github-watch.ts +255 -0
  125. package/plugins/kxm/src/hub-binding.ts +160 -0
  126. package/plugins/kxm/src/hub.ts +2502 -0
  127. package/plugins/kxm/src/improve.ts +383 -0
  128. package/plugins/kxm/src/inbox.ts +10 -0
  129. package/plugins/kxm/src/kxm-install-kind.ts +113 -0
  130. package/plugins/kxm/src/kxm-update-config.ts +39 -0
  131. package/plugins/kxm/src/kxm-update.ts +238 -0
  132. package/plugins/kxm/src/local-snapshot.ts +406 -0
  133. package/plugins/kxm/src/logger.ts +198 -0
  134. package/plugins/kxm/src/mcp-server.ts +143 -0
  135. package/plugins/kxm/src/memory.ts +385 -0
  136. package/plugins/kxm/src/nous-pi.ts +287 -0
  137. package/plugins/kxm/src/nous-provider.ts +729 -0
  138. package/plugins/kxm/src/price-calc.ts +87 -0
  139. package/plugins/kxm/src/prices.ts +121 -0
  140. package/plugins/kxm/src/protocol.ts +172 -0
  141. package/plugins/kxm/src/recovery.ts +211 -0
  142. package/plugins/kxm/src/redact.ts +26 -0
  143. package/plugins/kxm/src/retrospective.ts +400 -0
  144. package/plugins/kxm/src/routing.ts +830 -0
  145. package/plugins/kxm/src/runtime.ts +9 -0
  146. package/plugins/kxm/src/server.ts +117 -0
  147. package/plugins/kxm/src/session-work.ts +571 -0
  148. package/plugins/kxm/src/session.ts +184 -0
  149. package/plugins/kxm/src/skills.ts +535 -0
  150. package/plugins/kxm/src/state.ts +326 -0
  151. package/plugins/kxm/src/store.ts +637 -0
  152. package/plugins/kxm/src/studio-layout.ts +268 -0
  153. package/plugins/kxm/src/suggest.ts +162 -0
  154. package/plugins/kxm/src/task-manager.ts +244 -0
  155. package/plugins/kxm/src/telemetry.ts +116 -0
  156. package/plugins/kxm/src/tui.ts +1046 -0
  157. package/plugins/kxm/src/vnext-bindings.ts +403 -0
  158. package/plugins/kxm/src/vnext-config.ts +1646 -0
  159. package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
  160. package/plugins/kxm/src/vnext-engine-command.ts +533 -0
  161. package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
  162. package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
  163. package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
  164. package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
  165. package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
  166. package/plugins/kxm/src/vnext-engine.ts +2458 -0
  167. package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
  168. package/plugins/kxm/src/vnext-harness.ts +1142 -0
  169. package/plugins/kxm/src/vnext-init.ts +430 -0
  170. package/plugins/kxm/src/vnext-migrate.ts +1848 -0
  171. package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
  172. package/plugins/kxm/src/vnext-permission.ts +936 -0
  173. package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
  174. package/plugins/kxm/src/vnext-repair.ts +1094 -0
  175. package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
  176. package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
  177. package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
  178. package/plugins/kxm/src/vnext-runtime.ts +663 -0
  179. package/plugins/kxm/src/vnext-template.ts +247 -0
  180. package/plugins/kxm/src/wiki.ts +313 -0
  181. package/plugins/kxm/src/workflow.ts +1548 -0
  182. package/schemas/vnext/README.md +46 -0
  183. package/schemas/vnext/agent.schema.json +40 -0
  184. package/schemas/vnext/assignment-result.schema.json +66 -0
  185. package/schemas/vnext/backup-manifest.schema.json +89 -0
  186. package/schemas/vnext/candidate.schema.json +109 -0
  187. package/schemas/vnext/common.schema.json +422 -0
  188. package/schemas/vnext/context-candidate.schema.json +76 -0
  189. package/schemas/vnext/context-packet.schema.json +192 -0
  190. package/schemas/vnext/delivery-manifest.schema.json +159 -0
  191. package/schemas/vnext/environment.schema.json +66 -0
  192. package/schemas/vnext/gate-registry.schema.json +109 -0
  193. package/schemas/vnext/handoff-manifest.schema.json +146 -0
  194. package/schemas/vnext/init-operation.schema.json +61 -0
  195. package/schemas/vnext/local-repository-bindings.schema.json +30 -0
  196. package/schemas/vnext/memory-record.schema.json +45 -0
  197. package/schemas/vnext/migration-decision.schema.json +26 -0
  198. package/schemas/vnext/migration-plan.schema.json +123 -0
  199. package/schemas/vnext/migration-receipt.schema.json +52 -0
  200. package/schemas/vnext/model.schema.json +42 -0
  201. package/schemas/vnext/permission-diff.schema.json +57 -0
  202. package/schemas/vnext/prices.schema.json +115 -0
  203. package/schemas/vnext/project.schema.json +85 -0
  204. package/schemas/vnext/repository.schema.json +24 -0
  205. package/schemas/vnext/run-event.schema.json +460 -0
  206. package/schemas/vnext/session-brief.schema.json +153 -0
  207. package/schemas/vnext/sync-event.schema.json +234 -0
  208. package/schemas/vnext/template-provenance.schema.json +38 -0
  209. package/schemas/vnext/workflow.schema.json +248 -0
  210. package/scripts/assignment-run.d.mts +354 -0
  211. package/scripts/assignment-run.mjs +4451 -0
  212. package/scripts/build-runtime.mjs +56 -0
  213. package/scripts/check-generated.mjs +77 -0
  214. package/scripts/check-versions.mjs +34 -0
  215. package/scripts/emit-codex-artifacts.d.mts +9 -0
  216. package/scripts/emit-codex-artifacts.mjs +91 -0
  217. package/scripts/harness-run.d.mts +83 -0
  218. package/scripts/harness-run.mjs +2095 -0
  219. package/scripts/kxm-hub.mjs +105 -0
  220. package/scripts/kxm-publish-npm.mjs +327 -0
  221. package/scripts/kxm-release-github.mjs +472 -0
  222. package/scripts/kxm-runtime-supervisor.mjs +7 -0
  223. package/scripts/kxm-worker.mjs +1127 -0
  224. package/scripts/kxm.mjs +27 -0
  225. package/scripts/roster-policy.d.mts +20 -0
  226. package/scripts/roster-policy.mjs +161 -0
  227. package/scripts/smoke-multi-pi.mjs +479 -0
@@ -0,0 +1,1127 @@
1
+ #!/usr/bin/env node
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { closeSync, createWriteStream, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
5
+ import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { StringDecoder } from "node:string_decoder";
7
+
8
+ const name = process.env.KXM_AGENT_NAME?.trim();
9
+ const project = process.env.KXM_PROJECT?.trim();
10
+ if (!name || !project) {
11
+ throw new Error("KXM_AGENT_NAME and KXM_PROJECT are required for a long-lived worker");
12
+ }
13
+
14
+ const command = process.env.KXM_PI_COMMAND?.trim() || (process.platform === "win32" ? "pi.cmd" : "pi");
15
+ const workdir = resolve(process.env.KXM_WORKDIR?.trim() || process.cwd());
16
+
17
+ function validateModelSelector(value, variable) {
18
+ if (!value || value.length > 256 || /[\0\r\n,]/.test(value)) {
19
+ throw new Error(`${variable} contains an invalid model selector`);
20
+ }
21
+ return value;
22
+ }
23
+
24
+ const primaryModel = process.env.KXM_WORKER_MODEL?.trim();
25
+ const fallbackModelRaw = process.env.KXM_WORKER_FALLBACK_MODELS?.trim();
26
+ if (fallbackModelRaw && !primaryModel) {
27
+ throw new Error("KXM_WORKER_FALLBACK_MODELS requires KXM_WORKER_MODEL");
28
+ }
29
+ const fallbackModels = fallbackModelRaw
30
+ ? fallbackModelRaw.split(",").map((value) => validateModelSelector(value.trim(), "KXM_WORKER_FALLBACK_MODELS"))
31
+ : [];
32
+ if (fallbackModels.length > 8) {
33
+ throw new Error("KXM_WORKER_FALLBACK_MODELS accepts at most eight comma-separated selectors");
34
+ }
35
+ const modelCandidates = [...new Set([
36
+ ...(primaryModel ? [validateModelSelector(primaryModel, "KXM_WORKER_MODEL")] : []),
37
+ ...fallbackModels,
38
+ ])];
39
+ let modelIndex = 0;
40
+
41
+ function resolveToolAllowlist() {
42
+ const raw = process.env.KXM_WORKER_TOOLS?.trim();
43
+ if (!raw) return [];
44
+ const tools = [...new Set(raw.split(",").map((value) => value.trim()).filter(Boolean))];
45
+ if (tools.length < 1 || tools.length > 64 || tools.some((value) => !/^[A-Za-z0-9_.-]{1,64}$/.test(value))) {
46
+ throw new Error("KXM_WORKER_TOOLS must contain one to 64 comma-separated tool names");
47
+ }
48
+ return tools;
49
+ }
50
+
51
+ const toolAllowlist = resolveToolAllowlist();
52
+
53
+ function resolveResourcePaths(variable, kind) {
54
+ const raw = process.env[variable]?.trim();
55
+ if (!raw) return [];
56
+ const candidates = raw.split(delimiter).map((value) => value.trim()).filter(Boolean);
57
+ if (candidates.length < 1 || candidates.length > 16) {
58
+ throw new Error(`${variable} must contain between one and 16 paths separated by ${JSON.stringify(delimiter)}`);
59
+ }
60
+ const seen = new Set();
61
+ const paths = [];
62
+ for (const candidate of candidates) {
63
+ const absolutePath = resolve(workdir, candidate);
64
+ const identity = process.platform === "win32" ? absolutePath.toLowerCase() : absolutePath;
65
+ if (seen.has(identity)) continue;
66
+ seen.add(identity);
67
+ validateResourcePath(variable, kind, absolutePath);
68
+ paths.push(absolutePath);
69
+ }
70
+ return paths;
71
+ }
72
+
73
+ function validateResourcePath(variable, kind, absolutePath) {
74
+ let stats;
75
+ try {
76
+ stats = statSync(absolutePath);
77
+ } catch {
78
+ throw new Error(`${variable} path does not exist: ${absolutePath}`);
79
+ }
80
+ const valid = kind === "extension" ? stats.isFile() : stats.isFile() || stats.isDirectory();
81
+ if (!valid) {
82
+ throw new Error(
83
+ kind === "extension"
84
+ ? `${variable} path must be an extension file: ${absolutePath}`
85
+ : `${variable} path must be a skill file or directory: ${absolutePath}`,
86
+ );
87
+ }
88
+ }
89
+
90
+ const extensionPaths = resolveResourcePaths("KXM_WORKER_EXTENSION_PATHS", "extension");
91
+ const skillPaths = resolveResourcePaths("KXM_WORKER_SKILL_PATHS", "skill");
92
+ const workspaceDir = resolve(workdir, process.env.KXM_WORKSPACE_DIR?.trim() || ".kxm");
93
+ const configDir = resolve(workdir, process.env.KXM_CONFIG_DIR?.trim() || join(workspaceDir, "config"));
94
+ const logsDir = resolve(workdir, process.env.KXM_LOGS_DIR?.trim() || join(workspaceDir, "logs"));
95
+ const assetsDir = resolve(workdir, process.env.KXM_ASSETS_DIR?.trim() || join(workspaceDir, "assets"));
96
+ const stateDir = resolve(workdir, process.env.KXM_STATE_DIR?.trim() || join(workspaceDir, "state"));
97
+ const safeName = name.replace(/[^A-Za-z0-9_.-]/g, "_");
98
+ const safeProject = project.replace(/[^A-Za-z0-9_.-]/g, "_");
99
+ const identityDigest = createHash("sha256")
100
+ .update(JSON.stringify({ project, agentName: name }))
101
+ .digest("hex")
102
+ .slice(0, 24);
103
+ const workerKey = `${safeProject.slice(0, 24) || "project"}-${safeName.slice(0, 32) || "agent"}-${identityDigest}`;
104
+ const logPath = resolve(workdir, process.env.KXM_WORKER_LOG_PATH?.trim() || join(logsDir, `kxm-worker-${workerKey}.jsonl`));
105
+ const agentLogPath = resolve(workdir, process.env.KXM_AGENT_LOG_PATH?.trim() || join(logsDir, `pi-agent-${workerKey}.log`));
106
+ for (const directory of [configDir, logsDir, assetsDir, stateDir, dirname(logPath), dirname(agentLogPath)]) {
107
+ mkdirSync(directory, { recursive: true });
108
+ }
109
+ const minBackoffMs = 1_000;
110
+ const maxBackoffMs = 30_000;
111
+ const maxRestartsRaw = process.env.KXM_WORKER_MAX_RESTARTS?.trim();
112
+ const maxRestarts = maxRestartsRaw === undefined ? Number.POSITIVE_INFINITY : Number(maxRestartsRaw);
113
+ if ((!Number.isInteger(maxRestarts) && maxRestarts !== Number.POSITIVE_INFINITY) || maxRestarts < 0) {
114
+ throw new Error("KXM_WORKER_MAX_RESTARTS must be a non-negative integer");
115
+ }
116
+ const continueEnabled = process.env.KXM_WORKER_CONTINUE !== "false";
117
+ const initialContinue = process.env.KXM_WORKER_INITIAL_CONTINUE === undefined
118
+ ? continueEnabled
119
+ : process.env.KXM_WORKER_INITIAL_CONTINUE !== "false";
120
+ // Upgrade compatibility keeps the historical shared Pi session unless the
121
+ // operator explicitly enables workflow-scoped isolation.
122
+ const sessionIsolation = process.env.KXM_WORKER_SESSION_ISOLATION?.trim() || "off";
123
+ if (sessionIsolation !== "workflow" && sessionIsolation !== "off") {
124
+ throw new Error("KXM_WORKER_SESSION_ISOLATION must be workflow or off");
125
+ }
126
+ const maxRunSessions = Number(process.env.KXM_WORKER_MAX_RUN_SESSIONS?.trim() || 128);
127
+ if (!Number.isInteger(maxRunSessions) || maxRunSessions < 1 || maxRunSessions > 1_024) {
128
+ throw new Error("KXM_WORKER_MAX_RUN_SESSIONS must be an integer between 1 and 1024");
129
+ }
130
+ const drainTimeoutMs = Number(process.env.KXM_WORKER_DRAIN_MS?.trim() || 15_000);
131
+ const providerRetryMs = Number(process.env.KXM_WORKER_PROVIDER_RETRY_MS?.trim() || 60_000);
132
+ if (!Number.isInteger(providerRetryMs) || providerRetryMs < 1_000 || providerRetryMs > 3_600_000) {
133
+ throw new Error("KXM_WORKER_PROVIDER_RETRY_MS must be an integer between 1000 and 3600000");
134
+ }
135
+ // KXM wait tools may legitimately run for 30 minutes. Keep a one-minute
136
+ // supervisor grace so their own bounded timeout can return durable handles.
137
+ const toolTimeoutMs = Number(process.env.KXM_WORKER_TOOL_TIMEOUT_MS?.trim() || 1_860_000);
138
+ if (!Number.isInteger(toolTimeoutMs) || (toolTimeoutMs !== 0 && (toolTimeoutMs < 1_000 || toolTimeoutMs > 86_400_000))) {
139
+ throw new Error("KXM_WORKER_TOOL_TIMEOUT_MS must be 0 or an integer between 1000 and 86400000");
140
+ }
141
+ let backoffMs = minBackoffMs;
142
+ let restartCount = 0;
143
+ let child;
144
+ let stopping = false;
145
+ let logsClosed = false;
146
+ let continueThisStart = initialContinue;
147
+ let continueFallbackUsed = false;
148
+ let abortRequestId;
149
+ const pidPath = join(stateDir, `worker-${workerKey}.pid`);
150
+ const sessionBindingPath = join(stateDir, `worker-session-binding-${workerKey}.json`);
151
+ const sessionRequestPath = join(stateDir, `worker-session-request-${workerKey}.json`);
152
+ const workerSessionRoot = join(stateDir, "pi-sessions", workerKey);
153
+ const workerStartedAt = new Date().toISOString();
154
+ const workerGeneration = randomUUID();
155
+ let childIncarnation = 0;
156
+ const controlFile = `worker-${workerKey}.stop`;
157
+ const controlPath = join(stateDir, controlFile);
158
+
159
+ function processExists(pid) {
160
+ try {
161
+ process.kill(pid, 0);
162
+ return true;
163
+ } catch (error) {
164
+ return error?.code === "EPERM";
165
+ }
166
+ }
167
+
168
+ function claimPidFile() {
169
+ const record = {
170
+ version: 1,
171
+ pid: process.pid,
172
+ role: "worker",
173
+ agentName: name,
174
+ project,
175
+ workerKey,
176
+ generation: workerGeneration,
177
+ startedAt: workerStartedAt,
178
+ controlFile,
179
+ };
180
+ for (let attempt = 0; attempt < 3; attempt += 1) {
181
+ let descriptor;
182
+ try {
183
+ descriptor = openSync(pidPath, "wx", 0o600);
184
+ writeFileSync(descriptor, `${JSON.stringify(record)}\n`, "utf8");
185
+ closeSync(descriptor);
186
+ descriptor = undefined;
187
+ return;
188
+ } catch (error) {
189
+ if (descriptor !== undefined) closeSync(descriptor);
190
+ if (error?.code !== "EEXIST") throw error;
191
+ let existing;
192
+ try {
193
+ existing = JSON.parse(readFileSync(pidPath, "utf8"));
194
+ } catch (existingError) {
195
+ if (existingError?.code === "ENOENT") continue;
196
+ throw new Error(
197
+ `KXM worker PID claim is invalid at ${pidPath}; remove it only after verifying no matching worker is running`,
198
+ { cause: existingError },
199
+ );
200
+ }
201
+ if (existing?.version === 1 && existing.role === "worker" && Number.isInteger(existing.pid) && processExists(existing.pid)) {
202
+ throw new Error(`KXM worker ${project}/${name} is already managed by PID ${existing.pid}`);
203
+ }
204
+ throw new Error(
205
+ `KXM worker PID claim is stale at ${pidPath}; remove it only after verifying no matching worker is running`,
206
+ );
207
+ }
208
+ }
209
+ throw new Error("could not claim the KXM worker PID file");
210
+ }
211
+
212
+ function cleanupPid() {
213
+ try {
214
+ const record = JSON.parse(readFileSync(pidPath, "utf8"));
215
+ if (record.pid === process.pid && record.generation === workerGeneration) rmSync(pidPath, { force: true });
216
+ } catch {
217
+ // The record may already be removed or replaced by a newer worker.
218
+ }
219
+ }
220
+
221
+ claimPidFile();
222
+ process.once("exit", cleanupPid);
223
+ rmSync(controlPath, { force: true });
224
+ const logStream = createWriteStream(logPath, { flags: "a", mode: 0o600 });
225
+ const agentLogStream = createWriteStream(agentLogPath, { flags: "a", mode: 0o600 });
226
+ const workflowRunIdPattern = /^run_[a-f0-9]{32}$/;
227
+ const piSessionIdPattern = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
228
+ let sessionManifest = {
229
+ version: 1,
230
+ agentName: name,
231
+ project,
232
+ workerKey,
233
+ active: { kind: "default" },
234
+ runs: {},
235
+ updatedAt: workerStartedAt,
236
+ };
237
+ let activeSessionBinding = sessionManifest.active;
238
+ let recoveredCorruptManifest = false;
239
+
240
+ function bindingScope(binding) {
241
+ return binding.kind === "workflow" ? `workflow:${binding.runId}` : "default";
242
+ }
243
+
244
+ function sameBinding(left, right) {
245
+ return left.kind === right.kind && (left.kind === "default" || left.runId === right.runId);
246
+ }
247
+
248
+ function parseSessionBinding(value, label) {
249
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
250
+ if (value.kind === "default" && Object.keys(value).every((key) => key === "kind")) return { kind: "default" };
251
+ if (
252
+ value.kind === "workflow"
253
+ && typeof value.runId === "string"
254
+ && workflowRunIdPattern.test(value.runId)
255
+ && Object.keys(value).every((key) => key === "kind" || key === "runId")
256
+ ) return { kind: "workflow", runId: value.runId };
257
+ throw new Error(`${label} is not a valid default or workflow binding`);
258
+ }
259
+
260
+ function sessionDirForBinding(binding) {
261
+ return binding.kind === "workflow"
262
+ ? join(workerSessionRoot, "runs", binding.runId)
263
+ : join(workerSessionRoot, "default");
264
+ }
265
+
266
+ function ensureSessionDirectory(binding) {
267
+ mkdirSync(workerSessionRoot, { recursive: true, mode: 0o700 });
268
+ const rootStats = lstatSync(workerSessionRoot);
269
+ if (!rootStats.isDirectory() || rootStats.isSymbolicLink()) {
270
+ throw new Error("worker Pi session root must be a real directory, not a link");
271
+ }
272
+ if (binding.kind === "workflow") {
273
+ const runsRoot = join(workerSessionRoot, "runs");
274
+ mkdirSync(runsRoot, { recursive: true, mode: 0o700 });
275
+ const runsStats = lstatSync(runsRoot);
276
+ if (!runsStats.isDirectory() || runsStats.isSymbolicLink()) {
277
+ throw new Error("worker Pi workflow session root must be a real directory, not a link");
278
+ }
279
+ }
280
+ const directory = sessionDirForBinding(binding);
281
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
282
+ const targetStats = lstatSync(directory);
283
+ if (!targetStats.isDirectory() || targetStats.isSymbolicLink()) {
284
+ throw new Error("worker Pi session directory must be a real directory, not a link");
285
+ }
286
+ const canonicalRoot = realpathSync.native(workerSessionRoot);
287
+ const canonicalTarget = realpathSync.native(directory);
288
+ const descendant = relative(canonicalRoot, canonicalTarget);
289
+ if (!descendant || descendant === ".." || descendant.startsWith(`..${sep}`) || isAbsolute(descendant)) {
290
+ throw new Error("worker Pi session directory escapes or aliases its isolated root");
291
+ }
292
+ return directory;
293
+ }
294
+
295
+ function hasSessionHistory(directory, depth = 0) {
296
+ try {
297
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
298
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) return true;
299
+ if (entry.isDirectory() && depth < 2 && hasSessionHistory(join(directory, entry.name), depth + 1)) return true;
300
+ }
301
+ } catch {
302
+ return false;
303
+ }
304
+ return false;
305
+ }
306
+
307
+ function persistSessionManifest() {
308
+ if (sessionIsolation === "off") return;
309
+ sessionManifest.active = activeSessionBinding;
310
+ sessionManifest.updatedAt = new Date().toISOString();
311
+ mkdirSync(dirname(sessionBindingPath), { recursive: true, mode: 0o700 });
312
+ const tmp = `${sessionBindingPath}.${process.pid}.tmp`;
313
+ writeFileSync(tmp, `${JSON.stringify(sessionManifest)}\n`, { encoding: "utf8", mode: 0o600 });
314
+ renameSync(tmp, sessionBindingPath);
315
+ }
316
+
317
+ function loadSessionManifest() {
318
+ if (sessionIsolation === "off") return;
319
+ try {
320
+ const stats = statSync(sessionBindingPath);
321
+ if (!stats.isFile() || stats.size > 128 * 1024) throw new Error("session binding manifest is not a bounded regular file");
322
+ const parsed = JSON.parse(readFileSync(sessionBindingPath, "utf8"));
323
+ if (
324
+ parsed?.version !== 1
325
+ || parsed.agentName !== name
326
+ || parsed.project !== project
327
+ || parsed.workerKey !== workerKey
328
+ || !parsed.runs
329
+ || typeof parsed.runs !== "object"
330
+ || Array.isArray(parsed.runs)
331
+ ) throw new Error("session binding manifest identity or schema is invalid");
332
+ const active = parseSessionBinding(parsed.active, "session binding manifest active value");
333
+ const runs = {};
334
+ const entries = Object.entries(parsed.runs);
335
+ if (entries.length > maxRunSessions) throw new Error("session binding manifest exceeds the configured run limit");
336
+ for (const [runId, value] of entries) {
337
+ if (!workflowRunIdPattern.test(runId) || !value || typeof value !== "object" || Array.isArray(value)) {
338
+ throw new Error("session binding manifest contains an invalid run entry");
339
+ }
340
+ const createdAt = typeof value.createdAt === "string" && Number.isFinite(Date.parse(value.createdAt)) ? value.createdAt : undefined;
341
+ const lastUsedAt = typeof value.lastUsedAt === "string" && Number.isFinite(Date.parse(value.lastUsedAt)) ? value.lastUsedAt : undefined;
342
+ if (!createdAt || !lastUsedAt) throw new Error("session binding manifest contains invalid timestamps");
343
+ runs[runId] = { createdAt, lastUsedAt };
344
+ }
345
+ if (active.kind === "workflow" && !runs[active.runId]) {
346
+ throw new Error("active workflow binding is absent from the session manifest");
347
+ }
348
+ sessionManifest = { version: 1, agentName: name, project, workerKey, active, runs, updatedAt: parsed.updatedAt };
349
+ activeSessionBinding = active;
350
+ } catch (error) {
351
+ if (error?.code === "ENOENT") return;
352
+ const quarantinedPath = `${sessionBindingPath}.corrupt-${Date.now()}`;
353
+ // Preserve the invalid state for diagnosis, but never route from it.
354
+ // If quarantine itself fails (for example due to unsafe permissions),
355
+ // initialization fails closed instead of starting with ambiguous scope.
356
+ renameSync(sessionBindingPath, quarantinedPath);
357
+ activeSessionBinding = { kind: "default" };
358
+ recoveredCorruptManifest = true;
359
+ sessionManifest = {
360
+ version: 1,
361
+ agentName: name,
362
+ project,
363
+ workerKey,
364
+ active: activeSessionBinding,
365
+ runs: {},
366
+ updatedAt: new Date().toISOString(),
367
+ };
368
+ log("worker_session_state_recovered", {
369
+ reason: "corrupt_manifest",
370
+ quarantinedPath,
371
+ error: error instanceof Error ? error.message.slice(0, 512) : String(error).slice(0, 512),
372
+ });
373
+ }
374
+ }
375
+
376
+ function touchSessionBinding(binding) {
377
+ if (binding.kind !== "workflow") return;
378
+ const timestamp = new Date().toISOString();
379
+ if (!sessionManifest.runs[binding.runId]) {
380
+ sessionManifest.runs[binding.runId] = { createdAt: timestamp, lastUsedAt: timestamp };
381
+ } else {
382
+ sessionManifest.runs[binding.runId].lastUsedAt = timestamp;
383
+ }
384
+ const candidates = Object.entries(sessionManifest.runs)
385
+ .filter(([runId]) => runId !== binding.runId)
386
+ .sort((left, right) => left[1].lastUsedAt.localeCompare(right[1].lastUsedAt) || left[0].localeCompare(right[0]));
387
+ while (Object.keys(sessionManifest.runs).length > maxRunSessions) {
388
+ const oldest = candidates.shift();
389
+ if (!oldest) throw new Error("no inactive workflow session is available for bounded eviction");
390
+ delete sessionManifest.runs[oldest[0]];
391
+ // Commit the tombstone before deleting history. If the process stops
392
+ // between these operations, startup reconciliation removes the orphan.
393
+ persistSessionManifest();
394
+ rmSync(sessionDirForBinding({ kind: "workflow", runId: oldest[0] }), { recursive: true, force: true });
395
+ log("worker_session_evicted", { runId: oldest[0], maxRunSessions });
396
+ }
397
+ }
398
+
399
+ function reconcileSessionDirectories() {
400
+ const runsRoot = join(workerSessionRoot, "runs");
401
+ let removed = 0;
402
+ try {
403
+ const rootStats = lstatSync(workerSessionRoot);
404
+ if (!rootStats.isDirectory() || rootStats.isSymbolicLink()) {
405
+ throw new Error("worker Pi session root is not a safe real directory");
406
+ }
407
+ const runsStats = lstatSync(runsRoot);
408
+ if (!runsStats.isDirectory() || runsStats.isSymbolicLink()) {
409
+ rmSync(runsRoot, { recursive: true, force: true });
410
+ log("worker_session_orphans_removed", { count: 1, reason: "unsafe_runs_root" });
411
+ return;
412
+ }
413
+ const entries = readdirSync(runsRoot, { withFileTypes: true });
414
+ if (recoveredCorruptManifest) {
415
+ const recoverable = [];
416
+ for (const entry of entries) {
417
+ const entryPath = join(runsRoot, entry.name);
418
+ if (!workflowRunIdPattern.test(entry.name) || !entry.isDirectory() || entry.isSymbolicLink()) {
419
+ rmSync(entryPath, { recursive: true, force: true });
420
+ removed += 1;
421
+ continue;
422
+ }
423
+ const stats = lstatSync(entryPath);
424
+ recoverable.push({ runId: entry.name, timestamp: stats.mtime.toISOString() });
425
+ }
426
+ recoverable.sort((left, right) => right.timestamp.localeCompare(left.timestamp) || left.runId.localeCompare(right.runId));
427
+ for (const item of recoverable.slice(0, maxRunSessions)) {
428
+ sessionManifest.runs[item.runId] = { createdAt: item.timestamp, lastUsedAt: item.timestamp };
429
+ }
430
+ for (const item of recoverable.slice(maxRunSessions)) {
431
+ rmSync(join(runsRoot, item.runId), { recursive: true, force: true });
432
+ removed += 1;
433
+ }
434
+ if (recoverable.length > 0) {
435
+ log("worker_session_orphans_adopted", { count: Math.min(recoverable.length, maxRunSessions) });
436
+ }
437
+ } else {
438
+ for (const entry of entries) {
439
+ if (!workflowRunIdPattern.test(entry.name) || !sessionManifest.runs[entry.name] || entry.isSymbolicLink()) {
440
+ rmSync(join(runsRoot, entry.name), { recursive: true, force: true });
441
+ removed += 1;
442
+ }
443
+ }
444
+ }
445
+ } catch (error) {
446
+ if (error?.code !== "ENOENT") throw error;
447
+ }
448
+ if (removed > 0) log("worker_session_orphans_removed", { count: removed });
449
+ }
450
+
451
+ function initializeSessionRouting() {
452
+ if (sessionIsolation === "off") return;
453
+ loadSessionManifest();
454
+ reconcileSessionDirectories();
455
+ touchSessionBinding(activeSessionBinding);
456
+ ensureSessionDirectory(activeSessionBinding);
457
+ persistSessionManifest();
458
+ continueThisStart = initialContinue && hasSessionHistory(sessionDirForBinding(activeSessionBinding));
459
+ try {
460
+ const stale = JSON.parse(readFileSync(sessionRequestPath, "utf8"));
461
+ if (stale?.generation !== workerGeneration) {
462
+ rmSync(sessionRequestPath, { force: true });
463
+ log("worker_session_request_discarded", { reason: "stale_generation" });
464
+ }
465
+ } catch (error) {
466
+ if (error?.code !== "ENOENT") {
467
+ rmSync(sessionRequestPath, { force: true });
468
+ log("worker_session_request_discarded", { reason: "malformed_startup_request" });
469
+ }
470
+ }
471
+ }
472
+
473
+ function consumeSessionRouteRequest() {
474
+ if (sessionIsolation === "off") return undefined;
475
+ let request;
476
+ try {
477
+ const stats = statSync(sessionRequestPath);
478
+ if (!stats.isFile() || stats.size > 16 * 1024) throw new Error("session route request is not a bounded regular file");
479
+ request = JSON.parse(readFileSync(sessionRequestPath, "utf8"));
480
+ const from = parseSessionBinding(request.from, "session route source");
481
+ const to = parseSessionBinding(request.to, "session route target");
482
+ const createdAt = Date.parse(request.createdAt);
483
+ if (
484
+ request.version !== 1
485
+ || request.agentName !== name
486
+ || request.project !== project
487
+ || request.workerKey !== workerKey
488
+ || request.generation !== workerGeneration
489
+ || !Number.isInteger(request.childIncarnation)
490
+ || request.childIncarnation !== childIncarnation
491
+ || typeof request.messageId !== "string"
492
+ || !/^msg_[a-f0-9]{32}$/.test(request.messageId)
493
+ || typeof request.sourceSessionId !== "string"
494
+ || !piSessionIdPattern.test(request.sourceSessionId)
495
+ || !Number.isFinite(createdAt)
496
+ || createdAt < Date.now() - 10 * 60_000
497
+ || createdAt > Date.now() + 60_000
498
+ ) throw new Error("session route request identity, generation, or bounds are invalid");
499
+ if (!sameBinding(from, activeSessionBinding)) {
500
+ if (sameBinding(to, activeSessionBinding)) {
501
+ rmSync(sessionRequestPath, { force: true });
502
+ log("worker_session_request_discarded", { reason: "already_applied", messageId: request.messageId });
503
+ return undefined;
504
+ }
505
+ throw new Error("session route source does not match the active binding");
506
+ }
507
+ const previous = activeSessionBinding;
508
+ touchSessionBinding(previous);
509
+ activeSessionBinding = to;
510
+ touchSessionBinding(activeSessionBinding);
511
+ ensureSessionDirectory(activeSessionBinding);
512
+ persistSessionManifest();
513
+ rmSync(sessionRequestPath, { force: true });
514
+ continueThisStart = continueEnabled && hasSessionHistory(sessionDirForBinding(activeSessionBinding));
515
+ continueFallbackUsed = false;
516
+ log("worker_session_routed", {
517
+ from: bindingScope(previous),
518
+ to: bindingScope(activeSessionBinding),
519
+ messageId: request.messageId,
520
+ routeLatencyMs: Date.now() - createdAt,
521
+ });
522
+ return { from: previous, to: activeSessionBinding, messageId: request.messageId };
523
+ } catch (error) {
524
+ if (error?.code === "ENOENT") return undefined;
525
+ rmSync(sessionRequestPath, { force: true });
526
+ log("worker_session_request_rejected", { reason: error instanceof Error ? error.message : "invalid_request" });
527
+ return undefined;
528
+ }
529
+ }
530
+
531
+ function quoteWindowsCommandArgument(value, label) {
532
+ if (/[\0\r\n"%!]/.test(value)) {
533
+ throw new Error(`${label} contains characters that cannot be passed safely to a Windows command script`);
534
+ }
535
+ return `"${value}"`;
536
+ }
537
+
538
+ function log(event, details = {}) {
539
+ const line = `${JSON.stringify({
540
+ timestamp: new Date().toISOString(),
541
+ event,
542
+ worker: name,
543
+ project,
544
+ sessionScope: sessionIsolation === "workflow" ? bindingScope(activeSessionBinding) : "legacy",
545
+ ...details,
546
+ })}\n`;
547
+ logStream.write(line);
548
+ process.stdout.write(line);
549
+ }
550
+
551
+ initializeSessionRouting();
552
+
553
+ function closeLogs() {
554
+ if (logsClosed) return;
555
+ logsClosed = true;
556
+ logStream.end();
557
+ agentLogStream.end();
558
+ }
559
+
560
+ function writeRecoveryEnvelope(details) {
561
+ let persistedContext = {};
562
+ for (const contextPath of [
563
+ join(stateDir, `worker-context-${workerKey}.json`),
564
+ join(stateDir, `worker-context-${safeName}.json`),
565
+ ]) {
566
+ try {
567
+ const candidate = JSON.parse(readFileSync(contextPath, "utf8"));
568
+ if (candidate?.version === 1 && candidate.agentName === name && candidate.project === project) {
569
+ persistedContext = {
570
+ runId: typeof candidate.runId === "string" && workflowRunIdPattern.test(candidate.runId) ? candidate.runId : null,
571
+ stageId: typeof candidate.stageId === "string" && /^[A-Za-z0-9_.-]{1,128}$/.test(candidate.stageId) ? candidate.stageId : null,
572
+ activeMessageIds: Array.isArray(candidate.activeMessageIds)
573
+ ? candidate.activeMessageIds.filter((value) => typeof value === "string" && /^msg_[a-f0-9]{32}$/.test(value)).slice(0, 3)
574
+ : [],
575
+ pendingMessageIds: Array.isArray(candidate.pendingMessageIds)
576
+ ? candidate.pendingMessageIds.filter((value) => typeof value === "string" && /^msg_[a-f0-9]{32}$/.test(value)).slice(0, 16)
577
+ : [],
578
+ artifactPointers: Array.isArray(candidate.artifactPointers)
579
+ ? candidate.artifactPointers.filter((value) => typeof value === "string" && value.length > 0 && value.length <= 2_048 && !/[\0\r\n]/.test(value)).slice(0, 16)
580
+ : [],
581
+ };
582
+ break;
583
+ }
584
+ } catch {
585
+ // Try the legacy name-only context after the collision-safe path.
586
+ }
587
+ }
588
+ if (sessionIsolation === "workflow") {
589
+ const boundRunId = activeSessionBinding.kind === "workflow" ? activeSessionBinding.runId : null;
590
+ if (persistedContext.runId !== boundRunId) {
591
+ persistedContext = {
592
+ runId: null,
593
+ stageId: null,
594
+ activeMessageIds: persistedContext.activeMessageIds ?? [],
595
+ pendingMessageIds: persistedContext.pendingMessageIds ?? [],
596
+ artifactPointers: [],
597
+ };
598
+ }
599
+ }
600
+ const envelope = {
601
+ version: 1,
602
+ reason: details.reason,
603
+ agentName: name,
604
+ project,
605
+ workerKey,
606
+ generation: workerGeneration,
607
+ previousContinue: continueEnabled,
608
+ freshSession: details.freshSession === true,
609
+ sessionBinding: sessionIsolation === "workflow" ? activeSessionBinding : undefined,
610
+ createdAt: new Date().toISOString(),
611
+ runId: null,
612
+ stageId: null,
613
+ activeMessageIds: [],
614
+ pendingMessageIds: [],
615
+ artifactPointers: [],
616
+ ...persistedContext,
617
+ ...(details.signal ? { signal: details.signal } : {}),
618
+ ...(details.failureClass ? { failureClass: details.failureClass } : {}),
619
+ };
620
+ writeFileSync(join(stateDir, `worker-recovery-${workerKey}.json`), `${JSON.stringify(envelope)}\n`, {
621
+ encoding: "utf8",
622
+ mode: 0o600,
623
+ });
624
+ }
625
+
626
+ function start() {
627
+ childIncarnation += 1;
628
+ const thisChildIncarnation = childIncarnation;
629
+ try {
630
+ for (const extensionPath of extensionPaths) {
631
+ validateResourcePath("KXM_WORKER_EXTENSION_PATHS", "extension", extensionPath);
632
+ }
633
+ for (const skillPath of skillPaths) {
634
+ validateResourcePath("KXM_WORKER_SKILL_PATHS", "skill", skillPath);
635
+ }
636
+ } catch (error) {
637
+ const failure = error instanceof Error ? error : new Error(String(error));
638
+ log("worker_resource_validation_failed", { message: failure.message });
639
+ process.stderr.write(`${failure.stack ?? failure.message}\n`);
640
+ process.exitCode = 1;
641
+ closeLogs();
642
+ return;
643
+ }
644
+ const selectedModel = modelCandidates[modelIndex];
645
+ const activeSessionDir = sessionIsolation === "workflow" ? ensureSessionDirectory(activeSessionBinding) : undefined;
646
+ const sessionDisplayName = sessionIsolation === "workflow"
647
+ ? `${name}@${activeSessionBinding.kind === "workflow" ? activeSessionBinding.runId.slice(4, 12) : "default"}`
648
+ : name;
649
+ const args = ["--mode", "rpc", "--name", sessionDisplayName];
650
+ if (activeSessionDir) args.push("--session-dir", activeSessionDir);
651
+ if (continueThisStart) args.push("--continue");
652
+ if (selectedModel) args.push("--model", selectedModel);
653
+ if (toolAllowlist.length) args.push("--tools", toolAllowlist.join(","));
654
+ if (extensionPaths.length) {
655
+ args.push("--no-extensions");
656
+ for (const extensionPath of extensionPaths) args.push("--extension", extensionPath);
657
+ }
658
+ if (skillPaths.length) {
659
+ args.push("--no-skills");
660
+ for (const skillPath of skillPaths) args.push("--skill", skillPath);
661
+ }
662
+ const windowsCommandScript = process.platform === "win32" && /\.(?:cmd|bat)$/i.test(command);
663
+ const launchCommand = windowsCommandScript ? process.env.ComSpec?.trim() || "cmd.exe" : command;
664
+ const windowsCommandLine = windowsCommandScript
665
+ ? `"${[command, ...args].map((value, index) => quoteWindowsCommandArgument(value, index === 0 ? "KXM_PI_COMMAND" : "Pi argument")).join(" ")}"`
666
+ : undefined;
667
+ const launchArgs = windowsCommandLine ? ["/d", "/s", "/v:off", "/c", windowsCommandLine] : args;
668
+ log("worker_starting", {
669
+ command,
670
+ launcher: launchCommand,
671
+ workdir,
672
+ workspaceDir,
673
+ configDir,
674
+ logsDir,
675
+ assetsDir,
676
+ stateDir,
677
+ logPath,
678
+ agentLogPath,
679
+ workerKey,
680
+ toolTimeoutMs,
681
+ sessionIsolation,
682
+ childIncarnation: thisChildIncarnation,
683
+ ...(activeSessionDir ? { sessionDir: activeSessionDir } : {}),
684
+ ...(selectedModel ? { model: selectedModel, modelCandidate: modelIndex + 1, modelCandidateCount: modelCandidates.length } : {}),
685
+ explicitToolCount: toolAllowlist.length,
686
+ explicitExtensionCount: extensionPaths.length,
687
+ explicitSkillCount: skillPaths.length,
688
+ });
689
+ const startedAt = Date.now();
690
+ let continuationUnresumable = false;
691
+ let providerErrorBuffer = "";
692
+ const rpcDecoder = new StringDecoder("utf8");
693
+ let rpcLineFragments = [];
694
+ let rpcLineStoredChars = 0;
695
+ let rpcLineChars = 0;
696
+ let rpcLineOversized = false;
697
+ let rpcLineTail = "";
698
+ const maxRpcFrameChars = 4 * 1024 * 1024;
699
+ const rpcMetadataWindowChars = 64 * 1024;
700
+ let finalProviderFailure;
701
+ let recoveryRestartRequested;
702
+ let recoveryExitTimer;
703
+ const toolTimers = new Map();
704
+ const observeProviderOutput = (text) => {
705
+ providerErrorBuffer = `${providerErrorBuffer}${text}`.slice(-8_192);
706
+ if (/invalid_request_error|missing_tool_result|tool_use[\s\S]{0,256}tool_result/i.test(providerErrorBuffer)) {
707
+ continuationUnresumable = true;
708
+ }
709
+ };
710
+ let completed = false;
711
+ const clearToolTimers = () => {
712
+ for (const timer of toolTimers.values()) clearTimeout(timer);
713
+ toolTimers.clear();
714
+ };
715
+ const complete = (code, signal, error) => {
716
+ if (completed) return;
717
+ completed = true;
718
+ if (recoveryExitTimer) clearTimeout(recoveryExitTimer);
719
+ clearToolTimers();
720
+ child = undefined;
721
+ if (error) log("worker_process_error", { message: error.message, code: error.code });
722
+ const uptimeMs = Date.now() - startedAt;
723
+ log("worker_exited", { code, signal, ...(error ? { error: error.message } : {}), uptimeMs });
724
+ if (stopping) {
725
+ closeLogs();
726
+ process.exit(0);
727
+ return;
728
+ }
729
+ if (recoveryRestartRequested) {
730
+ if (recoveryRestartRequested.reason === "unresumable_session") {
731
+ start();
732
+ return;
733
+ }
734
+ if (restartCount >= maxRestarts) {
735
+ log("worker_restart_limit_reached", { restartCount, reason: recoveryRestartRequested.reason });
736
+ process.exitCode = code || 1;
737
+ closeLogs();
738
+ return;
739
+ }
740
+ restartCount += 1;
741
+ const delay = recoveryRestartRequested.delayMs;
742
+ log(recoveryRestartRequested.reason === "provider_error"
743
+ ? "worker_provider_restart_scheduled"
744
+ : "worker_tool_restart_scheduled", {
745
+ delayMs: delay,
746
+ restartCount,
747
+ failureClass: recoveryRestartRequested.failureClass,
748
+ fallbackSelected: recoveryRestartRequested.fallbackSelected,
749
+ ...(recoveryRestartRequested.nextModel ? { nextModel: recoveryRestartRequested.nextModel } : {}),
750
+ });
751
+ setTimeout(start, delay);
752
+ return;
753
+ }
754
+ const routedSession = consumeSessionRouteRequest();
755
+ if (routedSession) {
756
+ backoffMs = minBackoffMs;
757
+ start();
758
+ return;
759
+ }
760
+ if (continueThisStart && !continueFallbackUsed && continuationUnresumable && (code || 0) !== 0) {
761
+ continueFallbackUsed = true;
762
+ continueThisStart = false;
763
+ writeRecoveryEnvelope({ reason: "unresumable_session", freshSession: true });
764
+ log("worker_continue_fallback", { reason: "unresumable_session", uptimeMs });
765
+ start();
766
+ return;
767
+ }
768
+ if (uptimeMs > 60_000) {
769
+ continueFallbackUsed = false;
770
+ }
771
+ if (restartCount >= maxRestarts) {
772
+ log("worker_restart_limit_reached", { restartCount });
773
+ process.exitCode = code || 1;
774
+ closeLogs();
775
+ return;
776
+ }
777
+ restartCount += 1;
778
+ continueThisStart = continueEnabled
779
+ && (sessionIsolation === "off" || hasSessionHistory(sessionDirForBinding(activeSessionBinding)));
780
+ if (Date.now() - startedAt > 60_000) backoffMs = minBackoffMs;
781
+ const delay = backoffMs;
782
+ backoffMs = Math.min(maxBackoffMs, backoffMs * 2);
783
+ log("worker_restart_scheduled", { delayMs: delay, restartCount });
784
+ setTimeout(start, delay);
785
+ };
786
+
787
+ function providerFailureClass(errorMessage) {
788
+ return /\b(quota reached|quota exceeded|rate limit(?:ed)?|too many requests|resource exhausted|http 429)\b/i.test(errorMessage)
789
+ ? "quota"
790
+ : "provider_error";
791
+ }
792
+
793
+ function observeRpcEvent(response) {
794
+ if (response?.type === "tool_execution_start" && toolTimeoutMs > 0) {
795
+ const toolCallId = typeof response.toolCallId === "string" && response.toolCallId.length <= 256
796
+ ? response.toolCallId
797
+ : `unknown-${Date.now()}`;
798
+ const toolName = typeof response.toolName === "string" && /^[A-Za-z0-9_.-]{1,64}$/.test(response.toolName)
799
+ ? response.toolName
800
+ : "unknown";
801
+ const existing = toolTimers.get(toolCallId);
802
+ if (existing) clearTimeout(existing);
803
+ const timer = setTimeout(() => {
804
+ toolTimers.delete(toolCallId);
805
+ requestRecoveryRestart({
806
+ reason: "tool_timeout",
807
+ failureClass: "timeout",
808
+ rotateModel: false,
809
+ toolName,
810
+ });
811
+ }, toolTimeoutMs);
812
+ timer.unref();
813
+ toolTimers.set(toolCallId, timer);
814
+ return;
815
+ }
816
+ if (response?.type === "tool_execution_end") {
817
+ const timer = toolTimers.get(response.toolCallId);
818
+ if (timer) clearTimeout(timer);
819
+ toolTimers.delete(response.toolCallId);
820
+ return;
821
+ }
822
+ if (response?.type === "agent_end" && Array.isArray(response.messages)) {
823
+ const assistant = [...response.messages].reverse().find((message) => message?.role === "assistant");
824
+ if (assistant?.stopReason === "error") {
825
+ finalProviderFailure = providerFailureClass(
826
+ typeof assistant.errorMessage === "string" ? assistant.errorMessage : "provider error",
827
+ );
828
+ } else if (assistant) {
829
+ finalProviderFailure = undefined;
830
+ }
831
+ return;
832
+ }
833
+ if (response?.type !== "agent_settled") return;
834
+ if (continueThisStart && !continueFallbackUsed && continuationUnresumable) {
835
+ requestRecoveryRestart({
836
+ reason: "unresumable_session",
837
+ failureClass: "unresumable_session",
838
+ rotateModel: false,
839
+ });
840
+ return;
841
+ }
842
+ if (!finalProviderFailure) return;
843
+ requestRecoveryRestart({
844
+ reason: "provider_error",
845
+ failureClass: finalProviderFailure,
846
+ rotateModel: true,
847
+ });
848
+ }
849
+
850
+ function requestRecoveryRestart({ reason, failureClass, rotateModel, toolName }) {
851
+ if (stopping || completed || recoveryRestartRequested || !child) return;
852
+ clearToolTimers();
853
+ try {
854
+ statSync(sessionRequestPath);
855
+ rmSync(sessionRequestPath, { force: true });
856
+ log("worker_session_request_discarded", { reason: "active_turn_recovery" });
857
+ } catch {
858
+ // A missing route request is the normal active-turn recovery case.
859
+ }
860
+ const unresumableFallback = reason === "unresumable_session";
861
+ const fallbackSelected = Boolean(!unresumableFallback && rotateModel && modelIndex + 1 < modelCandidates.length);
862
+ if (fallbackSelected) modelIndex += 1;
863
+ const nextModel = modelCandidates[modelIndex];
864
+ const delayMs = unresumableFallback ? 0 : reason === "tool_timeout" || fallbackSelected ? minBackoffMs : providerRetryMs;
865
+ recoveryRestartRequested = { reason, failureClass, fallbackSelected, nextModel, delayMs };
866
+ if (unresumableFallback) {
867
+ continueThisStart = false;
868
+ continueFallbackUsed = true;
869
+ writeRecoveryEnvelope({ reason, freshSession: true });
870
+ log("worker_continue_fallback", { reason, settledError: true, uptimeMs: Date.now() - startedAt });
871
+ } else {
872
+ continueThisStart = continueEnabled
873
+ && (sessionIsolation === "off" || hasSessionHistory(sessionDirForBinding(activeSessionBinding)));
874
+ continueFallbackUsed = false;
875
+ writeRecoveryEnvelope({ reason, freshSession: !continueEnabled, failureClass });
876
+ log(reason === "provider_error" ? "worker_provider_failure" : "worker_tool_timeout", {
877
+ failureClass,
878
+ fallbackSelected,
879
+ ...(toolName ? { toolName } : {}),
880
+ ...(selectedModel ? { failedModel: selectedModel } : {}),
881
+ ...(nextModel ? { nextModel } : {}),
882
+ });
883
+ }
884
+ try {
885
+ child.stdin.end();
886
+ } catch {
887
+ // The bounded force timer below handles an already-broken stdin pipe.
888
+ }
889
+ const forceAfterMs = Math.min(Number.isFinite(drainTimeoutMs) ? drainTimeoutMs : 15_000, 5_000);
890
+ const restartTarget = child;
891
+ recoveryExitTimer = setTimeout(() => {
892
+ if (!restartTarget || child !== restartTarget || restartTarget.exitCode !== null || restartTarget.signalCode !== null) return;
893
+ log(reason === "provider_error"
894
+ ? "worker_provider_restart_killed"
895
+ : reason === "tool_timeout"
896
+ ? "worker_tool_restart_killed"
897
+ : "worker_continue_fallback_killed", {
898
+ failureClass,
899
+ signal: "SIGKILL",
900
+ });
901
+ if (process.platform === "win32" && restartTarget.pid) {
902
+ spawnSync("taskkill.exe", ["/pid", String(restartTarget.pid), "/t", "/f"], { windowsHide: true, stdio: "ignore" });
903
+ } else {
904
+ restartTarget.kill("SIGKILL");
905
+ }
906
+ }, forceAfterMs);
907
+ recoveryExitTimer.unref();
908
+ }
909
+
910
+ function observeRpcLine(line) {
911
+ const value = line.trim();
912
+ if (!value) return;
913
+ try {
914
+ const response = JSON.parse(value);
915
+ if (stopping && abortRequestId && response?.id === abortRequestId && response.type === "response" && response.command === "abort" && response.success === true) {
916
+ abortRequestId = undefined;
917
+ log("worker_drain_confirmed");
918
+ child?.stdin.end();
919
+ }
920
+ if (!stopping) observeRpcEvent(response);
921
+ } catch {
922
+ // Pi RPC output is newline-delimited JSON; unrelated output remains only in the protected agent log.
923
+ }
924
+ }
925
+
926
+ function decodedJsonString(value) {
927
+ try {
928
+ return JSON.parse(`"${value}"`);
929
+ } catch {
930
+ return undefined;
931
+ }
932
+ }
933
+
934
+ function decodedJsonStringFields(value, field) {
935
+ const matches = [];
936
+ const pattern = new RegExp(`(?:^|[{,])\\s*"${field}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`, "g");
937
+ for (const match of value.matchAll(pattern)) {
938
+ const decoded = decodedJsonString(match[1]);
939
+ if (typeof decoded === "string") matches.push(decoded);
940
+ }
941
+ return matches;
942
+ }
943
+
944
+ function observeOversizedRpcLine(prefix, tail, frameChars) {
945
+ const prefixWindow = prefix.slice(0, rpcMetadataWindowChars);
946
+ const tailWindow = tail.slice(-rpcMetadataWindowChars);
947
+ const decodedType = decodedJsonStringFields(prefixWindow, "type")[0];
948
+ let metadataRecovered = false;
949
+ if (decodedType === "tool_execution_start" || decodedType === "tool_execution_end") {
950
+ const toolCallId = decodedJsonStringFields(prefixWindow, "toolCallId")[0];
951
+ const toolName = decodedJsonStringFields(prefixWindow, "toolName")[0];
952
+ observeRpcEvent({
953
+ type: decodedType,
954
+ ...(toolCallId ? { toolCallId } : {}),
955
+ ...(toolName ? { toolName } : {}),
956
+ });
957
+ metadataRecovered = Boolean(toolCallId);
958
+ } else if (decodedType === "agent_end") {
959
+ const stopReasons = [
960
+ ...decodedJsonStringFields(prefixWindow, "stopReason"),
961
+ ...decodedJsonStringFields(tailWindow, "stopReason"),
962
+ ];
963
+ const errorMessages = [
964
+ ...decodedJsonStringFields(prefixWindow, "errorMessage"),
965
+ ...decodedJsonStringFields(tailWindow, "errorMessage"),
966
+ ];
967
+ const stopReason = stopReasons.at(-1);
968
+ const errorMessage = errorMessages.at(-1);
969
+ if (stopReason) {
970
+ observeRpcEvent({
971
+ type: "agent_end",
972
+ messages: [{
973
+ role: "assistant",
974
+ stopReason,
975
+ ...(errorMessage ? { errorMessage } : {}),
976
+ }],
977
+ });
978
+ metadataRecovered = true;
979
+ }
980
+ } else if (decodedType === "agent_settled") {
981
+ observeRpcEvent({ type: "agent_settled" });
982
+ metadataRecovered = true;
983
+ }
984
+ log("worker_rpc_frame_oversized", {
985
+ frameChars,
986
+ storedPrefixChars: prefix.length,
987
+ storedTailChars: tail.length,
988
+ eventType: metadataRecovered ? decodedType : "unknown",
989
+ metadataRecovered,
990
+ });
991
+ }
992
+
993
+ function appendRpcLineFragment(fragment) {
994
+ rpcLineChars += fragment.length;
995
+ if (!rpcLineOversized) {
996
+ const remaining = maxRpcFrameChars - rpcLineStoredChars;
997
+ if (fragment.length <= remaining) {
998
+ rpcLineFragments.push(fragment);
999
+ rpcLineStoredChars += fragment.length;
1000
+ return;
1001
+ }
1002
+ if (remaining > 0) {
1003
+ rpcLineFragments.push(fragment.slice(0, remaining));
1004
+ rpcLineStoredChars += remaining;
1005
+ }
1006
+ rpcLineOversized = true;
1007
+ rpcLineTail = fragment.slice(remaining).slice(-rpcMetadataWindowChars);
1008
+ return;
1009
+ }
1010
+ rpcLineTail = `${rpcLineTail}${fragment}`.slice(-rpcMetadataWindowChars);
1011
+ }
1012
+
1013
+ function finishRpcLine() {
1014
+ const prefix = rpcLineFragments.join("");
1015
+ if (rpcLineOversized) observeOversizedRpcLine(prefix, rpcLineTail, rpcLineChars);
1016
+ else observeRpcLine(prefix);
1017
+ rpcLineFragments = [];
1018
+ rpcLineStoredChars = 0;
1019
+ rpcLineChars = 0;
1020
+ rpcLineOversized = false;
1021
+ rpcLineTail = "";
1022
+ }
1023
+
1024
+ function observeRpcText(text) {
1025
+ let offset = 0;
1026
+ let newline = text.indexOf("\n", offset);
1027
+ while (newline >= 0) {
1028
+ appendRpcLineFragment(text.slice(offset, newline));
1029
+ finishRpcLine();
1030
+ offset = newline + 1;
1031
+ newline = text.indexOf("\n", offset);
1032
+ }
1033
+ if (offset < text.length) appendRpcLineFragment(text.slice(offset));
1034
+ }
1035
+ try {
1036
+ child = spawn(launchCommand, launchArgs, {
1037
+ cwd: workdir,
1038
+ env: {
1039
+ ...process.env,
1040
+ KXM_WORKSPACE_DIR: workspaceDir,
1041
+ KXM_CONFIG_DIR: configDir,
1042
+ KXM_LOGS_DIR: logsDir,
1043
+ KXM_ASSETS_DIR: assetsDir,
1044
+ KXM_STATE_DIR: stateDir,
1045
+ KXM_WORKER_IDENTITY_KEY: workerKey,
1046
+ KXM_WORKER_GENERATION: workerGeneration,
1047
+ KXM_WORKER_CHILD_INCARCATION: String(thisChildIncarnation),
1048
+ KXM_WORKER_SESSION_ISOLATION: sessionIsolation,
1049
+ KXM_WORKER_SESSION_SCOPE: sessionIsolation === "workflow" ? bindingScope(activeSessionBinding) : "legacy",
1050
+ },
1051
+ stdio: ["pipe", "pipe", "pipe"],
1052
+ windowsHide: true,
1053
+ windowsVerbatimArguments: windowsCommandScript,
1054
+ });
1055
+ } catch (error) {
1056
+ complete(null, null, error instanceof Error ? error : new Error(String(error)));
1057
+ return;
1058
+ }
1059
+ child.stdout.on("data", (chunk) => {
1060
+ agentLogStream.write(chunk);
1061
+ const text = rpcDecoder.write(chunk);
1062
+ observeProviderOutput(text);
1063
+ observeRpcText(text);
1064
+ });
1065
+ child.stdout.on("end", () => {
1066
+ const trailing = rpcDecoder.end();
1067
+ if (trailing) observeRpcText(trailing);
1068
+ if (rpcLineChars > 0) finishRpcLine();
1069
+ });
1070
+ child.stderr.on("data", (chunk) => {
1071
+ agentLogStream.write(chunk);
1072
+ observeProviderOutput(chunk.toString("utf8"));
1073
+ });
1074
+ child.once("error", (error) => complete(null, null, error));
1075
+ // `close` follows stdio completion, so a final unterminated frame is observed
1076
+ // before recovery decisions and log streams are finalized.
1077
+ child.once("close", (code, signal) => complete(code, signal));
1078
+ }
1079
+
1080
+ function shutdown(signal) {
1081
+ if (stopping) return;
1082
+ stopping = true;
1083
+ writeRecoveryEnvelope({ reason: "worker_signal", signal, freshSession: false });
1084
+ log("worker_stopping", { signal });
1085
+ if (!child) {
1086
+ closeLogs();
1087
+ process.exit(0);
1088
+ return;
1089
+ }
1090
+ log("worker_drain_wait", { timeoutMs: Number.isFinite(drainTimeoutMs) ? drainTimeoutMs : 15_000 });
1091
+ try {
1092
+ abortRequestId = `worker-abort-${process.pid}-${Date.now()}`;
1093
+ child.stdin.write(`${JSON.stringify({ id: abortRequestId, type: "abort" })}\n`);
1094
+ log("worker_abort_requested", { requestId: abortRequestId });
1095
+ } catch (error) {
1096
+ log("worker_abort_failed", { message: error instanceof Error ? error.message : String(error) });
1097
+ }
1098
+ const force = setTimeout(() => {
1099
+ if (!child || child.exitCode !== null || child.signalCode !== null) return;
1100
+ log("worker_killed", { signal: "SIGKILL" });
1101
+ if (process.platform === "win32" && child.pid) {
1102
+ spawnSync("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], { windowsHide: true, stdio: "ignore" });
1103
+ } else {
1104
+ child.kill("SIGKILL");
1105
+ }
1106
+ }, Number.isFinite(drainTimeoutMs) ? drainTimeoutMs : 15_000);
1107
+ force.unref();
1108
+ }
1109
+
1110
+ process.once("SIGINT", () => shutdown("SIGINT"));
1111
+ process.once("SIGTERM", () => shutdown("SIGTERM"));
1112
+ const controlTimer = setInterval(() => {
1113
+ try {
1114
+ const request = JSON.parse(readFileSync(controlPath, "utf8"));
1115
+ if (request.startedAt !== workerStartedAt || request.generation !== workerGeneration) return;
1116
+ rmSync(controlPath, { force: true });
1117
+ shutdown("operator");
1118
+ } catch {
1119
+ // No stop request is waiting.
1120
+ }
1121
+ }, 250);
1122
+ controlTimer.unref();
1123
+ const stopAfterMs = Number(process.env.KXM_WORKER_STOP_AFTER_MS?.trim() || 0);
1124
+ if (Number.isInteger(stopAfterMs) && stopAfterMs > 0) {
1125
+ setTimeout(() => shutdown("timeout"), stopAfterMs).unref();
1126
+ }
1127
+ start();