@sleep2agi/commhub-server 0.9.0-preview.47 → 0.9.0-preview.49
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/report-status-resume-id-handover.test.ts +150 -0
- package/src/serve-error.test.ts +38 -0
- package/src/serve-error.ts +27 -0
- package/src/server.ts +4 -0
- package/src/tools.ts +50 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleep2agi/commhub-server",
|
|
3
|
-
"version": "0.9.0-preview.
|
|
3
|
+
"version": "0.9.0-preview.49",
|
|
4
4
|
"description": "CommHub Server — AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and MCP tools (17 collaboration-core + node/provider ops tools; authoritative list: docs-site/docs/api/mcp-tools.md).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// 同一 alias 换 resume_id 上报时,hub 的 report_status 走的是「DELETE 旧行 + INSERT 新行」,
|
|
2
|
+
// ON CONFLICT 里的 COALESCE 一次都不会触发 —— 上报里没带的描述性列(version / agent /
|
|
3
|
+
// hostname / 遥测 / registered_at)全部变 NULL。
|
|
4
|
+
//
|
|
5
|
+
// 2026-09-04 DEV 真机(hub 日志):
|
|
6
|
+
// 12:46:47 grok-v1 (sdk-n_6c) → report_status: working version=2.5.0-preview.64
|
|
7
|
+
// 12:46:53 grok-v1 (grok-cli) → report_status: working ← TUI 的 MCP 上报,不带 version
|
|
8
|
+
// 12:46:54 grok-v1 (sdk-n_6c) → report_status: idle ← 又换回来,也不带 version
|
|
9
|
+
// 结果:version=NULL、registered_at 被改成 12:46:54。舰队里 grok 3/10、claude 5/15 有 version,
|
|
10
|
+
// codex 34/34 有 —— 差别就是有没有第二个上报者。
|
|
11
|
+
//
|
|
12
|
+
// 判据:换 resume_id 之后,只有一行;上报了的列以上报为准,没上报的列从被替换的那行接手。
|
|
13
|
+
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
|
|
14
|
+
import { db } from "./db.js";
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
import { registerTools } from "./tools.js";
|
|
17
|
+
|
|
18
|
+
const NET_ID = "net_handover";
|
|
19
|
+
const USER_ID = "u_handover";
|
|
20
|
+
const NODE_ID = "n_handover";
|
|
21
|
+
const ALIAS = "grok-handover";
|
|
22
|
+
const TOK_ID = "tok_handover";
|
|
23
|
+
const RESUME_SDK = `sdk-${NODE_ID}`;
|
|
24
|
+
const RESUME_CLI = `grok-cli-${NODE_ID}`;
|
|
25
|
+
const OLD_REGISTERED_AT = "2026-01-02 03:04:05";
|
|
26
|
+
|
|
27
|
+
type ToolHandler = (args: any, extra?: any) => Promise<{ content: Array<{ type: "text"; text: string }> }>;
|
|
28
|
+
|
|
29
|
+
function cleanup() {
|
|
30
|
+
for (const [sql, params] of [
|
|
31
|
+
["DELETE FROM agent_telemetry WHERE network_id = ?1", [NET_ID]],
|
|
32
|
+
["DELETE FROM sessions WHERE network_id = ?1", [NET_ID]],
|
|
33
|
+
["DELETE FROM nodes WHERE network_id = ?1", [NET_ID]],
|
|
34
|
+
["DELETE FROM api_tokens WHERE network_id = ?1", [NET_ID]],
|
|
35
|
+
["DELETE FROM network_members WHERE network_id = ?1", [NET_ID]],
|
|
36
|
+
["DELETE FROM networks WHERE network_id = ?1", [NET_ID]],
|
|
37
|
+
["DELETE FROM users WHERE user_id = ?1", [USER_ID]],
|
|
38
|
+
] as const) {
|
|
39
|
+
try { db.run(sql, params as any); } catch {}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
beforeEach(cleanup);
|
|
43
|
+
afterAll(cleanup);
|
|
44
|
+
|
|
45
|
+
function seed() {
|
|
46
|
+
db.run(`INSERT INTO users (user_id, username, password_hash, role, created_at) VALUES (?1, ?2, 'x', 'user', datetime('now'))`, [USER_ID, USER_ID]);
|
|
47
|
+
db.run(`INSERT INTO networks (network_id, network_name, owner_id, created_at) VALUES (?1, ?2, ?3, datetime('now'))`, [NET_ID, NET_ID, USER_ID]);
|
|
48
|
+
db.run(`INSERT INTO network_members (user_id, network_id, role, joined_at) VALUES (?1, ?2, 'owner', datetime('now'))`, [USER_ID, NET_ID]);
|
|
49
|
+
db.run(
|
|
50
|
+
`INSERT INTO nodes (node_id, node_name, alias, network_id, hostname, created_at, updated_at, lifecycle_state)
|
|
51
|
+
VALUES (?1, ?2, ?3, ?4, ?5, datetime('now'), datetime('now'), 'active')`,
|
|
52
|
+
[NODE_ID, ALIAS, ALIAS, NET_ID, "dev-box"],
|
|
53
|
+
);
|
|
54
|
+
// agent-node 自己注册的那行:带 version / agent / hostname / 遥测 / 早就存在的 registered_at
|
|
55
|
+
db.run(
|
|
56
|
+
`INSERT INTO sessions (resume_id, alias, status, node_id, network_id, version, agent, hostname, server, project_dir,
|
|
57
|
+
cpu_cores, mem_total_gb, disk_total_gb, registered_at, updated_at, last_seen_at)
|
|
58
|
+
VALUES (?1, ?2, 'working', ?3, ?4, '2.5.0-preview.64', 'agent-node:grok-build-cli', 'dev-box', 'dev-box', '/home/x/proj',
|
|
59
|
+
8, 61.4, 500, ?5, datetime('now'), datetime('now'))`,
|
|
60
|
+
[RESUME_SDK, ALIAS, NODE_ID, NET_ID, OLD_REGISTERED_AT],
|
|
61
|
+
);
|
|
62
|
+
db.run(
|
|
63
|
+
`INSERT INTO api_tokens (token_id, user_id, network_id, scope, name, token_hash, expires_at, revoked_at)
|
|
64
|
+
VALUES (?1, ?2, ?3, 'network', ?4, ?5, NULL, NULL)`,
|
|
65
|
+
[TOK_ID, USER_ID, NET_ID, `node:${ALIAS}`, `hash_${TOK_ID}`],
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildHandlers(): Record<string, ToolHandler> {
|
|
70
|
+
const server = new McpServer({ name: "test", version: "0" }) as any;
|
|
71
|
+
const tools: Record<string, ToolHandler> = {};
|
|
72
|
+
const origTool = server.tool.bind(server);
|
|
73
|
+
server.tool = (name: string, _desc: string, _schema: any, handler: ToolHandler) => { tools[name] = handler; return origTool(name, _desc, _schema, handler); };
|
|
74
|
+
const origRegisterTool = server.registerTool?.bind(server);
|
|
75
|
+
if (origRegisterTool) {
|
|
76
|
+
server.registerTool = (name: string, _cfg: any, handler: ToolHandler) => { tools[name] = handler; return origRegisterTool(name, _cfg, handler); };
|
|
77
|
+
}
|
|
78
|
+
registerTools(server, undefined, NET_ID, USER_ID, ALIAS, true, TOK_ID);
|
|
79
|
+
return tools;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function call(handler: ToolHandler, args: any): Promise<any> {
|
|
83
|
+
const r = await handler(args);
|
|
84
|
+
return JSON.parse(r.content[0]!.text);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type Row = { resume_id: string; status: string; version: string | null; agent: string | null; hostname: string | null;
|
|
88
|
+
cpu_cores: number | null; mem_total_gb: number | null; registered_at: string; task: string | null };
|
|
89
|
+
function rows(): Row[] {
|
|
90
|
+
return db.all("SELECT resume_id, status, version, agent, hostname, cpu_cores, mem_total_gb, registered_at, task FROM sessions WHERE alias = ?1 AND network_id = ?2", [ALIAS, NET_ID]) as Row[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe("report_status: 同 alias 换 resume_id 时,描述性列从被替换的那行接手", () => {
|
|
94
|
+
test("第二个上报者(grok-cli)不带 version/agent/遥测 → 这些列保留,status/task 以本次为准,仍只有一行", async () => {
|
|
95
|
+
seed();
|
|
96
|
+
const tools = buildHandlers();
|
|
97
|
+
const r = await call(tools.report_status!, { resume_id: RESUME_CLI, alias: ALIAS, status: "working", task: "PING 已收到", network_id: NET_ID });
|
|
98
|
+
expect(r.ok, JSON.stringify(r)).toBe(true);
|
|
99
|
+
const all = rows();
|
|
100
|
+
expect(all.length).toBe(1);
|
|
101
|
+
const row = all[0]!;
|
|
102
|
+
expect(row.resume_id).toBe(RESUME_CLI);
|
|
103
|
+
expect(row.status).toBe("working");
|
|
104
|
+
expect(row.task).toBe("PING 已收到");
|
|
105
|
+
expect(row.version).toBe("2.5.0-preview.64");
|
|
106
|
+
expect(row.agent).toBe("agent-node:grok-build-cli");
|
|
107
|
+
expect(row.hostname).toBe("dev-box");
|
|
108
|
+
expect(row.cpu_cores).toBe(8);
|
|
109
|
+
expect(row.mem_total_gb).toBe(61.4);
|
|
110
|
+
expect(row.registered_at).toBe(OLD_REGISTERED_AT);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("上报里带了的列以上报为准(version 变了就用新的)", async () => {
|
|
114
|
+
seed();
|
|
115
|
+
const tools = buildHandlers();
|
|
116
|
+
const r = await call(tools.report_status!, { resume_id: RESUME_CLI, alias: ALIAS, status: "idle", version: "9.9.9", hostname: "other-box", network_id: NET_ID });
|
|
117
|
+
expect(r.ok, JSON.stringify(r)).toBe(true);
|
|
118
|
+
const row = rows()[0]!;
|
|
119
|
+
expect(rows().length).toBe(1);
|
|
120
|
+
expect(row.version).toBe("9.9.9");
|
|
121
|
+
expect(row.hostname).toBe("other-box");
|
|
122
|
+
expect(row.agent).toBe("agent-node:grok-build-cli");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("再换回原 resume_id(sdk)也不丢:两个上报者轮流说话,version 始终在", async () => {
|
|
126
|
+
seed();
|
|
127
|
+
const tools = buildHandlers();
|
|
128
|
+
await call(tools.report_status!, { resume_id: RESUME_CLI, alias: ALIAS, status: "working", network_id: NET_ID });
|
|
129
|
+
const r = await call(tools.report_status!, { resume_id: RESUME_SDK, alias: ALIAS, status: "idle", network_id: NET_ID });
|
|
130
|
+
expect(r.ok, JSON.stringify(r)).toBe(true);
|
|
131
|
+
const all = rows();
|
|
132
|
+
expect(all.length).toBe(1);
|
|
133
|
+
expect(all[0]!.resume_id).toBe(RESUME_SDK);
|
|
134
|
+
expect(all[0]!.status).toBe("idle");
|
|
135
|
+
expect(all[0]!.version).toBe("2.5.0-preview.64");
|
|
136
|
+
expect(all[0]!.registered_at).toBe(OLD_REGISTERED_AT);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("没有别的行可接手时(全新 alias)行为不变:INSERT 一行,registered_at 取 now", async () => {
|
|
140
|
+
seed();
|
|
141
|
+
db.run("DELETE FROM sessions WHERE network_id = ?1", [NET_ID]);
|
|
142
|
+
const tools = buildHandlers();
|
|
143
|
+
const r = await call(tools.report_status!, { resume_id: RESUME_SDK, alias: ALIAS, status: "idle", network_id: NET_ID });
|
|
144
|
+
expect(r.ok, JSON.stringify(r)).toBe(true);
|
|
145
|
+
const all = rows();
|
|
146
|
+
expect(all.length).toBe(1);
|
|
147
|
+
expect(all[0]!.version).toBeNull();
|
|
148
|
+
expect(all[0]!.registered_at).not.toBe(OLD_REGISTERED_AT);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { buildServeErrorResponse, serveErrorBody } from "./serve-error.js";
|
|
3
|
+
|
|
4
|
+
// 注:「没有 error 回调时 Bun 回默认非 JSON 500」这一半没法在 bun test 里当用例:
|
|
5
|
+
// 运行器会把 fetch 里抛出的异常当成未处理错误直接判这条用例失败(实测),证据在 #695 末评。
|
|
6
|
+
describe("#695 fetch exceptions become JSON with the exception's own message", () => {
|
|
7
|
+
test("body carries THE message, both JSON-RPC-shaped and REST-shaped", () => {
|
|
8
|
+
const b = serveErrorBody(new Error("boom-695: sqlite is locked"));
|
|
9
|
+
expect(b.error.code).toBe(-32603);
|
|
10
|
+
expect(b.error.message).toBe("boom-695: sqlite is locked");
|
|
11
|
+
expect(b.message).toBe("boom-695: sqlite is locked");
|
|
12
|
+
expect(b.ok).toBe(false);
|
|
13
|
+
// 不是固定 JSON:换一个异常,message 跟着变
|
|
14
|
+
expect(serveErrorBody(new Error("other")).error.message).toBe("other");
|
|
15
|
+
expect(serveErrorBody("plain string").error.message).toBe("plain string");
|
|
16
|
+
expect(serveErrorBody(new Error("")).error.message).toBe("internal error");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("real Bun.serve: a throwing fetch answers 500 + application/json + the message (was a non-JSON default page)", async () => {
|
|
20
|
+
const server = Bun.serve({
|
|
21
|
+
port: 0,
|
|
22
|
+
fetch() { throw new Error("boom-695-live"); },
|
|
23
|
+
error: (err) => buildServeErrorResponse(err),
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch(`http://127.0.0.1:${server.port}/mcp`, { method: "POST", body: "{}" });
|
|
27
|
+
expect(res.status).toBe(500);
|
|
28
|
+
expect(res.headers.get("content-type") ?? "").toContain("application/json");
|
|
29
|
+
const json = await res.json() as { error: { code: number; message: string }; ok: boolean };
|
|
30
|
+
expect(json.error.code).toBe(-32603);
|
|
31
|
+
expect(json.error.message).toBe("boom-695-live");
|
|
32
|
+
expect(json.ok).toBe(false);
|
|
33
|
+
} finally {
|
|
34
|
+
server.stop(true);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// #695 —— Bun.serve 没注册 `error` 回调时,fetch 处理器抛出的异常会变成 Bun 的默认 500,
|
|
2
|
+
// body 不是 JSON;MCP 客户端于是只能报一个泛化的 -32603,拿不到任何一句人话。
|
|
3
|
+
// 这里把异常变成一个带 message 的 JSON 响应:/mcp 的客户端按 JSON-RPC 形状读 error.message,
|
|
4
|
+
// REST 客户端按本仓其它路由的 { ok:false, error, message } 形状读。
|
|
5
|
+
// 🔴 判据(#695 末评):同一个异常,响应里要带**那个异常的 message**,不是一个固定 JSON。
|
|
6
|
+
|
|
7
|
+
export interface ServeErrorBody {
|
|
8
|
+
jsonrpc: "2.0";
|
|
9
|
+
id: null;
|
|
10
|
+
error: { code: -32603; message: string };
|
|
11
|
+
ok: false;
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function serveErrorBody(err: unknown): ServeErrorBody {
|
|
16
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
17
|
+
const message = (raw && raw.trim()) ? raw.trim().slice(0, 500) : "internal error";
|
|
18
|
+
return { jsonrpc: "2.0", id: null, error: { code: -32603, message }, ok: false, message };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildServeErrorResponse(err: unknown): Response {
|
|
22
|
+
const body = serveErrorBody(err);
|
|
23
|
+
return new Response(JSON.stringify(body), {
|
|
24
|
+
status: 500,
|
|
25
|
+
headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" },
|
|
26
|
+
});
|
|
27
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildServeErrorResponse } from "./serve-error.js";
|
|
1
2
|
import { redactMessageRow } from "./redact-tokens.js";
|
|
2
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
4
|
import { parseDbTimestampMs } from "./db-timestamp.js";
|
|
@@ -786,6 +787,9 @@ return Bun.serve({
|
|
|
786
787
|
// route. Default Bun ceiling is 128 MiB which is too permissive for
|
|
787
788
|
// a hub now exposed to the public internet.
|
|
788
789
|
maxRequestBodySize: MAX_REQUEST_CONTENT_LENGTH,
|
|
790
|
+
// #695 —— fetch 处理器抛出时不要让 Bun 回默认的非 JSON 500;带上异常 message,
|
|
791
|
+
// /mcp 客户端按 JSON-RPC 读 error.message,REST 客户端按 { ok:false, message } 读。
|
|
792
|
+
error: (err) => buildServeErrorResponse(err),
|
|
789
793
|
|
|
790
794
|
async fetch(req, server) {
|
|
791
795
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
package/src/tools.ts
CHANGED
|
@@ -830,6 +830,26 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
830
830
|
|
|
831
831
|
db.transaction(() => {
|
|
832
832
|
// Only delete same-alias sessions within the same network
|
|
833
|
+
// 同一 alias 可以有两个上报者轮流说话(agent-node 自己 `sdk-<node>` +
|
|
834
|
+
// grok 共存里 TUI 的 MCP `grok-cli-<node>`)。下面这条 DELETE 会把「另一个
|
|
835
|
+
// resume_id」的那行整行删掉,再 INSERT 一行新的 —— ON CONFLICT 里那一串
|
|
836
|
+
// COALESCE 在这条路径上一次都不会触发,于是上报里没带的列(version / agent /
|
|
837
|
+
// hostname / 六个遥测字段 / registered_at …)全部变 NULL。2026-09-04 DEV 真机:
|
|
838
|
+
// grok-v1 working 时 version=.64,7 秒后 idle 就成了 NULL,registered_at 被改成
|
|
839
|
+
// 那一秒。先把要被删的那行读出来,INSERT 之后把仍为 NULL 的列从它接手 —— 让「换 resume_id」
|
|
840
|
+
// 和「同 resume_id 更新」对描述性列的语义一致:上报了就覆盖,没上报就保留。
|
|
841
|
+
// 状态类列(status / task / output / progress / score)不接手,那是本次上报的事实。
|
|
842
|
+
const handover = db.get<Record<string, unknown>>(
|
|
843
|
+
`SELECT tmux_name, server, ip, hostname, agent, project_dir, version, node_id, session_id, config_path,
|
|
844
|
+
channels, model, cpu_load_1min, cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb, disk_total_gb,
|
|
845
|
+
disk_used_gb, disk_avail_gb, process_rss_bytes, process_rss_mb, process_cpu_pct,
|
|
846
|
+
process_uptime_seconds, process_in_flight_count, external_schedules, registered_at
|
|
847
|
+
FROM sessions WHERE alias = ?1 AND resume_id != ?2 AND network_id = ?3
|
|
848
|
+
ORDER BY updated_at DESC LIMIT 1`,
|
|
849
|
+
effectiveAlias, resume_id, sessionNetId,
|
|
850
|
+
) ?? null;
|
|
851
|
+
const keep = <T,>(fresh: T | null | undefined, column: string): T | null =>
|
|
852
|
+
(fresh ?? (handover?.[column] as T | null | undefined) ?? null);
|
|
833
853
|
db.run("DELETE FROM sessions WHERE alias = ?1 AND resume_id != ?2 AND network_id = ?3", [effectiveAlias, resume_id, sessionNetId]);
|
|
834
854
|
db.run(
|
|
835
855
|
`INSERT INTO sessions (resume_id, alias, tmux_name, server, ip, hostname, agent, project_dir, version, status, task, output, progress, score, node_id, session_id, config_path, channels, network_id, model, cpu_load_1min, cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb, disk_total_gb, disk_used_gb, disk_avail_gb, process_rss_bytes, process_rss_mb, process_cpu_pct, process_uptime_seconds, process_in_flight_count, external_schedules, peer_reply_inbox_capable, last_seen_at, updated_at)
|
|
@@ -863,6 +883,36 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
863
883
|
last_seen_at = datetime('now'), updated_at = datetime('now')`,
|
|
864
884
|
[resume_id, effectiveAlias, tmux ?? null, srv ?? null, hostIp, hostHostname, ag ?? null, pd ?? null, ver ?? null, status, task ?? null, trimmedOutput ?? null, progress ?? null, score ?? null, node_id ?? null, session_id ?? null, config_path ?? null, channels ?? null, sessionNetId, mdl ?? null, cpuLoad1m, cpuCores, memTotalGb, memUsedGb, memAvailGb, diskTotalGb, diskUsedGb, diskAvailGb, processRssBytes, processRssMb, processCpuPct, processUptimeSeconds, processInFlightCount, externalSchedulesJson, peerReplyInboxCapable ? 1 : 0]
|
|
865
885
|
);
|
|
886
|
+
if (handover) {
|
|
887
|
+
// 换 resume_id 那条路径上 ON CONFLICT 不会触发;INSERT 完再把没上报(仍为 NULL)的
|
|
888
|
+
// 描述性列从被替换的那行接手。INSERT 语句与参数列表保持原样 —— test698 的变异
|
|
889
|
+
// 用 sed 钉着它的字节形状。
|
|
890
|
+
db.run(
|
|
891
|
+
`UPDATE sessions SET
|
|
892
|
+
tmux_name = COALESCE(tmux_name, ?2), server = COALESCE(server, ?3), ip = COALESCE(ip, ?4),
|
|
893
|
+
hostname = COALESCE(hostname, ?5), agent = COALESCE(agent, ?6), project_dir = COALESCE(project_dir, ?7),
|
|
894
|
+
version = COALESCE(version, ?8), node_id = COALESCE(node_id, ?9), session_id = COALESCE(session_id, ?10),
|
|
895
|
+
config_path = COALESCE(config_path, ?11), channels = COALESCE(channels, ?12), model = COALESCE(model, ?13),
|
|
896
|
+
cpu_load_1min = COALESCE(cpu_load_1min, ?14), cpu_cores = COALESCE(cpu_cores, ?15),
|
|
897
|
+
mem_total_gb = COALESCE(mem_total_gb, ?16), mem_used_gb = COALESCE(mem_used_gb, ?17),
|
|
898
|
+
mem_avail_gb = COALESCE(mem_avail_gb, ?18), disk_total_gb = COALESCE(disk_total_gb, ?19),
|
|
899
|
+
disk_used_gb = COALESCE(disk_used_gb, ?20), disk_avail_gb = COALESCE(disk_avail_gb, ?21),
|
|
900
|
+
process_rss_bytes = COALESCE(process_rss_bytes, ?22), process_rss_mb = COALESCE(process_rss_mb, ?23),
|
|
901
|
+
process_cpu_pct = COALESCE(process_cpu_pct, ?24), process_uptime_seconds = COALESCE(process_uptime_seconds, ?25),
|
|
902
|
+
process_in_flight_count = COALESCE(process_in_flight_count, ?26),
|
|
903
|
+
external_schedules = COALESCE(external_schedules, ?27),
|
|
904
|
+
registered_at = COALESCE(?28, registered_at)
|
|
905
|
+
WHERE resume_id = ?1`,
|
|
906
|
+
[resume_id, handover.tmux_name ?? null, handover.server ?? null, handover.ip ?? null, handover.hostname ?? null,
|
|
907
|
+
handover.agent ?? null, handover.project_dir ?? null, handover.version ?? null, handover.node_id ?? null,
|
|
908
|
+
handover.session_id ?? null, handover.config_path ?? null, handover.channels ?? null, handover.model ?? null,
|
|
909
|
+
handover.cpu_load_1min ?? null, handover.cpu_cores ?? null, handover.mem_total_gb ?? null, handover.mem_used_gb ?? null,
|
|
910
|
+
handover.mem_avail_gb ?? null, handover.disk_total_gb ?? null, handover.disk_used_gb ?? null, handover.disk_avail_gb ?? null,
|
|
911
|
+
handover.process_rss_bytes ?? null, handover.process_rss_mb ?? null, handover.process_cpu_pct ?? null,
|
|
912
|
+
handover.process_uptime_seconds ?? null, handover.process_in_flight_count ?? null, handover.external_schedules ?? null,
|
|
913
|
+
handover.registered_at ?? null],
|
|
914
|
+
);
|
|
915
|
+
}
|
|
866
916
|
if (host || proc) {
|
|
867
917
|
db.run(
|
|
868
918
|
`INSERT INTO agent_telemetry (id, network_id, resume_id, alias, hostname, ip, cpu_load_1min, cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb, disk_total_gb, disk_used_gb, disk_avail_gb, process_rss_bytes, process_rss_mb, process_cpu_pct, process_uptime_seconds, process_in_flight_count, created_at)
|