clawmatrix 0.2.8 → 0.2.11
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/package.json +1 -1
- package/src/acp-proxy.ts +122 -12
- package/src/cluster-service.ts +68 -2
- package/src/config.ts +9 -0
- package/src/file-transfer.ts +671 -0
- package/src/health-tracker.ts +586 -0
- package/src/index.ts +11 -6
- package/src/knowledge-sync.ts +30 -1
- package/src/model-proxy.ts +16 -1
- package/src/peer-manager.ts +28 -4
- package/src/router.ts +25 -0
- package/src/sentinel-manager.ts +8 -7
- package/src/sentinel.ts +29 -7
- package/src/terminal.ts +2 -1
- package/src/tools/cluster-diagnostic.ts +2 -0
- package/src/tools/cluster-transfer.ts +91 -0
- package/src/types.ts +97 -1
- package/src/web.ts +33 -0
package/package.json
CHANGED
package/src/acp-proxy.ts
CHANGED
|
@@ -153,6 +153,8 @@ export class AcpProxy {
|
|
|
153
153
|
private warmPool = new Map<string, AcpSession[]>();
|
|
154
154
|
// Track pending prewarm timers so they can be cancelled on dispose
|
|
155
155
|
private prewarmTimers = new Set<ReturnType<typeof setTimeout>>();
|
|
156
|
+
// Track pending retry timers for sendResponse so they can be cancelled on destroy
|
|
157
|
+
private retryTimers = new Set<ReturnType<typeof setTimeout>>();
|
|
156
158
|
private disposed = false;
|
|
157
159
|
// Agent daemon pool: long-lived process per agent type, reused across sessions
|
|
158
160
|
private daemons = new Map<string, AgentDaemon>();
|
|
@@ -584,7 +586,7 @@ export class AcpProxy {
|
|
|
584
586
|
let newSession: AcpSession;
|
|
585
587
|
if (resumeAcpSessionId) {
|
|
586
588
|
debug("acp", `Session "${sessionId}" not found, resuming via acpSessionId "${resumeAcpSessionId.slice(0, 8)}..."`);
|
|
587
|
-
const effectiveCwd = cwd
|
|
589
|
+
const effectiveCwd = cwd || process.cwd();
|
|
588
590
|
try {
|
|
589
591
|
newSession = await this.createSessionWithResume(agent, resumeAcpSessionId, effectiveCwd, from);
|
|
590
592
|
} catch (resumeErr) {
|
|
@@ -611,7 +613,7 @@ export class AcpProxy {
|
|
|
611
613
|
});
|
|
612
614
|
}
|
|
613
615
|
if (mode === "oneshot" && task) {
|
|
614
|
-
this.returnToReusePool(newSession, cwd
|
|
616
|
+
this.returnToReusePool(newSession, cwd || process.cwd());
|
|
615
617
|
}
|
|
616
618
|
} else {
|
|
617
619
|
// Re-create native session
|
|
@@ -671,7 +673,7 @@ export class AcpProxy {
|
|
|
671
673
|
});
|
|
672
674
|
}
|
|
673
675
|
if (mode === "oneshot" && task) {
|
|
674
|
-
this.returnToReusePool(session, cwd
|
|
676
|
+
this.returnToReusePool(session, cwd || process.cwd());
|
|
675
677
|
}
|
|
676
678
|
} else {
|
|
677
679
|
// Create new native OpenClaw session
|
|
@@ -778,7 +780,8 @@ export class AcpProxy {
|
|
|
778
780
|
/** Handle resume session request (receiver side): resume an ACP session and track it. */
|
|
779
781
|
async handleResumeRequest(frame: AcpResumeRequest): Promise<void> {
|
|
780
782
|
const { id, from, payload } = frame;
|
|
781
|
-
const { agent, acpSessionId
|
|
783
|
+
const { agent, acpSessionId } = payload;
|
|
784
|
+
const cwd = payload.cwd || process.cwd();
|
|
782
785
|
|
|
783
786
|
try {
|
|
784
787
|
// Enforce concurrent session limit
|
|
@@ -1089,6 +1092,30 @@ export class AcpProxy {
|
|
|
1089
1092
|
}
|
|
1090
1093
|
} catch { /* no claude projects directory */ }
|
|
1091
1094
|
|
|
1095
|
+
// 3. Search Codex sessions (~/.codex/sessions/YYYY/MM/DD/rollout-*-{sessionId}.jsonl)
|
|
1096
|
+
// Reverse sort so we search recent dates first (most likely to match)
|
|
1097
|
+
const codexSessionsDir = join(homedir(), ".codex", "sessions");
|
|
1098
|
+
try {
|
|
1099
|
+
const years = (await readdir(codexSessionsDir)).sort().reverse();
|
|
1100
|
+
for (const year of years) {
|
|
1101
|
+
const monthsDir = join(codexSessionsDir, year);
|
|
1102
|
+
let months: string[];
|
|
1103
|
+
try { months = (await readdir(monthsDir)).sort().reverse(); } catch { continue; }
|
|
1104
|
+
for (const month of months) {
|
|
1105
|
+
const daysDir = join(monthsDir, month);
|
|
1106
|
+
let days: string[];
|
|
1107
|
+
try { days = (await readdir(daysDir)).sort().reverse(); } catch { continue; }
|
|
1108
|
+
for (const day of days) {
|
|
1109
|
+
const dayDir = join(daysDir, day);
|
|
1110
|
+
let files: string[];
|
|
1111
|
+
try { files = await readdir(dayDir); } catch { continue; }
|
|
1112
|
+
const match = files.find((f) => f.endsWith(`-${sessionId}.jsonl`));
|
|
1113
|
+
if (match) return join(dayDir, match);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
} catch { /* no codex sessions directory */ }
|
|
1118
|
+
|
|
1092
1119
|
return null;
|
|
1093
1120
|
}
|
|
1094
1121
|
|
|
@@ -1221,6 +1248,7 @@ export class AcpProxy {
|
|
|
1221
1248
|
},
|
|
1222
1249
|
availableModes,
|
|
1223
1250
|
currentModeId: response.modes?.currentModeId,
|
|
1251
|
+
|
|
1224
1252
|
};
|
|
1225
1253
|
|
|
1226
1254
|
return session;
|
|
@@ -1313,7 +1341,7 @@ export class AcpProxy {
|
|
|
1313
1341
|
// ── Internal: ACP session management (receiver) ────────────────
|
|
1314
1342
|
|
|
1315
1343
|
private async createSession(agent: string, cwd: string | undefined, from: string): Promise<AcpSession> {
|
|
1316
|
-
const effectiveCwd = cwd
|
|
1344
|
+
const effectiveCwd = cwd || process.cwd();
|
|
1317
1345
|
|
|
1318
1346
|
// 1. Try daemon pool first (long-lived process, instant newSession)
|
|
1319
1347
|
try {
|
|
@@ -1499,6 +1527,7 @@ export class AcpProxy {
|
|
|
1499
1527
|
setStreamCallback: (cb) => { streamCallback = cb; },
|
|
1500
1528
|
availableModes,
|
|
1501
1529
|
currentModeId: response.modes?.currentModeId,
|
|
1530
|
+
|
|
1502
1531
|
};
|
|
1503
1532
|
|
|
1504
1533
|
this.monitorProcess(session);
|
|
@@ -1752,9 +1781,21 @@ export class AcpProxy {
|
|
|
1752
1781
|
cwd: string,
|
|
1753
1782
|
from: string,
|
|
1754
1783
|
): Promise<AcpSession> {
|
|
1755
|
-
return this.spawnAndConnect(agent, cwd, from, "createSessionWithResume", (conn) =>
|
|
1756
|
-
|
|
1757
|
-
|
|
1784
|
+
return this.spawnAndConnect(agent, cwd, from, "createSessionWithResume", async (conn, effectiveCwd) => {
|
|
1785
|
+
// Try session/load first (supported by Codex, Claude Code, and most ACP agents).
|
|
1786
|
+
// It restores conversation history and replays it via notifications.
|
|
1787
|
+
try {
|
|
1788
|
+
const loadResp = await conn.loadSession({ sessionId: acpSessionId, cwd: effectiveCwd, mcpServers: [] });
|
|
1789
|
+
debug("acp", `loadSession succeeded for ${agent} (acpSessionId=${acpSessionId.slice(0, 8)}...)`);
|
|
1790
|
+
return { sessionId: acpSessionId, modes: loadResp.modes };
|
|
1791
|
+
} catch (loadErr) {
|
|
1792
|
+
debug("acp", `loadSession failed for ${agent}: ${errorMessage(loadErr)}, trying session/resume`);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// Fallback to session/resume (unstable, supported by Claude Code).
|
|
1796
|
+
// Resumes without replaying history — faster but less widely supported.
|
|
1797
|
+
return conn.unstable_resumeSession({ sessionId: acpSessionId, cwd: effectiveCwd });
|
|
1798
|
+
});
|
|
1758
1799
|
}
|
|
1759
1800
|
|
|
1760
1801
|
/** Read all session stores from disk (OpenClaw + Claude Code). */
|
|
@@ -1995,11 +2036,14 @@ export class AcpProxy {
|
|
|
1995
2036
|
const retryDelays = [2_000, 5_000, 10_000];
|
|
1996
2037
|
let attempt = 0;
|
|
1997
2038
|
const retry = () => {
|
|
1998
|
-
if (attempt >= retryDelays.length) {
|
|
1999
|
-
|
|
2039
|
+
if (this.disposed || attempt >= retryDelays.length) {
|
|
2040
|
+
if (!this.disposed) {
|
|
2041
|
+
console.error(`[clawmatrix:acp] Failed to deliver acp_res to ${to} after ${retryDelays.length} retries`);
|
|
2042
|
+
}
|
|
2000
2043
|
return;
|
|
2001
2044
|
}
|
|
2002
|
-
setTimeout(() => {
|
|
2045
|
+
const timer = setTimeout(() => {
|
|
2046
|
+
this.retryTimers.delete(timer);
|
|
2003
2047
|
frame.timestamp = Date.now();
|
|
2004
2048
|
if (this.peerManager.sendTo(to, frame)) {
|
|
2005
2049
|
debug("acp", `acp_res to ${to} delivered on retry ${attempt + 1}`);
|
|
@@ -2008,6 +2052,7 @@ export class AcpProxy {
|
|
|
2008
2052
|
retry();
|
|
2009
2053
|
}
|
|
2010
2054
|
}, retryDelays[attempt]);
|
|
2055
|
+
this.retryTimers.add(timer);
|
|
2011
2056
|
};
|
|
2012
2057
|
retry();
|
|
2013
2058
|
}
|
|
@@ -2048,6 +2093,10 @@ export class AcpProxy {
|
|
|
2048
2093
|
|
|
2049
2094
|
this.disposed = true;
|
|
2050
2095
|
|
|
2096
|
+
// Cancel pending retry timers
|
|
2097
|
+
for (const timer of this.retryTimers) clearTimeout(timer);
|
|
2098
|
+
this.retryTimers.clear();
|
|
2099
|
+
|
|
2051
2100
|
// Cancel pending prewarm timers
|
|
2052
2101
|
for (const timer of this.prewarmTimers) clearTimeout(timer);
|
|
2053
2102
|
this.prewarmTimers.clear();
|
|
@@ -2256,6 +2305,62 @@ async function fetchSessionListFromDisk(): Promise<AcpSessionInfo[]> {
|
|
|
2256
2305
|
// history.jsonl missing or unreadable — skip
|
|
2257
2306
|
}
|
|
2258
2307
|
|
|
2308
|
+
// 3. Codex session history (~/.codex/history.jsonl + session_index.jsonl)
|
|
2309
|
+
try {
|
|
2310
|
+
const codexDir = join(homedir(), ".codex");
|
|
2311
|
+
// Read session index for titles
|
|
2312
|
+
const titleMap = new Map<string, string>();
|
|
2313
|
+
try {
|
|
2314
|
+
const indexContent = await readFileText(join(codexDir, "session_index.jsonl"));
|
|
2315
|
+
for (const line of indexContent.split("\n")) {
|
|
2316
|
+
if (!line.trim()) continue;
|
|
2317
|
+
try {
|
|
2318
|
+
const entry = JSON.parse(line) as { id?: string; thread_name?: string };
|
|
2319
|
+
if (entry.id && entry.thread_name) titleMap.set(entry.id, entry.thread_name);
|
|
2320
|
+
} catch { /* skip */ }
|
|
2321
|
+
}
|
|
2322
|
+
} catch { /* index missing */ }
|
|
2323
|
+
|
|
2324
|
+
const codexHistoryPath = join(codexDir, "history.jsonl");
|
|
2325
|
+
const codexContent = await readFileTail(codexHistoryPath, 512 * 1024);
|
|
2326
|
+
const codexSessions = new Map<string, { title: string; updatedAt: number }>();
|
|
2327
|
+
|
|
2328
|
+
for (const line of codexContent.split("\n")) {
|
|
2329
|
+
if (!line.trim()) continue;
|
|
2330
|
+
try {
|
|
2331
|
+
const entry = JSON.parse(line) as { session_id?: string; ts?: number; text?: string };
|
|
2332
|
+
if (!entry.session_id) continue;
|
|
2333
|
+
const existing = codexSessions.get(entry.session_id);
|
|
2334
|
+
if (!existing) {
|
|
2335
|
+
const title = (entry.text ?? "").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
2336
|
+
codexSessions.set(entry.session_id, {
|
|
2337
|
+
title,
|
|
2338
|
+
updatedAt: entry.ts ? entry.ts * 1000 : 0, // ts is in seconds
|
|
2339
|
+
});
|
|
2340
|
+
} else if (entry.ts && entry.ts * 1000 > existing.updatedAt) {
|
|
2341
|
+
existing.updatedAt = entry.ts * 1000;
|
|
2342
|
+
}
|
|
2343
|
+
} catch { /* skip */ }
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
const codexResults: AcpSessionInfo[] = [];
|
|
2347
|
+
const existingIds2 = new Set(results.map((r) => r.sessionId));
|
|
2348
|
+
for (const [sessionId, info] of codexSessions) {
|
|
2349
|
+
if (existingIds2.has(sessionId)) continue;
|
|
2350
|
+
codexResults.push({
|
|
2351
|
+
sessionId,
|
|
2352
|
+
cwd: "",
|
|
2353
|
+
title: titleMap.get(sessionId) ?? info.title,
|
|
2354
|
+
updatedAt: info.updatedAt ? new Date(info.updatedAt).toISOString() : undefined,
|
|
2355
|
+
agent: "codex",
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
codexResults.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
|
|
2359
|
+
results.push(...codexResults);
|
|
2360
|
+
} catch {
|
|
2361
|
+
// Codex history missing or unreadable — skip
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2259
2364
|
return results;
|
|
2260
2365
|
}
|
|
2261
2366
|
|
|
@@ -2444,7 +2549,12 @@ function normalizeTranscriptMessages(lines: string[], limit: number): ChatHistor
|
|
|
2444
2549
|
for (const line of lines) {
|
|
2445
2550
|
try {
|
|
2446
2551
|
const parsed = JSON.parse(line);
|
|
2447
|
-
|
|
2552
|
+
|
|
2553
|
+
// Support both OpenClaw/Claude format (parsed.message) and Codex format (parsed.payload with type=response_item)
|
|
2554
|
+
let msg: Record<string, unknown> | undefined = parsed?.message;
|
|
2555
|
+
if (!msg && parsed?.type === "response_item" && parsed?.payload?.role) {
|
|
2556
|
+
msg = parsed.payload;
|
|
2557
|
+
}
|
|
2448
2558
|
if (!msg) continue;
|
|
2449
2559
|
|
|
2450
2560
|
const role = typeof msg.role === "string" ? msg.role : "";
|
package/src/cluster-service.ts
CHANGED
|
@@ -14,8 +14,10 @@ import { ModelProxy } from "./model-proxy.ts";
|
|
|
14
14
|
import { ToolProxy, type GatewayInfo } from "./tool-proxy.ts";
|
|
15
15
|
import { AcpProxy, readAllSessionStoresFromDisk } from "./acp-proxy.ts";
|
|
16
16
|
import { TerminalManager } from "./terminal.ts";
|
|
17
|
+
import { FileTransferManager } from "./file-transfer.ts";
|
|
17
18
|
import { WebHandler } from "./web.ts";
|
|
18
19
|
import { KnowledgeSync } from "./knowledge-sync.ts";
|
|
20
|
+
import { HealthTracker } from "./health-tracker.ts";
|
|
19
21
|
import { SentinelManager } from "./sentinel-manager.ts";
|
|
20
22
|
import type {
|
|
21
23
|
AnyClusterFrame,
|
|
@@ -28,6 +30,9 @@ import type {
|
|
|
28
30
|
HandoffInputRequired,
|
|
29
31
|
HandoffInput,
|
|
30
32
|
KnowledgeSyncFrame,
|
|
33
|
+
HealthSyncFrame,
|
|
34
|
+
AvailabilityRequest,
|
|
35
|
+
AvailabilityResponse,
|
|
31
36
|
ModelRequest,
|
|
32
37
|
ModelResponse,
|
|
33
38
|
ModelStreamChunk,
|
|
@@ -61,6 +66,11 @@ import type {
|
|
|
61
66
|
TerminalResize,
|
|
62
67
|
TerminalCloseRequest,
|
|
63
68
|
TerminalCloseResponse,
|
|
69
|
+
FileTransferInit,
|
|
70
|
+
FileTransferAck,
|
|
71
|
+
FileTransferChunk,
|
|
72
|
+
FileTransferChunkAck,
|
|
73
|
+
FileTransferComplete,
|
|
64
74
|
} from "./types.ts";
|
|
65
75
|
|
|
66
76
|
function resolveGatewayInfo(openclawConfig: OpenClawConfig): GatewayInfo {
|
|
@@ -84,7 +94,9 @@ export class ClusterRuntime {
|
|
|
84
94
|
readonly toolProxy: ToolProxy;
|
|
85
95
|
readonly acpProxy: AcpProxy | null;
|
|
86
96
|
readonly terminalManager: TerminalManager;
|
|
97
|
+
readonly fileTransferManager: FileTransferManager | null;
|
|
87
98
|
knowledgeSync: KnowledgeSync | null = null;
|
|
99
|
+
healthTracker: HealthTracker;
|
|
88
100
|
webHandler: WebHandler | null = null;
|
|
89
101
|
private sentinelManager: SentinelManager | null = null;
|
|
90
102
|
private logger: PluginLogger;
|
|
@@ -104,6 +116,13 @@ export class ClusterRuntime {
|
|
|
104
116
|
const acpEnabled = config.acp?.enabled || (openclawConfig as Record<string, any>).acp?.enabled;
|
|
105
117
|
this.acpProxy = acpEnabled ? new AcpProxy(config, this.peerManager, openclawConfig as Record<string, unknown>, gatewayInfo) : null;
|
|
106
118
|
this.terminalManager = new TerminalManager(config, this.peerManager);
|
|
119
|
+
this.fileTransferManager = config.fileTransfer?.enabled
|
|
120
|
+
? new FileTransferManager(config, this.peerManager)
|
|
121
|
+
: null;
|
|
122
|
+
this.healthTracker = new HealthTracker({
|
|
123
|
+
nodeId: config.nodeId,
|
|
124
|
+
peerManager: this.peerManager,
|
|
125
|
+
});
|
|
107
126
|
}
|
|
108
127
|
|
|
109
128
|
start() {
|
|
@@ -115,11 +134,15 @@ export class ClusterRuntime {
|
|
|
115
134
|
this.peerManager.on("peerConnected", (nodeId) => {
|
|
116
135
|
this.logger.info(`[clawmatrix] Peer connected: ${nodeId}`);
|
|
117
136
|
this.refreshDiscoveredModels();
|
|
137
|
+
this.healthTracker.recordPeerOnline(nodeId, "direct");
|
|
138
|
+
this.healthTracker.initPeerSync(nodeId);
|
|
118
139
|
});
|
|
119
140
|
|
|
120
141
|
this.peerManager.on("peerDisconnected", (nodeId) => {
|
|
121
142
|
this.logger.info(`[clawmatrix] Peer disconnected: ${nodeId}`);
|
|
122
143
|
this.refreshDiscoveredModels();
|
|
144
|
+
this.healthTracker.recordPeerOffline(nodeId);
|
|
145
|
+
this.healthTracker.removePeerSync(nodeId);
|
|
123
146
|
});
|
|
124
147
|
|
|
125
148
|
this.peerManager.on("peerCapabilitiesChanged", () => {
|
|
@@ -132,6 +155,7 @@ export class ClusterRuntime {
|
|
|
132
155
|
this.peerManager.setHttpHandler((req, res) => this.webHandler!.handle(req, res));
|
|
133
156
|
// Enable satellite tool routing through WebHandler
|
|
134
157
|
this.toolProxy.setSatelliteHandler(this.webHandler);
|
|
158
|
+
this.webHandler.setHealthTracker(this.healthTracker);
|
|
135
159
|
this.logger.info(`[clawmatrix] Web dashboard enabled on listen port`);
|
|
136
160
|
}
|
|
137
161
|
|
|
@@ -166,8 +190,9 @@ export class ClusterRuntime {
|
|
|
166
190
|
}
|
|
167
191
|
|
|
168
192
|
// Auto-detect ACP agents if ACP is enabled but no agents are explicitly configured
|
|
169
|
-
|
|
170
|
-
|
|
193
|
+
// Check both ClawMatrix and OpenClaw configs (consistent with acpProxy creation above)
|
|
194
|
+
if (this.acpProxy && (!this.config.acp?.agents || this.config.acp.agents.length === 0)) {
|
|
195
|
+
AcpProxy.detectAvailableAgents(this.config.acp?.commands).then((detected) => {
|
|
171
196
|
if (detected.length > 0) {
|
|
172
197
|
this.logger.info(`[clawmatrix] Auto-detected ACP agents: ${detected.map((a) => a.id).join(", ")}`);
|
|
173
198
|
this.peerManager.updateAcpAgents(detected);
|
|
@@ -177,6 +202,11 @@ export class ClusterRuntime {
|
|
|
177
202
|
});
|
|
178
203
|
}
|
|
179
204
|
|
|
205
|
+
// Health tracker (always enabled, no config gate)
|
|
206
|
+
this.healthTracker.start().catch((err) => {
|
|
207
|
+
this.logger.error(`[clawmatrix] Health tracker failed to start: ${err}`);
|
|
208
|
+
});
|
|
209
|
+
|
|
180
210
|
// Start subsystems
|
|
181
211
|
this.peerManager.start();
|
|
182
212
|
this.modelProxy.start();
|
|
@@ -213,12 +243,14 @@ export class ClusterRuntime {
|
|
|
213
243
|
// NOTE: intentionally do NOT stop sentinel here.
|
|
214
244
|
// Sentinel must survive gateway shutdown — that's its entire purpose.
|
|
215
245
|
// It will be replaced by killOldSentinel() on next gateway start.
|
|
246
|
+
await this.healthTracker.stop();
|
|
216
247
|
await this.knowledgeSync?.stop();
|
|
217
248
|
this.webHandler?.destroy();
|
|
218
249
|
this.handoffManager.destroy();
|
|
219
250
|
this.acpProxy?.destroy();
|
|
220
251
|
this.terminalManager.destroy();
|
|
221
252
|
this.modelProxy.stop();
|
|
253
|
+
this.fileTransferManager?.destroy();
|
|
222
254
|
this.toolProxy.destroy();
|
|
223
255
|
await this.peerManager.stop();
|
|
224
256
|
this.logger.info(`[clawmatrix] Node "${this.config.nodeId}" stopped`);
|
|
@@ -316,6 +348,23 @@ export class ClusterRuntime {
|
|
|
316
348
|
this.logger.error(`[clawmatrix] Knowledge sync error: ${err}`);
|
|
317
349
|
});
|
|
318
350
|
break;
|
|
351
|
+
case "health_sync":
|
|
352
|
+
this.healthTracker.handleSyncMessage(frame as HealthSyncFrame);
|
|
353
|
+
break;
|
|
354
|
+
case "availability_req": {
|
|
355
|
+
const af = frame as AvailabilityRequest;
|
|
356
|
+
const range = af.payload.range ?? "24h";
|
|
357
|
+
const data = this.healthTracker.getAvailability(range);
|
|
358
|
+
this.peerManager.sendTo(af.from, {
|
|
359
|
+
type: "availability_res",
|
|
360
|
+
id: af.id,
|
|
361
|
+
from: this.config.nodeId,
|
|
362
|
+
to: af.from,
|
|
363
|
+
timestamp: Date.now(),
|
|
364
|
+
payload: { success: true, data },
|
|
365
|
+
} as AvailabilityResponse);
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
319
368
|
case "acp_req":
|
|
320
369
|
if (this.acpProxy) {
|
|
321
370
|
this.acpProxy.handleRequest(frame as AcpTaskRequest).catch((err) => {
|
|
@@ -472,6 +521,23 @@ export class ClusterRuntime {
|
|
|
472
521
|
frame as TerminalOpenRequest | TerminalOpenResponse | TerminalData | TerminalResize | TerminalCloseRequest | TerminalCloseResponse,
|
|
473
522
|
);
|
|
474
523
|
break;
|
|
524
|
+
case "file_transfer_init":
|
|
525
|
+
this.fileTransferManager?.handleInit(frame as FileTransferInit).catch((err) => {
|
|
526
|
+
this.logger.error(`[clawmatrix] File transfer init error: ${err}`);
|
|
527
|
+
});
|
|
528
|
+
break;
|
|
529
|
+
case "file_transfer_ack":
|
|
530
|
+
this.fileTransferManager?.handleAck(frame as FileTransferAck);
|
|
531
|
+
break;
|
|
532
|
+
case "file_transfer_chunk":
|
|
533
|
+
this.fileTransferManager?.handleChunk(frame as FileTransferChunk);
|
|
534
|
+
break;
|
|
535
|
+
case "file_transfer_chunk_ack":
|
|
536
|
+
this.fileTransferManager?.handleChunkAck(frame as FileTransferChunkAck);
|
|
537
|
+
break;
|
|
538
|
+
case "file_transfer_complete":
|
|
539
|
+
this.fileTransferManager?.handleComplete(frame as FileTransferComplete);
|
|
540
|
+
break;
|
|
475
541
|
}
|
|
476
542
|
}
|
|
477
543
|
|
package/src/config.ts
CHANGED
|
@@ -134,6 +134,14 @@ const TerminalConfigSchema = z.object({
|
|
|
134
134
|
allowFrom: z.array(z.string()).default([]),
|
|
135
135
|
}).optional();
|
|
136
136
|
|
|
137
|
+
const FileTransferConfigSchema = z.object({
|
|
138
|
+
enabled: z.boolean().default(false),
|
|
139
|
+
chunkSize: z.number().default(262_144), // 256KB
|
|
140
|
+
maxFileSize: z.number().default(104_857_600), // 100MB
|
|
141
|
+
timeout: z.number().default(300_000), // 5min per-chunk
|
|
142
|
+
allowedPaths: z.array(z.string()).default([]), // empty = no restriction
|
|
143
|
+
}).optional();
|
|
144
|
+
|
|
137
145
|
const AcpConfigSchema = z.object({
|
|
138
146
|
enabled: z.boolean().default(false),
|
|
139
147
|
/** ACP agents available on this node. Advertised to peers via capabilities. */
|
|
@@ -169,6 +177,7 @@ const RawClawMatrixConfigSchema = z.object({
|
|
|
169
177
|
knowledge: KnowledgeConfigSchema,
|
|
170
178
|
terminal: TerminalConfigSchema,
|
|
171
179
|
acp: AcpConfigSchema,
|
|
180
|
+
fileTransfer: FileTransferConfigSchema,
|
|
172
181
|
peerApproval: z.union([
|
|
173
182
|
z.boolean(), // true = required mode, false = disabled
|
|
174
183
|
PeerApprovalConfigSchema,
|