pilotswarm-cli 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
- import { spawn } from "node:child_process";
1
+ import { spawn, spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
+ import https from "node:https";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
5
6
  import {
@@ -10,9 +11,65 @@ import {
10
11
  SessionBlobStore,
11
12
  } from "pilotswarm-sdk";
12
13
  import { startEmbeddedWorkers, stopEmbeddedWorkers } from "./embedded-workers.js";
14
+ import { getPluginDirsFromEnv } from "./plugin-config.js";
13
15
 
14
16
  const EXPORTS_DIR = path.join(os.homedir(), "pilotswarm-exports");
15
17
  fs.mkdirSync(EXPORTS_DIR, { recursive: true });
18
+ const K8S_SERVICE_ACCOUNT_DIR = "/var/run/secrets/kubernetes.io/serviceaccount";
19
+
20
+ function fileExists(filePath) {
21
+ try {
22
+ return fs.existsSync(filePath);
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ function getInClusterK8sPaths() {
29
+ const baseDir = process.env.PILOTSWARM_K8S_SERVICE_ACCOUNT_DIR || K8S_SERVICE_ACCOUNT_DIR;
30
+ return {
31
+ tokenPath: process.env.PILOTSWARM_K8S_TOKEN_PATH || path.join(baseDir, "token"),
32
+ caPath: process.env.PILOTSWARM_K8S_CA_PATH || path.join(baseDir, "ca.crt"),
33
+ namespacePath: process.env.PILOTSWARM_K8S_NAMESPACE_PATH || path.join(baseDir, "namespace"),
34
+ };
35
+ }
36
+
37
+ function readOptionalTextFile(filePath) {
38
+ try {
39
+ return fs.readFileSync(filePath, "utf8").trim();
40
+ } catch {
41
+ return "";
42
+ }
43
+ }
44
+
45
+ function hasInClusterK8sAccess() {
46
+ const { tokenPath, caPath } = getInClusterK8sPaths();
47
+ return Boolean(process.env.KUBERNETES_SERVICE_HOST)
48
+ && fileExists(tokenPath)
49
+ && fileExists(caPath);
50
+ }
51
+
52
+ function getInClusterK8sConfig() {
53
+ if (!hasInClusterK8sAccess()) return null;
54
+
55
+ const { tokenPath, caPath, namespacePath } = getInClusterK8sPaths();
56
+ return {
57
+ host: String(process.env.KUBERNETES_SERVICE_HOST || "").trim(),
58
+ port: Number(process.env.KUBERNETES_SERVICE_PORT || 443) || 443,
59
+ token: readOptionalTextFile(tokenPath),
60
+ ca: fs.readFileSync(caPath),
61
+ namespace: String(process.env.K8S_NAMESPACE || "").trim() || readOptionalTextFile(namespacePath) || "default",
62
+ };
63
+ }
64
+
65
+ function hasExplicitKubectlConfig() {
66
+ return Boolean((process.env.K8S_CONTEXT || "").trim() || (process.env.KUBECONFIG || "").trim());
67
+ }
68
+
69
+ function isKubectlAvailable() {
70
+ const result = spawnSync("kubectl", ["version", "--client=true"], { stdio: "ignore" });
71
+ return !result.error;
72
+ }
16
73
 
17
74
  function stripAnsi(value) {
18
75
  return String(value || "").replace(/\x1b\[[0-9;]*m/g, "");
@@ -105,9 +162,14 @@ function buildLogEntry(line, counter) {
105
162
  const prefixMatch = line.match(/^\[pod\/([^/\]]+)/);
106
163
  const podName = prefixMatch ? prefixMatch[1] : "unknown";
107
164
  const rawLine = trimLogText(stripAnsi(line.replace(/^\[pod\/[^\]]+\]\s*/, "")).trim());
108
- const orchMatch = rawLine.match(/\b(instance_id|orchestration_id)=(session-[^\s,]+)/i)
165
+ const orchMatch = rawLine.match(/\b(?:instance_id|orchestration_id|orch)=(session-[^\s,]+)/i)
109
166
  || rawLine.match(/\b(session-[0-9a-f-]{8,})\b/i);
110
- const orchId = orchMatch ? orchMatch[2] || orchMatch[1] : null;
167
+ const parsedOrchId = orchMatch ? orchMatch[1] : null;
168
+ const sessionIdMatch = rawLine.match(/\b(?:sessionId|session|durableSessionId)=([0-9a-f-]{8,})\b/i);
169
+ const sessionId = sessionIdMatch
170
+ ? sessionIdMatch[1]
171
+ : (parsedOrchId && parsedOrchId.startsWith("session-") ? parsedOrchId.slice("session-".length) : null);
172
+ const orchId = parsedOrchId || (sessionId ? `session-${sessionId}` : null);
111
173
  const category = rawLine.includes("duroxide::activity")
112
174
  ? "activity"
113
175
  : rawLine.includes("duroxide::orchestration") || rawLine.includes("::orchestration")
@@ -120,6 +182,7 @@ function buildLogEntry(line, counter) {
120
182
  podName,
121
183
  level: normalizeLogLevel(rawLine),
122
184
  orchId,
185
+ sessionId,
123
186
  category,
124
187
  rawLine,
125
188
  message: extractPrettyLogMessage(rawLine),
@@ -127,6 +190,22 @@ function buildLogEntry(line, counter) {
127
190
  };
128
191
  }
129
192
 
193
+ function buildSyntheticLogEntry({ message, level = "info", podName = "k8s", counter = 0 }) {
194
+ const safeMessage = trimLogText(String(message || "").trim());
195
+ return {
196
+ id: `log:${Date.now()}:${counter}`,
197
+ time: extractLogTime(safeMessage),
198
+ podName,
199
+ level,
200
+ orchId: null,
201
+ sessionId: null,
202
+ category: "log",
203
+ rawLine: safeMessage,
204
+ message: safeMessage,
205
+ prettyMessage: safeMessage,
206
+ };
207
+ }
208
+
130
209
  function sanitizeArtifactFilename(filename) {
131
210
  return String(filename || "").replace(/[/\\]/g, "_");
132
211
  }
@@ -179,13 +258,6 @@ function isTerminalSendError(error) {
179
258
  return /instance is terminal|terminal orchestration|cannot accept new messages/i.test(message);
180
259
  }
181
260
 
182
- function getPluginDirsFromEnv() {
183
- return String(process.env.PLUGIN_DIRS || "")
184
- .split(",")
185
- .map((value) => value.trim())
186
- .filter(Boolean);
187
- }
188
-
189
261
  function normalizeCreatableAgent(agent) {
190
262
  const name = String(agent?.name || "").trim();
191
263
  if (!name) return null;
@@ -235,8 +307,8 @@ function loadSessionCreationMetadataFromPluginDirs(pluginDirs = []) {
235
307
  }
236
308
 
237
309
  function buildTerminalSendError(sessionId, session) {
238
- if (session?.status === "failed" || session?.orchestrationStatus === "Failed") {
239
- return `Session ${sessionId.slice(0, 8)} is a failed terminal orchestration and cannot accept new messages.`;
310
+ if (session?.status === "failed" || session?.status === "cancelled" || session?.orchestrationStatus === "Failed") {
311
+ return `Session ${sessionId.slice(0, 8)} is a terminal orchestration and cannot accept new messages.`;
240
312
  }
241
313
 
242
314
  const statusLabel = String(session?.orchestrationStatus || session?.status || "Unknown");
@@ -256,10 +328,12 @@ export class NodeSdkTransport {
256
328
  this.allowedAgentNames = [];
257
329
  this.creatableAgents = [];
258
330
  this.logProc = null;
331
+ this.logTailHandle = null;
259
332
  this.logBuffer = "";
260
333
  this.logRestartTimer = null;
261
334
  this.logSubscribers = new Set();
262
335
  this.logEntryCounter = 0;
336
+ this.kubectlAvailable = null;
263
337
  }
264
338
 
265
339
  async start() {
@@ -314,12 +388,30 @@ export class NodeSdkTransport {
314
388
  }
315
389
 
316
390
  getLogConfig() {
317
- const hasK8sConfig = Boolean((process.env.K8S_CONTEXT || "").trim() || (process.env.KUBECONFIG || "").trim());
391
+ const hasInClusterConfig = hasInClusterK8sAccess();
392
+ const hasKubectlConfig = hasExplicitKubectlConfig();
393
+ if (hasInClusterConfig) {
394
+ return {
395
+ available: true,
396
+ availabilityReason: "",
397
+ };
398
+ }
399
+
400
+ if (hasKubectlConfig) {
401
+ if (this.kubectlAvailable == null) {
402
+ this.kubectlAvailable = isKubectlAvailable();
403
+ }
404
+ return {
405
+ available: this.kubectlAvailable,
406
+ availabilityReason: this.kubectlAvailable
407
+ ? ""
408
+ : "Log tailing disabled: kubectl is not installed in this environment.",
409
+ };
410
+ }
411
+
318
412
  return {
319
- available: hasK8sConfig,
320
- availabilityReason: hasK8sConfig
321
- ? ""
322
- : "Log tailing disabled: no K8S_CONTEXT configured in the env file.",
413
+ available: false,
414
+ availabilityReason: "Log tailing disabled: no K8S_CONTEXT/KUBECONFIG or in-cluster Kubernetes access detected.",
323
415
  };
324
416
  }
325
417
 
@@ -335,6 +427,10 @@ export class NodeSdkTransport {
335
427
  return this.mgmt.getOrchestrationStats(sessionId);
336
428
  }
337
429
 
430
+ async getExecutionHistory(sessionId, executionId) {
431
+ return this.mgmt.getExecutionHistory(sessionId, executionId);
432
+ }
433
+
338
434
  async createSession({ model } = {}) {
339
435
  const effectiveModel = model || this.mgmt.getDefaultModel();
340
436
  const session = await this.client.createSession(effectiveModel ? { model: effectiveModel } : undefined);
@@ -371,11 +467,11 @@ export class NodeSdkTransport {
371
467
  if (!session) {
372
468
  throw new Error(`Session ${sessionId.slice(0, 8)} was not found.`);
373
469
  }
374
- if (session.status === "failed" || session.orchestrationStatus === "Failed") {
470
+ if (session.status === "failed" || session.status === "cancelled" || session.orchestrationStatus === "Failed") {
375
471
  throw new Error(buildTerminalSendError(sessionId, session));
376
472
  }
377
473
  if (
378
- session.status === "completed"
474
+ (session.status === "completed" || session.status === "cancelled")
379
475
  && session.parentSessionId
380
476
  && !session.isSystem
381
477
  && !session.cronActive
@@ -476,6 +572,36 @@ export class NodeSdkTransport {
476
572
  };
477
573
  }
478
574
 
575
+ async uploadArtifactContent(sessionId, filename, content, contentType = guessArtifactContentType(filename)) {
576
+ if (!this.artifactStore) {
577
+ throw new Error("Artifact store is not available for this transport.");
578
+ }
579
+ const safeSessionId = String(sessionId || "").trim();
580
+ const safeFilename = path.basename(String(filename || "").trim());
581
+ const safeContent = typeof content === "string" ? content : String(content || "");
582
+ if (!safeSessionId) {
583
+ throw new Error("Session id is required for artifact upload.");
584
+ }
585
+ if (!safeFilename) {
586
+ throw new Error("Filename is required for artifact upload.");
587
+ }
588
+
589
+ await this.artifactStore.uploadArtifact(
590
+ safeSessionId,
591
+ safeFilename,
592
+ safeContent,
593
+ contentType || guessArtifactContentType(safeFilename),
594
+ );
595
+
596
+ return {
597
+ sessionId: safeSessionId,
598
+ filename: safeFilename,
599
+ resolvedPath: safeFilename,
600
+ sizeBytes: Buffer.byteLength(safeContent, "utf8"),
601
+ contentType: contentType || guessArtifactContentType(safeFilename),
602
+ };
603
+ }
604
+
479
605
  getArtifactExportDirectory() {
480
606
  return EXPORTS_DIR;
481
607
  }
@@ -495,6 +621,43 @@ export class NodeSdkTransport {
495
621
  };
496
622
  }
497
623
 
624
+ async exportExecutionHistory(sessionId) {
625
+ if (!this.artifactStore) {
626
+ throw new Error("Artifact store is not available for this transport.");
627
+ }
628
+ const shortId = String(sessionId || "").slice(0, 8);
629
+ const [history, stats] = await Promise.all([
630
+ this.mgmt.getExecutionHistory(sessionId),
631
+ this.mgmt.getOrchestrationStats(sessionId),
632
+ ]);
633
+ const sessionInfo = await this.mgmt.getSession(sessionId).catch(() => null);
634
+ const exportData = {
635
+ exportedAt: new Date().toISOString(),
636
+ sessionId,
637
+ title: sessionInfo?.title || null,
638
+ agentId: sessionInfo?.agentId || null,
639
+ model: sessionInfo?.model || null,
640
+ orchestrationStats: stats || null,
641
+ eventCount: history?.length || 0,
642
+ events: (history || []).map((e) => {
643
+ const evt = { ...e };
644
+ if (evt.data) {
645
+ try { evt.data = JSON.parse(evt.data); } catch { /* keep raw */ }
646
+ }
647
+ return evt;
648
+ }),
649
+ };
650
+ const filename = `execution-history-${shortId}-${Date.now()}.json`;
651
+ const content = JSON.stringify(exportData, null, 2);
652
+ await this.artifactStore.uploadArtifact(sessionId, filename, content, guessArtifactContentType(filename));
653
+ return {
654
+ sessionId,
655
+ filename,
656
+ artifactLink: `artifact://${sessionId}/${filename}`,
657
+ sizeBytes: Buffer.byteLength(content, "utf8"),
658
+ };
659
+ }
660
+
498
661
  async openPathInDefaultApp(targetPath) {
499
662
  const resolvedPath = path.resolve(expandUserPath(targetPath));
500
663
  if (!resolvedPath) {
@@ -534,10 +697,19 @@ export class NodeSdkTransport {
534
697
  }
535
698
 
536
699
  emitLogEntry(entry) {
537
- for (const handler of this.logSubscribers) {
538
- try {
539
- handler(entry);
540
- } catch {}
700
+ if (!this._logBatch) this._logBatch = [];
701
+ this._logBatch.push(entry);
702
+ if (!this._logBatchTimer) {
703
+ this._logBatchTimer = setTimeout(() => {
704
+ const batch = this._logBatch;
705
+ this._logBatch = [];
706
+ this._logBatchTimer = null;
707
+ for (const handler of this.logSubscribers) {
708
+ try {
709
+ handler(batch);
710
+ } catch {}
711
+ }
712
+ }, 250);
541
713
  }
542
714
  }
543
715
 
@@ -551,9 +723,208 @@ export class NodeSdkTransport {
551
723
  }, 5000);
552
724
  }
553
725
 
554
- startLogProcess() {
726
+ emitSyntheticLogMessage(message, level = "info", podName = "k8s") {
727
+ this.logEntryCounter += 1;
728
+ this.emitLogEntry(buildSyntheticLogEntry({
729
+ message,
730
+ level,
731
+ podName,
732
+ counter: this.logEntryCounter,
733
+ }));
734
+ }
735
+
736
+ async listPodsFromKubeApi(config, labelSelector) {
737
+ const params = new URLSearchParams();
738
+ if (labelSelector) params.set("labelSelector", labelSelector);
739
+ const pathName = `/api/v1/namespaces/${encodeURIComponent(config.namespace)}/pods${params.size > 0 ? `?${params.toString()}` : ""}`;
740
+
741
+ return await new Promise((resolve, reject) => {
742
+ const req = https.request({
743
+ method: "GET",
744
+ hostname: config.host,
745
+ port: config.port,
746
+ path: pathName,
747
+ ca: config.ca,
748
+ headers: {
749
+ Authorization: `Bearer ${config.token}`,
750
+ Accept: "application/json",
751
+ },
752
+ }, (res) => {
753
+ let body = "";
754
+ res.setEncoding("utf8");
755
+ res.on("data", (chunk) => {
756
+ body += chunk;
757
+ });
758
+ res.on("end", () => {
759
+ if ((res.statusCode || 0) >= 400) {
760
+ reject(new Error(
761
+ `Kubernetes API pod list failed (${res.statusCode}): ${trimLogText(body || res.statusMessage || "unknown error")}`,
762
+ ));
763
+ return;
764
+ }
765
+ try {
766
+ const payload = JSON.parse(body || "{}");
767
+ const items = Array.isArray(payload?.items) ? payload.items : [];
768
+ resolve(items
769
+ .map((item) => String(item?.metadata?.name || "").trim())
770
+ .filter(Boolean));
771
+ } catch (error) {
772
+ reject(error);
773
+ }
774
+ });
775
+ });
776
+
777
+ req.on("error", reject);
778
+ req.end();
779
+ });
780
+ }
781
+
782
+ streamPodLogsFromKubeApi(config, podName, handle, options = {}) {
783
+ const params = new URLSearchParams({
784
+ follow: "true",
785
+ timestamps: "true",
786
+ tailLines: String(options.tailLines ?? 500),
787
+ });
788
+ const pathName = `/api/v1/namespaces/${encodeURIComponent(config.namespace)}/pods/${encodeURIComponent(podName)}/log?${params.toString()}`;
789
+
790
+ return new Promise((resolve, reject) => {
791
+ let buffer = "";
792
+ let settled = false;
793
+ let response = null;
794
+
795
+ const finish = (error = null) => {
796
+ if (settled) return;
797
+ settled = true;
798
+
799
+ if (buffer.trim()) {
800
+ this.logEntryCounter += 1;
801
+ this.emitLogEntry(buildLogEntry(`[pod/${podName}] ${buffer.trim()}`, this.logEntryCounter));
802
+ buffer = "";
803
+ }
804
+
805
+ if (response) {
806
+ handle.responses.delete(response);
807
+ }
808
+ handle.requests.delete(request);
809
+
810
+ if (error) reject(error);
811
+ else resolve();
812
+ };
813
+
814
+ const request = https.request({
815
+ method: "GET",
816
+ hostname: config.host,
817
+ port: config.port,
818
+ path: pathName,
819
+ ca: config.ca,
820
+ headers: {
821
+ Authorization: `Bearer ${config.token}`,
822
+ Accept: "*/*",
823
+ },
824
+ }, (res) => {
825
+ response = res;
826
+ handle.responses.add(res);
827
+
828
+ if ((res.statusCode || 0) >= 400) {
829
+ let body = "";
830
+ res.setEncoding("utf8");
831
+ res.on("data", (chunk) => {
832
+ body += chunk;
833
+ });
834
+ res.on("end", () => {
835
+ finish(new Error(
836
+ `Kubernetes log stream failed for ${podName} (${res.statusCode}): ${trimLogText(body || res.statusMessage || "unknown error")}`,
837
+ ));
838
+ });
839
+ return;
840
+ }
841
+
842
+ res.setEncoding("utf8");
843
+ res.on("data", (chunk) => {
844
+ buffer += chunk;
845
+ const lines = buffer.split("\n");
846
+ buffer = lines.pop() || "";
847
+ for (const line of lines) {
848
+ if (!line.trim()) continue;
849
+ this.logEntryCounter += 1;
850
+ this.emitLogEntry(buildLogEntry(`[pod/${podName}] ${line}`, this.logEntryCounter));
851
+ }
852
+ });
853
+ res.on("end", () => finish());
854
+ res.on("close", () => finish());
855
+ res.on("error", (error) => finish(error));
856
+ });
857
+
858
+ handle.requests.add(request);
859
+ request.on("error", (error) => finish(error));
860
+ request.end();
861
+ });
862
+ }
863
+
864
+ startInClusterLogProcess() {
865
+ const config = getInClusterK8sConfig();
866
+ if (!config || this.logTailHandle) return;
867
+
868
+ const labelSelector = process.env.K8S_POD_LABEL || "app.kubernetes.io/component=worker";
869
+ const handle = {
870
+ stopped: false,
871
+ requests: new Set(),
872
+ responses: new Set(),
873
+ stop: () => {
874
+ if (handle.stopped) return;
875
+ handle.stopped = true;
876
+ for (const response of handle.responses) {
877
+ try { response.destroy(); } catch {}
878
+ }
879
+ handle.responses.clear();
880
+ for (const request of handle.requests) {
881
+ try { request.destroy(); } catch {}
882
+ }
883
+ handle.requests.clear();
884
+ },
885
+ };
886
+ this.logTailHandle = handle;
887
+
888
+ this.listPodsFromKubeApi(config, labelSelector)
889
+ .then(async (podNames) => {
890
+ if (handle.stopped || this.logTailHandle !== handle) return;
891
+ if (podNames.length === 0) {
892
+ this.emitSyntheticLogMessage(
893
+ `No pods matched label selector ${JSON.stringify(labelSelector)} in namespace ${config.namespace}.`,
894
+ "warn",
895
+ );
896
+ return;
897
+ }
898
+
899
+ const results = await Promise.allSettled(
900
+ podNames.map((podName) => this.streamPodLogsFromKubeApi(config, podName, handle)),
901
+ );
902
+
903
+ if (handle.stopped || this.logTailHandle !== handle) return;
904
+ for (const result of results) {
905
+ if (result.status === "fulfilled") continue;
906
+ this.emitSyntheticLogMessage(result.reason?.message || String(result.reason), "error");
907
+ }
908
+ })
909
+ .catch((error) => {
910
+ if (handle.stopped || this.logTailHandle !== handle) return;
911
+ this.emitSyntheticLogMessage(error?.message || String(error), "error");
912
+ })
913
+ .finally(() => {
914
+ if (this.logTailHandle === handle) {
915
+ this.logTailHandle = null;
916
+ }
917
+ if (!handle.stopped) {
918
+ this.scheduleLogRestart();
919
+ }
920
+ });
921
+ }
922
+
923
+ startKubectlLogProcess() {
924
+ if (this.logProc) return;
925
+
555
926
  const config = this.getLogConfig();
556
- if (!config.available || this.logProc) return;
927
+ if (!config.available) return;
557
928
 
558
929
  const k8sContext = process.env.K8S_CONTEXT || "";
559
930
  const k8sNamespace = process.env.K8S_NAMESPACE || "copilot-runtime";
@@ -585,53 +956,32 @@ export class NodeSdkTransport {
585
956
  this.logProc.stderr.on("data", (chunk) => {
586
957
  const text = stripAnsi(chunk.toString()).trim();
587
958
  if (!text) return;
588
- this.logEntryCounter += 1;
589
- this.emitLogEntry({
590
- id: `log:${Date.now()}:${this.logEntryCounter}`,
591
- time: extractLogTime(text),
592
- podName: "kubectl",
593
- level: "warn",
594
- orchId: null,
595
- category: "log",
596
- rawLine: trimLogText(text),
597
- message: trimLogText(text),
598
- prettyMessage: trimLogText(text),
599
- });
959
+ this.emitSyntheticLogMessage(text, "warn", "kubectl");
600
960
  });
601
961
 
602
962
  this.logProc.on("error", (error) => {
603
- this.logEntryCounter += 1;
604
- this.emitLogEntry({
605
- id: `log:${Date.now()}:${this.logEntryCounter}`,
606
- time: extractLogTime(""),
607
- podName: "kubectl",
608
- level: "error",
609
- orchId: null,
610
- category: "log",
611
- rawLine: trimLogText(`kubectl error: ${error.message}`),
612
- message: trimLogText(`kubectl error: ${error.message}`),
613
- prettyMessage: trimLogText(`kubectl error: ${error.message}`),
614
- });
963
+ this.emitSyntheticLogMessage(`kubectl error: ${error.message}`, "error", "kubectl");
615
964
  });
616
965
 
617
966
  this.logProc.on("exit", (code, signal) => {
618
967
  this.logProc = null;
619
- this.logEntryCounter += 1;
620
- this.emitLogEntry({
621
- id: `log:${Date.now()}:${this.logEntryCounter}`,
622
- time: extractLogTime(""),
623
- podName: "kubectl",
624
- level: "warn",
625
- orchId: null,
626
- category: "log",
627
- rawLine: trimLogText(`kubectl exited (code=${code} signal=${signal})`),
628
- message: trimLogText(`kubectl exited (code=${code} signal=${signal})`),
629
- prettyMessage: trimLogText(`kubectl exited (code=${code} signal=${signal})`),
630
- });
968
+ this.emitSyntheticLogMessage(`kubectl exited (code=${code} signal=${signal})`, "warn", "kubectl");
631
969
  this.scheduleLogRestart();
632
970
  });
633
971
  }
634
972
 
973
+ startLogProcess() {
974
+ const config = this.getLogConfig();
975
+ if (!config.available || this.logProc || this.logTailHandle) return;
976
+
977
+ if (hasInClusterK8sAccess()) {
978
+ this.startInClusterLogProcess();
979
+ return;
980
+ }
981
+
982
+ this.startKubectlLogProcess();
983
+ }
984
+
635
985
  startLogTail(handler) {
636
986
  if (typeof handler === "function") {
637
987
  this.logSubscribers.add(handler);
@@ -649,10 +999,21 @@ export class NodeSdkTransport {
649
999
  }
650
1000
 
651
1001
  async stopLogTail() {
1002
+ if (this._logBatchTimer) {
1003
+ clearTimeout(this._logBatchTimer);
1004
+ this._logBatchTimer = null;
1005
+ this._logBatch = [];
1006
+ }
652
1007
  if (this.logRestartTimer) {
653
1008
  clearTimeout(this.logRestartTimer);
654
1009
  this.logRestartTimer = null;
655
1010
  }
1011
+ if (this.logTailHandle) {
1012
+ try {
1013
+ this.logTailHandle.stop();
1014
+ } catch {}
1015
+ this.logTailHandle = null;
1016
+ }
656
1017
  if (this.logProc) {
657
1018
  try {
658
1019
  this.logProc.kill("SIGKILL");