@feynmanzhang/open-party 0.1.4 → 0.1.5
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.
- package/dist/claude-code/{open-party-0.1.0 → open-party-0.1.5}/.claude-plugin/plugin.json +1 -1
- package/dist/claude-code/open-party-0.1.5/BUILD_INFO.json +6 -0
- package/dist/claude-code/{open-party-0.1.1 → open-party-0.1.5}/dist/hook-handler.js +113 -46
- package/dist/claude-code/{open-party-0.1.1 → open-party-0.1.5}/dist/mcp-server.js +156 -25
- package/dist/claude-code/{open-party-0.1.1 → open-party-0.1.5}/dist/party-server.js +549 -64
- package/dist/claude-code/{open-party-0.1.1 → open-party-0.1.5}/hooks/hooks.json +1 -1
- package/dist/claude-code/{open-party-0.1.1 → open-party-0.1.5}/package.json +1 -1
- package/dist/cli/index.js +700 -134
- package/dist/cli/index.js.map +1 -1
- package/dist/openclaw/open-party-0.1.5/BUILD_INFO.json +6 -0
- package/dist/openclaw/open-party-0.1.5/SKILL.md +127 -0
- package/dist/openclaw/open-party-0.1.5/dist/index.js +550 -0
- package/dist/openclaw/open-party-0.1.5/openclaw.plugin.json +28 -0
- package/dist/openclaw/open-party-0.1.5/package.json +12 -0
- package/dist/openclaw/open-party-0.1.5/skills/open-party/SKILL.md +90 -0
- package/dist/party-server.js +549 -64
- package/dist/party-server.js.map +1 -1
- package/package.json +35 -4
- package/dist/claude-code/open-party-0.1.1/.claude-plugin/plugin.json +0 -5
- package/dist/claude-code/open-party-0.1.1/.mcp.json +0 -9
- package/dist/claude-code/open-party-0.1.1/BUILD_INFO.json +0 -6
- package/dist/hook-handler.js +0 -555
- package/dist/hook-handler.js.map +0 -1
- package/dist/mcp-server.js +0 -21408
- package/dist/mcp-server.js.map +0 -1
- /package/dist/claude-code/{open-party-0.1.0 → open-party-0.1.5}/.mcp.json +0 -0
- /package/dist/claude-code/{open-party-0.1.1 → open-party-0.1.5}/skills/open-party/SKILL.md +0 -0
|
@@ -16,18 +16,352 @@ var init_config = __esm({
|
|
|
16
16
|
"src/server/config.ts"() {
|
|
17
17
|
"use strict";
|
|
18
18
|
PARTY_PORT = parseInt(process.env.PARTY_PORT || "8000", 10);
|
|
19
|
-
HEARTBEAT_TIMEOUT = parseFloat(process.env.HEARTBEAT_TIMEOUT || "
|
|
20
|
-
CLEANUP_INTERVAL = parseFloat(process.env.CLEANUP_INTERVAL || "
|
|
19
|
+
HEARTBEAT_TIMEOUT = parseFloat(process.env.HEARTBEAT_TIMEOUT || "60");
|
|
20
|
+
CLEANUP_INTERVAL = parseFloat(process.env.CLEANUP_INTERVAL || "60");
|
|
21
21
|
DISCOVERY_INTERVAL = parseFloat(process.env.DISCOVERY_INTERVAL || "20");
|
|
22
22
|
REMOTE_STALE_FACTOR = parseInt(process.env.REMOTE_STALE_FACTOR || "3", 10);
|
|
23
23
|
PROBE_TIMEOUT = parseFloat(process.env.PROBE_TIMEOUT || "5");
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
// src/server/logger.ts
|
|
28
|
+
import { existsSync, mkdirSync, appendFileSync, readdirSync, unlinkSync, statSync } from "fs";
|
|
29
|
+
import { join } from "path";
|
|
30
|
+
import { homedir } from "os";
|
|
31
|
+
function getEffectiveLevel() {
|
|
32
|
+
const env = (process.env.LOG_LEVEL || "info").toLowerCase().trim();
|
|
33
|
+
if (env in LEVEL_ORDER) return env;
|
|
34
|
+
return "info";
|
|
35
|
+
}
|
|
36
|
+
function initLogFile() {
|
|
37
|
+
if (!existsSync(LOG_DIR)) {
|
|
38
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
const cutoff = now - LOG_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
|
|
44
|
+
const files = readdirSync(LOG_DIR);
|
|
45
|
+
for (const f of files) {
|
|
46
|
+
if (!f.endsWith("-open-party.log")) continue;
|
|
47
|
+
try {
|
|
48
|
+
const stat = statSync(join(LOG_DIR, f));
|
|
49
|
+
if (stat.mtimeMs < cutoff) {
|
|
50
|
+
unlinkSync(join(LOG_DIR, f));
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function getLogFilePath() {
|
|
59
|
+
const d = /* @__PURE__ */ new Date();
|
|
60
|
+
const yy = String(d.getFullYear()).slice(2);
|
|
61
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
62
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
63
|
+
return join(LOG_DIR, `${yy}-${mm}-${dd}-open-party.log`);
|
|
64
|
+
}
|
|
65
|
+
function shouldLog(level) {
|
|
66
|
+
return LEVEL_ORDER[level] >= LEVEL_ORDER[effectiveLevel];
|
|
67
|
+
}
|
|
68
|
+
function extractError(err) {
|
|
69
|
+
if (err instanceof Error) {
|
|
70
|
+
const stack = err.stack ? `
|
|
71
|
+
${err.stack}` : "";
|
|
72
|
+
return `${err.message}${stack}`;
|
|
73
|
+
}
|
|
74
|
+
return String(err);
|
|
75
|
+
}
|
|
76
|
+
function format(level, tag, message) {
|
|
77
|
+
const now = /* @__PURE__ */ new Date();
|
|
78
|
+
const levelStr = level.toUpperCase().padEnd(5);
|
|
79
|
+
const yy = String(now.getFullYear()).slice(2);
|
|
80
|
+
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
81
|
+
const dd = String(now.getDate()).padStart(2, "0");
|
|
82
|
+
const hh = String(now.getHours()).padStart(2, "0");
|
|
83
|
+
const min = String(now.getMinutes()).padStart(2, "0");
|
|
84
|
+
const ss = String(now.getSeconds()).padStart(2, "0");
|
|
85
|
+
const ts = `${yy}-${mm}-${dd} ${hh}:${min}:${ss}`;
|
|
86
|
+
return `${ts} [${levelStr}] [${tag}] ${message}`;
|
|
87
|
+
}
|
|
88
|
+
function output(consoleFn, level, tag, message) {
|
|
89
|
+
if (!shouldLog(level)) return;
|
|
90
|
+
const line = format(level, tag, message);
|
|
91
|
+
consoleFn(line);
|
|
92
|
+
try {
|
|
93
|
+
appendFileSync(getLogFilePath(), line + "\n", "utf-8");
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
var LEVEL_ORDER, effectiveLevel, LOG_DIR, LOG_RETENTION_DAYS, logger;
|
|
98
|
+
var init_logger = __esm({
|
|
99
|
+
"src/server/logger.ts"() {
|
|
100
|
+
"use strict";
|
|
101
|
+
LEVEL_ORDER = {
|
|
102
|
+
debug: 0,
|
|
103
|
+
info: 1,
|
|
104
|
+
warn: 2,
|
|
105
|
+
error: 3
|
|
106
|
+
};
|
|
107
|
+
effectiveLevel = getEffectiveLevel();
|
|
108
|
+
LOG_DIR = join(homedir(), ".open-party", "logs");
|
|
109
|
+
LOG_RETENTION_DAYS = 7;
|
|
110
|
+
initLogFile();
|
|
111
|
+
logger = {
|
|
112
|
+
info(tag, message, data) {
|
|
113
|
+
output(console.log, "info", tag, data ? `${message} ${JSON.stringify(data)}` : message);
|
|
114
|
+
},
|
|
115
|
+
warn(tag, message, data) {
|
|
116
|
+
output(console.warn, "warn", tag, data ? `${message} ${JSON.stringify(data)}` : message);
|
|
117
|
+
},
|
|
118
|
+
error(tag, message, err) {
|
|
119
|
+
const detail = err ? `: ${extractError(err)}` : "";
|
|
120
|
+
output(console.error, "error", tag, message + detail);
|
|
121
|
+
},
|
|
122
|
+
debug(tag, message, data) {
|
|
123
|
+
output(console.debug, "debug", tag, data ? `${message} ${JSON.stringify(data)}` : message);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// src/server/persistence.ts
|
|
130
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync, unlinkSync as unlinkSync2, renameSync, mkdirSync as mkdirSync2 } from "fs";
|
|
131
|
+
import { join as join2 } from "path";
|
|
132
|
+
import { homedir as homedir2 } from "os";
|
|
133
|
+
function dataDirPath() {
|
|
134
|
+
const pluginData = process.env.CLAUDE_PLUGIN_DATA || "";
|
|
135
|
+
if (pluginData) return join2(pluginData, "data");
|
|
136
|
+
return join2(homedir2(), ".open-party", "data");
|
|
137
|
+
}
|
|
138
|
+
function runMigrations(raw2) {
|
|
139
|
+
const data = raw2;
|
|
140
|
+
if (!data || typeof data !== "object") {
|
|
141
|
+
throw new Error("Snapshot is not a valid object");
|
|
142
|
+
}
|
|
143
|
+
const version = typeof data.version === "number" ? data.version : 0;
|
|
144
|
+
let snapshot = {
|
|
145
|
+
version,
|
|
146
|
+
saved_at: typeof data.saved_at === "number" ? data.saved_at : 0,
|
|
147
|
+
agents: Array.isArray(data.agents) ? data.agents : [],
|
|
148
|
+
history: typeof data.history === "object" && data.history !== null && !Array.isArray(data.history) ? data.history : {}
|
|
149
|
+
};
|
|
150
|
+
for (const m of MIGRATORS) {
|
|
151
|
+
if (m.version > snapshot.version) {
|
|
152
|
+
snapshot = m.migrate(snapshot);
|
|
153
|
+
snapshot.version = m.version;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return snapshot;
|
|
157
|
+
}
|
|
158
|
+
function abortableSleep(ms, signal) {
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
const timer = setTimeout(resolve, ms);
|
|
161
|
+
signal?.addEventListener(
|
|
162
|
+
"abort",
|
|
163
|
+
() => {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
166
|
+
},
|
|
167
|
+
{ once: true }
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
var CURRENT_SCHEMA_VERSION, SNAPSHOT_FILE, SHUTDOWN_MARKER_FILE, DEFAULT_SNAPSHOT_INTERVAL_MS, DEBOUNCE_MS, MIGRATORS, SnapshotManager;
|
|
172
|
+
var init_persistence = __esm({
|
|
173
|
+
"src/server/persistence.ts"() {
|
|
174
|
+
"use strict";
|
|
175
|
+
init_logger();
|
|
176
|
+
CURRENT_SCHEMA_VERSION = 1;
|
|
177
|
+
SNAPSHOT_FILE = "snapshot.json";
|
|
178
|
+
SHUTDOWN_MARKER_FILE = "shutdown-marker.json";
|
|
179
|
+
DEFAULT_SNAPSHOT_INTERVAL_MS = 6e4;
|
|
180
|
+
DEBOUNCE_MS = 5e3;
|
|
181
|
+
MIGRATORS = [
|
|
182
|
+
// Future: { version: 2, migrate(snapshot) { ... } },
|
|
183
|
+
];
|
|
184
|
+
SnapshotManager = class {
|
|
185
|
+
_dir;
|
|
186
|
+
_snapshotPath;
|
|
187
|
+
_markerPath;
|
|
188
|
+
_debounceTimer = null;
|
|
189
|
+
constructor(dataDir) {
|
|
190
|
+
this._dir = dataDir ?? dataDirPath();
|
|
191
|
+
this._snapshotPath = join2(this._dir, SNAPSHOT_FILE);
|
|
192
|
+
this._markerPath = join2(this._dir, SHUTDOWN_MARKER_FILE);
|
|
193
|
+
mkdirSync2(this._dir, { recursive: true });
|
|
194
|
+
const tmpPath = this._snapshotPath + ".tmp";
|
|
195
|
+
try {
|
|
196
|
+
if (existsSync2(tmpPath)) {
|
|
197
|
+
unlinkSync2(tmpPath);
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
logger.warn("Persistence", "Failed to clean up stale tmp file", error);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// ------------------------------------------------------------------
|
|
204
|
+
// Write / Load
|
|
205
|
+
// ------------------------------------------------------------------
|
|
206
|
+
/** Atomically write a snapshot of registry agents and message queue history. */
|
|
207
|
+
writeSnapshot(agents, history) {
|
|
208
|
+
const snapshot = {
|
|
209
|
+
version: CURRENT_SCHEMA_VERSION,
|
|
210
|
+
saved_at: Date.now(),
|
|
211
|
+
agents,
|
|
212
|
+
history
|
|
213
|
+
};
|
|
214
|
+
const serialized = JSON.stringify(snapshot, null, 2);
|
|
215
|
+
const tmpPath = this._snapshotPath + ".tmp";
|
|
216
|
+
try {
|
|
217
|
+
writeFileSync(tmpPath, serialized, "utf-8");
|
|
218
|
+
renameSync(tmpPath, this._snapshotPath);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
logger.error("Persistence", "Failed to write snapshot", error);
|
|
221
|
+
try {
|
|
222
|
+
unlinkSync2(tmpPath);
|
|
223
|
+
} catch {
|
|
224
|
+
}
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** Load and validate snapshot. Returns null if file missing or corrupt. */
|
|
229
|
+
loadSnapshot() {
|
|
230
|
+
if (!existsSync2(this._snapshotPath)) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const raw2 = JSON.parse(readFileSync(this._snapshotPath, "utf-8"));
|
|
235
|
+
return runMigrations(raw2);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
logger.warn("Persistence", "Failed to load snapshot (starting fresh)", error);
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// ------------------------------------------------------------------
|
|
242
|
+
// Hydration (restore in-memory state from snapshot)
|
|
243
|
+
// ------------------------------------------------------------------
|
|
244
|
+
/** Restore agents into registry. Overwrites host_ip with current selfIp. */
|
|
245
|
+
hydrateAgents(registry2, selfIp) {
|
|
246
|
+
const snapshot = this.loadSnapshot();
|
|
247
|
+
if (!snapshot || snapshot.agents.length === 0) return 0;
|
|
248
|
+
const now = Date.now() / 1e3;
|
|
249
|
+
let count = 0;
|
|
250
|
+
for (const agent of snapshot.agents) {
|
|
251
|
+
agent.last_heartbeat = now;
|
|
252
|
+
const info = registry2.register({
|
|
253
|
+
agent_id: agent.agent_id,
|
|
254
|
+
display_name: agent.display_name,
|
|
255
|
+
metadata: agent.metadata ?? {}
|
|
256
|
+
});
|
|
257
|
+
info.host_ip = selfIp;
|
|
258
|
+
count++;
|
|
259
|
+
}
|
|
260
|
+
return count;
|
|
261
|
+
}
|
|
262
|
+
/** Restore message history into queue. */
|
|
263
|
+
hydrateHistory(queue) {
|
|
264
|
+
const snapshot = this.loadSnapshot();
|
|
265
|
+
if (!snapshot || Object.keys(snapshot.history).length === 0) return 0;
|
|
266
|
+
let totalEntries = 0;
|
|
267
|
+
for (const [agentId, entries] of Object.entries(snapshot.history)) {
|
|
268
|
+
for (const entry of entries) {
|
|
269
|
+
queue.logToHistory(agentId, entry.direction, {
|
|
270
|
+
sender_id: entry.sender_id,
|
|
271
|
+
recipient_id: entry.recipient_id,
|
|
272
|
+
summary: entry.summary,
|
|
273
|
+
content: entry.content,
|
|
274
|
+
timestamp: entry.timestamp
|
|
275
|
+
});
|
|
276
|
+
totalEntries++;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return totalEntries;
|
|
280
|
+
}
|
|
281
|
+
// ------------------------------------------------------------------
|
|
282
|
+
// Shutdown marker
|
|
283
|
+
// ------------------------------------------------------------------
|
|
284
|
+
/** Write shutdown marker to detect interrupted shutdown on next start. */
|
|
285
|
+
writeShutdownMarker() {
|
|
286
|
+
try {
|
|
287
|
+
writeFileSync(
|
|
288
|
+
this._markerPath,
|
|
289
|
+
JSON.stringify({ started_at: Date.now() }),
|
|
290
|
+
"utf-8"
|
|
291
|
+
);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
logger.warn("Persistence", "Failed to write shutdown marker", error);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/** Remove shutdown marker — called after successful shutdown. */
|
|
297
|
+
removeShutdownMarker() {
|
|
298
|
+
try {
|
|
299
|
+
if (existsSync2(this._markerPath)) {
|
|
300
|
+
unlinkSync2(this._markerPath);
|
|
301
|
+
}
|
|
302
|
+
} catch (error) {
|
|
303
|
+
logger.warn("Persistence", "Failed to remove shutdown marker", error);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/** Check if a shutdown marker exists (indicates previous shutdown was interrupted). */
|
|
307
|
+
hasShutdownMarker() {
|
|
308
|
+
return existsSync2(this._markerPath);
|
|
309
|
+
}
|
|
310
|
+
// ------------------------------------------------------------------
|
|
311
|
+
// Snapshot loop & debounce
|
|
312
|
+
// ------------------------------------------------------------------
|
|
313
|
+
/**
|
|
314
|
+
* Start periodic snapshot background loop.
|
|
315
|
+
* Writes snapshot every `intervalMs` milliseconds until signal is aborted.
|
|
316
|
+
*/
|
|
317
|
+
async startSnapshotLoop(signal, getAgents, getHistory, intervalMs = DEFAULT_SNAPSHOT_INTERVAL_MS) {
|
|
318
|
+
while (!signal.aborted) {
|
|
319
|
+
try {
|
|
320
|
+
await abortableSleep(intervalMs, signal);
|
|
321
|
+
} catch (e) {
|
|
322
|
+
if (signal.aborted) break;
|
|
323
|
+
throw e;
|
|
324
|
+
}
|
|
325
|
+
if (signal.aborted) break;
|
|
326
|
+
try {
|
|
327
|
+
this.writeSnapshot(getAgents(), getHistory());
|
|
328
|
+
} catch (error) {
|
|
329
|
+
logger.warn("Persistence", "Periodic snapshot failed", error);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Schedule a debounced snapshot write.
|
|
335
|
+
* Multiple calls within DEBOUNCE_MS window are coalesced into one write.
|
|
336
|
+
*/
|
|
337
|
+
scheduleSnapshot(getAgents, getHistory) {
|
|
338
|
+
if (this._debounceTimer !== null) {
|
|
339
|
+
clearTimeout(this._debounceTimer);
|
|
340
|
+
}
|
|
341
|
+
this._debounceTimer = setTimeout(() => {
|
|
342
|
+
this._debounceTimer = null;
|
|
343
|
+
try {
|
|
344
|
+
this.writeSnapshot(getAgents(), getHistory());
|
|
345
|
+
} catch (error) {
|
|
346
|
+
logger.warn("Persistence", "Debounced snapshot failed", error);
|
|
347
|
+
}
|
|
348
|
+
}, DEBOUNCE_MS);
|
|
349
|
+
}
|
|
350
|
+
/** Cancel pending debounced snapshot (called during shutdown). */
|
|
351
|
+
cancelDebounce() {
|
|
352
|
+
if (this._debounceTimer !== null) {
|
|
353
|
+
clearTimeout(this._debounceTimer);
|
|
354
|
+
this._debounceTimer = null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
27
361
|
// src/infra/tailscale.ts
|
|
28
362
|
import { execFileSync, execSync } from "child_process";
|
|
29
|
-
import { existsSync } from "fs";
|
|
30
|
-
import { join } from "path";
|
|
363
|
+
import { existsSync as existsSync3 } from "fs";
|
|
364
|
+
import { join as join3 } from "path";
|
|
31
365
|
import { spawn as nodeSpawn } from "child_process";
|
|
32
366
|
function parsePossiblyNoisyJson(raw2) {
|
|
33
367
|
const trimmed = raw2.trim();
|
|
@@ -47,7 +381,7 @@ function runExec(cmd, timeout = 5e3) {
|
|
|
47
381
|
});
|
|
48
382
|
}
|
|
49
383
|
function checkBinary(path, timeout = 3e3) {
|
|
50
|
-
if (!path || !
|
|
384
|
+
if (!path || !existsSync3(path)) return false;
|
|
51
385
|
try {
|
|
52
386
|
execFileSync(path, ["--version"], { timeout, encoding: "utf-8", stdio: "pipe", windowsHide: true });
|
|
53
387
|
return true;
|
|
@@ -64,9 +398,9 @@ function findTailscaleBinary() {
|
|
|
64
398
|
} catch {
|
|
65
399
|
}
|
|
66
400
|
const knownPaths = process.platform === "win32" ? [
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
401
|
+
join3(process.env.ProgramFiles || "C:\\Program Files", "Tailscale", "tailscale.exe"),
|
|
402
|
+
join3(process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)", "Tailscale", "tailscale.exe"),
|
|
403
|
+
join3(process.env.LOCALAPPDATA || "", "Tailscale", "tailscale.exe")
|
|
70
404
|
] : [
|
|
71
405
|
"/Applications/Tailscale.app/Contents/MacOS/Tailscale",
|
|
72
406
|
"/usr/bin/tailscale",
|
|
@@ -110,7 +444,7 @@ function getTailnetHostname(binary) {
|
|
|
110
444
|
];
|
|
111
445
|
let lastErr = null;
|
|
112
446
|
for (const candidate of candidates) {
|
|
113
|
-
if (candidate.startsWith("/") && !
|
|
447
|
+
if (candidate.startsWith("/") && !existsSync3(candidate)) continue;
|
|
114
448
|
try {
|
|
115
449
|
const stdout = runExec([candidate, "status", "--json"], 5e3);
|
|
116
450
|
const parsed = stdout.trim() ? parsePossiblyNoisyJson(stdout) : {};
|
|
@@ -152,11 +486,11 @@ function execWithSudoFallback(cmd, timeout = 15e3) {
|
|
|
152
486
|
function joinTailnet(authKey, timeout = 3e4) {
|
|
153
487
|
const binary = getTailscaleBinary();
|
|
154
488
|
try {
|
|
155
|
-
const
|
|
489
|
+
const output2 = execWithSudoFallback(
|
|
156
490
|
[binary, "up", "--authkey", authKey],
|
|
157
491
|
timeout
|
|
158
492
|
);
|
|
159
|
-
return { success: true, output:
|
|
493
|
+
return { success: true, output: output2.trim() };
|
|
160
494
|
} catch (e) {
|
|
161
495
|
const err = e;
|
|
162
496
|
return { success: false, output: (err.stderr ?? err.message ?? "").trim() };
|
|
@@ -182,11 +516,12 @@ function getTailscaleInstallationStatus() {
|
|
|
182
516
|
};
|
|
183
517
|
}
|
|
184
518
|
return { state: "not_connected", binary };
|
|
185
|
-
} catch (
|
|
519
|
+
} catch (error) {
|
|
520
|
+
console.error("[Tailscale] Failed to get installation status:", error);
|
|
186
521
|
return {
|
|
187
522
|
state: "not_connected",
|
|
188
523
|
binary,
|
|
189
|
-
error:
|
|
524
|
+
error: error instanceof Error ? error.message : String(error)
|
|
190
525
|
};
|
|
191
526
|
}
|
|
192
527
|
}
|
|
@@ -196,9 +531,9 @@ function resetTailscaleBinaryCache() {
|
|
|
196
531
|
function logoutTailscale(timeout = 15e3) {
|
|
197
532
|
const binary = getTailscaleBinary();
|
|
198
533
|
try {
|
|
199
|
-
const
|
|
534
|
+
const output2 = runExec([binary, "logout"], timeout);
|
|
200
535
|
resetTailscaleBinaryCache();
|
|
201
|
-
return { success: true, output:
|
|
536
|
+
return { success: true, output: output2.trim() };
|
|
202
537
|
} catch (e) {
|
|
203
538
|
const err = e;
|
|
204
539
|
return { success: false, output: (err.stderr ?? err.message ?? "").trim() };
|
|
@@ -362,6 +697,14 @@ var init_message_queue = __esm({
|
|
|
362
697
|
removeAgentHistory(agentId) {
|
|
363
698
|
this._history.delete(agentId);
|
|
364
699
|
}
|
|
700
|
+
/** Return a shallow copy of the full history map (for persistence snapshots). */
|
|
701
|
+
getHistorySnapshot() {
|
|
702
|
+
const copy = {};
|
|
703
|
+
for (const [agentId, entries] of this._history) {
|
|
704
|
+
copy[agentId] = [...entries];
|
|
705
|
+
}
|
|
706
|
+
return copy;
|
|
707
|
+
}
|
|
365
708
|
};
|
|
366
709
|
}
|
|
367
710
|
});
|
|
@@ -376,15 +719,14 @@ function classifyFetchError(error) {
|
|
|
376
719
|
if (error instanceof DOMException && error.name === "AbortError") return null;
|
|
377
720
|
return null;
|
|
378
721
|
}
|
|
379
|
-
function sleep(ms) {
|
|
380
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
381
|
-
}
|
|
382
722
|
var UNKNOWN, PARTY_SERVER, DEGRADED, SUSPECT, DOWN, NOT_SERVER, MAYBE, MAYBE_MAX_RETRIES, BACKOFF_BASE, BACKOFF_CAP, FAILURE_SUSPECT, FAILURE_DOWN, PeerDiscovery;
|
|
383
723
|
var init_peer_discovery = __esm({
|
|
384
724
|
"src/server/peer-discovery.ts"() {
|
|
385
725
|
"use strict";
|
|
386
726
|
init_config();
|
|
387
727
|
init_tailscale();
|
|
728
|
+
init_persistence();
|
|
729
|
+
init_logger();
|
|
388
730
|
UNKNOWN = "UNKNOWN";
|
|
389
731
|
PARTY_SERVER = "PARTY_SERVER";
|
|
390
732
|
DEGRADED = "DEGRADED";
|
|
@@ -446,14 +788,14 @@ var init_peer_discovery = __esm({
|
|
|
446
788
|
// ------------------------------------------------------------------
|
|
447
789
|
// Main discovery loop
|
|
448
790
|
// ------------------------------------------------------------------
|
|
449
|
-
async runLoop() {
|
|
450
|
-
while (
|
|
791
|
+
async runLoop(signal) {
|
|
792
|
+
while (!signal?.aborted) {
|
|
451
793
|
try {
|
|
452
794
|
await this.discoveryCycle();
|
|
453
795
|
} catch (e) {
|
|
454
|
-
|
|
796
|
+
logger.error("Discovery", "Cycle failed", e);
|
|
455
797
|
}
|
|
456
|
-
await
|
|
798
|
+
await abortableSleep(DISCOVERY_INTERVAL * 1e3, signal);
|
|
457
799
|
}
|
|
458
800
|
}
|
|
459
801
|
async discoveryCycle() {
|
|
@@ -486,8 +828,8 @@ var init_peer_discovery = __esm({
|
|
|
486
828
|
let status;
|
|
487
829
|
try {
|
|
488
830
|
status = readTailscaleStatus();
|
|
489
|
-
} catch {
|
|
490
|
-
|
|
831
|
+
} catch (error) {
|
|
832
|
+
logger.warn("Discovery", "Failed to read Tailscale status", error);
|
|
491
833
|
return [];
|
|
492
834
|
}
|
|
493
835
|
const peersRaw = status.Peer;
|
|
@@ -512,7 +854,7 @@ var init_peer_discovery = __esm({
|
|
|
512
854
|
// Peer probing
|
|
513
855
|
// ------------------------------------------------------------------
|
|
514
856
|
async probePeer(ps) {
|
|
515
|
-
ps.lastProbeAt =
|
|
857
|
+
ps.lastProbeAt = Date.now() / 1e3;
|
|
516
858
|
const healthy = await this.checkHealth(ps.ip);
|
|
517
859
|
if (healthy === null) {
|
|
518
860
|
this.handleProbeFailure(ps, true);
|
|
@@ -581,7 +923,7 @@ var init_peer_discovery = __esm({
|
|
|
581
923
|
const old = ps.status;
|
|
582
924
|
ps.status = newStatus;
|
|
583
925
|
if (old !== newStatus) {
|
|
584
|
-
|
|
926
|
+
logger.info("Discovery", `Peer ${ps.ip}: ${old} -> ${newStatus}`);
|
|
585
927
|
}
|
|
586
928
|
if (newStatus === NOT_SERVER) {
|
|
587
929
|
const retries = ps.maybeRetries > 0 ? ps.maybeRetries : 1;
|
|
@@ -624,8 +966,8 @@ var init_peer_discovery = __esm({
|
|
|
624
966
|
this._remoteAgents.delete(aid);
|
|
625
967
|
}
|
|
626
968
|
}
|
|
627
|
-
} catch {
|
|
628
|
-
|
|
969
|
+
} catch (error) {
|
|
970
|
+
logger.warn("Discovery", `Failed to sync agents from ${peerIp}`, error);
|
|
629
971
|
}
|
|
630
972
|
}
|
|
631
973
|
// ------------------------------------------------------------------
|
|
@@ -681,7 +1023,8 @@ var init_registry = __esm({
|
|
|
681
1023
|
return info;
|
|
682
1024
|
}
|
|
683
1025
|
remove(agentId) {
|
|
684
|
-
|
|
1026
|
+
const existed = this._agents.delete(agentId);
|
|
1027
|
+
return existed;
|
|
685
1028
|
}
|
|
686
1029
|
heartbeat(agentId) {
|
|
687
1030
|
const info = this._agents.get(agentId);
|
|
@@ -719,9 +1062,14 @@ __export(state_exports, {
|
|
|
719
1062
|
STARTED_AT: () => STARTED_AT,
|
|
720
1063
|
discovery: () => discovery,
|
|
721
1064
|
getSelfIp: () => getSelfIp,
|
|
1065
|
+
getSnapshotManager: () => getSnapshotManager,
|
|
1066
|
+
initSnapshotManager: () => initSnapshotManager,
|
|
1067
|
+
lifecycleController: () => lifecycleController,
|
|
722
1068
|
messageQueue: () => messageQueue,
|
|
723
1069
|
refreshSelfIp: () => refreshSelfIp,
|
|
724
|
-
registry: () => registry
|
|
1070
|
+
registry: () => registry,
|
|
1071
|
+
scheduleSnapshot: () => scheduleSnapshot,
|
|
1072
|
+
snapshotManager: () => snapshotManager
|
|
725
1073
|
});
|
|
726
1074
|
function resolveSelfIp() {
|
|
727
1075
|
try {
|
|
@@ -741,7 +1089,19 @@ function refreshSelfIp() {
|
|
|
741
1089
|
_selfIp = resolveSelfIp();
|
|
742
1090
|
return _selfIp;
|
|
743
1091
|
}
|
|
744
|
-
|
|
1092
|
+
function scheduleSnapshot() {
|
|
1093
|
+
snapshotManager?.scheduleSnapshot(
|
|
1094
|
+
() => registry.listAll(),
|
|
1095
|
+
() => messageQueue.getHistorySnapshot()
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
function initSnapshotManager(mgr) {
|
|
1099
|
+
snapshotManager = mgr;
|
|
1100
|
+
}
|
|
1101
|
+
function getSnapshotManager() {
|
|
1102
|
+
return snapshotManager;
|
|
1103
|
+
}
|
|
1104
|
+
var _selfIp, STARTED_AT, registry, messageQueue, discovery, snapshotManager, lifecycleController;
|
|
745
1105
|
var init_state = __esm({
|
|
746
1106
|
"src/server/state.ts"() {
|
|
747
1107
|
"use strict";
|
|
@@ -754,6 +1114,8 @@ var init_state = __esm({
|
|
|
754
1114
|
registry = new AgentRegistry(getSelfIp());
|
|
755
1115
|
messageQueue = new MessageQueue();
|
|
756
1116
|
discovery = new PeerDiscovery(getSelfIp());
|
|
1117
|
+
snapshotManager = null;
|
|
1118
|
+
lifecycleController = new AbortController();
|
|
757
1119
|
}
|
|
758
1120
|
});
|
|
759
1121
|
|
|
@@ -3610,10 +3972,12 @@ var serve = (options, listeningListener) => {
|
|
|
3610
3972
|
|
|
3611
3973
|
// src/server/index.ts
|
|
3612
3974
|
init_config();
|
|
3975
|
+
init_persistence();
|
|
3976
|
+
init_logger();
|
|
3613
3977
|
init_state();
|
|
3614
|
-
import { mkdirSync, writeFileSync, unlinkSync } from "fs";
|
|
3615
|
-
import { join as
|
|
3616
|
-
import { homedir } from "os";
|
|
3978
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync3 } from "fs";
|
|
3979
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
3980
|
+
import { homedir as homedir3 } from "os";
|
|
3617
3981
|
|
|
3618
3982
|
// src/server/models.ts
|
|
3619
3983
|
function sanitizeAgentInfo(info) {
|
|
@@ -3626,6 +3990,7 @@ function sanitizeAgentList(agents) {
|
|
|
3626
3990
|
|
|
3627
3991
|
// src/server/routes/agent.ts
|
|
3628
3992
|
init_config();
|
|
3993
|
+
init_logger();
|
|
3629
3994
|
var agentRoutes = new Hono2();
|
|
3630
3995
|
async function forwardToPeer(peerIp, envelope) {
|
|
3631
3996
|
const url = `http://${peerIp}:${PARTY_PORT}/proxy/receive`;
|
|
@@ -3644,21 +4009,26 @@ async function forwardToPeer(peerIp, envelope) {
|
|
|
3644
4009
|
}
|
|
3645
4010
|
}
|
|
3646
4011
|
agentRoutes.post("/register", async (c) => {
|
|
3647
|
-
const { registry: registry2 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
4012
|
+
const { registry: registry2, scheduleSnapshot: scheduleSnapshot3 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
3648
4013
|
const req = await c.req.json();
|
|
3649
4014
|
const info = registry2.register(req);
|
|
4015
|
+
scheduleSnapshot3();
|
|
4016
|
+
logger.info("Agent", `Registered: ${info.agent_id} (display: "${info.display_name ?? "N/A"}")`);
|
|
3650
4017
|
return c.json(sanitizeAgentInfo(info));
|
|
3651
4018
|
});
|
|
3652
4019
|
agentRoutes.post("/remove", async (c) => {
|
|
3653
|
-
const { registry: registry2 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
4020
|
+
const { registry: registry2, scheduleSnapshot: scheduleSnapshot3 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
3654
4021
|
const req = await c.req.json();
|
|
3655
4022
|
const removed = registry2.remove(req.agent_id);
|
|
4023
|
+
if (removed) scheduleSnapshot3();
|
|
4024
|
+
logger.info("Agent", removed ? `Removed: ${req.agent_id}` : `Remove failed: ${req.agent_id} (not found)`);
|
|
3656
4025
|
return c.json({ status: removed ? "removed" : "not_found" });
|
|
3657
4026
|
});
|
|
3658
4027
|
agentRoutes.post("/heartbeat", async (c) => {
|
|
3659
4028
|
const { registry: registry2 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
3660
4029
|
const req = await c.req.json();
|
|
3661
4030
|
const info = registry2.heartbeat(req.agent_id);
|
|
4031
|
+
logger.info("Agent", `Heartbeat from ${req.agent_id}`);
|
|
3662
4032
|
return c.json({ status: "ok", last_heartbeat: info.last_heartbeat });
|
|
3663
4033
|
});
|
|
3664
4034
|
agentRoutes.get("/list", async (c) => {
|
|
@@ -3669,7 +4039,7 @@ agentRoutes.get("/list", async (c) => {
|
|
|
3669
4039
|
return c.json({ agents: sanitizeAgentList(allAgents), count: allAgents.length });
|
|
3670
4040
|
});
|
|
3671
4041
|
agentRoutes.post("/send", async (c) => {
|
|
3672
|
-
const { registry: registry2, messageQueue: messageQueue2, discovery: discovery2 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
4042
|
+
const { registry: registry2, messageQueue: messageQueue2, discovery: discovery2, scheduleSnapshot: scheduleSnapshot3 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
3673
4043
|
const envelope = await c.req.json();
|
|
3674
4044
|
const recipient = envelope.recipient_id;
|
|
3675
4045
|
if (!recipient) {
|
|
@@ -3680,6 +4050,8 @@ agentRoutes.post("/send", async (c) => {
|
|
|
3680
4050
|
const count = messageQueue2.enqueue(recipient, stamped);
|
|
3681
4051
|
messageQueue2.logToHistory(envelope.sender_id, "sent", stamped);
|
|
3682
4052
|
messageQueue2.logToHistory(recipient, "received", stamped);
|
|
4053
|
+
scheduleSnapshot3();
|
|
4054
|
+
logger.info("Agent", `Send ${envelope.sender_id} -> ${recipient}: delivered_locally`);
|
|
3683
4055
|
return c.json({ status: "delivered_locally", target: recipient });
|
|
3684
4056
|
}
|
|
3685
4057
|
const peerIp = discovery2.getPeerForAgent(recipient);
|
|
@@ -3688,12 +4060,16 @@ agentRoutes.post("/send", async (c) => {
|
|
|
3688
4060
|
const result = await forwardToPeer(peerIp, envelope);
|
|
3689
4061
|
if (result.status === "forwarded") {
|
|
3690
4062
|
messageQueue2.logToHistory(envelope.sender_id, "sent", { ...envelope, timestamp: envelope.timestamp ?? Date.now() / 1e3 });
|
|
4063
|
+
scheduleSnapshot3();
|
|
4064
|
+
logger.info("Agent", `Send ${envelope.sender_id} -> ${recipient}: forwarded (peer ${peerIp})`);
|
|
3691
4065
|
}
|
|
3692
4066
|
return c.json(result);
|
|
3693
4067
|
} else {
|
|
4068
|
+
logger.info("Agent", `Send ${envelope.sender_id} -> ${recipient}: peer_unreachable (peer ${peerIp})`);
|
|
3694
4069
|
return c.json({ status: "peer_unreachable", target: peerIp });
|
|
3695
4070
|
}
|
|
3696
4071
|
}
|
|
4072
|
+
logger.info("Agent", `Send ${envelope.sender_id} -> ${recipient}: agent_not_found`);
|
|
3697
4073
|
return c.json({ status: "agent_not_found", target: recipient });
|
|
3698
4074
|
});
|
|
3699
4075
|
agentRoutes.get("/messages/:agent_id", async (c) => {
|
|
@@ -3715,6 +4091,7 @@ agentRoutes.get("/history/:agent_id", async (c) => {
|
|
|
3715
4091
|
init_config();
|
|
3716
4092
|
init_tailscale();
|
|
3717
4093
|
init_state();
|
|
4094
|
+
init_logger();
|
|
3718
4095
|
var proxyRoutes = new Hono2();
|
|
3719
4096
|
proxyRoutes.get("/health", async (c) => {
|
|
3720
4097
|
let hostname = "127.0.0.1";
|
|
@@ -3739,13 +4116,13 @@ proxyRoutes.post("/receive", async (c) => {
|
|
|
3739
4116
|
if (envelope.recipient_id) {
|
|
3740
4117
|
messageQueue.enqueue(envelope.recipient_id, stamped);
|
|
3741
4118
|
messageQueue.logToHistory(envelope.recipient_id, "received", stamped);
|
|
3742
|
-
|
|
4119
|
+
logger.info("Proxy", `Received msg ${envelope.sender_id} -> ${envelope.recipient_id}`);
|
|
3743
4120
|
} else {
|
|
3744
4121
|
for (const agent of registry.listAll()) {
|
|
3745
4122
|
messageQueue.enqueue(agent.agent_id, stamped);
|
|
3746
4123
|
messageQueue.logToHistory(agent.agent_id, "received", stamped);
|
|
3747
4124
|
}
|
|
3748
|
-
|
|
4125
|
+
logger.info("Proxy", `Broadcast from ${envelope.sender_id} to all agents`);
|
|
3749
4126
|
}
|
|
3750
4127
|
return c.json({ status: "received" });
|
|
3751
4128
|
});
|
|
@@ -3763,10 +4140,10 @@ proxyRoutes.post("/send/:target_ip", async (c) => {
|
|
|
3763
4140
|
headers: { "Content-Type": "application/json" },
|
|
3764
4141
|
body: JSON.stringify(payload)
|
|
3765
4142
|
});
|
|
3766
|
-
|
|
4143
|
+
logger.info("Proxy", `Forwarded to ${targetIp}`);
|
|
3767
4144
|
return c.json({ status: "forwarded", target: targetIp });
|
|
3768
4145
|
} catch (e) {
|
|
3769
|
-
|
|
4146
|
+
logger.warn("Proxy", `Forward to ${targetIp} failed: ${e.message}`);
|
|
3770
4147
|
return c.json({ status: "error", target: targetIp, error: e.message });
|
|
3771
4148
|
}
|
|
3772
4149
|
});
|
|
@@ -4420,6 +4797,16 @@ body::after{
|
|
|
4420
4797
|
}).join('');
|
|
4421
4798
|
}
|
|
4422
4799
|
|
|
4800
|
+
// Build agent_id \u2192 display_name lookup from local + remote agents
|
|
4801
|
+
function buildNameMap(data) {
|
|
4802
|
+
const map = {};
|
|
4803
|
+
const all = [...(data.agents.local_agents || []), ...(data.agents.remote_agents || [])];
|
|
4804
|
+
for (const a of all) { map[a.agent_id] = a.display_name || a.agent_id; }
|
|
4805
|
+
return map;
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
function resolveName(map, id) { return map[id] || id; }
|
|
4809
|
+
|
|
4423
4810
|
function renderMessages(data) {
|
|
4424
4811
|
const container = $('#msgFeed');
|
|
4425
4812
|
const msgs = data.messages.recent || [];
|
|
@@ -4429,12 +4816,14 @@ body::after{
|
|
|
4429
4816
|
return;
|
|
4430
4817
|
}
|
|
4431
4818
|
|
|
4819
|
+
const names = buildNameMap(data);
|
|
4820
|
+
|
|
4432
4821
|
container.innerHTML = msgs.map(function(m) {
|
|
4433
4822
|
const dir = m.direction === 'received' ? 'received' : '';
|
|
4434
4823
|
const arrow = m.direction === 'received' ? '←' : '→';
|
|
4435
4824
|
const flow = m.direction === 'received'
|
|
4436
|
-
? m.sender_id + ' <span class="arrow">' + arrow + '</span> ' + (m.agent_id
|
|
4437
|
-
: (m.agent_id
|
|
4825
|
+
? resolveName(names, m.sender_id) + ' <span class="arrow">' + arrow + '</span> ' + resolveName(names, m.agent_id)
|
|
4826
|
+
: resolveName(names, m.agent_id) + ' <span class="arrow">' + arrow + '</span> ' + resolveName(names, m.recipient_id) || 'broadcast';
|
|
4438
4827
|
return '<div class="msg-item ' + dir + '">'
|
|
4439
4828
|
+ '<div class="msg-top">'
|
|
4440
4829
|
+ '<div class="msg-flow">' + flow + '</div>'
|
|
@@ -4839,6 +5228,7 @@ body::after{
|
|
|
4839
5228
|
// src/server/routes/dashboard.ts
|
|
4840
5229
|
init_tailscale();
|
|
4841
5230
|
init_state();
|
|
5231
|
+
init_logger();
|
|
4842
5232
|
var dashboardRoutes = new Hono2();
|
|
4843
5233
|
dashboardRoutes.get("/", (c) => {
|
|
4844
5234
|
return c.html(DASHBOARD_HTML);
|
|
@@ -4864,10 +5254,14 @@ dashboardRoutes.get("/api/overview", async (c) => {
|
|
|
4864
5254
|
const localAgents = sanitizeAgentList(registry.listAll());
|
|
4865
5255
|
const remoteEntries = discovery.getRemoteAgentEntries();
|
|
4866
5256
|
const peerStates = discovery.getPeerStates();
|
|
5257
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4867
5258
|
const recentMessages = [];
|
|
4868
5259
|
for (const agent of localAgents) {
|
|
4869
5260
|
const history = messageQueue.getHistory(agent.agent_id, 5);
|
|
4870
5261
|
for (const entry of history) {
|
|
5262
|
+
const key = `${entry.sender_id}:${entry.recipient_id ?? ""}:${Math.floor(entry.timestamp)}`;
|
|
5263
|
+
if (seen.has(key)) continue;
|
|
5264
|
+
seen.add(key);
|
|
4871
5265
|
recentMessages.push({ agent_id: agent.agent_id, ...entry });
|
|
4872
5266
|
}
|
|
4873
5267
|
}
|
|
@@ -4931,6 +5325,7 @@ dashboardRoutes.post("/api/join-network", async (c) => {
|
|
|
4931
5325
|
return c.json({ success: false, output: "auth_key is required" }, 400);
|
|
4932
5326
|
}
|
|
4933
5327
|
const result = joinTailnet(authKey);
|
|
5328
|
+
logger.info("Dashboard", `Join network: ${result.success ? "success" : "failed"}`);
|
|
4934
5329
|
return c.json(result, result.success ? 200 : 500);
|
|
4935
5330
|
} catch (e) {
|
|
4936
5331
|
return c.json({ success: false, output: e.message }, 500);
|
|
@@ -4939,6 +5334,7 @@ dashboardRoutes.post("/api/join-network", async (c) => {
|
|
|
4939
5334
|
var activeLogin = null;
|
|
4940
5335
|
dashboardRoutes.post("/api/logout", async (c) => {
|
|
4941
5336
|
const result = logoutTailscale();
|
|
5337
|
+
logger.info("Dashboard", `Logout: ${result.success ? "success" : "failed"}`);
|
|
4942
5338
|
if (result.success) {
|
|
4943
5339
|
resetTailscaleBinaryCache();
|
|
4944
5340
|
refreshSelfIp();
|
|
@@ -4951,6 +5347,7 @@ dashboardRoutes.post("/api/tailscale-login", async (c) => {
|
|
|
4951
5347
|
}
|
|
4952
5348
|
const { promise, process: process2 } = startInteractiveLogin();
|
|
4953
5349
|
activeLogin = { process: process2 };
|
|
5350
|
+
logger.info("Dashboard", "Tailscale login initiated");
|
|
4954
5351
|
const result = await promise;
|
|
4955
5352
|
if (result.success && result.url) {
|
|
4956
5353
|
activeLogin.url = result.url;
|
|
@@ -4962,6 +5359,7 @@ dashboardRoutes.post("/api/tailscale-login", async (c) => {
|
|
|
4962
5359
|
dashboardRoutes.post("/api/install-tailscale", async (c) => {
|
|
4963
5360
|
const { installTailscale: installTailscale2 } = await Promise.resolve().then(() => (init_tailscale_installer(), tailscale_installer_exports));
|
|
4964
5361
|
const result = await installTailscale2(process.platform);
|
|
5362
|
+
logger.info("Dashboard", `Install Tailscale: ${result.success ? "success" : "failed"}`);
|
|
4965
5363
|
if (result.success) {
|
|
4966
5364
|
resetTailscaleBinaryCache();
|
|
4967
5365
|
}
|
|
@@ -4970,48 +5368,135 @@ dashboardRoutes.post("/api/install-tailscale", async (c) => {
|
|
|
4970
5368
|
|
|
4971
5369
|
// src/server/index.ts
|
|
4972
5370
|
async function periodicCleanup() {
|
|
5371
|
+
while (!lifecycleController.signal.aborted) {
|
|
5372
|
+
try {
|
|
5373
|
+
const removed = registry.cleanupStale(HEARTBEAT_TIMEOUT);
|
|
5374
|
+
if (removed.length > 0) {
|
|
5375
|
+
logger.info("Cleanup", `Removed ${removed.length} stale agent(s): ${removed.join(", ")}`);
|
|
5376
|
+
}
|
|
5377
|
+
} catch (e) {
|
|
5378
|
+
logger.error("Cleanup", "Error during cleanup", e);
|
|
5379
|
+
}
|
|
5380
|
+
await abortableSleep(CLEANUP_INTERVAL * 1e3, lifecycleController.signal);
|
|
5381
|
+
}
|
|
4973
5382
|
}
|
|
4974
5383
|
var app = new Hono2();
|
|
4975
5384
|
app.use("*", cors());
|
|
5385
|
+
app.use("*", async (c, next) => {
|
|
5386
|
+
const start = Date.now();
|
|
5387
|
+
await next();
|
|
5388
|
+
const ms = Date.now() - start;
|
|
5389
|
+
const ip = c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ?? c.info?.remote?.address ?? "unknown";
|
|
5390
|
+
const path = c.req.path;
|
|
5391
|
+
if (path === "/proxy/health") {
|
|
5392
|
+
logger.debug("HTTP", `${c.req.method} ${path} ${c.res.status} ${ms}ms ${ip}`);
|
|
5393
|
+
} else {
|
|
5394
|
+
logger.info("HTTP", `${c.req.method} ${path} ${c.res.status} ${ms}ms ${ip}`);
|
|
5395
|
+
}
|
|
5396
|
+
});
|
|
5397
|
+
app.onError((err, c) => {
|
|
5398
|
+
logger.error("HTTP", `${c.req.method} ${c.req.path} failed (${err.status ?? 500}): ${err.message}`, err);
|
|
5399
|
+
return c.json({
|
|
5400
|
+
status: "error",
|
|
5401
|
+
error: (err.status ?? 500) >= 500 ? "Internal server error" : err.message
|
|
5402
|
+
}, err.status ?? 500);
|
|
5403
|
+
});
|
|
4976
5404
|
app.route("/agent", agentRoutes);
|
|
4977
5405
|
app.route("/proxy", proxyRoutes);
|
|
4978
5406
|
app.route("/dashboard", dashboardRoutes);
|
|
4979
5407
|
function pidFilePath() {
|
|
4980
5408
|
const pluginData = process.env.CLAUDE_PLUGIN_DATA || "";
|
|
4981
|
-
if (pluginData) return
|
|
4982
|
-
return
|
|
5409
|
+
if (pluginData) return join4(pluginData, "server.pid");
|
|
5410
|
+
return join4(homedir3(), ".open-party", "server.pid");
|
|
5411
|
+
}
|
|
5412
|
+
var shutdownInitiated = false;
|
|
5413
|
+
async function performShutdown(server, pidPath) {
|
|
5414
|
+
if (shutdownInitiated) return;
|
|
5415
|
+
shutdownInitiated = true;
|
|
5416
|
+
logger.info("Shutdown", "Shutting down Party Server...");
|
|
5417
|
+
try {
|
|
5418
|
+
lifecycleController.abort();
|
|
5419
|
+
getSnapshotManager()?.cancelDebounce();
|
|
5420
|
+
try {
|
|
5421
|
+
getSnapshotManager()?.writeSnapshot(registry.listAll(), messageQueue.getHistorySnapshot());
|
|
5422
|
+
logger.info("Shutdown", "Final snapshot written.");
|
|
5423
|
+
} catch (error) {
|
|
5424
|
+
logger.error("Shutdown", "Failed to write final snapshot", error);
|
|
5425
|
+
}
|
|
5426
|
+
if (server.closeAllConnections) {
|
|
5427
|
+
server.closeAllConnections();
|
|
5428
|
+
}
|
|
5429
|
+
if (process.platform === "win32") {
|
|
5430
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
5431
|
+
}
|
|
5432
|
+
await new Promise((resolve, reject) => {
|
|
5433
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
5434
|
+
});
|
|
5435
|
+
if (process.platform === "win32") {
|
|
5436
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
5437
|
+
}
|
|
5438
|
+
getSnapshotManager()?.removeShutdownMarker();
|
|
5439
|
+
try {
|
|
5440
|
+
unlinkSync3(pidPath);
|
|
5441
|
+
} catch {
|
|
5442
|
+
}
|
|
5443
|
+
logger.info("Shutdown", "Party Server shut down cleanly.");
|
|
5444
|
+
} catch (error) {
|
|
5445
|
+
logger.error("Shutdown", "Error during shutdown sequence", error);
|
|
5446
|
+
} finally {
|
|
5447
|
+
process.exit(0);
|
|
5448
|
+
}
|
|
4983
5449
|
}
|
|
4984
5450
|
async function main() {
|
|
4985
5451
|
const pidPath = pidFilePath();
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
5452
|
+
mkdirSync3(dirname2(pidPath), { recursive: true });
|
|
5453
|
+
writeFileSync2(pidPath, String(process.pid));
|
|
5454
|
+
initSnapshotManager(new SnapshotManager());
|
|
5455
|
+
const sm = getSnapshotManager();
|
|
5456
|
+
let recoveredAgents = 0;
|
|
5457
|
+
let recoveredHistoryEntries = 0;
|
|
5458
|
+
if (sm.hasShutdownMarker()) {
|
|
5459
|
+
logger.info("Recovery", "Previous shutdown was interrupted (shutdown marker found).");
|
|
5460
|
+
}
|
|
5461
|
+
const savedSnapshot = sm.loadSnapshot();
|
|
5462
|
+
if (savedSnapshot) {
|
|
5463
|
+
recoveredAgents = sm.hydrateAgents(registry, getSelfIp());
|
|
5464
|
+
recoveredHistoryEntries = sm.hydrateHistory(messageQueue);
|
|
5465
|
+
if (recoveredAgents > 0 || recoveredHistoryEntries > 0) {
|
|
5466
|
+
logger.info(
|
|
5467
|
+
"Recovery",
|
|
5468
|
+
`Recovered ${recoveredAgents} agent(s), ${recoveredHistoryEntries} history entry/entries.`
|
|
5469
|
+
);
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5472
|
+
logger.info("Server", `Starting Party Server on port ${PARTY_PORT} (Tailscale IP: ${getSelfIp()})`);
|
|
4989
5473
|
process.on("SIGHUP", () => {
|
|
4990
5474
|
});
|
|
4991
5475
|
const server = serve({ fetch: app.fetch, port: PARTY_PORT });
|
|
4992
|
-
const discoveryPromise = discovery.runLoop();
|
|
5476
|
+
const discoveryPromise = discovery.runLoop(lifecycleController.signal);
|
|
4993
5477
|
const cleanupPromise = periodicCleanup();
|
|
4994
|
-
const
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5478
|
+
const snapshotLoopPromise = sm.startSnapshotLoop(
|
|
5479
|
+
lifecycleController.signal,
|
|
5480
|
+
() => registry.listAll(),
|
|
5481
|
+
() => messageQueue.getHistorySnapshot()
|
|
5482
|
+
);
|
|
5483
|
+
const shutdownHandler = () => void performShutdown(server, pidPath);
|
|
5484
|
+
process.on("SIGINT", shutdownHandler);
|
|
5485
|
+
process.on("SIGTERM", shutdownHandler);
|
|
5486
|
+
app.post("/api/shutdown", (c) => {
|
|
5487
|
+
void performShutdown(server, pidPath);
|
|
5488
|
+
return c.json({ status: "shutting_down" });
|
|
5489
|
+
});
|
|
5490
|
+
await Promise.race([discoveryPromise, cleanupPromise, snapshotLoopPromise]);
|
|
5006
5491
|
}
|
|
5007
5492
|
main().catch((e) => {
|
|
5008
|
-
|
|
5493
|
+
logger.error("Server", "Fatal error", e);
|
|
5009
5494
|
process.exit(1);
|
|
5010
5495
|
});
|
|
5011
5496
|
process.on("uncaughtException", (e) => {
|
|
5012
|
-
|
|
5497
|
+
logger.error("Server", "Uncaught exception", e);
|
|
5013
5498
|
});
|
|
5014
5499
|
process.on("unhandledRejection", (e) => {
|
|
5015
|
-
|
|
5500
|
+
logger.error("Server", "Unhandled rejection", e);
|
|
5016
5501
|
});
|
|
5017
5502
|
//# sourceMappingURL=party-server.js.map
|