pilotswarm 0.0.1 → 0.4.1

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 (150) hide show
  1. package/README.md +37 -1
  2. package/mcp/README.md +484 -0
  3. package/mcp/dist/bin/pilotswarm-mcp.d.ts +3 -0
  4. package/mcp/dist/bin/pilotswarm-mcp.d.ts.map +1 -0
  5. package/mcp/dist/bin/pilotswarm-mcp.js +367 -0
  6. package/mcp/dist/bin/pilotswarm-mcp.js.map +1 -0
  7. package/mcp/dist/src/auth.d.ts +7 -0
  8. package/mcp/dist/src/auth.d.ts.map +1 -0
  9. package/mcp/dist/src/auth.js +99 -0
  10. package/mcp/dist/src/auth.js.map +1 -0
  11. package/mcp/dist/src/context.d.ts +48 -0
  12. package/mcp/dist/src/context.d.ts.map +1 -0
  13. package/mcp/dist/src/context.js +83 -0
  14. package/mcp/dist/src/context.js.map +1 -0
  15. package/mcp/dist/src/index.d.ts +4 -0
  16. package/mcp/dist/src/index.d.ts.map +1 -0
  17. package/mcp/dist/src/index.js +3 -0
  18. package/mcp/dist/src/index.js.map +1 -0
  19. package/mcp/dist/src/prompts/skills.d.ts +4 -0
  20. package/mcp/dist/src/prompts/skills.d.ts.map +1 -0
  21. package/mcp/dist/src/prompts/skills.js +11 -0
  22. package/mcp/dist/src/prompts/skills.js.map +1 -0
  23. package/mcp/dist/src/resources/agents.d.ts +4 -0
  24. package/mcp/dist/src/resources/agents.d.ts.map +1 -0
  25. package/mcp/dist/src/resources/agents.js +64 -0
  26. package/mcp/dist/src/resources/agents.js.map +1 -0
  27. package/mcp/dist/src/resources/facts.d.ts +4 -0
  28. package/mcp/dist/src/resources/facts.d.ts.map +1 -0
  29. package/mcp/dist/src/resources/facts.js +125 -0
  30. package/mcp/dist/src/resources/facts.js.map +1 -0
  31. package/mcp/dist/src/resources/models.d.ts +4 -0
  32. package/mcp/dist/src/resources/models.d.ts.map +1 -0
  33. package/mcp/dist/src/resources/models.js +43 -0
  34. package/mcp/dist/src/resources/models.js.map +1 -0
  35. package/mcp/dist/src/resources/sessions.d.ts +4 -0
  36. package/mcp/dist/src/resources/sessions.d.ts.map +1 -0
  37. package/mcp/dist/src/resources/sessions.js +190 -0
  38. package/mcp/dist/src/resources/sessions.js.map +1 -0
  39. package/mcp/dist/src/resources/subscriptions.d.ts +9 -0
  40. package/mcp/dist/src/resources/subscriptions.d.ts.map +1 -0
  41. package/mcp/dist/src/resources/subscriptions.js +157 -0
  42. package/mcp/dist/src/resources/subscriptions.js.map +1 -0
  43. package/mcp/dist/src/server.d.ts +4 -0
  44. package/mcp/dist/src/server.d.ts.map +1 -0
  45. package/mcp/dist/src/server.js +59 -0
  46. package/mcp/dist/src/server.js.map +1 -0
  47. package/mcp/dist/src/tools/agents.d.ts +4 -0
  48. package/mcp/dist/src/tools/agents.d.ts.map +1 -0
  49. package/mcp/dist/src/tools/agents.js +317 -0
  50. package/mcp/dist/src/tools/agents.js.map +1 -0
  51. package/mcp/dist/src/tools/facts.d.ts +4 -0
  52. package/mcp/dist/src/tools/facts.d.ts.map +1 -0
  53. package/mcp/dist/src/tools/facts.js +151 -0
  54. package/mcp/dist/src/tools/facts.js.map +1 -0
  55. package/mcp/dist/src/tools/models.d.ts +4 -0
  56. package/mcp/dist/src/tools/models.d.ts.map +1 -0
  57. package/mcp/dist/src/tools/models.js +256 -0
  58. package/mcp/dist/src/tools/models.js.map +1 -0
  59. package/mcp/dist/src/tools/sessions.d.ts +4 -0
  60. package/mcp/dist/src/tools/sessions.d.ts.map +1 -0
  61. package/mcp/dist/src/tools/sessions.js +606 -0
  62. package/mcp/dist/src/tools/sessions.js.map +1 -0
  63. package/mcp/dist/src/util/command.d.ts +52 -0
  64. package/mcp/dist/src/util/command.d.ts.map +1 -0
  65. package/mcp/dist/src/util/command.js +78 -0
  66. package/mcp/dist/src/util/command.js.map +1 -0
  67. package/package.json +82 -6
  68. package/tui/README.md +35 -0
  69. package/tui/bin/tui.js +30 -0
  70. package/tui/plugins/.mcp.json +7 -0
  71. package/tui/plugins/plugin.json +13 -0
  72. package/tui/plugins/session-policy.json +8 -0
  73. package/tui/src/app.js +850 -0
  74. package/tui/src/auth/cli.js +111 -0
  75. package/tui/src/auth/entra-auth.js +218 -0
  76. package/tui/src/bootstrap-env.js +176 -0
  77. package/tui/src/embedded-workers.js +79 -0
  78. package/tui/src/http-transport-host.js +106 -0
  79. package/tui/src/index.js +340 -0
  80. package/tui/src/node-sdk-transport.js +1794 -0
  81. package/tui/src/platform.js +984 -0
  82. package/tui/src/plugin-config.js +273 -0
  83. package/tui/src/portal.js +7 -0
  84. package/tui/src/version.js +7 -0
  85. package/tui/tui-splash-mobile.txt +7 -0
  86. package/tui/tui-splash.txt +11 -0
  87. package/ui/core/README.md +6 -0
  88. package/ui/core/src/commands.js +93 -0
  89. package/ui/core/src/context-usage.js +212 -0
  90. package/ui/core/src/controller.js +6201 -0
  91. package/ui/core/src/formatting.js +1036 -0
  92. package/ui/core/src/history.js +950 -0
  93. package/ui/core/src/index.js +13 -0
  94. package/ui/core/src/layout.js +332 -0
  95. package/ui/core/src/reducer.js +1952 -0
  96. package/ui/core/src/selectors.js +5419 -0
  97. package/ui/core/src/session-errors.js +14 -0
  98. package/ui/core/src/session-tree.js +151 -0
  99. package/ui/core/src/state.js +251 -0
  100. package/ui/core/src/store.js +23 -0
  101. package/ui/core/src/system-titles.js +24 -0
  102. package/ui/core/src/themes/catppuccin-mocha.js +56 -0
  103. package/ui/core/src/themes/cobalt2.js +56 -0
  104. package/ui/core/src/themes/dark-high-contrast.js +56 -0
  105. package/ui/core/src/themes/daylight.js +62 -0
  106. package/ui/core/src/themes/dracula.js +56 -0
  107. package/ui/core/src/themes/github-dark.js +56 -0
  108. package/ui/core/src/themes/github-light.js +59 -0
  109. package/ui/core/src/themes/gruvbox-dark.js +56 -0
  110. package/ui/core/src/themes/hacker-x-matrix.js +56 -0
  111. package/ui/core/src/themes/hacker-x-orion-prime.js +56 -0
  112. package/ui/core/src/themes/helpers.js +79 -0
  113. package/ui/core/src/themes/high-contrast-mono.js +59 -0
  114. package/ui/core/src/themes/index.js +52 -0
  115. package/ui/core/src/themes/light-high-contrast.js +62 -0
  116. package/ui/core/src/themes/noctis-obscuro.js +56 -0
  117. package/ui/core/src/themes/noctis.js +56 -0
  118. package/ui/core/src/themes/paper-ink.js +62 -0
  119. package/ui/core/src/themes/solarized-ops.js +59 -0
  120. package/ui/core/src/themes/terminal-green.js +59 -0
  121. package/ui/core/src/themes/tokyo-night.js +56 -0
  122. package/ui/react/README.md +5 -0
  123. package/ui/react/src/chat-status.js +39 -0
  124. package/ui/react/src/components.js +1989 -0
  125. package/ui/react/src/index.js +4 -0
  126. package/ui/react/src/platform.js +15 -0
  127. package/ui/react/src/use-controller-state.js +38 -0
  128. package/ui/react/src/web-app.js +4457 -0
  129. package/web/README.md +198 -0
  130. package/web/api/router.js +196 -0
  131. package/web/api/ws.js +152 -0
  132. package/web/auth/authz/engine.js +204 -0
  133. package/web/auth/config.js +115 -0
  134. package/web/auth/index.js +175 -0
  135. package/web/auth/normalize/entra.js +22 -0
  136. package/web/auth/providers/entra.js +76 -0
  137. package/web/auth/providers/none.js +24 -0
  138. package/web/auth.js +10 -0
  139. package/web/bin/serve.js +53 -0
  140. package/web/config.js +20 -0
  141. package/web/dist/app.js +469 -0
  142. package/web/dist/assets/index-DmGOcKR-.css +1 -0
  143. package/web/dist/assets/index-xJ8IzIZY.js +24 -0
  144. package/web/dist/assets/msal-CytV9RFv.js +7 -0
  145. package/web/dist/assets/pilotswarm-D9pEmenA.js +90 -0
  146. package/web/dist/assets/react-CEPDSRB6.js +1 -0
  147. package/web/dist/index.html +16 -0
  148. package/web/runtime.js +455 -0
  149. package/web/server.js +276 -0
  150. package/index.js +0 -1
@@ -0,0 +1,1794 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import https from "node:https";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { createRequire } from "node:module";
8
+ import {
9
+ FilesystemArtifactStore,
10
+ loadAgentFiles,
11
+ PilotSwarmClient,
12
+ PilotSwarmManagementClient,
13
+ createSessionBlobStore,
14
+ horizonConfigFromEnv,
15
+ LOCAL_DEFAULT_USER_PRINCIPAL,
16
+ } from "pilotswarm-sdk";
17
+ import { startEmbeddedWorkers, stopEmbeddedWorkers } from "./embedded-workers.js";
18
+ import { getPluginDirsFromEnv } from "./plugin-config.js";
19
+
20
+ const EXPORTS_DIR = path.resolve(
21
+ expandUserPath(process.env.PILOTSWARM_EXPORT_DIR || path.join(os.homedir(), "pilotswarm-exports")),
22
+ );
23
+ fs.mkdirSync(EXPORTS_DIR, { recursive: true });
24
+ const K8S_SERVICE_ACCOUNT_DIR = "/var/run/secrets/kubernetes.io/serviceaccount";
25
+ const CLI_SRC_DIR = path.dirname(fileURLToPath(import.meta.url));
26
+ const CLI_PACKAGE_ROOT = path.resolve(CLI_SRC_DIR, "..");
27
+ const PACKAGE_PARENT_DIR = path.resolve(CLI_PACKAGE_ROOT, "..");
28
+
29
+ function fileExists(filePath) {
30
+ try {
31
+ return fs.existsSync(filePath);
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ function getInClusterK8sPaths() {
38
+ const baseDir = process.env.PILOTSWARM_K8S_SERVICE_ACCOUNT_DIR || K8S_SERVICE_ACCOUNT_DIR;
39
+ return {
40
+ tokenPath: process.env.PILOTSWARM_K8S_TOKEN_PATH || path.join(baseDir, "token"),
41
+ caPath: process.env.PILOTSWARM_K8S_CA_PATH || path.join(baseDir, "ca.crt"),
42
+ namespacePath: process.env.PILOTSWARM_K8S_NAMESPACE_PATH || path.join(baseDir, "namespace"),
43
+ };
44
+ }
45
+
46
+ function readOptionalTextFile(filePath) {
47
+ try {
48
+ return fs.readFileSync(filePath, "utf8").trim();
49
+ } catch {
50
+ return "";
51
+ }
52
+ }
53
+
54
+ function hasInClusterK8sAccess() {
55
+ const { tokenPath, caPath } = getInClusterK8sPaths();
56
+ return Boolean(process.env.KUBERNETES_SERVICE_HOST)
57
+ && fileExists(tokenPath)
58
+ && fileExists(caPath);
59
+ }
60
+
61
+ function getInClusterK8sConfig() {
62
+ if (!hasInClusterK8sAccess()) return null;
63
+
64
+ const { tokenPath, caPath, namespacePath } = getInClusterK8sPaths();
65
+ return {
66
+ host: String(process.env.KUBERNETES_SERVICE_HOST || "").trim(),
67
+ port: Number(process.env.KUBERNETES_SERVICE_PORT || 443) || 443,
68
+ token: readOptionalTextFile(tokenPath),
69
+ ca: fs.readFileSync(caPath),
70
+ namespace: String(process.env.K8S_NAMESPACE || "").trim() || readOptionalTextFile(namespacePath) || "default",
71
+ };
72
+ }
73
+
74
+ function hasExplicitKubectlConfig() {
75
+ return Boolean((process.env.K8S_CONTEXT || "").trim() || (process.env.KUBECONFIG || "").trim());
76
+ }
77
+
78
+ function isKubectlAvailable() {
79
+ const result = spawnSync("kubectl", ["version", "--client=true"], { stdio: "ignore" });
80
+ return !result.error;
81
+ }
82
+
83
+ function stripAnsi(value) {
84
+ return String(value || "").replace(/\x1b\[[0-9;]*m/g, "");
85
+ }
86
+
87
+ function trimLogText(value, maxLength = 2_000) {
88
+ const text = String(value || "");
89
+ return text.length > maxLength
90
+ ? `${text.slice(0, maxLength - 1)}…`
91
+ : text;
92
+ }
93
+
94
+ function extractPrettyLogMessage(rawLine) {
95
+ const source = trimLogText(stripAnsi(rawLine)).trim();
96
+ if (!source) return "";
97
+
98
+ let message = source
99
+ .replace(/^\d{4}-\d{2}-\d{2}T\S+\s+(TRACE|DEBUG|INFO|WARN|ERROR)\s+\S+:\s*/i, "")
100
+ .replace(/^(TRACE|DEBUG|INFO|WARN|ERROR)\s+/i, "")
101
+ .replace(/^\[v[^\]]+\]\s*/i, "")
102
+ .trim();
103
+
104
+ const metadataMarkers = [
105
+ " instance_id=",
106
+ " orchestration_id=",
107
+ " execution_id=",
108
+ " orchestration_name=",
109
+ " orchestration_version=",
110
+ " activity_name=",
111
+ " activity_id=",
112
+ " worker_id=",
113
+ " filter=",
114
+ " options=",
115
+ " instances_deleted=",
116
+ " executions_deleted=",
117
+ " events_deleted=",
118
+ " queue_messages_deleted=",
119
+ " instances_processed=",
120
+ " instance=",
121
+ ];
122
+
123
+ let cutIndex = -1;
124
+ for (const marker of metadataMarkers) {
125
+ const nextIndex = message.indexOf(marker);
126
+ if (nextIndex <= 0) continue;
127
+ if (cutIndex === -1 || nextIndex < cutIndex) {
128
+ cutIndex = nextIndex;
129
+ }
130
+ }
131
+
132
+ if (cutIndex > 0) {
133
+ message = message.slice(0, cutIndex).trim();
134
+ }
135
+
136
+ return message || source;
137
+ }
138
+
139
+ function normalizeLogLevel(line) {
140
+ const match = stripAnsi(line).match(/\b(ERROR|WARN|INFO|DEBUG|TRACE)\b/i);
141
+ return match ? match[1].toLowerCase() : "info";
142
+ }
143
+
144
+ function extractLogTime(line) {
145
+ const plain = stripAnsi(line);
146
+ const hhmmss = plain.match(/\b(\d{2}:\d{2}:\d{2})(?:\.\d+)?\b/);
147
+ if (hhmmss) return hhmmss[1];
148
+
149
+ const iso = plain.match(/\b(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?)\b/);
150
+ if (iso) {
151
+ const parsed = new Date(iso[1]);
152
+ if (!Number.isNaN(parsed.getTime())) {
153
+ return parsed.toLocaleTimeString("en-US", {
154
+ hour12: false,
155
+ hour: "2-digit",
156
+ minute: "2-digit",
157
+ second: "2-digit",
158
+ });
159
+ }
160
+ }
161
+
162
+ return new Date().toLocaleTimeString("en-US", {
163
+ hour12: false,
164
+ hour: "2-digit",
165
+ minute: "2-digit",
166
+ second: "2-digit",
167
+ });
168
+ }
169
+
170
+ function buildLogEntry(line, counter) {
171
+ const prefixMatch = line.match(/^\[pod\/([^/\]]+)/);
172
+ const podName = prefixMatch ? prefixMatch[1] : "unknown";
173
+ const rawLine = trimLogText(stripAnsi(line.replace(/^\[pod\/[^\]]+\]\s*/, "")).trim());
174
+ const orchMatch = rawLine.match(/\b(?:instance_id|orchestration_id|orch)=(session-[^\s,]+)/i)
175
+ || rawLine.match(/\b(session-[0-9a-f-]{8,})\b/i);
176
+ const parsedOrchId = orchMatch ? orchMatch[1] : null;
177
+ const sessionIdMatch = rawLine.match(/\b(?:sessionId|session|durableSessionId)=([0-9a-f-]{8,})\b/i);
178
+ const sessionId = sessionIdMatch
179
+ ? sessionIdMatch[1]
180
+ : (parsedOrchId && parsedOrchId.startsWith("session-") ? parsedOrchId.slice("session-".length) : null);
181
+ const orchId = parsedOrchId || (sessionId ? `session-${sessionId}` : null);
182
+ const category = rawLine.includes("duroxide::activity")
183
+ ? "activity"
184
+ : rawLine.includes("duroxide::orchestration") || rawLine.includes("::orchestration")
185
+ ? "orchestration"
186
+ : "log";
187
+
188
+ return {
189
+ id: `log:${Date.now()}:${counter}`,
190
+ time: extractLogTime(rawLine),
191
+ podName,
192
+ level: normalizeLogLevel(rawLine),
193
+ orchId,
194
+ sessionId,
195
+ category,
196
+ rawLine,
197
+ message: extractPrettyLogMessage(rawLine),
198
+ prettyMessage: extractPrettyLogMessage(rawLine),
199
+ };
200
+ }
201
+
202
+ function buildSyntheticLogEntry({ message, level = "info", podName = "k8s", counter = 0 }) {
203
+ const safeMessage = trimLogText(String(message || "").trim());
204
+ return {
205
+ id: `log:${Date.now()}:${counter}`,
206
+ time: extractLogTime(safeMessage),
207
+ podName,
208
+ level,
209
+ orchId: null,
210
+ sessionId: null,
211
+ category: "log",
212
+ rawLine: safeMessage,
213
+ message: safeMessage,
214
+ prettyMessage: safeMessage,
215
+ };
216
+ }
217
+
218
+ function sanitizeArtifactFilename(filename) {
219
+ return String(filename || "").replace(/[/\\]/g, "_");
220
+ }
221
+
222
+ function expandUserPath(filePath) {
223
+ const value = String(filePath || "").trim();
224
+ if (!value) return "";
225
+ return value.startsWith("~")
226
+ ? path.join(os.homedir(), value.slice(1))
227
+ : value;
228
+ }
229
+
230
+ function getLocalLogDir() {
231
+ const configured = expandUserPath(process.env.PILOTSWARM_LOG_DIR || "");
232
+ return configured ? path.resolve(configured) : "";
233
+ }
234
+
235
+ function listLocalLogFiles(logDir) {
236
+ if (!logDir || !fileExists(logDir)) return [];
237
+ try {
238
+ return fs.readdirSync(logDir, { withFileTypes: true })
239
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".log"))
240
+ .map((entry) => path.join(logDir, entry.name))
241
+ .sort();
242
+ } catch {
243
+ return [];
244
+ }
245
+ }
246
+
247
+ function readRecentLogLines(filePath, maxBytes = 128 * 1024, maxLines = 200) {
248
+ try {
249
+ const stats = fs.statSync(filePath);
250
+ if (!stats.isFile() || stats.size <= 0) return [];
251
+ const fd = fs.openSync(filePath, "r");
252
+ try {
253
+ const bytesToRead = Math.min(stats.size, maxBytes);
254
+ const buffer = Buffer.alloc(bytesToRead);
255
+ fs.readSync(fd, buffer, 0, bytesToRead, stats.size - bytesToRead);
256
+ let text = buffer.toString("utf8");
257
+ if (bytesToRead < stats.size) {
258
+ const newlineIndex = text.indexOf("\n");
259
+ text = newlineIndex >= 0 ? text.slice(newlineIndex + 1) : "";
260
+ }
261
+ return text
262
+ .split(/\r?\n/u)
263
+ .map((line) => line.trimEnd())
264
+ .filter(Boolean)
265
+ .slice(-maxLines);
266
+ } finally {
267
+ fs.closeSync(fd);
268
+ }
269
+ } catch {
270
+ return [];
271
+ }
272
+ }
273
+
274
+ function readLogChunk(filePath, start, end) {
275
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return "";
276
+ try {
277
+ const fd = fs.openSync(filePath, "r");
278
+ try {
279
+ const length = end - start;
280
+ const buffer = Buffer.alloc(length);
281
+ const bytesRead = fs.readSync(fd, buffer, 0, length, start);
282
+ return buffer.toString("utf8", 0, bytesRead);
283
+ } finally {
284
+ fs.closeSync(fd);
285
+ }
286
+ } catch {
287
+ return "";
288
+ }
289
+ }
290
+
291
+ function getLocalLogPollIntervalMs() {
292
+ const value = Number.parseInt(process.env.PILOTSWARM_LOG_POLL_INTERVAL_MS || "", 10);
293
+ if (Number.isFinite(value) && value >= 50) return value;
294
+ return 500;
295
+ }
296
+
297
+ function guessArtifactContentType(filename) {
298
+ const ext = path.extname(String(filename || "")).toLowerCase();
299
+ if (ext === ".md" || ext === ".markdown" || ext === ".mdx") return "text/markdown";
300
+ if (ext === ".json" || ext === ".jsonl") return "application/json";
301
+ if (ext === ".html" || ext === ".htm") return "text/html";
302
+ if (ext === ".csv") return "text/csv";
303
+ if (ext === ".yaml" || ext === ".yml") return "text/yaml";
304
+ if (ext === ".xml") return "application/xml";
305
+ if (ext === ".js" || ext === ".mjs" || ext === ".cjs") return "application/javascript";
306
+ if (ext === ".pdf") return "application/pdf";
307
+ if (ext === ".zip") return "application/zip";
308
+ if (ext === ".tar") return "application/x-tar";
309
+ if (ext === ".tgz" || ext === ".gz") return "application/gzip";
310
+ if (ext === ".png") return "image/png";
311
+ if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
312
+ if (ext === ".gif") return "image/gif";
313
+ if (ext === ".webp") return "image/webp";
314
+ if (ext === ".svg") return "image/svg+xml";
315
+ if (ext === ".xlsx") return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
316
+ if (ext === ".docx") return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
317
+ if (ext === ".pptx") return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
318
+ if (ext === ".bin") return "application/octet-stream";
319
+ return "text/plain";
320
+ }
321
+
322
+ function spawnDetached(command, args) {
323
+ return new Promise((resolve, reject) => {
324
+ let settled = false;
325
+ const child = spawn(command, args, {
326
+ detached: true,
327
+ stdio: "ignore",
328
+ });
329
+ child.once("error", (error) => {
330
+ if (settled) return;
331
+ settled = true;
332
+ reject(error);
333
+ });
334
+ child.once("spawn", () => {
335
+ if (settled) return;
336
+ settled = true;
337
+ child.unref();
338
+ resolve();
339
+ });
340
+ });
341
+ }
342
+
343
+ function isTerminalOrchestrationStatus(status) {
344
+ return status === "Completed" || status === "Failed" || status === "Terminated";
345
+ }
346
+
347
+ function isTerminalSendError(error) {
348
+ const message = String(error?.message || error || "");
349
+ return /instance is terminal|terminal orchestration|cannot accept new messages/i.test(message);
350
+ }
351
+
352
+ function normalizeCreatableAgent(agent) {
353
+ const name = String(agent?.name || "").trim();
354
+ if (!name) return null;
355
+ return {
356
+ name,
357
+ title: String(agent?.title || "").trim() || (name.charAt(0).toUpperCase() + name.slice(1)),
358
+ description: String(agent?.description || "").trim(),
359
+ splash: typeof agent?.splash === "string" && agent.splash.trim() ? agent.splash : null,
360
+ initialPrompt: typeof agent?.initialPrompt === "string" && agent.initialPrompt.trim() ? agent.initialPrompt : null,
361
+ tools: Array.isArray(agent?.tools) ? agent.tools.filter(Boolean) : [],
362
+ };
363
+ }
364
+
365
+ function normalizeAgentIdentity(value) {
366
+ return String(value || "").trim().toLowerCase();
367
+ }
368
+
369
+ function loadBundledDefaultAgents() {
370
+ const agentsByKey = new Map();
371
+ // Resolve the SDK package root through module resolution so the lookup
372
+ // works identically in the monorepo (workspace symlink) and in published
373
+ // installs, regardless of where this file lives inside the app package.
374
+ const candidates = [];
375
+ try {
376
+ const sdkRoot = path.dirname(createRequire(import.meta.url).resolve("pilotswarm-sdk/package.json"));
377
+ candidates.push(path.join(sdkRoot, "plugins", "default-agents", "agents"));
378
+ } catch {
379
+ // pilotswarm-sdk not resolvable — fall through to path heuristics.
380
+ }
381
+ candidates.push(
382
+ path.join(PACKAGE_PARENT_DIR, "..", "sdk", "plugins", "default-agents", "agents"),
383
+ path.join(PACKAGE_PARENT_DIR, "..", "pilotswarm-sdk", "plugins", "default-agents", "agents"),
384
+ path.join(CLI_PACKAGE_ROOT, "node_modules", "pilotswarm-sdk", "plugins", "default-agents", "agents"),
385
+ );
386
+
387
+ for (const agentsDir of candidates) {
388
+ if (!fs.existsSync(agentsDir)) continue;
389
+ try {
390
+ for (const agent of loadAgentFiles(agentsDir)) {
391
+ if (!agent || agent.system || agent.name === "default") continue;
392
+ const normalized = normalizeCreatableAgent(agent);
393
+ if (!normalized) continue;
394
+ agentsByKey.set(normalizeAgentIdentity(normalized.name), normalized);
395
+ }
396
+ } catch {}
397
+ }
398
+ return agentsByKey;
399
+ }
400
+
401
+ export function loadSessionCreationMetadataFromPluginDirs(pluginDirs = []) {
402
+ let sessionPolicy = null;
403
+ const agentsByName = new Map();
404
+
405
+ for (const pluginDir of pluginDirs) {
406
+ const absDir = path.resolve(pluginDir);
407
+ if (!fs.existsSync(absDir)) continue;
408
+
409
+ const policyPath = path.join(absDir, "session-policy.json");
410
+ if (fs.existsSync(policyPath)) {
411
+ try {
412
+ sessionPolicy = JSON.parse(fs.readFileSync(policyPath, "utf-8"));
413
+ } catch {}
414
+ }
415
+
416
+ const agentsDir = path.join(absDir, "agents");
417
+ if (!fs.existsSync(agentsDir)) continue;
418
+ try {
419
+ for (const agent of loadAgentFiles(agentsDir)) {
420
+ if (!agent || agent.system || agent.name === "default") continue;
421
+ const normalized = normalizeCreatableAgent(agent);
422
+ if (!normalized) continue;
423
+ agentsByName.set(normalized.name, normalized);
424
+ }
425
+ } catch {}
426
+ }
427
+
428
+ const bundledAgents = loadBundledDefaultAgents();
429
+ const requestedBundledAgents = Array.isArray(sessionPolicy?.creation?.bundledAgents)
430
+ ? sessionPolicy.creation.bundledAgents
431
+ : [];
432
+ const appAgentKeys = new Set([...agentsByName.keys()].map((name) => normalizeAgentIdentity(name)));
433
+ const defaultAgentKey = normalizeAgentIdentity(sessionPolicy?.creation?.defaultAgent);
434
+
435
+ if (requestedBundledAgents.length === 0) {
436
+ if (defaultAgentKey && bundledAgents.has(defaultAgentKey) && !appAgentKeys.has(defaultAgentKey)) {
437
+ throw new Error(`[PilotSwarm] session-policy.json creation.defaultAgent=${JSON.stringify(sessionPolicy.creation.defaultAgent)} references a bundled default agent but creation.bundledAgents does not opt it in.`);
438
+ }
439
+ } else {
440
+ const requestedKeys = new Set();
441
+ for (const name of requestedBundledAgents) {
442
+ const key = normalizeAgentIdentity(name);
443
+ if (!key || !bundledAgents.has(key)) {
444
+ throw new Error(`[PilotSwarm] session-policy.json creation.bundledAgents contains unknown bundled agent ${JSON.stringify(name)}.`);
445
+ }
446
+ requestedKeys.add(key);
447
+ }
448
+ if (defaultAgentKey && bundledAgents.has(defaultAgentKey) && !requestedKeys.has(defaultAgentKey) && !appAgentKeys.has(defaultAgentKey)) {
449
+ throw new Error(`[PilotSwarm] session-policy.json creation.defaultAgent=${JSON.stringify(sessionPolicy.creation.defaultAgent)} references a bundled default agent but creation.bundledAgents does not opt it in.`);
450
+ }
451
+ for (const key of requestedKeys) {
452
+ if (appAgentKeys.has(key)) continue;
453
+ const agent = bundledAgents.get(key);
454
+ agentsByName.set(agent.name, agent);
455
+ appAgentKeys.add(key);
456
+ }
457
+ }
458
+
459
+ const creatableAgents = [...agentsByName.values()];
460
+ return {
461
+ sessionPolicy,
462
+ allowedAgentNames: creatableAgents.map((agent) => agent.name),
463
+ creatableAgents,
464
+ };
465
+ }
466
+
467
+ function buildTerminalSendError(sessionId, session) {
468
+ if (session?.status === "failed" || session?.status === "cancelled" || session?.orchestrationStatus === "Failed") {
469
+ return `Session ${sessionId.slice(0, 8)} is a terminal orchestration and cannot accept new messages.`;
470
+ }
471
+
472
+ const statusLabel = String(session?.orchestrationStatus || session?.status || "Unknown");
473
+ return `Session ${sessionId.slice(0, 8)} is a terminal orchestration instance (${statusLabel}) and cannot accept new messages.`;
474
+ }
475
+
476
+ function normalizeUserPrincipal(principal) {
477
+ const provider = String(principal?.provider || "").trim();
478
+ const subject = String(principal?.subject || "").trim();
479
+ if (!provider || !subject) return { ...LOCAL_DEFAULT_USER_PRINCIPAL };
480
+ const email = String(principal?.email || "").trim();
481
+ const displayName = String(principal?.displayName || "").trim();
482
+ return {
483
+ provider,
484
+ subject,
485
+ email: email || null,
486
+ displayName: displayName || null,
487
+ };
488
+ }
489
+
490
+ function resolveUserPrincipalFor(transport, principal) {
491
+ if (principal && principal.provider && principal.subject) {
492
+ return normalizeUserPrincipal(principal);
493
+ }
494
+ return normalizeUserPrincipal(transport.currentUser);
495
+ }
496
+
497
+ export class NodeSdkTransport {
498
+ constructor({ store, mode, currentUser, useManagedIdentity, cmsFactsDatabaseUrl, aadDbUser } = {}) {
499
+ this.store = store;
500
+ this.mode = mode;
501
+ this.useManagedIdentity = useManagedIdentity;
502
+ this.cmsFactsDatabaseUrl = cmsFactsDatabaseUrl;
503
+ this.aadDbUser = aadDbUser;
504
+ this.pluginDirs = getPluginDirsFromEnv();
505
+ // Optional EnhancedFactStore (HorizonDB) target. The client and
506
+ // management client each build their own fact store, so they MUST
507
+ // resolve the SAME facts DB as the embedded worker or session
508
+ // cleanup / facts reads would hit the wrong database. Empty unless
509
+ // HORIZON_DATABASE_URL is set. Graph/embedder fields are worker-only
510
+ // and intentionally not forwarded here.
511
+ const horizon = horizonConfigFromEnv();
512
+ this.enhancedFacts = horizon.enhancedFactsDatabaseUrl
513
+ ? {
514
+ enhancedFactsDatabaseUrl: horizon.enhancedFactsDatabaseUrl,
515
+ ...(horizon.enhancedFactsSchema ? { enhancedFactsSchema: horizon.enhancedFactsSchema } : {}),
516
+ }
517
+ : {};
518
+ this.client = null;
519
+ this.mgmt = new PilotSwarmManagementClient({
520
+ store,
521
+ ...(useManagedIdentity !== undefined ? { useManagedIdentity } : {}),
522
+ ...(cmsFactsDatabaseUrl ? { cmsFactsDatabaseUrl } : {}),
523
+ ...(aadDbUser ? { aadDbUser } : {}),
524
+ ...this.enhancedFacts,
525
+ pluginDirs: this.pluginDirs,
526
+ blobEnabled: Boolean(process.env.AZURE_STORAGE_ACCOUNT_URL || process.env.AZURE_STORAGE_CONNECTION_STRING),
527
+ });
528
+ this.artifactStore = createArtifactStore();
529
+ this.sessionHandles = new Map();
530
+ this.workers = [];
531
+ this.sessionPolicy = null;
532
+ this.allowedAgentNames = [];
533
+ this.creatableAgents = [];
534
+ this.logProc = null;
535
+ this.logTailHandle = null;
536
+ this.logBuffer = "";
537
+ this.logRestartTimer = null;
538
+ this.logSubscribers = new Set();
539
+ this.logEntryCounter = 0;
540
+ this.kubectlAvailable = null;
541
+ // The native TUI runs as the local user. Portal deployments override
542
+ // this per-RPC from the auth context inside PortalRuntime.call().
543
+ this.currentUser = currentUser ? normalizeUserPrincipal(currentUser) : { ...LOCAL_DEFAULT_USER_PRINCIPAL };
544
+ }
545
+
546
+ /**
547
+ * Replace the principal that user-scoped RPCs (Admin Console,
548
+ * profile settings, GitHub Copilot key) attach to. Local TUI hosts
549
+ * may set this once at startup; portal hosts pass principals per
550
+ * RPC instead and never call this method.
551
+ */
552
+ setCurrentUser(principal) {
553
+ if (!principal || !principal.provider || !principal.subject) {
554
+ this.currentUser = { ...LOCAL_DEFAULT_USER_PRINCIPAL };
555
+ return;
556
+ }
557
+ this.currentUser = normalizeUserPrincipal(principal);
558
+ }
559
+
560
+ getCurrentUserPrincipal() {
561
+ return { ...this.currentUser };
562
+ }
563
+
564
+ async start() {
565
+ const workerCount = this.mode === "remote" ? 0 : parseInt(process.env.WORKERS || "4", 10);
566
+ if (workerCount > 0) {
567
+ this.workers = await startEmbeddedWorkers({
568
+ count: workerCount,
569
+ store: this.store,
570
+ });
571
+ }
572
+ const sessionCreationMetadata = this.resolveSessionCreationMetadata();
573
+ this.sessionPolicy = sessionCreationMetadata.sessionPolicy;
574
+ this.allowedAgentNames = sessionCreationMetadata.allowedAgentNames;
575
+ this.creatableAgents = sessionCreationMetadata.creatableAgents;
576
+ this.client = new PilotSwarmClient({
577
+ store: this.store,
578
+ ...(this.useManagedIdentity !== undefined ? { useManagedIdentity: this.useManagedIdentity } : {}),
579
+ ...(this.cmsFactsDatabaseUrl ? { cmsFactsDatabaseUrl: this.cmsFactsDatabaseUrl } : {}),
580
+ ...(this.aadDbUser ? { aadDbUser: this.aadDbUser } : {}),
581
+ ...this.enhancedFacts,
582
+ ...(this.sessionPolicy ? { sessionPolicy: this.sessionPolicy } : {}),
583
+ ...(this.allowedAgentNames.length > 0 ? { allowedAgentNames: this.allowedAgentNames } : {}),
584
+ });
585
+ await this.client.start();
586
+ await this.mgmt.start();
587
+ }
588
+
589
+ async stop() {
590
+ this.sessionHandles.clear();
591
+ await this.stopLogTail();
592
+ await Promise.allSettled([
593
+ this.client ? this.client.stop() : Promise.resolve(),
594
+ this.mgmt.stop(),
595
+ stopEmbeddedWorkers(this.workers),
596
+ ]);
597
+ this.client = null;
598
+ }
599
+
600
+ resolveSessionCreationMetadata() {
601
+ if (this.workers.length > 0) {
602
+ const firstWorker = this.workers[0];
603
+ const creatableAgents = Array.isArray(firstWorker?.loadedAgents)
604
+ ? firstWorker.loadedAgents.map((agent) => normalizeCreatableAgent(agent)).filter(Boolean)
605
+ : [];
606
+ return {
607
+ sessionPolicy: firstWorker?.sessionPolicy || null,
608
+ allowedAgentNames: Array.isArray(firstWorker?.allowedAgentNames) ? firstWorker.allowedAgentNames.filter(Boolean) : creatableAgents.map((agent) => agent.name),
609
+ creatableAgents,
610
+ };
611
+ }
612
+ return loadSessionCreationMetadataFromPluginDirs(getPluginDirsFromEnv());
613
+ }
614
+
615
+ getWorkerCount() {
616
+ return this.workers.length || (this.mode === "remote" ? 0 : parseInt(process.env.WORKERS || "4", 10));
617
+ }
618
+
619
+ getLogConfig() {
620
+ const localLogDir = getLocalLogDir();
621
+ if (localLogDir) {
622
+ const exists = fileExists(localLogDir);
623
+ return {
624
+ available: exists,
625
+ availabilityReason: exists
626
+ ? ""
627
+ : `Log tailing disabled: local log directory ${JSON.stringify(localLogDir)} does not exist.`,
628
+ };
629
+ }
630
+
631
+ const hasInClusterConfig = hasInClusterK8sAccess();
632
+ const hasKubectlConfig = hasExplicitKubectlConfig();
633
+ if (hasInClusterConfig) {
634
+ return {
635
+ available: true,
636
+ availabilityReason: "",
637
+ };
638
+ }
639
+
640
+ if (hasKubectlConfig) {
641
+ if (this.kubectlAvailable == null) {
642
+ this.kubectlAvailable = isKubectlAvailable();
643
+ }
644
+ return {
645
+ available: this.kubectlAvailable,
646
+ availabilityReason: this.kubectlAvailable
647
+ ? ""
648
+ : "Log tailing disabled: kubectl is not installed in this environment.",
649
+ };
650
+ }
651
+
652
+ return {
653
+ available: false,
654
+ availabilityReason: "Log tailing disabled: no K8S_CONTEXT/KUBECONFIG or in-cluster Kubernetes access detected.",
655
+ };
656
+ }
657
+
658
+ getAuthContext() {
659
+ return {
660
+ principal: null,
661
+ authorization: {
662
+ allowed: true,
663
+ role: null,
664
+ reason: "Local transport",
665
+ matchedGroups: [],
666
+ },
667
+ };
668
+ }
669
+
670
+ async listSessions() {
671
+ return this.mgmt.listSessions();
672
+ }
673
+
674
+ async listSessionGroups() {
675
+ return this.mgmt.listSessionGroups();
676
+ }
677
+
678
+ async createSessionGroup(input) {
679
+ return this.mgmt.createSessionGroup(input || {});
680
+ }
681
+
682
+ async updateSessionGroup(groupId, patch) {
683
+ return this.mgmt.updateSessionGroup(groupId, patch || {});
684
+ }
685
+
686
+ async assignSessionsToGroup(groupId, sessionIds) {
687
+ return this.mgmt.assignSessionsToGroup(groupId, sessionIds || []);
688
+ }
689
+
690
+ async moveSessionsToGroup(groupId, sessionIds) {
691
+ return this.mgmt.moveSessionsToGroup(groupId ?? null, sessionIds || []);
692
+ }
693
+
694
+ async getChildOutcome(childSessionId) {
695
+ return this.mgmt.getChildOutcome(childSessionId);
696
+ }
697
+
698
+ async listChildOutcomes(parentSessionId) {
699
+ return this.mgmt.listChildOutcomes(parentSessionId);
700
+ }
701
+
702
+ async listSessionsPage(opts) {
703
+ return this.mgmt.listSessionsPage(opts);
704
+ }
705
+
706
+ async getSession(sessionId) {
707
+ return this.mgmt.getSession(sessionId);
708
+ }
709
+
710
+ async getOrchestrationStats(sessionId) {
711
+ return this.mgmt.getOrchestrationStats(sessionId);
712
+ }
713
+
714
+ async getSessionMetricSummary(sessionId) {
715
+ return this.mgmt.getSessionMetricSummary(sessionId);
716
+ }
717
+
718
+ async getSessionTokensByModel(sessionId) {
719
+ return this.mgmt.getSessionTokensByModel(sessionId);
720
+ }
721
+
722
+ async getSessionTreeStats(sessionId) {
723
+ return this.mgmt.getSessionTreeStats(sessionId);
724
+ }
725
+
726
+ async getFleetStats(opts) {
727
+ return this.mgmt.getFleetStats(opts);
728
+ }
729
+
730
+ async getUserStats(opts) {
731
+ return this.mgmt.getUserStats(opts);
732
+ }
733
+
734
+ async getTopEventEmitters(opts) {
735
+ return this.mgmt.getTopEventEmitters(opts);
736
+ }
737
+
738
+ /**
739
+ * Read the current user's profile (settings + ghcp key-set flag).
740
+ * The portal supplies `principal` from the auth context per-request;
741
+ * the native TUI omits it and falls back to the transport's
742
+ * `currentUser` (defaults to LOCAL_DEFAULT_USER_PRINCIPAL).
743
+ */
744
+ async getCurrentUserProfile({ principal } = {}) {
745
+ const resolved = resolveUserPrincipalFor(this, principal);
746
+ return this.mgmt.getUserProfile(resolved);
747
+ }
748
+
749
+ /**
750
+ * Replace the current user's profile_settings JSON document.
751
+ */
752
+ async setCurrentUserProfileSettings({ principal, settings } = {}) {
753
+ const resolved = resolveUserPrincipalFor(this, principal);
754
+ const safeSettings = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
755
+ return this.mgmt.setUserProfileSettings(resolved, safeSettings);
756
+ }
757
+
758
+ /**
759
+ * Set or clear the per-user GitHub Copilot key. Pass `null` (or an
760
+ * all-whitespace string) to clear the override and revert to the
761
+ * worker's env-supplied default.
762
+ */
763
+ async setCurrentUserGitHubCopilotKey({ principal, key } = {}) {
764
+ const resolved = resolveUserPrincipalFor(this, principal);
765
+ const normalized = typeof key === "string" && key.trim().length > 0 ? key : null;
766
+ return this.mgmt.setUserGitHubCopilotKey(resolved, normalized);
767
+ }
768
+
769
+ async getSessionSkillUsage(sessionId, opts) {
770
+ return this.mgmt.getSessionSkillUsage(sessionId, opts);
771
+ }
772
+
773
+ async getSessionTreeSkillUsage(sessionId, opts) {
774
+ return this.mgmt.getSessionTreeSkillUsage(sessionId, opts);
775
+ }
776
+
777
+ async getFleetSkillUsage(opts) {
778
+ return this.mgmt.getFleetSkillUsage(opts);
779
+ }
780
+
781
+ async getFleetRetrievalUsage(opts) {
782
+ return this.mgmt.getFleetRetrievalUsage(opts);
783
+ }
784
+
785
+ async getSessionFactsStats(sessionId) {
786
+ return this.mgmt.getSessionFactsStats(sessionId);
787
+ }
788
+
789
+ async getSessionTreeFactsStats(sessionId) {
790
+ return this.mgmt.getSessionTreeFactsStats(sessionId);
791
+ }
792
+
793
+ async getSharedFactsStats() {
794
+ return this.mgmt.getSharedFactsStats();
795
+ }
796
+
797
+ async getFactsTombstoneStats(opts) {
798
+ return this.mgmt.getFactsTombstoneStats(opts);
799
+ }
800
+
801
+ // ─── Facts data-plane ────────────────────────────────────────────
802
+ async factsCapabilities() {
803
+ return this.mgmt.factsCapabilities();
804
+ }
805
+
806
+ async readFacts(query, roleOpts) {
807
+ return this.mgmt.readFacts(query || {}, roleOpts);
808
+ }
809
+
810
+ async storeFact(input) {
811
+ return this.mgmt.storeFact(input);
812
+ }
813
+
814
+ async deleteFactRecord(input) {
815
+ return this.mgmt.deleteFact(input || {});
816
+ }
817
+
818
+ async searchFacts(query, opts, roleOpts) {
819
+ return this.mgmt.searchFacts(query, opts, roleOpts);
820
+ }
821
+
822
+ async similarFacts(scopeKey, opts, roleOpts) {
823
+ return this.mgmt.similarFacts(scopeKey, opts, roleOpts);
824
+ }
825
+
826
+ async getFactsEmbedderStatus() {
827
+ return this.mgmt.getEmbedderStatus();
828
+ }
829
+
830
+ async startFactsEmbedder(opts) {
831
+ return this.mgmt.startEmbedder(opts);
832
+ }
833
+
834
+ async stopFactsEmbedder(reason) {
835
+ return this.mgmt.stopEmbedder(reason);
836
+ }
837
+
838
+ async forcePurgeFacts(input) {
839
+ return this.mgmt.forcePurgeFacts(input || {});
840
+ }
841
+
842
+ // ─── Graph data-plane ────────────────────────────────────────────
843
+ async searchGraphNodes(query) {
844
+ return this.mgmt.searchGraphNodes(query || {});
845
+ }
846
+
847
+ async searchGraphEdges(query) {
848
+ return this.mgmt.searchGraphEdges(query || {});
849
+ }
850
+
851
+ async graphNeighbourhood(nodeKey, depth, opts) {
852
+ return this.mgmt.graphNeighbourhood(nodeKey, depth, opts);
853
+ }
854
+
855
+ async upsertGraphNode(input) {
856
+ return this.mgmt.upsertGraphNode(input);
857
+ }
858
+
859
+ async upsertGraphEdge(input) {
860
+ return this.mgmt.upsertGraphEdge(input);
861
+ }
862
+
863
+ async deleteGraphNode(nodeKey, opts) {
864
+ return this.mgmt.deleteGraphNode(nodeKey, opts);
865
+ }
866
+
867
+ async deleteGraphEdge(fromKey, toKey, predicateKey, opts) {
868
+ return this.mgmt.deleteGraphEdge(fromKey, toKey, predicateKey, opts);
869
+ }
870
+
871
+ async graphStats(opts) {
872
+ return this.mgmt.graphStats(opts);
873
+ }
874
+
875
+ async listGraphNamespaces(query) {
876
+ return this.mgmt.listGraphNamespaces(query);
877
+ }
878
+
879
+ async getGraphNamespace(namespace) {
880
+ return this.mgmt.getGraphNamespace(namespace);
881
+ }
882
+
883
+ async upsertGraphNamespace(input) {
884
+ return this.mgmt.upsertGraphNamespace(input);
885
+ }
886
+
887
+ async deleteGraphNamespace(namespace) {
888
+ return this.mgmt.deleteGraphNamespace(namespace);
889
+ }
890
+
891
+ async pruneDeletedSummaries(olderThan) {
892
+ return this.mgmt.pruneDeletedSummaries(olderThan);
893
+ }
894
+
895
+ async getExecutionHistory(sessionId, executionId) {
896
+ return this.mgmt.getExecutionHistory(sessionId, executionId);
897
+ }
898
+
899
+ async assertSessionModelCreatable({ model, owner } = {}) {
900
+ const effectiveModel = model || this.mgmt.getDefaultModel();
901
+ if (!effectiveModel || typeof this.mgmt.getModelCredentialStatus !== "function") return effectiveModel;
902
+
903
+ const credentialStatus = this.mgmt.getModelCredentialStatus(effectiveModel);
904
+ if (credentialStatus.providerType !== "github") return effectiveModel;
905
+ if (credentialStatus.credentialAvailable) return effectiveModel;
906
+
907
+ const principal = owner ? normalizeUserPrincipal(owner) : resolveUserPrincipalFor(this, null);
908
+ const profile = principal
909
+ ? await this.mgmt.getUserProfile(principal).catch(() => null)
910
+ : null;
911
+ if (profile?.githubCopilotKeySet === true) return effectiveModel;
912
+
913
+ throw new Error(
914
+ "GitHub Copilot key not configured. Set GITHUB_TOKEN on the worker or set your per-user GitHub Copilot key in Admin before creating GitHub Copilot model sessions.",
915
+ );
916
+ }
917
+
918
+ async createSession({ model, reasoningEffort, owner, groupId } = {}) {
919
+ const effectiveModel = await this.assertSessionModelCreatable({ model, owner });
920
+ const session = await this.client.createSession({
921
+ ...(effectiveModel ? { model: effectiveModel } : {}),
922
+ ...(reasoningEffort ? { reasoningEffort } : {}),
923
+ ...(owner ? { owner } : {}),
924
+ ...(groupId ? { groupId } : {}),
925
+ });
926
+ this.sessionHandles.set(session.sessionId, session);
927
+ return { sessionId: session.sessionId, model: effectiveModel, reasoningEffort: reasoningEffort || undefined };
928
+ }
929
+
930
+ async createSessionForAgent(agentName, { model, reasoningEffort, title, splash, splashMobile, initialPrompt, owner, groupId } = {}) {
931
+ const effectiveModel = await this.assertSessionModelCreatable({ model, owner });
932
+ const session = await this.client.createSessionForAgent(agentName, {
933
+ ...(effectiveModel ? { model: effectiveModel } : {}),
934
+ ...(reasoningEffort ? { reasoningEffort } : {}),
935
+ ...(title ? { title } : {}),
936
+ ...(splash ? { splash } : {}),
937
+ ...(splashMobile ? { splashMobile } : {}),
938
+ ...(initialPrompt ? { initialPrompt } : {}),
939
+ ...(owner ? { owner } : {}),
940
+ ...(groupId ? { groupId } : {}),
941
+ });
942
+ this.sessionHandles.set(session.sessionId, session);
943
+ return {
944
+ sessionId: session.sessionId,
945
+ model: effectiveModel,
946
+ reasoningEffort: reasoningEffort || undefined,
947
+ agentName,
948
+ };
949
+ }
950
+
951
+ listCreatableAgents() {
952
+ return [...this.creatableAgents];
953
+ }
954
+
955
+ getSessionCreationPolicy() {
956
+ return this.sessionPolicy;
957
+ }
958
+
959
+ async sendMessage(sessionId, prompt, options = {}) {
960
+ const session = await this.mgmt.getSession(sessionId);
961
+ if (!session) {
962
+ throw new Error(`Session ${sessionId.slice(0, 8)} was not found.`);
963
+ }
964
+ if (session.status === "failed" || session.status === "cancelled" || session.orchestrationStatus === "Failed") {
965
+ throw new Error(buildTerminalSendError(sessionId, session));
966
+ }
967
+ if (
968
+ (session.status === "completed" || session.status === "cancelled")
969
+ && session.parentSessionId
970
+ && !session.isSystem
971
+ && !session.cronActive
972
+ && !session.cronInterval
973
+ ) {
974
+ throw new Error(buildTerminalSendError(sessionId, session));
975
+ }
976
+ if (this.mode === "remote" && isTerminalOrchestrationStatus(session.orchestrationStatus)) {
977
+ throw new Error(buildTerminalSendError(sessionId, session));
978
+ }
979
+
980
+ const sendOptions = options?.clientMessageIds && Array.isArray(options.clientMessageIds) && options.clientMessageIds.length > 0
981
+ ? { clientMessageIds: options.clientMessageIds }
982
+ : undefined;
983
+
984
+ if (options?.enqueueOnly) {
985
+ // enqueueOnly originally routed through mgmt.sendMessage to skip
986
+ // the wait-for-result polling, but PilotSwarmSession.send is
987
+ // already fire-and-forget. Routing through the session handle
988
+ // ensures _ensureOrchestrationAndSend starts the orchestration
989
+ // on the very first message — mgmt.sendMessage only enqueues
990
+ // and would silently produce orphan queue messages for fresh
991
+ // sessions.
992
+ const sessionHandleEnqueue = await this.getSessionHandle(sessionId);
993
+ await sessionHandleEnqueue.send(prompt, sendOptions);
994
+ return;
995
+ }
996
+
997
+ // IMPORTANT: do NOT silently fall back to mgmt.sendMessage on transient
998
+ // errors. mgmt.sendMessage only enqueues onto the durable messages
999
+ // queue — it never starts the orchestration. If sessionHandle.send
1000
+ // fails (for example startOrchestrationVersioned threw transiently),
1001
+ // falling back to a pure enqueue produces an "orphan queue message"
1002
+ // that duroxide-pg eventually drops, leaving the CMS row in `running`
1003
+ // state with `orchestration_id = NULL` and the UI stuck on
1004
+ // "Working…" forever. Propagate the error so the caller can retry
1005
+ // through the full sessionHandle.send path that owns the start.
1006
+ const sessionHandle = await this.getSessionHandle(sessionId);
1007
+ await sessionHandle.send(prompt);
1008
+ }
1009
+
1010
+ async sendAnswer(sessionId, answer) {
1011
+ await this.mgmt.sendAnswer(sessionId, answer);
1012
+ }
1013
+
1014
+ async sendSessionEvent(sessionId, eventName, data) {
1015
+ const sessionHandle = await this.getSessionHandle(sessionId);
1016
+ await sessionHandle.sendEvent(eventName, data);
1017
+ }
1018
+
1019
+ async getSessionStatus(sessionId) {
1020
+ return this.mgmt.getSessionStatus(sessionId);
1021
+ }
1022
+
1023
+ async waitForStatusChange(sessionId, afterVersion, timeoutMs) {
1024
+ return this.mgmt.waitForStatusChange(sessionId, afterVersion, undefined, timeoutMs);
1025
+ }
1026
+
1027
+ async getLatestResponse(sessionId) {
1028
+ return this.mgmt.getLatestResponse(sessionId);
1029
+ }
1030
+
1031
+ async cancelPendingMessage(sessionId, clientMessageIds) {
1032
+ const ids = Array.isArray(clientMessageIds)
1033
+ ? clientMessageIds.filter((id) => typeof id === "string" && id)
1034
+ : [];
1035
+ if (ids.length === 0) return;
1036
+ await this.mgmt.cancelPendingMessage(sessionId, ids);
1037
+ }
1038
+
1039
+ async renameSession(sessionId, title) {
1040
+ await this.mgmt.renameSession(sessionId, title);
1041
+ }
1042
+
1043
+ async cancelSession(sessionId) {
1044
+ await this.mgmt.cancelSession(sessionId);
1045
+ }
1046
+
1047
+ async cancelSessionGroup(groupId, reason) {
1048
+ await this.mgmt.cancelSessionGroup(groupId, reason);
1049
+ }
1050
+
1051
+ async completeSession(sessionId, reason = "Completed by user") {
1052
+ await this.mgmt.sendCommand(sessionId, {
1053
+ cmd: "done",
1054
+ id: `done-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1055
+ args: { reason },
1056
+ });
1057
+ }
1058
+
1059
+ async completeSessionGroup(groupId, options = {}) {
1060
+ await this.mgmt.completeSessionGroup(groupId, options);
1061
+ }
1062
+
1063
+ async deleteSession(sessionId) {
1064
+ await this.mgmt.deleteSession(sessionId);
1065
+ this.sessionHandles.delete(sessionId);
1066
+ }
1067
+
1068
+ async restartSystemSession(agentIdOrSessionId, options) {
1069
+ return this.mgmt.restartSystemSession(agentIdOrSessionId, options || {});
1070
+ }
1071
+
1072
+ async setSessionModel(sessionId, options = {}) {
1073
+ return this.mgmt.setSessionModel(sessionId, options.model, {
1074
+ ...("reasoningEffort" in options ? { reasoningEffort: options.reasoningEffort ?? null } : {}),
1075
+ source: options.source || "ui",
1076
+ });
1077
+ }
1078
+
1079
+ async stopSessionTurn(sessionId, options = {}) {
1080
+ return this.mgmt.stopSessionTurn(sessionId, {
1081
+ reason: options.reason || "Stopped by user",
1082
+ ...(options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}),
1083
+ });
1084
+ }
1085
+
1086
+ async deleteSessionGroup(groupId) {
1087
+ await this.mgmt.deleteSessionGroup(groupId);
1088
+ }
1089
+
1090
+ async listModels() {
1091
+ return this.mgmt.listModels();
1092
+ }
1093
+
1094
+ async listArtifacts(sessionId) {
1095
+ if (!this.artifactStore || !sessionId) return [];
1096
+ const artifacts = await this.artifactStore.listArtifacts(sessionId);
1097
+ return Array.isArray(artifacts)
1098
+ ? [...artifacts].sort((left, right) => String(left?.filename || "").localeCompare(String(right?.filename || "")))
1099
+ : [];
1100
+ }
1101
+
1102
+ async getArtifactMetadata(sessionId, filename) {
1103
+ if (!this.artifactStore || !sessionId || !filename) return null;
1104
+ const artifacts = await this.artifactStore.listArtifacts(sessionId);
1105
+ return (artifacts || []).find((artifact) => artifact?.filename === filename) || null;
1106
+ }
1107
+
1108
+ async deleteArtifact(sessionId, filename) {
1109
+ if (!this.artifactStore) {
1110
+ throw new Error("Artifact store is not available for this transport.");
1111
+ }
1112
+ return this.artifactStore.deleteArtifact(sessionId, filename);
1113
+ }
1114
+
1115
+ async downloadArtifact(sessionId, filename) {
1116
+ if (!this.artifactStore) {
1117
+ throw new Error("Artifact store is not available for this transport.");
1118
+ }
1119
+ return this.artifactStore.downloadArtifactText(sessionId, filename);
1120
+ }
1121
+
1122
+ async downloadArtifactBinary(sessionId, filename) {
1123
+ if (!this.artifactStore) {
1124
+ throw new Error("Artifact store is not available for this transport.");
1125
+ }
1126
+ return this.artifactStore.downloadArtifact(sessionId, filename);
1127
+ }
1128
+
1129
+ async uploadArtifactFromPath(sessionId, filePath) {
1130
+ if (!this.artifactStore) {
1131
+ throw new Error("Artifact store is not available for this transport.");
1132
+ }
1133
+ const resolvedPath = path.resolve(expandUserPath(filePath));
1134
+ if (!resolvedPath) {
1135
+ throw new Error("File path cannot be empty.");
1136
+ }
1137
+
1138
+ const stat = await fs.promises.stat(resolvedPath).catch(() => null);
1139
+ if (!stat) {
1140
+ throw new Error(`File not found: ${filePath}`);
1141
+ }
1142
+ if (!stat.isFile()) {
1143
+ throw new Error(`Not a file: ${filePath}`);
1144
+ }
1145
+
1146
+ const filename = path.basename(resolvedPath);
1147
+ const content = await fs.promises.readFile(resolvedPath);
1148
+ const contentType = guessArtifactContentType(filename);
1149
+ await this.artifactStore.uploadArtifact(sessionId, filename, content, contentType);
1150
+
1151
+ return {
1152
+ sessionId,
1153
+ filename,
1154
+ resolvedPath,
1155
+ sizeBytes: content.length,
1156
+ contentType,
1157
+ };
1158
+ }
1159
+
1160
+ async uploadArtifactContent(sessionId, filename, content, contentType = guessArtifactContentType(filename), contentEncoding = null) {
1161
+ if (!this.artifactStore) {
1162
+ throw new Error("Artifact store is not available for this transport.");
1163
+ }
1164
+ const safeSessionId = String(sessionId || "").trim();
1165
+ const safeFilename = path.basename(String(filename || "").trim());
1166
+ let safeContent;
1167
+ if (!safeSessionId) {
1168
+ throw new Error("Session id is required for artifact upload.");
1169
+ }
1170
+ if (!safeFilename) {
1171
+ throw new Error("Filename is required for artifact upload.");
1172
+ }
1173
+
1174
+ if (contentEncoding === "base64") {
1175
+ safeContent = Buffer.from(String(content || ""), "base64");
1176
+ } else if (Buffer.isBuffer(content)) {
1177
+ safeContent = content;
1178
+ } else if (content instanceof Uint8Array) {
1179
+ safeContent = Buffer.from(content);
1180
+ } else {
1181
+ safeContent = typeof content === "string" ? content : String(content || "");
1182
+ }
1183
+
1184
+ await this.artifactStore.uploadArtifact(
1185
+ safeSessionId,
1186
+ safeFilename,
1187
+ safeContent,
1188
+ contentType || guessArtifactContentType(safeFilename),
1189
+ );
1190
+
1191
+ return {
1192
+ sessionId: safeSessionId,
1193
+ filename: safeFilename,
1194
+ resolvedPath: safeFilename,
1195
+ sizeBytes: Buffer.isBuffer(safeContent)
1196
+ ? safeContent.length
1197
+ : Buffer.byteLength(safeContent, "utf8"),
1198
+ contentType: contentType || guessArtifactContentType(safeFilename),
1199
+ };
1200
+ }
1201
+
1202
+ getArtifactExportDirectory() {
1203
+ return EXPORTS_DIR;
1204
+ }
1205
+
1206
+ async saveArtifactDownload(sessionId, filename) {
1207
+ if (!this.artifactStore) {
1208
+ throw new Error("Artifact store is not available for this transport.");
1209
+ }
1210
+
1211
+ const content = await this.artifactStore.downloadArtifact(sessionId, filename);
1212
+ const sessionDir = path.join(EXPORTS_DIR, String(sessionId || "").slice(0, 8));
1213
+ const localPath = path.join(sessionDir, sanitizeArtifactFilename(filename));
1214
+ await fs.promises.mkdir(sessionDir, { recursive: true });
1215
+ await fs.promises.writeFile(localPath, content.body);
1216
+ return {
1217
+ localPath,
1218
+ };
1219
+ }
1220
+
1221
+ async exportExecutionHistory(sessionId) {
1222
+ if (!this.artifactStore) {
1223
+ throw new Error("Artifact store is not available for this transport.");
1224
+ }
1225
+ const shortId = String(sessionId || "").slice(0, 8);
1226
+ const [history, stats] = await Promise.all([
1227
+ this.mgmt.getExecutionHistory(sessionId),
1228
+ this.mgmt.getOrchestrationStats(sessionId),
1229
+ ]);
1230
+ const sessionInfo = await this.mgmt.getSession(sessionId).catch(() => null);
1231
+ const exportData = {
1232
+ exportedAt: new Date().toISOString(),
1233
+ sessionId,
1234
+ title: sessionInfo?.title || null,
1235
+ agentId: sessionInfo?.agentId || null,
1236
+ model: sessionInfo?.model || null,
1237
+ orchestrationStats: stats || null,
1238
+ eventCount: history?.length || 0,
1239
+ events: (history || []).map((e) => {
1240
+ const evt = { ...e };
1241
+ if (evt.data) {
1242
+ try { evt.data = JSON.parse(evt.data); } catch { /* keep raw */ }
1243
+ }
1244
+ return evt;
1245
+ }),
1246
+ };
1247
+ const filename = `execution-history-${shortId}-${Date.now()}.json`;
1248
+ const content = JSON.stringify(exportData, null, 2);
1249
+ await this.artifactStore.uploadArtifact(sessionId, filename, content, guessArtifactContentType(filename));
1250
+ return {
1251
+ sessionId,
1252
+ filename,
1253
+ artifactLink: `artifact://${sessionId}/${filename}`,
1254
+ sizeBytes: Buffer.byteLength(content, "utf8"),
1255
+ };
1256
+ }
1257
+
1258
+ async openPathInDefaultApp(targetPath) {
1259
+ const resolvedPath = path.resolve(expandUserPath(targetPath));
1260
+ if (!resolvedPath) {
1261
+ throw new Error("File path cannot be empty.");
1262
+ }
1263
+ const stat = await fs.promises.stat(resolvedPath).catch(() => null);
1264
+ if (!stat || !stat.isFile()) {
1265
+ throw new Error(`File not found: ${targetPath}`);
1266
+ }
1267
+
1268
+ if (process.platform === "darwin") {
1269
+ await spawnDetached("open", [resolvedPath]);
1270
+ } else if (process.platform === "win32") {
1271
+ await spawnDetached("cmd", ["/c", "start", "", resolvedPath]);
1272
+ } else {
1273
+ await spawnDetached("xdg-open", [resolvedPath]);
1274
+ }
1275
+
1276
+ return { localPath: resolvedPath };
1277
+ }
1278
+
1279
+ async openUrlInDefaultBrowser(targetUrl) {
1280
+ const href = String(targetUrl || "").trim();
1281
+ if (!href) {
1282
+ throw new Error("URL cannot be empty.");
1283
+ }
1284
+
1285
+ let parsedUrl;
1286
+ try {
1287
+ parsedUrl = new URL(href);
1288
+ } catch {
1289
+ throw new Error(`Invalid URL: ${targetUrl}`);
1290
+ }
1291
+
1292
+ if (!/^https?:$/i.test(parsedUrl.protocol)) {
1293
+ throw new Error(`Unsupported URL protocol: ${parsedUrl.protocol}`);
1294
+ }
1295
+
1296
+ if (process.platform === "darwin") {
1297
+ await spawnDetached("open", [parsedUrl.toString()]);
1298
+ } else if (process.platform === "win32") {
1299
+ await spawnDetached("cmd", ["/c", "start", "", parsedUrl.toString()]);
1300
+ } else {
1301
+ await spawnDetached("xdg-open", [parsedUrl.toString()]);
1302
+ }
1303
+
1304
+ return { url: parsedUrl.toString() };
1305
+ }
1306
+
1307
+ getModelsByProvider() {
1308
+ return this.mgmt.getModelsByProvider();
1309
+ }
1310
+
1311
+ getDefaultModel() {
1312
+ return this.mgmt.getDefaultModel();
1313
+ }
1314
+
1315
+ async getSessionEvents(sessionId, afterSeq, limit, eventTypes) {
1316
+ return this.mgmt.getSessionEvents(sessionId, afterSeq, limit, eventTypes);
1317
+ }
1318
+
1319
+ async getSessionEventsBefore(sessionId, beforeSeq, limit, eventTypes) {
1320
+ if (typeof this.mgmt.getSessionEventsBefore !== "function") return [];
1321
+ return this.mgmt.getSessionEventsBefore(sessionId, beforeSeq, limit, eventTypes);
1322
+ }
1323
+
1324
+ emitLogEntry(entry) {
1325
+ if (!this._logBatch) this._logBatch = [];
1326
+ this._logBatch.push(entry);
1327
+ if (!this._logBatchTimer) {
1328
+ this._logBatchTimer = setTimeout(() => {
1329
+ const batch = this._logBatch;
1330
+ this._logBatch = [];
1331
+ this._logBatchTimer = null;
1332
+ for (const handler of this.logSubscribers) {
1333
+ try {
1334
+ handler(batch);
1335
+ } catch {}
1336
+ }
1337
+ }, 250);
1338
+ }
1339
+ }
1340
+
1341
+ scheduleLogRestart() {
1342
+ if (this.logRestartTimer || this.logSubscribers.size === 0) return;
1343
+ this.logRestartTimer = setTimeout(() => {
1344
+ this.logRestartTimer = null;
1345
+ if (this.logSubscribers.size > 0) {
1346
+ this.startLogProcess();
1347
+ }
1348
+ }, 5000);
1349
+ }
1350
+
1351
+ emitSyntheticLogMessage(message, level = "info", podName = "k8s") {
1352
+ this.logEntryCounter += 1;
1353
+ this.emitLogEntry(buildSyntheticLogEntry({
1354
+ message,
1355
+ level,
1356
+ podName,
1357
+ counter: this.logEntryCounter,
1358
+ }));
1359
+ }
1360
+
1361
+ async listPodsFromKubeApi(config, labelSelector) {
1362
+ const params = new URLSearchParams();
1363
+ if (labelSelector) params.set("labelSelector", labelSelector);
1364
+ const pathName = `/api/v1/namespaces/${encodeURIComponent(config.namespace)}/pods${params.size > 0 ? `?${params.toString()}` : ""}`;
1365
+
1366
+ return await new Promise((resolve, reject) => {
1367
+ const req = https.request({
1368
+ method: "GET",
1369
+ hostname: config.host,
1370
+ port: config.port,
1371
+ path: pathName,
1372
+ ca: config.ca,
1373
+ headers: {
1374
+ Authorization: `Bearer ${config.token}`,
1375
+ Accept: "application/json",
1376
+ },
1377
+ }, (res) => {
1378
+ let body = "";
1379
+ res.setEncoding("utf8");
1380
+ res.on("data", (chunk) => {
1381
+ body += chunk;
1382
+ });
1383
+ res.on("end", () => {
1384
+ if ((res.statusCode || 0) >= 400) {
1385
+ reject(new Error(
1386
+ `Kubernetes API pod list failed (${res.statusCode}): ${trimLogText(body || res.statusMessage || "unknown error")}`,
1387
+ ));
1388
+ return;
1389
+ }
1390
+ try {
1391
+ const payload = JSON.parse(body || "{}");
1392
+ const items = Array.isArray(payload?.items) ? payload.items : [];
1393
+ resolve(items
1394
+ .map((item) => String(item?.metadata?.name || "").trim())
1395
+ .filter(Boolean));
1396
+ } catch (error) {
1397
+ reject(error);
1398
+ }
1399
+ });
1400
+ });
1401
+
1402
+ req.on("error", reject);
1403
+ req.end();
1404
+ });
1405
+ }
1406
+
1407
+ streamPodLogsFromKubeApi(config, podName, handle, options = {}) {
1408
+ const params = new URLSearchParams({
1409
+ follow: "true",
1410
+ timestamps: "true",
1411
+ tailLines: String(options.tailLines ?? 500),
1412
+ });
1413
+ const pathName = `/api/v1/namespaces/${encodeURIComponent(config.namespace)}/pods/${encodeURIComponent(podName)}/log?${params.toString()}`;
1414
+
1415
+ return new Promise((resolve, reject) => {
1416
+ let buffer = "";
1417
+ let settled = false;
1418
+ let response = null;
1419
+
1420
+ const finish = (error = null) => {
1421
+ if (settled) return;
1422
+ settled = true;
1423
+
1424
+ if (buffer.trim()) {
1425
+ this.logEntryCounter += 1;
1426
+ this.emitLogEntry(buildLogEntry(`[pod/${podName}] ${buffer.trim()}`, this.logEntryCounter));
1427
+ buffer = "";
1428
+ }
1429
+
1430
+ if (response) {
1431
+ handle.responses.delete(response);
1432
+ }
1433
+ handle.requests.delete(request);
1434
+
1435
+ if (error) reject(error);
1436
+ else resolve();
1437
+ };
1438
+
1439
+ const request = https.request({
1440
+ method: "GET",
1441
+ hostname: config.host,
1442
+ port: config.port,
1443
+ path: pathName,
1444
+ ca: config.ca,
1445
+ headers: {
1446
+ Authorization: `Bearer ${config.token}`,
1447
+ Accept: "*/*",
1448
+ },
1449
+ }, (res) => {
1450
+ response = res;
1451
+ handle.responses.add(res);
1452
+
1453
+ if ((res.statusCode || 0) >= 400) {
1454
+ let body = "";
1455
+ res.setEncoding("utf8");
1456
+ res.on("data", (chunk) => {
1457
+ body += chunk;
1458
+ });
1459
+ res.on("end", () => {
1460
+ finish(new Error(
1461
+ `Kubernetes log stream failed for ${podName} (${res.statusCode}): ${trimLogText(body || res.statusMessage || "unknown error")}`,
1462
+ ));
1463
+ });
1464
+ return;
1465
+ }
1466
+
1467
+ res.setEncoding("utf8");
1468
+ res.on("data", (chunk) => {
1469
+ buffer += chunk;
1470
+ const lines = buffer.split("\n");
1471
+ buffer = lines.pop() || "";
1472
+ for (const line of lines) {
1473
+ if (!line.trim()) continue;
1474
+ this.logEntryCounter += 1;
1475
+ this.emitLogEntry(buildLogEntry(`[pod/${podName}] ${line}`, this.logEntryCounter));
1476
+ }
1477
+ });
1478
+ res.on("end", () => finish());
1479
+ res.on("close", () => finish());
1480
+ res.on("error", (error) => finish(error));
1481
+ });
1482
+
1483
+ handle.requests.add(request);
1484
+ request.on("error", (error) => finish(error));
1485
+ request.end();
1486
+ });
1487
+ }
1488
+
1489
+ startInClusterLogProcess() {
1490
+ const config = getInClusterK8sConfig();
1491
+ if (!config || this.logTailHandle) return;
1492
+
1493
+ const labelSelector = process.env.K8S_POD_LABEL || "app.kubernetes.io/component=worker";
1494
+ const handle = {
1495
+ stopped: false,
1496
+ requests: new Set(),
1497
+ responses: new Set(),
1498
+ stop: () => {
1499
+ if (handle.stopped) return;
1500
+ handle.stopped = true;
1501
+ for (const response of handle.responses) {
1502
+ try { response.destroy(); } catch {}
1503
+ }
1504
+ handle.responses.clear();
1505
+ for (const request of handle.requests) {
1506
+ try { request.destroy(); } catch {}
1507
+ }
1508
+ handle.requests.clear();
1509
+ },
1510
+ };
1511
+ this.logTailHandle = handle;
1512
+
1513
+ this.listPodsFromKubeApi(config, labelSelector)
1514
+ .then(async (podNames) => {
1515
+ if (handle.stopped || this.logTailHandle !== handle) return;
1516
+ if (podNames.length === 0) {
1517
+ this.emitSyntheticLogMessage(
1518
+ `No pods matched label selector ${JSON.stringify(labelSelector)} in namespace ${config.namespace}.`,
1519
+ "warn",
1520
+ );
1521
+ return;
1522
+ }
1523
+
1524
+ const results = await Promise.allSettled(
1525
+ podNames.map((podName) => this.streamPodLogsFromKubeApi(config, podName, handle)),
1526
+ );
1527
+
1528
+ if (handle.stopped || this.logTailHandle !== handle) return;
1529
+ for (const result of results) {
1530
+ if (result.status === "fulfilled") continue;
1531
+ this.emitSyntheticLogMessage(result.reason?.message || String(result.reason), "error");
1532
+ }
1533
+ })
1534
+ .catch((error) => {
1535
+ if (handle.stopped || this.logTailHandle !== handle) return;
1536
+ this.emitSyntheticLogMessage(error?.message || String(error), "error");
1537
+ })
1538
+ .finally(() => {
1539
+ if (this.logTailHandle === handle) {
1540
+ this.logTailHandle = null;
1541
+ }
1542
+ if (!handle.stopped) {
1543
+ this.scheduleLogRestart();
1544
+ }
1545
+ });
1546
+ }
1547
+
1548
+ startKubectlLogProcess() {
1549
+ if (this.logProc) return;
1550
+
1551
+ const config = this.getLogConfig();
1552
+ if (!config.available) return;
1553
+
1554
+ const k8sContext = process.env.K8S_CONTEXT || "";
1555
+ const k8sNamespace = process.env.K8S_NAMESPACE || "copilot-runtime";
1556
+ const k8sPodLabel = process.env.K8S_POD_LABEL || "app.kubernetes.io/component=worker";
1557
+ const k8sCtxArgs = k8sContext ? ["--context", k8sContext] : [];
1558
+ this.logBuffer = "";
1559
+ this.logProc = spawn("kubectl", [
1560
+ ...k8sCtxArgs,
1561
+ "logs",
1562
+ "--follow=true",
1563
+ "-n", k8sNamespace,
1564
+ "-l", k8sPodLabel,
1565
+ "--prefix",
1566
+ "--tail=500",
1567
+ "--max-log-requests=20",
1568
+ ], { stdio: ["ignore", "pipe", "pipe"] });
1569
+
1570
+ this.logProc.stdout.on("data", (chunk) => {
1571
+ this.logBuffer += chunk.toString();
1572
+ const lines = this.logBuffer.split("\n");
1573
+ this.logBuffer = lines.pop() || "";
1574
+ for (const line of lines) {
1575
+ if (!line.trim()) continue;
1576
+ this.logEntryCounter += 1;
1577
+ this.emitLogEntry(buildLogEntry(line, this.logEntryCounter));
1578
+ }
1579
+ });
1580
+
1581
+ this.logProc.stderr.on("data", (chunk) => {
1582
+ const text = stripAnsi(chunk.toString()).trim();
1583
+ if (!text) return;
1584
+ this.emitSyntheticLogMessage(text, "warn", "kubectl");
1585
+ });
1586
+
1587
+ this.logProc.on("error", (error) => {
1588
+ this.emitSyntheticLogMessage(`kubectl error: ${error.message}`, "error", "kubectl");
1589
+ });
1590
+
1591
+ this.logProc.on("exit", (code, signal) => {
1592
+ this.logProc = null;
1593
+ this.emitSyntheticLogMessage(`kubectl exited (code=${code} signal=${signal})`, "warn", "kubectl");
1594
+ this.scheduleLogRestart();
1595
+ });
1596
+ }
1597
+
1598
+ startLocalLogProcess() {
1599
+ const logDir = getLocalLogDir();
1600
+ if (!logDir || this.logTailHandle) return;
1601
+
1602
+ const handle = {
1603
+ stopped: false,
1604
+ files: new Map(),
1605
+ interval: null,
1606
+ stop: () => {
1607
+ if (handle.stopped) return;
1608
+ handle.stopped = true;
1609
+ if (handle.interval) {
1610
+ clearInterval(handle.interval);
1611
+ handle.interval = null;
1612
+ }
1613
+ handle.files.clear();
1614
+ },
1615
+ };
1616
+ this.logTailHandle = handle;
1617
+
1618
+ const emitLine = (filePath, line) => {
1619
+ const text = String(line || "").trim();
1620
+ if (!text) return;
1621
+ const pseudoPod = path.basename(filePath, path.extname(filePath));
1622
+ this.logEntryCounter += 1;
1623
+ this.emitLogEntry(buildLogEntry(`[pod/${pseudoPod}] ${text}`, this.logEntryCounter));
1624
+ };
1625
+
1626
+ const refresh = () => {
1627
+ if (handle.stopped || this.logTailHandle !== handle) return;
1628
+ for (const filePath of listLocalLogFiles(logDir)) {
1629
+ let state = handle.files.get(filePath);
1630
+ let stats;
1631
+ try {
1632
+ stats = fs.statSync(filePath);
1633
+ } catch {
1634
+ continue;
1635
+ }
1636
+ if (!stats.isFile()) continue;
1637
+
1638
+ if (!state) {
1639
+ state = {
1640
+ position: stats.size,
1641
+ inode: stats.ino,
1642
+ buffer: "",
1643
+ };
1644
+ handle.files.set(filePath, state);
1645
+ for (const line of readRecentLogLines(filePath)) {
1646
+ emitLine(filePath, line);
1647
+ }
1648
+ state.position = stats.size;
1649
+ state.inode = stats.ino;
1650
+ continue;
1651
+ }
1652
+
1653
+ if (state.inode !== stats.ino || stats.size < state.position) {
1654
+ state.position = 0;
1655
+ state.buffer = "";
1656
+ state.inode = stats.ino;
1657
+ }
1658
+
1659
+ if (stats.size <= state.position) continue;
1660
+
1661
+ const chunk = readLogChunk(filePath, state.position, stats.size);
1662
+ state.position = stats.size;
1663
+ if (!chunk) continue;
1664
+ const combined = state.buffer + chunk;
1665
+ const lines = combined.split(/\r?\n/u);
1666
+ state.buffer = lines.pop() || "";
1667
+ for (const line of lines) {
1668
+ emitLine(filePath, line);
1669
+ }
1670
+ }
1671
+ };
1672
+
1673
+ try {
1674
+ refresh();
1675
+ handle.interval = setInterval(refresh, getLocalLogPollIntervalMs());
1676
+ if (typeof handle.interval.unref === "function") {
1677
+ handle.interval.unref();
1678
+ }
1679
+ } catch (error) {
1680
+ this.logTailHandle = null;
1681
+ handle.stop();
1682
+ this.emitSyntheticLogMessage(error?.message || String(error), "error", "local-log");
1683
+ }
1684
+ }
1685
+
1686
+ startLogProcess() {
1687
+ const config = this.getLogConfig();
1688
+ if (!config.available || this.logProc || this.logTailHandle) return;
1689
+
1690
+ if (getLocalLogDir()) {
1691
+ this.startLocalLogProcess();
1692
+ return;
1693
+ }
1694
+
1695
+ if (hasInClusterK8sAccess()) {
1696
+ this.startInClusterLogProcess();
1697
+ return;
1698
+ }
1699
+
1700
+ this.startKubectlLogProcess();
1701
+ }
1702
+
1703
+ startLogTail(handler) {
1704
+ if (typeof handler === "function") {
1705
+ this.logSubscribers.add(handler);
1706
+ }
1707
+ this.startLogProcess();
1708
+
1709
+ return () => {
1710
+ if (typeof handler === "function") {
1711
+ this.logSubscribers.delete(handler);
1712
+ }
1713
+ if (this.logSubscribers.size === 0) {
1714
+ this.stopLogTail().catch(() => {});
1715
+ }
1716
+ };
1717
+ }
1718
+
1719
+ async stopLogTail() {
1720
+ if (this._logBatchTimer) {
1721
+ clearTimeout(this._logBatchTimer);
1722
+ this._logBatchTimer = null;
1723
+ this._logBatch = [];
1724
+ }
1725
+ if (this.logRestartTimer) {
1726
+ clearTimeout(this.logRestartTimer);
1727
+ this.logRestartTimer = null;
1728
+ }
1729
+ if (this.logTailHandle) {
1730
+ try {
1731
+ this.logTailHandle.stop();
1732
+ } catch {}
1733
+ this.logTailHandle = null;
1734
+ }
1735
+ if (this.logProc) {
1736
+ try {
1737
+ this.logProc.kill("SIGKILL");
1738
+ } catch {}
1739
+ this.logProc = null;
1740
+ }
1741
+ this.logBuffer = "";
1742
+ }
1743
+
1744
+ subscribeSession(sessionId, handler) {
1745
+ let unsubscribe = () => {};
1746
+ let active = true;
1747
+ this.getSessionHandle(sessionId)
1748
+ .then((session) => {
1749
+ if (!active) return;
1750
+ unsubscribe = session.on((event) => handler(event));
1751
+ })
1752
+ .catch(() => {});
1753
+
1754
+ return () => {
1755
+ active = false;
1756
+ unsubscribe();
1757
+ };
1758
+ }
1759
+
1760
+ async getSessionHandle(sessionId) {
1761
+ if (this.sessionHandles.has(sessionId)) {
1762
+ return this.sessionHandles.get(sessionId);
1763
+ }
1764
+ const session = await this.client.resumeSession(sessionId);
1765
+ this.sessionHandles.set(sessionId, session);
1766
+ return session;
1767
+ }
1768
+ }
1769
+
1770
+ function createArtifactStore() {
1771
+ const sessionStateDir = (process.env.SESSION_STATE_DIR || "").trim() || undefined;
1772
+ const artifactDir = (process.env.ARTIFACT_DIR || "").trim() || undefined;
1773
+
1774
+ try {
1775
+ const blobStore = createSessionBlobStore(process.env, { sessionStateDir });
1776
+ if (blobStore) return blobStore;
1777
+ } catch (err) {
1778
+ // AZURE_STORAGE_CONNECTION_STRING is set but unparseable
1779
+ // (typically a truncated or placeholder value left over in the
1780
+ // shell — e.g. "DefaultEndpointsProtocol=https" with no
1781
+ // AccountName/AccountKey), or PILOTSWARM_USE_MANAGED_IDENTITY=1
1782
+ // is set without AZURE_STORAGE_ACCOUNT_URL. Halt with an
1783
+ // actionable error instead of silently falling back to disk:
1784
+ // silent fallback would mask blob-storage misconfiguration in
1785
+ // production.
1786
+ const reason = err?.message || String(err);
1787
+ throw new Error(
1788
+ `Azure Blob Storage is configured but cannot be initialized (reason: ${reason}). ` +
1789
+ `Either fix the configuration or unset AZURE_STORAGE_CONNECTION_STRING / PILOTSWARM_USE_MANAGED_IDENTITY to fall back to the local filesystem artifact store.`,
1790
+ );
1791
+ }
1792
+
1793
+ return new FilesystemArtifactStore(artifactDir);
1794
+ }