@slock-ai/daemon 0.29.0 → 0.29.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +280 -10
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -6,30 +6,44 @@ import os2 from "os";
6
6
  import { createRequire } from "module";
7
7
  import { execSync as execSync2 } from "child_process";
8
8
  import { accessSync } from "fs";
9
+ import { readFile as readFile2, readdir as readdir2, stat as stat2, mkdir as mkdir2, appendFile } from "fs/promises";
9
10
  import { fileURLToPath } from "url";
10
11
 
11
12
  // src/connection.ts
12
13
  import WebSocket from "ws";
14
+ var systemClock = {
15
+ now: () => Date.now(),
16
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
17
+ clearTimeout: (timer) => clearTimeout(timer)
18
+ };
19
+ var INBOUND_WATCHDOG_MS = 7e4;
13
20
  var DaemonConnection = class {
14
21
  ws = null;
15
22
  options;
23
+ clock;
16
24
  reconnectTimer = null;
17
- reconnectDelay = 1e3;
25
+ watchdogTimer = null;
26
+ reconnectDelay;
18
27
  maxReconnectDelay = 3e4;
19
28
  shouldConnect = true;
20
29
  reconnectAttempt = 0;
21
30
  lastDroppedSendLogAt = 0;
22
31
  constructor(options) {
23
32
  this.options = options;
33
+ this.clock = options.clock ?? systemClock;
34
+ this.reconnectDelay = options.minReconnectDelayMs ?? 1e3;
24
35
  }
25
36
  connect() {
26
37
  this.shouldConnect = true;
38
+ if (this.reconnectTimer) return;
39
+ if (this.ws && this.ws.readyState !== WebSocket.CLOSED) return;
27
40
  this.doConnect();
28
41
  }
29
42
  disconnect() {
30
43
  this.shouldConnect = false;
44
+ this.clearWatchdog();
31
45
  if (this.reconnectTimer) {
32
- clearTimeout(this.reconnectTimer);
46
+ this.clock.clearTimeout(this.reconnectTimer);
33
47
  this.reconnectTimer = null;
34
48
  }
35
49
  if (this.ws) {
@@ -43,7 +57,7 @@ var DaemonConnection = class {
43
57
  this.ws.send(JSON.stringify(msg));
44
58
  return;
45
59
  }
46
- const now = Date.now();
60
+ const now = this.clock.now();
47
61
  if (now - this.lastDroppedSendLogAt > 5e3) {
48
62
  this.lastDroppedSendLogAt = now;
49
63
  console.warn(`[Daemon] Dropping outbound message while disconnected: ${msg.type}`);
@@ -54,16 +68,23 @@ var DaemonConnection = class {
54
68
  }
55
69
  doConnect() {
56
70
  if (!this.shouldConnect) return;
71
+ if (this.ws && this.ws.readyState !== WebSocket.CLOSED) return;
57
72
  const wsUrl = this.options.serverUrl.replace(/^http/, "ws") + `/daemon/connect?key=${this.options.apiKey}`;
58
73
  console.log(`[Daemon] Connecting to ${this.options.serverUrl}...`);
59
- this.ws = new WebSocket(wsUrl);
60
- this.ws.on("open", () => {
74
+ const ws = this.options.wsFactory ? this.options.wsFactory(wsUrl) : new WebSocket(wsUrl);
75
+ this.ws = ws;
76
+ ws.on("open", () => {
77
+ if (this.ws !== ws) return;
78
+ if (!this.shouldConnect) return;
61
79
  console.log("[Daemon] Connected to server");
62
80
  this.reconnectAttempt = 0;
63
- this.reconnectDelay = 1e3;
81
+ this.reconnectDelay = this.options.minReconnectDelayMs ?? 1e3;
82
+ this.resetWatchdog();
64
83
  this.options.onConnect();
65
84
  });
66
- this.ws.on("message", (data) => {
85
+ ws.on("message", (data) => {
86
+ if (this.ws !== ws) return;
87
+ this.resetWatchdog();
67
88
  try {
68
89
  const msg = JSON.parse(data.toString());
69
90
  this.options.onMessage(msg);
@@ -71,7 +92,10 @@ var DaemonConnection = class {
71
92
  console.error("[Daemon] Invalid message from server:", err);
72
93
  }
73
94
  });
74
- this.ws.on("close", (code, reasonBuffer) => {
95
+ ws.on("close", (code, reasonBuffer) => {
96
+ if (this.ws !== ws) return;
97
+ this.ws = null;
98
+ this.clearWatchdog();
75
99
  const reason = reasonBuffer.toString("utf8");
76
100
  console.log(
77
101
  `[Daemon] Disconnected from server (code=${code}, reason=${JSON.stringify(reason)}, reconnecting=${this.shouldConnect})`
@@ -79,7 +103,8 @@ var DaemonConnection = class {
79
103
  this.options.onDisconnect();
80
104
  this.scheduleReconnect();
81
105
  });
82
- this.ws.on("error", (err) => {
106
+ ws.on("error", (err) => {
107
+ if (this.ws !== ws) return;
83
108
  console.error("[Daemon] WebSocket error:", err.message);
84
109
  });
85
110
  }
@@ -88,12 +113,29 @@ var DaemonConnection = class {
88
113
  if (this.reconnectTimer) return;
89
114
  this.reconnectAttempt += 1;
90
115
  console.log(`[Daemon] Reconnecting to server in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempt})`);
91
- this.reconnectTimer = setTimeout(() => {
116
+ this.reconnectTimer = this.clock.setTimeout(() => {
92
117
  this.reconnectTimer = null;
93
118
  this.doConnect();
94
119
  }, this.reconnectDelay);
95
120
  this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
96
121
  }
122
+ resetWatchdog() {
123
+ this.clearWatchdog();
124
+ const ms = this.options.inboundWatchdogMs ?? INBOUND_WATCHDOG_MS;
125
+ this.watchdogTimer = this.clock.setTimeout(() => {
126
+ console.warn(`[Daemon] No inbound traffic for ${ms / 1e3}s \u2014 forcing reconnect`);
127
+ try {
128
+ this.ws?.terminate();
129
+ } catch {
130
+ }
131
+ }, ms);
132
+ }
133
+ clearWatchdog() {
134
+ if (this.watchdogTimer) {
135
+ this.clock.clearTimeout(this.watchdogTimer);
136
+ this.watchdogTimer = null;
137
+ }
138
+ }
97
139
  };
98
140
 
99
141
  // src/agentProcessManager.ts
@@ -1288,6 +1330,19 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1288
1330
  getRunningAgentIds() {
1289
1331
  return [...this.agents.keys()];
1290
1332
  }
1333
+ getDataDir() {
1334
+ return this.dataDir;
1335
+ }
1336
+ getAgentSessionId(agentId) {
1337
+ return this.agents.get(agentId)?.sessionId ?? null;
1338
+ }
1339
+ getIdleAgentSessionIds() {
1340
+ const result = [];
1341
+ for (const [agentId, { sessionId }] of this.idleAgentConfigs) {
1342
+ if (sessionId) result.push({ agentId, sessionId });
1343
+ }
1344
+ return result;
1345
+ }
1291
1346
  // Machine-level workspace scanning
1292
1347
  async scanAllWorkspaces() {
1293
1348
  const results = [];
@@ -1532,6 +1587,11 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1532
1587
  case "session_init":
1533
1588
  if (ap) ap.sessionId = event.sessionId;
1534
1589
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId });
1590
+ writeFile(
1591
+ path3.join(this.dataDir, agentId, "session-meta.json"),
1592
+ JSON.stringify({ sessionId: event.sessionId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() })
1593
+ ).catch(() => {
1594
+ });
1535
1595
  break;
1536
1596
  case "thinking": {
1537
1597
  const text = event.text.length > MAX_TRAJECTORY_TEXT ? event.text.slice(0, MAX_TRAJECTORY_TEXT) + "\u2026" : event.text;
@@ -1568,6 +1628,11 @@ Note: While you are busy, you may receive [System notification: ...] messages ab
1568
1628
  }
1569
1629
  if (event.sessionId) {
1570
1630
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId });
1631
+ writeFile(
1632
+ path3.join(this.dataDir, agentId, "session-meta.json"),
1633
+ JSON.stringify({ sessionId: event.sessionId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() })
1634
+ ).catch(() => {
1635
+ });
1571
1636
  }
1572
1637
  break;
1573
1638
  case "error": {
@@ -1702,6 +1767,47 @@ var RUNTIMES = [
1702
1767
  // src/index.ts
1703
1768
  var require2 = createRequire(import.meta.url);
1704
1769
  var DAEMON_VERSION = require2("../package.json").version;
1770
+ var LOG_DIR = path4.join(os2.homedir(), ".slock", "logs");
1771
+ var LOG_FILE = path4.join(LOG_DIR, "daemon.log");
1772
+ var MAX_LOG_BYTES = 5 * 1024 * 1024;
1773
+ async function initLogFile() {
1774
+ try {
1775
+ await mkdir2(LOG_DIR, { recursive: true });
1776
+ try {
1777
+ const s = await stat2(LOG_FILE);
1778
+ if (s.size > MAX_LOG_BYTES) {
1779
+ const content = await readFile2(LOG_FILE, "utf-8");
1780
+ const trimmed = content.slice(Math.floor(content.length / 2));
1781
+ await appendFile(LOG_FILE, "");
1782
+ const { writeFile: writeFile2 } = await import("fs/promises");
1783
+ await writeFile2(LOG_FILE, trimmed);
1784
+ }
1785
+ } catch {
1786
+ }
1787
+ } catch {
1788
+ }
1789
+ }
1790
+ function logLine(level, ...args2) {
1791
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${args2.map(String).join(" ")}
1792
+ `;
1793
+ appendFile(LOG_FILE, line).catch(() => {
1794
+ });
1795
+ }
1796
+ var _origLog = console.log.bind(console);
1797
+ var _origErr = console.error.bind(console);
1798
+ var _origWarn = console.warn.bind(console);
1799
+ console.log = (...args2) => {
1800
+ _origLog(...args2);
1801
+ logLine("INFO", ...args2);
1802
+ };
1803
+ console.error = (...args2) => {
1804
+ _origErr(...args2);
1805
+ logLine("ERROR", ...args2);
1806
+ };
1807
+ console.warn = (...args2) => {
1808
+ _origWarn(...args2);
1809
+ logLine("WARN", ...args2);
1810
+ };
1705
1811
  function formatChannelTarget(msg) {
1706
1812
  return msg.message.channel_type === "dm" ? `dm:@${msg.message.channel_name}` : `#${msg.message.channel_name}`;
1707
1813
  }
@@ -1723,6 +1829,8 @@ function summarizeIncomingMessage(msg) {
1723
1829
  return `(agent=${msg.agentId}, runtime=${msg.runtime || "auto"})`;
1724
1830
  case "machine:workspace:delete":
1725
1831
  return `(directory=${msg.directoryName})`;
1832
+ case "machine:feedback:collect":
1833
+ return `(reportId=${msg.reportId}, agents=${msg.agents.length})`;
1726
1834
  default:
1727
1835
  return "";
1728
1836
  }
@@ -1758,6 +1866,126 @@ try {
1758
1866
  chatBridgePath = path4.resolve(__dirname, "chat-bridge.ts");
1759
1867
  }
1760
1868
  var connection;
1869
+ async function collectAndUploadAgent(opts) {
1870
+ const { agentId, reportAgentId, uploadUrl, authToken, timeRangeHours, includeSessionFiles, includeDaemonLogs, includeWorkspaceSnapshot, dataDir } = opts;
1871
+ const sinceMs = Date.now() - timeRangeHours * 60 * 60 * 1e3;
1872
+ let bytesUploaded = 0;
1873
+ let anyUploadFailed = false;
1874
+ try {
1875
+ const agentDir = path4.join(dataDir, agentId);
1876
+ let sessionId = opts.sessionId;
1877
+ try {
1878
+ const meta = JSON.parse(await readFile2(path4.join(agentDir, "session-meta.json"), "utf-8"));
1879
+ if (meta.sessionId) sessionId = meta.sessionId;
1880
+ } catch {
1881
+ }
1882
+ const filesToUpload = [];
1883
+ if (includeSessionFiles && sessionId) {
1884
+ const claudeProjectsDir = path4.join(os2.homedir(), ".claude", "projects");
1885
+ try {
1886
+ const projectDirs = await readdir2(claudeProjectsDir, { withFileTypes: true });
1887
+ for (const pd of projectDirs) {
1888
+ if (!pd.isDirectory()) continue;
1889
+ const pdPath = path4.join(claudeProjectsDir, pd.name);
1890
+ const files = await readdir2(pdPath, { withFileTypes: true });
1891
+ for (const f of files) {
1892
+ if (!f.isFile() || !f.name.endsWith(".jsonl")) continue;
1893
+ if (!f.name.startsWith(sessionId)) continue;
1894
+ const filePath = path4.join(pdPath, f.name);
1895
+ const raw = await readFile2(filePath, "utf-8");
1896
+ const filteredLines = raw.split("\n").filter((line) => {
1897
+ if (!line.trim()) return false;
1898
+ try {
1899
+ const obj = JSON.parse(line);
1900
+ if (!obj.timestamp) return true;
1901
+ return new Date(obj.timestamp).getTime() >= sinceMs;
1902
+ } catch {
1903
+ return false;
1904
+ }
1905
+ }).join("\n");
1906
+ if (!filteredLines.trim()) continue;
1907
+ filesToUpload.push({ filename: f.name, data: Buffer.from(filteredLines), kind: "session_jsonl" });
1908
+ }
1909
+ }
1910
+ } catch {
1911
+ }
1912
+ }
1913
+ if (includeDaemonLogs) {
1914
+ try {
1915
+ const raw = await readFile2(LOG_FILE, "utf-8");
1916
+ const filtered = raw.split("\n").filter((line) => {
1917
+ if (!line.trim()) return false;
1918
+ const ts = line.slice(0, 24);
1919
+ return new Date(ts).getTime() >= sinceMs;
1920
+ }).join("\n");
1921
+ if (filtered.trim()) {
1922
+ filesToUpload.push({ filename: "daemon.log", data: Buffer.from(filtered), kind: "daemon_log" });
1923
+ }
1924
+ } catch {
1925
+ }
1926
+ }
1927
+ if (includeWorkspaceSnapshot) {
1928
+ try {
1929
+ const agentStat = await stat2(agentDir);
1930
+ if (agentStat.isDirectory()) {
1931
+ const files = await readdir2(agentDir, { withFileTypes: true });
1932
+ for (const f of files) {
1933
+ if (!f.isFile()) continue;
1934
+ if (f.name === "session-meta.json") continue;
1935
+ const filePath = path4.join(agentDir, f.name);
1936
+ const s = await stat2(filePath);
1937
+ if (s.size > 1024 * 1024) continue;
1938
+ const data = await readFile2(filePath);
1939
+ filesToUpload.push({ filename: f.name, data, kind: "runtime_log" });
1940
+ }
1941
+ }
1942
+ } catch {
1943
+ }
1944
+ }
1945
+ const manifest = {
1946
+ reportAgentId,
1947
+ agentId,
1948
+ sessionId,
1949
+ runtime: opts.runtime,
1950
+ daemonVersion: DAEMON_VERSION,
1951
+ hostname: os2.hostname(),
1952
+ os: `${os2.platform()} ${os2.arch()}`,
1953
+ collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
1954
+ timeRangeHours,
1955
+ includeSessionFiles,
1956
+ includeDaemonLogs,
1957
+ includeWorkspaceSnapshot,
1958
+ artifactCount: filesToUpload.length
1959
+ };
1960
+ filesToUpload.unshift({ filename: "manifest.json", data: Buffer.from(JSON.stringify(manifest, null, 2)), kind: "manifest" });
1961
+ for (const file of filesToUpload) {
1962
+ const formData = new FormData();
1963
+ formData.append("reportAgentId", reportAgentId);
1964
+ formData.append("kind", file.kind);
1965
+ formData.append("filename", file.filename);
1966
+ formData.append("file", new Blob([new Uint8Array(file.data)]), file.filename);
1967
+ const resp = await fetch(uploadUrl, {
1968
+ method: "POST",
1969
+ headers: { Authorization: `Bearer ${authToken}` },
1970
+ body: formData
1971
+ });
1972
+ if (resp.ok) {
1973
+ bytesUploaded += file.data.length;
1974
+ } else {
1975
+ console.warn(`[Feedback] Upload failed for ${file.filename}: ${resp.status}`);
1976
+ anyUploadFailed = true;
1977
+ }
1978
+ }
1979
+ if (anyUploadFailed) {
1980
+ return { reportAgentId, status: "error", daemonVersion: DAEMON_VERSION, error: "One or more files failed to upload", bytesUploaded };
1981
+ }
1982
+ return { reportAgentId, status: "collected", daemonVersion: DAEMON_VERSION, bytesUploaded };
1983
+ } catch (err) {
1984
+ const error = err instanceof Error ? err.message : String(err);
1985
+ console.error(`[Feedback] Collection failed for agent ${agentId}:`, error);
1986
+ return { reportAgentId, status: "error", daemonVersion: DAEMON_VERSION, error };
1987
+ }
1988
+ }
1761
1989
  var agentManager = new AgentProcessManager(chatBridgePath, (msg) => {
1762
1990
  connection.send(msg);
1763
1991
  }, apiKey);
@@ -1837,6 +2065,37 @@ connection = new DaemonConnection({
1837
2065
  case "ping":
1838
2066
  connection.send({ type: "pong" });
1839
2067
  break;
2068
+ case "machine:feedback:collect": {
2069
+ const { reportId, agents, timeRangeHours, includeSessionFiles, includeDaemonLogs, includeWorkspaceSnapshot, uploadUrl, authToken } = msg;
2070
+ console.log(`[Daemon] Collecting feedback for report ${reportId} (${agents.length} agents)`);
2071
+ const dataDir = agentManager.getDataDir();
2072
+ Promise.all(agents.map(
2073
+ (agent) => collectAndUploadAgent({
2074
+ agentId: agent.agentId,
2075
+ reportAgentId: agent.reportAgentId,
2076
+ runtime: agent.runtime,
2077
+ sessionId: agent.sessionId,
2078
+ uploadUrl,
2079
+ authToken,
2080
+ timeRangeHours,
2081
+ includeSessionFiles,
2082
+ includeDaemonLogs,
2083
+ includeWorkspaceSnapshot: includeWorkspaceSnapshot ?? false,
2084
+ dataDir
2085
+ })
2086
+ )).then((agentResults) => {
2087
+ connection.send({ type: "machine:feedback:result", reportId, agentResults });
2088
+ }).catch((err) => {
2089
+ console.error(`[Daemon] Feedback collection failed for report ${reportId}:`, err);
2090
+ const agentResults = agents.map((a) => ({
2091
+ reportAgentId: a.reportAgentId,
2092
+ status: "error",
2093
+ error: err instanceof Error ? err.message : String(err)
2094
+ }));
2095
+ connection.send({ type: "machine:feedback:result", reportId, agentResults });
2096
+ });
2097
+ break;
2098
+ }
1840
2099
  }
1841
2100
  },
1842
2101
  onConnect: () => {
@@ -1851,11 +2110,22 @@ connection = new DaemonConnection({
1851
2110
  os: `${os2.platform()} ${os2.arch()}`,
1852
2111
  daemonVersion: DAEMON_VERSION
1853
2112
  });
2113
+ for (const agentId of agentManager.getRunningAgentIds()) {
2114
+ const sessionId = agentManager.getAgentSessionId(agentId);
2115
+ if (sessionId) {
2116
+ connection.send({ type: "agent:session", agentId, sessionId });
2117
+ }
2118
+ }
2119
+ for (const { agentId, sessionId } of agentManager.getIdleAgentSessionIds()) {
2120
+ connection.send({ type: "agent:session", agentId, sessionId });
2121
+ }
1854
2122
  },
1855
2123
  onDisconnect: () => {
1856
2124
  console.log("[Daemon] Lost connection \u2014 agents continue running locally");
1857
2125
  }
1858
2126
  });
2127
+ initLogFile().catch(() => {
2128
+ });
1859
2129
  console.log("[Slock Daemon] Starting...");
1860
2130
  connection.connect();
1861
2131
  var shutdown = async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/daemon",
3
- "version": "0.29.0",
3
+ "version": "0.29.1-alpha.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "slock-daemon": "dist/index.js"
@@ -32,7 +32,7 @@
32
32
  "dev": "tsx watch src/index.ts",
33
33
  "start": "tsx src/index.ts",
34
34
  "build": "tsup",
35
- "test": "node --import tsx --test src/**/*.test.ts",
35
+ "test": "node --import tsx --test --test-force-exit 'src/**/*.test.ts'",
36
36
  "typecheck": "tsc --noEmit",
37
37
  "release:patch": "npm version patch --no-git-tag-version && cd ../.. && pnpm install --lockfile-only && git add packages/daemon/package.json pnpm-lock.yaml && git commit -m \"chore: bump @slock-ai/daemon to v$(node -p \"require('./packages/daemon/package.json').version\")\" && git tag daemon-v$(node -p \"require('./packages/daemon/package.json').version\") && git push && git push --tags",
38
38
  "release:minor": "npm version minor --no-git-tag-version && cd ../.. && pnpm install --lockfile-only && git add packages/daemon/package.json pnpm-lock.yaml && git commit -m \"chore: bump @slock-ai/daemon to v$(node -p \"require('./packages/daemon/package.json').version\")\" && git tag daemon-v$(node -p \"require('./packages/daemon/package.json').version\") && git push && git push --tags",