@lark-codem/codem-cli 0.1.269-wfqa.100103558174.0.gc09ffa9 → 0.1.269
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/bin/tt-env.js +171 -0
- package/bin/worker-runtime-telemetry.js +75 -0
- package/bin/worker-runtime.js +38 -7
- package/package.json +8 -8
- package/profile/runtime-logs.js +238 -60
- package/profile/slardar-pc-transport.js +7 -3
package/bin/tt-env.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// CODEM_X_TT_ENV 泳道支持:按环境变量把 codem 请求打入 BOE 环境 / PPE·BOE 泳道。
|
|
2
|
+
// 与 linco 侧(linco-code crates/linco-core/src/tt_env.rs)同一契约、同一矩阵:
|
|
3
|
+
//
|
|
4
|
+
// | 值 | 域名 | x-tt-env | x-use-ppe |
|
|
5
|
+
// |---------|---------------------------------------|----------|-----------|
|
|
6
|
+
// | boe | codem.feishu.cn → codem.feishu-boe.cn | — | — |
|
|
7
|
+
// | boe_* | codem.feishu.cn → codem.feishu-boe.cn | 原值 | — |
|
|
8
|
+
// | ppe | 不变(PPE 与线上同域名) | — | 1 |
|
|
9
|
+
// | ppe_* | 不变 | 原值 | 1 |
|
|
10
|
+
// | 其他 | 完全惰性 | — | — |
|
|
11
|
+
//
|
|
12
|
+
// daemon spawn linco 时 env 全量继承(linco-runner buildLincoProcessEnv),
|
|
13
|
+
// 该变量无需任何桥接代码即透传给 linco 进程。
|
|
14
|
+
//
|
|
15
|
+
// 头注入统一走 ttEnvHeadersFor(url):只对官方 codem host(prod/BOE)产出头,
|
|
16
|
+
// 任何可被 env 覆写到第三方 host 的出口(agent-config / project 端点)天然
|
|
17
|
+
// 拿不到泳道头——泄露风险在构造上排除,而非靠调用点纪律。
|
|
18
|
+
//
|
|
19
|
+
// 个人和 native sandbox 的 serverUrl 经 resolveServerUrl 派生;cloud/custom
|
|
20
|
+
// sandbox 保留自己的配置优先级,再用 rewriteUrlForTtEnv 改写选中的官方域名。
|
|
21
|
+
// Runtime POST 使用已解析的 serverUrl 拼接路径,与 managed-config 保持同一环境;
|
|
22
|
+
// 自定义服务地址不受影响。BOE 身份态通过 resolveStateHome 隔离到 .boe 子树。
|
|
23
|
+
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
|
|
26
|
+
export const CODEM_X_TT_ENV = "CODEM_X_TT_ENV";
|
|
27
|
+
|
|
28
|
+
const PROD_ROUTER_HOST = "codem.feishu.cn";
|
|
29
|
+
const BOE_ROUTER_HOST = "codem.feishu-boe.cn";
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
// 合法形状:boe / ppe 裸值,或 boe_ / ppe_ 前缀 + [A-Za-z0-9_-] 泳道名。其余
|
|
33
|
+
// 一律 null(惰性)。显式剥 BOM(U+FEFF,Windows env 文件复制常见混入);泳道
|
|
34
|
+
// 名限定字符集,控制字符等坏值直接判惰性而不是变成非法 header value。两条都
|
|
35
|
+
// 是与 linco 侧(tt_env.rs)钉死的契约——同一变量被两个进程各自解析,任何
|
|
36
|
+
// 语义分叉都会造成"bridge 进泳道、linco 留 prod"的半程泳道。
|
|
37
|
+
export function parseTtEnvLane(raw) {
|
|
38
|
+
if (typeof raw !== "string") {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
// 字符类显式含 U+0085(NEL):JS \s 不含它而 Rust char::is_whitespace 含,
|
|
42
|
+
// 不补齐会在这一个字符上出现两侧判定分叉。
|
|
43
|
+
const value = raw.replace(/^[\s
]+|[\s
]+$/gu, "");
|
|
44
|
+
const bare = value === "boe" || value === "ppe";
|
|
45
|
+
const named = /^(?:boe|ppe)_[A-Za-z0-9_-]+$/u.test(value);
|
|
46
|
+
if (!bare && !named) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
value,
|
|
51
|
+
isBoe: value === "boe" || value.startsWith("boe_"),
|
|
52
|
+
xTtEnv: named ? value : null,
|
|
53
|
+
usePpe: value === "ppe" || value.startsWith("ppe_")
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolveTtEnvLane(env = process.env) {
|
|
58
|
+
return parseTtEnvLane(env?.[CODEM_X_TT_ENV]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isOfficialCodemHost(hostname) {
|
|
62
|
+
const host = hostname.toLowerCase();
|
|
63
|
+
return host === PROD_ROUTER_HOST || host === BOE_ROUTER_HOST;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// BOE 泳道下把官方 prod host 重写为 BOE host;显式自定义 server(localhost /
|
|
67
|
+
// 私有化)与非法 URL 原样返回,保证 --server 显式指向永远赢过泳道。
|
|
68
|
+
export function rewriteUrlForTtEnv(url, env = process.env) {
|
|
69
|
+
const lane = resolveTtEnvLane(env);
|
|
70
|
+
if (!lane?.isBoe) {
|
|
71
|
+
return url;
|
|
72
|
+
}
|
|
73
|
+
let parsed;
|
|
74
|
+
try {
|
|
75
|
+
parsed = new URL(url);
|
|
76
|
+
} catch {
|
|
77
|
+
return url;
|
|
78
|
+
}
|
|
79
|
+
if (parsed.hostname.toLowerCase() !== PROD_ROUTER_HOST) {
|
|
80
|
+
return url;
|
|
81
|
+
}
|
|
82
|
+
parsed.hostname = BOE_ROUTER_HOST;
|
|
83
|
+
let rewritten = parsed.toString();
|
|
84
|
+
// URL 序列化会给裸域名补尾部 "/",与既有 serverUrl 约定(仅存域名)不符:
|
|
85
|
+
// 输入没带尾斜杠时保持原样。
|
|
86
|
+
if (!url.endsWith("/") && parsed.pathname === "/" && rewritten.endsWith("/")) {
|
|
87
|
+
rewritten = rewritten.slice(0, -1);
|
|
88
|
+
}
|
|
89
|
+
return rewritten;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 目标是官方 codem host(prod/BOE)时产出泳道头,否则 {}——第三方 host 永远拿不到。
|
|
93
|
+
export function ttEnvHeadersFor(url, env = process.env) {
|
|
94
|
+
const lane = resolveTtEnvLane(env);
|
|
95
|
+
if (!lane || (!lane.xTtEnv && !lane.usePpe)) {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
let hostname;
|
|
99
|
+
try {
|
|
100
|
+
hostname = (url instanceof URL ? url : new URL(String(url))).hostname;
|
|
101
|
+
} catch {
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
if (!isOfficialCodemHost(hostname)) {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
const headers = {};
|
|
108
|
+
if (lane.xTtEnv) {
|
|
109
|
+
headers["x-tt-env"] = lane.xTtEnv;
|
|
110
|
+
}
|
|
111
|
+
if (lane.usePpe) {
|
|
112
|
+
headers["x-use-ppe"] = "1";
|
|
113
|
+
}
|
|
114
|
+
return headers;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 环境状态根:身份/环境态(config/credentials/sessions/daemon 锁等)的落盘
|
|
118
|
+
// 基目录。boe 与 prod(ppe) 账号体系独立,各存一份互不打爆:boe* 泳道落
|
|
119
|
+
// `$CODEM_HOME/.boe`,其余共用 `$CODEM_HOME`。程序安装态(runtime/ 二进制、
|
|
120
|
+
// 更新状态、bin)**不走本函数**、始终留在 CODEM_HOME——否则首次 boe 启动
|
|
121
|
+
// launcher 在空的 .boe/runtime 找不到版本会触发重装。spawn linco 时子进程
|
|
122
|
+
// CODEM_HOME 指到本函数产物,linco(含 LINCO_HOME 派生的全部状态)自动跟随。
|
|
123
|
+
export function resolveStateHome(codemHome, env = process.env) {
|
|
124
|
+
const lane = resolveTtEnvLane(env);
|
|
125
|
+
if (!lane?.isBoe) {
|
|
126
|
+
return codemHome;
|
|
127
|
+
}
|
|
128
|
+
// 幂等:daemon 把 linco 子进程 CODEM_HOME 设为已派生的状态根并透传泳道变量,
|
|
129
|
+
// linco 回调 bridge CLI 的嵌套进程会再走一次派生——basename 已是 .boe 时
|
|
130
|
+
// 原样返回,否则出现 .boe/.boe 双目录(首次 BOE 登录 / AK 刷新读写错位)。
|
|
131
|
+
if (path.basename(codemHome) === ".boe") {
|
|
132
|
+
return codemHome;
|
|
133
|
+
}
|
|
134
|
+
return path.join(codemHome, ".boe");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 泳道作用域文件名:/v1/models 这类**按泳道头返回不同内容**的缓存,必须以
|
|
138
|
+
// 完整泳道值分文件——boe_a 与 boe_b、裸 ppe 与 prod 的 catalog 都可能不同,
|
|
139
|
+
// 只分 boe/非 boe 两档会让跨泳道切换在 TTL 内复用错误 endpoint(与 linco
|
|
140
|
+
// model_discovery::cache_file_name 同契约)。泳道名字符集由 parseTtEnvLane
|
|
141
|
+
// 限定 [A-Za-z0-9_-],可安全嵌入文件名。
|
|
142
|
+
export function laneScopedFileName(baseName, env = process.env) {
|
|
143
|
+
const lane = resolveTtEnvLane(env);
|
|
144
|
+
if (!lane) {
|
|
145
|
+
return baseName;
|
|
146
|
+
}
|
|
147
|
+
const dot = baseName.lastIndexOf(".");
|
|
148
|
+
return dot > 0
|
|
149
|
+
? `${baseName.slice(0, dot)}.${lane.value}${baseName.slice(dot)}`
|
|
150
|
+
: `${baseName}.${lane.value}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 启动日志一行(无泳道时 null):泳道头漏注入的故障模式是"静默打到线上",
|
|
154
|
+
// 这行日志是运行现场唯一的可观测抓手。
|
|
155
|
+
export function describeTtEnvLane(env = process.env) {
|
|
156
|
+
const lane = resolveTtEnvLane(env);
|
|
157
|
+
if (!lane) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const parts = [`CODEM_X_TT_ENV=${lane.value}`];
|
|
161
|
+
if (lane.isBoe) {
|
|
162
|
+
parts.push(`host→${BOE_ROUTER_HOST}`);
|
|
163
|
+
}
|
|
164
|
+
if (lane.xTtEnv) {
|
|
165
|
+
parts.push(`x-tt-env:${lane.xTtEnv}`);
|
|
166
|
+
}
|
|
167
|
+
if (lane.usePpe) {
|
|
168
|
+
parts.push("x-use-ppe:1");
|
|
169
|
+
}
|
|
170
|
+
return parts.join(" ");
|
|
171
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Observation only. This module runs before either business Bridge (including
|
|
2
|
+
// an unmodified stable CLI), and imports only stdlib and shared lane rules.
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import https from 'node:https';
|
|
5
|
+
import { rewriteUrlForTtEnv, ttEnvHeadersFor } from './tt-env.js';
|
|
6
|
+
|
|
7
|
+
export function workerRuntimeTelemetryTarget({env=process.env,args=process.argv.slice(2)}={}) {
|
|
8
|
+
if (/^(1|true|yes|on)$/i.test(env.CODEM_TELEMETRY_DISABLED || '') || !env.CODEM_TOKEN || !env.CODEM_SANDBOX_DISPATCH_ID) return null;
|
|
9
|
+
// Mirror daemon's Commander options without importing business Bridge code:
|
|
10
|
+
// optional --sandbox [mode], required --server <url>, last occurrence wins.
|
|
11
|
+
let sandbox,serverFlag;
|
|
12
|
+
for(let index=0;index<args.length;index++) {
|
|
13
|
+
const arg=args[index];
|
|
14
|
+
if(arg==='--')break;
|
|
15
|
+
if(arg==='--sandbox') {
|
|
16
|
+
sandbox=args[index+1] && !args[index+1].startsWith('-') ? args[++index] : true;
|
|
17
|
+
} else if(arg.startsWith('--sandbox=')) {
|
|
18
|
+
sandbox=arg.slice('--sandbox='.length);
|
|
19
|
+
} else if(arg==='--server') {
|
|
20
|
+
if(!args[index+1] || args[index+1].startsWith('-'))return null;
|
|
21
|
+
serverFlag=args[++index];
|
|
22
|
+
} else if(arg.startsWith('--server=')) {
|
|
23
|
+
serverFlag=arg.slice('--server='.length);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if(sandbox!==undefined && sandbox!==true && sandbox!=='native')return null;
|
|
27
|
+
const native=sandbox==='native';
|
|
28
|
+
// Native rejects --server and ignores legacy sandbox URL aliases. Cloud's
|
|
29
|
+
// --server default is CODEM_SERVER_URL, before the sandbox-config fallbacks.
|
|
30
|
+
if(native && serverFlag!==undefined)return null;
|
|
31
|
+
const candidates=native ? [env.CODEM_SERVER_URL,'https://codem.feishu.cn'] : [serverFlag===undefined ? env.CODEM_SERVER_URL : serverFlag,env.CODEM_SANDBOX_SERVER_URL,env.SANDBOX_SERVER_URL,env.SERVER_URL,'http://localhost:3000'];
|
|
32
|
+
const server=candidates.find(value=>typeof value==='string' && value.trim()).trim();
|
|
33
|
+
const url=new URL(rewriteUrlForTtEnv(server,env));
|
|
34
|
+
if (!['http:','https:'].includes(url.protocol) || url.username || url.password) return null;
|
|
35
|
+
url.pathname=`${url.pathname.replace(/\/+$/,'') || '/goapi/codem'}/sandbox/telemetry/workflow_metrics`;
|
|
36
|
+
url.search='';url.hash='';
|
|
37
|
+
return {url,headers:{'Content-Type':'application/json',Authorization:`Bearer sandbox_${env.CODEM_TOKEN}`,...ttEnvHeadersFor(url,env)}};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function reportWorkerRuntime(runtime,{env=process.env,args=process.argv.slice(2),signal,timeoutMS=750}={}) {
|
|
41
|
+
try {
|
|
42
|
+
const target=workerRuntimeTelemetryTarget({env,args});
|
|
43
|
+
if (!target || signal?.aborted) return false;
|
|
44
|
+
const body=JSON.stringify({worker_runtime:{dispatch_id:env.CODEM_SANDBOX_DISPATCH_ID,family:runtime.family,cli_version:runtime.cliVersion,core_version:runtime.coreVersion}});
|
|
45
|
+
return await new Promise(resolve=>{
|
|
46
|
+
let timer,finished=false;
|
|
47
|
+
const done=value=>{if(finished)return;finished=true;clearTimeout(timer);resolve(value);};
|
|
48
|
+
const request=(target.url.protocol==='https:'?https:http).request(target.url,{method:'POST',headers:{...target.headers,'Content-Length':Buffer.byteLength(body)},signal},response=>{
|
|
49
|
+
let payload='';
|
|
50
|
+
response.on('data',chunk=>{payload+=chunk;if(payload.length>4096){done(false);request.destroy();}});
|
|
51
|
+
response.once('error',()=>done(false));
|
|
52
|
+
response.once('end',()=>{try{done(response.statusCode===200 && JSON.parse(payload).accepted===true);}catch{done(false);}});
|
|
53
|
+
});
|
|
54
|
+
request.once('error',()=>done(false));
|
|
55
|
+
timer=setTimeout(()=>{done(false);request.destroy();},timeoutMS);
|
|
56
|
+
timer.unref();
|
|
57
|
+
request.end(body);
|
|
58
|
+
});
|
|
59
|
+
} catch { return false; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function observeWorkerRuntime(runtime,options={}) {
|
|
63
|
+
const controller=new AbortController();
|
|
64
|
+
let timer;
|
|
65
|
+
// Native's dispatch token may only be materialized by the child's first
|
|
66
|
+
// normal Sync. Bounded retries do not acquire a lease or replay a task.
|
|
67
|
+
const delays=[1000,5000,30000];
|
|
68
|
+
const attempt=async index=>{
|
|
69
|
+
const accepted=await reportWorkerRuntime(runtime,{...options,signal:controller.signal});
|
|
70
|
+
if(accepted || controller.signal.aborted || index===delays.length)return;
|
|
71
|
+
timer=setTimeout(()=>void attempt(index+1),delays[index]);timer.unref();
|
|
72
|
+
};
|
|
73
|
+
void attempt(0);
|
|
74
|
+
return ()=>{clearTimeout(timer);controller.abort();};
|
|
75
|
+
}
|
package/bin/worker-runtime.js
CHANGED
|
@@ -7,8 +7,10 @@ import { constants as fsConstants } from 'node:fs';
|
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { pathToFileURL } from 'node:url';
|
|
10
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
10
11
|
import { applyHostedWindowsChildOptions } from './platform-child-process.js';
|
|
11
12
|
import { resolveEffectiveLincoHome, resolveLincoSessionsRoot, sameRecoveryPath } from './linco-paths.js';
|
|
13
|
+
import { observeWorkerRuntime } from './worker-runtime-telemetry.js';
|
|
12
14
|
|
|
13
15
|
export const WORKER_HOST_HOME = process.env.CODEM_WORKER_HOST_HOME || path.join(os.homedir(), '.codem');
|
|
14
16
|
export const WORKER_RUNTIME_ROOT = path.join(WORKER_HOST_HOME, 'worker-runtimes');
|
|
@@ -29,7 +31,7 @@ async function confinedFile(root, relative) {
|
|
|
29
31
|
|
|
30
32
|
// Only installers write these immutable pair manifests. No npm resolution or
|
|
31
33
|
// floating global/current Core lookup happens in the execution process.
|
|
32
|
-
export async function readWorkerRuntime(root, family, { version = null } = {}) {
|
|
34
|
+
export async function readWorkerRuntime(root, family, { version = null, generation = null } = {}) {
|
|
33
35
|
if (!families.includes(family)) throw new Error('invalid worker runtime');
|
|
34
36
|
const familyRoot = path.join(root, family);
|
|
35
37
|
let pointerChannel = null;
|
|
@@ -39,14 +41,23 @@ export async function readWorkerRuntime(root, family, { version = null } = {}) {
|
|
|
39
41
|
const pointer = await readJSON(path.join(familyRoot, 'current.json'));
|
|
40
42
|
if (!EXACT_RUNTIME_VERSION.test(pointer.version)) throw new Error('invalid runtime pointer');
|
|
41
43
|
pointerChannel = pointer.channel;
|
|
42
|
-
|
|
44
|
+
version = pointer.version;
|
|
45
|
+
generation = pointer.generation ?? null;
|
|
46
|
+
runtimeRoot = path.join(familyRoot, 'versions', version);
|
|
43
47
|
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
48
|
+
if (generation !== null) {
|
|
49
|
+
if (typeof generation !== 'string' || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(generation) || !version) throw new Error('invalid runtime generation');
|
|
50
|
+
runtimeRoot = path.join(familyRoot, 'generations', generation, 'versions', version);
|
|
51
|
+
}
|
|
52
|
+
const relative = path.relative(await fs.realpath(familyRoot), await fs.realpath(runtimeRoot));
|
|
53
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error('runtime generation escapes family');
|
|
44
54
|
const manifest = await readJSON(path.join(runtimeRoot, 'runtime-manifest.json'));
|
|
45
55
|
if (manifest.schema !== 1 || manifest.family !== family || !EXACT_RUNTIME_VERSION.test(manifest.cliVersion) || !EXACT_RUNTIME_VERSION.test(manifest.coreVersion)) throw new Error('invalid runtime manifest');
|
|
46
56
|
if (manifest.workflowPrebound !== (family === 'current')) throw new Error('runtime architecture does not match family');
|
|
57
|
+
if (version && manifest.cliVersion !== version) throw new Error('runtime pointer version does not match pair');
|
|
47
58
|
const cliEntry = await confinedFile(runtimeRoot, manifest.cliEntry);
|
|
48
59
|
const corePath = await confinedFile(runtimeRoot, manifest.coreBinaryPath);
|
|
49
|
-
return { ...manifest, ...(pointerChannel ? { channel: pointerChannel } : {}), root: runtimeRoot, cliEntry, corePath };
|
|
60
|
+
return { ...manifest, ...(pointerChannel ? { channel: pointerChannel } : {}), generation, root: runtimeRoot, cliEntry, corePath };
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
function routeIdentity(env) {
|
|
@@ -146,6 +157,9 @@ async function historicalFamily(sessions, session, lincoHome) {
|
|
|
146
157
|
}
|
|
147
158
|
|
|
148
159
|
async function promoteRoute(markerPath, scope) {
|
|
160
|
+
// Join any first publisher before promotion, so a delayed stable reservation
|
|
161
|
+
// cannot replace a current record published from durable recovery evidence.
|
|
162
|
+
if (await recordRoute(markerPath, scope, 'current') === 'current') return;
|
|
149
163
|
const temporary = `${markerPath}.${randomUUID()}.tmp`;
|
|
150
164
|
try {
|
|
151
165
|
await fs.mkdir(path.dirname(markerPath), { recursive: true, mode: 0o700 });
|
|
@@ -155,7 +169,16 @@ async function promoteRoute(markerPath, scope) {
|
|
|
155
169
|
}
|
|
156
170
|
async function readRoute(markerPath, scope) {
|
|
157
171
|
try {
|
|
158
|
-
|
|
172
|
+
let contents = await fs.readFile(markerPath, 'utf8');
|
|
173
|
+
// Exclusive creation reserves the name before complete-log rename on TOS.
|
|
174
|
+
// Only the empty reservation is transient; corrupt records fail immediately.
|
|
175
|
+
// A killed publisher leaves refusal evidence, never a reusable empty slot.
|
|
176
|
+
for (let attempt = 0; contents === '' && attempt < 100; attempt++) {
|
|
177
|
+
await delay(50);
|
|
178
|
+
contents = await fs.readFile(markerPath, 'utf8');
|
|
179
|
+
}
|
|
180
|
+
if (contents === '') throw new Error('runtime route publication incomplete');
|
|
181
|
+
const marker = JSON.parse(contents);
|
|
159
182
|
if (marker.schema !== 1 || marker.scope !== scope || !families.includes(marker.family)) throw new Error('identity or schema mismatch');
|
|
160
183
|
return marker.family;
|
|
161
184
|
} catch (error) {
|
|
@@ -168,8 +191,15 @@ async function recordRoute(markerPath, scope, family) {
|
|
|
168
191
|
const temporary = `${markerPath}.${randomUUID()}.tmp`;
|
|
169
192
|
try {
|
|
170
193
|
await fs.writeFile(temporary, `${JSON.stringify({ schema: 1, scope, family })}\n`, { mode: 0o600, flag: 'wx' });
|
|
171
|
-
//
|
|
172
|
-
|
|
194
|
+
// TOS rejects hard links and no-replace rename. Only the exclusive creator
|
|
195
|
+
// may replace its empty reservation with the complete record. Losers read it.
|
|
196
|
+
let reservation;
|
|
197
|
+
try { reservation = await fs.open(markerPath, 'wx', 0o600); }
|
|
198
|
+
catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
199
|
+
if (reservation) {
|
|
200
|
+
await reservation.close();
|
|
201
|
+
await fs.rename(temporary, markerPath);
|
|
202
|
+
}
|
|
173
203
|
} finally { await fs.rm(temporary, { force: true }); }
|
|
174
204
|
return readRoute(markerPath, scope);
|
|
175
205
|
}
|
|
@@ -239,10 +269,11 @@ export function launchWorkerRuntime(runtime, { args = process.argv.slice(2), env
|
|
|
239
269
|
CODEM_LINCO_PATH: runtime.corePath, CODEM_WORKER_RUNTIME_ROOT: runtime.runtimeRoot, CODEM_WORKER_RUNTIME_READY: '1', CODEM_WORKER_RUNTIME_ACTUAL: runtime.family, CODEM_WORKER_CLI_VERSION: runtime.cliVersion, CODEM_WORKER_CORE_VERSION: runtime.coreVersion,
|
|
240
270
|
CODEM_PLUGIN_NODE: env.CODEM_PLUGIN_NODE || nodePath,
|
|
241
271
|
CODEM_SKIP_UPDATE_CHECK: '1', CODEM_DISABLE_DAEMON_AUTO_UPDATE: '1' } }));
|
|
272
|
+
const stopObservation=observeWorkerRuntime(runtime,{env,args});
|
|
242
273
|
const signals = ['SIGTERM', 'SIGINT', 'SIGHUP'];
|
|
243
274
|
const handlers = signals.map(signal => () => { if (child.exitCode === null && child.signalCode === null) { try { child.kill(signal); } catch {} } });
|
|
244
275
|
signals.forEach((signal, index) => process.on(signal, handlers[index]));
|
|
245
|
-
const cleanup = () => signals.forEach((signal, index) => process.off(signal, handlers[index]));
|
|
276
|
+
const cleanup = () => {stopObservation();signals.forEach((signal, index) => process.off(signal, handlers[index]));};
|
|
246
277
|
child.once('error', error => { cleanup(); reject(error); });
|
|
247
278
|
child.once('exit', (code, signal) => { cleanup(); resolve(code ?? (signal ? 128 + (os.constants.signals[signal] || 1) : 1)); });
|
|
248
279
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-codem/codem-cli",
|
|
3
|
-
"version": "0.1.269
|
|
3
|
+
"version": "0.1.269",
|
|
4
4
|
"codemServiceManagementProtocol": 3,
|
|
5
5
|
"codemWorkerRuntimeProtocol": 1,
|
|
6
6
|
"codemProfileProtocol": 1,
|
|
@@ -30,17 +30,17 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@lark-codem/codem-core": "0.8.86
|
|
33
|
+
"@lark-codem/codem-core": "0.8.86",
|
|
34
34
|
"zod": "3.25.76",
|
|
35
35
|
"@lark-codem/lark-cli": "0.1.1"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|
|
38
|
-
"@lark-codem/codem-cli-darwin-arm64": "0.1.269
|
|
39
|
-
"@lark-codem/codem-cli-darwin-x64": "0.1.269
|
|
40
|
-
"@lark-codem/codem-cli-linux-arm64": "0.1.269
|
|
41
|
-
"@lark-codem/codem-cli-linux-x64": "0.1.269
|
|
42
|
-
"@lark-codem/codem-cli-win32-arm64": "0.1.269
|
|
43
|
-
"@lark-codem/codem-cli-win32-x64": "0.1.269
|
|
38
|
+
"@lark-codem/codem-cli-darwin-arm64": "0.1.269",
|
|
39
|
+
"@lark-codem/codem-cli-darwin-x64": "0.1.269",
|
|
40
|
+
"@lark-codem/codem-cli-linux-arm64": "0.1.269",
|
|
41
|
+
"@lark-codem/codem-cli-linux-x64": "0.1.269",
|
|
42
|
+
"@lark-codem/codem-cli-win32-arm64": "0.1.269",
|
|
43
|
+
"@lark-codem/codem-cli-win32-x64": "0.1.269"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
|
46
46
|
"node": ">=18"
|
package/profile/runtime-logs.js
CHANGED
|
@@ -11,11 +11,43 @@ const CAPACITY = 256;
|
|
|
11
11
|
const BATCH_SIZE = 32;
|
|
12
12
|
const INTERVAL_MS = 10_000;
|
|
13
13
|
const REQUEST_TIMEOUT_MS = 3_000;
|
|
14
|
+
const MAX_BATCH_BYTES = 256 * 1024;
|
|
15
|
+
const LIFECYCLE_EVENT_NAME = "codem_bridge_lifecycle";
|
|
16
|
+
const LIFECYCLE_COMPONENTS = {
|
|
17
|
+
daemon_startup: "daemon", runtime_sync: "runtime-post", command_handoff: "bridge",
|
|
18
|
+
runner_operation: "bridge", delivery_settled: "runtime-post", runtime_state: "bridge",
|
|
19
|
+
event_discarded: "runtime-post", process_lifecycle: "linco"
|
|
20
|
+
};
|
|
21
|
+
const LIFECYCLE_METRICS = [
|
|
22
|
+
"duration_ms", "ack_settle_duration_ms", "startup_wait_ms", "operation_age_ms",
|
|
23
|
+
"outage_duration_ms", "consecutive_failures", "deferred_age_ms", "pending_command_ack_count",
|
|
24
|
+
"pending_ack_age_ms", "pending_event_count", "oldest_event_age_ms", "lifetime_ms"
|
|
25
|
+
];
|
|
26
|
+
const LIFECYCLE_DIMENSIONS = {
|
|
27
|
+
outcome: ["success", "failed", "rejected", "canceled", "unknown", "accepted", "duplicate", "conflicting", "observed", "recovered"],
|
|
28
|
+
operation: ["prepare", "spawn", "ready", "cancel", "shutdown"],
|
|
29
|
+
scope: ["startup", "runner", "sync", "command", "queue"],
|
|
30
|
+
kind: ["command_ack", "runtime_event", "control", "events", "relay", "offline"],
|
|
31
|
+
phase: ["startup_marker", "prelude", "healthcheck", "config_load", "update_check", "service_reconcile", "login_gate", "launch", "authentication", "singleton", "initialize", "start_client", "first_sync", "trusted_device",
|
|
32
|
+
"build_request", "request", "validate_response", "apply_response", "persist_state",
|
|
33
|
+
"admission", "handler", "resolve_binary", "build_env", "host_capabilities", "parse_capabilities",
|
|
34
|
+
"strict_resume", "verify_binding", "prepare", "spawn", "ready", "cancel", "teardown"],
|
|
35
|
+
reason: ["overflow", "oversized", "timeout", "cleanup_failed", "unsupported_command", "invalid_payload",
|
|
36
|
+
"shutting_down_handoff", "handoff_target_not_active", "handler_inflight", "duplicate_inflight",
|
|
37
|
+
"runtime_unavailable", "runtime_environment_failed", "capability_probe_failed", "runtime_binary_changed",
|
|
38
|
+
"strict_resume_unsupported", "turn_context_unsupported", "active_run_protocol_mismatch", "trusted_device_blocked",
|
|
39
|
+
"invalid_runtime_event", "event_rejected", "task_terminated:canceled", "task_terminated:failed", "task_terminated:succeeded"],
|
|
40
|
+
error_source: ["bridge", "base", "linco", "transport"],
|
|
41
|
+
stability: ["stable", "unstable", "unknown"],
|
|
42
|
+
action: ["exit", "restart"],
|
|
43
|
+
expected: ["true", "false", "unknown"],
|
|
44
|
+
critical: ["true", "false"]
|
|
45
|
+
};
|
|
14
46
|
const TRUSTED_DEVICE_EVENTS = new Set(["host_identity_collection", "host_identity_command", "host_session_attempt", "host_session_expired", "trusted_device_admission_blocked", "trusted_device_admission_recovered"]);
|
|
15
47
|
const TRUSTED_DEVICE_PHASES = ["auth", "identity", "provider", "host_key", "activation_state", "activation_apply", "activation_sign", "activation_complete", "credential_resolve", "session_apply", "session_sign", "session_complete", "trusted_device"];
|
|
16
48
|
const TRUSTED_DEVICE_DIMENSIONS = {
|
|
17
49
|
phase: TRUSTED_DEVICE_PHASES,
|
|
18
|
-
operation: ["create", "renew", "recovery", "platform_identity", "network_interfaces", "network_ports"],
|
|
50
|
+
operation: ["create", "renew", "recovery", "platform_identity", "network_interfaces", "network_ports", "serial_number", "platform_uuid", "network_adapters"],
|
|
19
51
|
outcome: ["success", "partial", "failed", "ready", "not_registered", "no_auth", "reset", "blocked", "invalid", "verified", "not_required", "absent", "expired"],
|
|
20
52
|
failure_kind: ["none", "timeout", "exec_failed", "invalid_json", "empty_identity", "read_failed", "authorization", "unknown"]
|
|
21
53
|
};
|
|
@@ -45,11 +77,13 @@ const OBSERVATION_METRICS = {
|
|
|
45
77
|
diagnostic_reporter_health: ["dropped_delta", "upload_failure_delta", "pending_event_count"]
|
|
46
78
|
};
|
|
47
79
|
const EXTRA_FIELDS = ["observation_id", "runtime_client_instance_id", "command_id", "delivery_id", "probe_id", "sync_id", "transport_request_id", "attempt_id"];
|
|
80
|
+
const LIFECYCLE_EXTRA_FIELDS = [...EXTRA_FIELDS, "process_instance_id", "startup_id", "request_id", "event_id"];
|
|
48
81
|
const STRING_FIELDS = new Set([
|
|
49
82
|
...EXTRA_FIELDS, "command_type", "ack_status", "ack_outcome", "readback_scope", "timing_origin", "required_capability",
|
|
50
83
|
"request_id", "command_id", "session_id", "task_id", "dispatch_id", "event_id", "sync_id",
|
|
51
84
|
"process_instance_id", "run_id", "stage", "phase", "status", "outcome", "reason", "action",
|
|
52
85
|
"error_class", "error_code", "error_kind", "error_type", "code", "signal", "stop_reason",
|
|
86
|
+
"first_error", "retry_source", "cleanup_result",
|
|
53
87
|
"terminal_event", "event_type", "actor_type", "source", "trigger", "hook_id", "hook_event",
|
|
54
88
|
"target_version", "cli_version", "core_version", "version", "previous_state", "state",
|
|
55
89
|
"failure_kind", "failure_impact", "failure_type", "failure_subtype", "failure_code", "failure_status", "delay_source", "mode", "provider", "name", "termination", "interaction_id", "interaction_kind", "owner_state", "artifact_id", "resource_type", "delivery_mode", "host", "scheme", "tool_call_id", "tool_name", "background_task_id", "model", "artifact_kind", "codem_user_id", "device_id", "operation", "foreground_error_code",
|
|
@@ -59,11 +93,12 @@ const VALUE_FIELDS = new Set([
|
|
|
59
93
|
"session_retained",
|
|
60
94
|
...new Set(Object.values(OBSERVATION_METRICS).flat()),
|
|
61
95
|
"pid", "exit_code", "duration_ms", "elapsed_ms", "wait_ms", "run_ms", "total_ms", "took_ms",
|
|
62
|
-
"attempt", "attempts", "attempt_count", "retryable", "http_status", "active_task_count",
|
|
96
|
+
"attempt", "attempts", "attempt_count", "retryable", "http_status", "errno", "active_task_count",
|
|
63
97
|
"pending_event_count", "pending_command_ack_count", "next_retry_ms", "consecutive_failures",
|
|
64
98
|
"outage_duration_ms", "startup_duration_ms", "failed_attempts", "active_runs", "completed",
|
|
65
99
|
"delivered", "result_delivered", "queued_behind_existing", "expected", "timed_out",
|
|
66
|
-
"force_exit_ms", "teardown_ceiling_ms", "recovered", "count", "ms_since_process_start", "owner_pid", "consumed", "delivery_attempt", "ack_deadline_ms", "command_to_bridge_ms", "size_bytes", "source_size_bytes", "processed_bytes", "before", "after", "omitted_detail_count", "opened", "selected", "foreground"
|
|
100
|
+
"force_exit_ms", "teardown_ceiling_ms", "recovered", "count", "ms_since_process_start", "owner_pid", "consumed", "delivery_attempt", "ack_deadline_ms", "command_to_bridge_ms", "size_bytes", "source_size_bytes", "processed_bytes", "before", "after", "omitted_detail_count", "opened", "selected", "foreground",
|
|
101
|
+
...LIFECYCLE_METRICS
|
|
67
102
|
]);
|
|
68
103
|
|
|
69
104
|
function token(value) {
|
|
@@ -89,7 +124,7 @@ function extraProjection(fields = {}) {
|
|
|
89
124
|
return Object.fromEntries(EXTRA_FIELDS.filter(key => token(fields?.[key])).map(key => [key, token(fields[key])]));
|
|
90
125
|
}
|
|
91
126
|
|
|
92
|
-
function projectFields(input) {
|
|
127
|
+
function projectFields(input, fingerprintErrors = true) {
|
|
93
128
|
const result = {};
|
|
94
129
|
for (const [key, value] of Object.entries(input ?? {}).slice(0, 100)) {
|
|
95
130
|
const name = fieldName(key);
|
|
@@ -100,6 +135,7 @@ function projectFields(input) {
|
|
|
100
135
|
result[name] = value;
|
|
101
136
|
}
|
|
102
137
|
}
|
|
138
|
+
if (!fingerprintErrors) return result;
|
|
103
139
|
const error = input?.error ?? input?.terminalError;
|
|
104
140
|
const fingerprint = input?.error_fingerprint ?? input?.errorFingerprint;
|
|
105
141
|
if (typeof fingerprint === "string" && /^[a-f0-9]{24}$/u.test(fingerprint)) result.error_fingerprint = fingerprint;
|
|
@@ -122,6 +158,36 @@ function projectFields(input) {
|
|
|
122
158
|
return result;
|
|
123
159
|
}
|
|
124
160
|
|
|
161
|
+
// Build the display summary from classified fields. Keep free-form text at
|
|
162
|
+
// its source so collecting telemetry never scans upstream or local messages.
|
|
163
|
+
function lifecycleErrorSummary(dimensions, fields) {
|
|
164
|
+
return [
|
|
165
|
+
dimensions.phase ?? dimensions.operation ?? dimensions.scope,
|
|
166
|
+
dimensions.error_source,
|
|
167
|
+
dimensions.error_code,
|
|
168
|
+
dimensions.reason !== dimensions.error_code ? dimensions.reason : undefined,
|
|
169
|
+
Number.isSafeInteger(fields.errno) && fields.errno !== 0 ? `errno=${fields.errno}` : undefined,
|
|
170
|
+
Number.isSafeInteger(fields.exit_code) ? `exit_code=${fields.exit_code}` : undefined
|
|
171
|
+
].filter(Boolean).join(" · ");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function lifecycleErrorCode(fields) {
|
|
175
|
+
const code = token(fields.error_code ?? fields.error?.code ?? fields.error?.cause?.code)?.toLowerCase();
|
|
176
|
+
if (code && /^(?:e(?:connreset|connrefused|pipe|timedout|netunreach|hostunreach|notfound|ai_again|acces|perm|noent|exist|nospace|nospc|io)|runtime_post_[a-z_]{1,48}|http_[1-5]\d\d|(?:[a-z_]{1,35}_)?timeout|not_logged_in|unsupported_command|invalid_payload|invalid_response|relogin_required|rotation_pending|cleanup_failed|oversized|overflow|unexpected_exit|spawn_failed|ready_missing|prepare_failed|ack_rejected|ack_conflicting)$/u.test(code)) return code;
|
|
177
|
+
const status = Number(fields.http_status);
|
|
178
|
+
if (Number.isInteger(status) && status >= 100 && status <= 599) return `http_${status}`;
|
|
179
|
+
return fields.outcome === "failed" || fields.outcome === "rejected" || fields.outcome === "conflicting" ? "unknown" : undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function keepLegacyMetric(event, fields, role, severity) {
|
|
183
|
+
if (/^startup\.(?:main|update_check|daemon_service_reconcile|login_gate|login_handoff_to_linco)$/u.test(event) && severity === "info") return false;
|
|
184
|
+
if (/(?:^|\.)drain_(?:started|completed)$/u.test(event) && severity === "info") return false;
|
|
185
|
+
if (role === "daemon" && event === "runtime_capability_probe" && severity === "info") return false;
|
|
186
|
+
if (role === "daemon" && ["sync_ready", "sync_failed", "sync_recovered", "shutdown_completed"].includes(event)) return false;
|
|
187
|
+
if (event === "command_ack" && ["settled", "pending"].includes(fields.stage)) return false;
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
125
191
|
export function runtimeLogIdentity(config) {
|
|
126
192
|
const oauth = optional(() => config?.auth?.oauth);
|
|
127
193
|
return {
|
|
@@ -134,6 +200,15 @@ export function runtimeLogIdentity(config) {
|
|
|
134
200
|
};
|
|
135
201
|
}
|
|
136
202
|
|
|
203
|
+
// Prefer an operation's attribution over the identity of its eventual sender.
|
|
204
|
+
export function runtimeObservationTenant(value) {
|
|
205
|
+
return optional(() => {
|
|
206
|
+
const payload = value?.payload ?? value;
|
|
207
|
+
const context = payload?.telemetryContext ?? payload?.telemetry_context;
|
|
208
|
+
return token(context?.tenantId ?? context?.tenant_id ?? payload?.tenantId ?? payload?.tenant_id ?? value?.tenantId ?? value?.tenant_id);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
137
212
|
export class RuntimeLogReporter {
|
|
138
213
|
#live = { queue: [], inflight: undefined, abort: undefined };
|
|
139
214
|
#replay = { queue: [], inflight: undefined, abort: undefined };
|
|
@@ -153,6 +228,8 @@ export class RuntimeLogReporter {
|
|
|
153
228
|
#platform;
|
|
154
229
|
#osVersion;
|
|
155
230
|
#crashDirectory;
|
|
231
|
+
#restoring;
|
|
232
|
+
#savedDuringRestore = new Set();
|
|
156
233
|
|
|
157
234
|
constructor({ role = "cli", version = "unknown", send = sendSlardarPcBatch, instanceId = randomUUID(), diagnosticId: supportId = diagnosticId(), platform = process.platform, osVersion = os.release(), crashDirectory } = {}) {
|
|
158
235
|
this.#send = send;
|
|
@@ -165,7 +242,7 @@ export class RuntimeLogReporter {
|
|
|
165
242
|
this.#osVersion = osVersion;
|
|
166
243
|
this.#timer = setInterval(() => { void this.flush(); }, INTERVAL_MS);
|
|
167
244
|
this.#timer.unref?.();
|
|
168
|
-
this.#restoreCrashes();
|
|
245
|
+
this.#restoring = this.#restoreCrashes().finally(() => { this.#savedDuringRestore = undefined; });
|
|
169
246
|
}
|
|
170
247
|
|
|
171
248
|
// Crash files hold already-projected records and the identity at failure time.
|
|
@@ -173,33 +250,48 @@ export class RuntimeLogReporter {
|
|
|
173
250
|
// deliverNow also queues the record for this process's next flush. A
|
|
174
251
|
// successful send deletes its file; otherwise the next process replays it.
|
|
175
252
|
persistCrash(component, event, fields = {}, { deliverNow = false } = {}) {
|
|
176
|
-
let entry;
|
|
177
|
-
let crashFile;
|
|
178
253
|
try {
|
|
179
254
|
if (this.#closed || (!this.#crashDirectory && !deliverNow)) return;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (
|
|
255
|
+
const entry = this.#logEntry("fatal", component, event, fields);
|
|
256
|
+
this.#saveCrash(entry);
|
|
257
|
+
if (deliverNow) this.#replay.queue.push(entry);
|
|
258
|
+
} catch { /* Crash diagnostics must preserve the original exit behavior. */ }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#saveCrash(entry) {
|
|
262
|
+
if (!this.#crashDirectory) return;
|
|
263
|
+
try {
|
|
183
264
|
fs.mkdirSync(this.#crashDirectory, { recursive: true, mode: 0o700 });
|
|
184
265
|
const names = fs.readdirSync(this.#crashDirectory).filter(name => /^js-[a-f0-9-]+\.json$/u.test(name));
|
|
185
266
|
if (names.length >= 32) return;
|
|
186
|
-
const
|
|
267
|
+
const name = `js-${randomUUID()}.json`;
|
|
268
|
+
const file = path.join(this.#crashDirectory, name);
|
|
187
269
|
fs.writeFileSync(file, JSON.stringify(entry), { flag: "wx", mode: 0o600 });
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
270
|
+
this.#savedDuringRestore?.add(name);
|
|
271
|
+
entry.crashFile = file;
|
|
272
|
+
} catch { /* Local storage can be full or unavailable. */ }
|
|
192
273
|
}
|
|
193
274
|
|
|
194
|
-
#restoreCrashes() {
|
|
275
|
+
async #restoreCrashes() {
|
|
276
|
+
if (!this.#crashDirectory) return;
|
|
195
277
|
try {
|
|
196
|
-
for (const name of fs.
|
|
278
|
+
for (const name of (await fs.promises.readdir(this.#crashDirectory)).filter(name => /^js-[a-f0-9-]+\.json$/u.test(name)).slice(0, 32)) {
|
|
279
|
+
if (this.#closed) return;
|
|
280
|
+
if (this.#savedDuringRestore?.has(name)) continue;
|
|
197
281
|
const file = path.join(this.#crashDirectory, name);
|
|
198
282
|
try {
|
|
199
|
-
|
|
200
|
-
|
|
283
|
+
const stat = await fs.promises.lstat(file);
|
|
284
|
+
if (!stat.isFile() || stat.size > 65536) continue;
|
|
285
|
+
const entry = JSON.parse(await fs.promises.readFile(file, "utf8"));
|
|
201
286
|
if (entry?.record?.category?.reporter !== "codem_js" || entry?.record?.log_type !== "log" || !entry.header) continue;
|
|
202
|
-
this.#
|
|
287
|
+
if (this.#closed) return;
|
|
288
|
+
if (entry.metricName === LIFECYCLE_EVENT_NAME) {
|
|
289
|
+
entry.record.category.replay = "1";
|
|
290
|
+
this.#replay.queue.push({ ...entry, attempts: 0, crashFile: file });
|
|
291
|
+
} else {
|
|
292
|
+
this.#replay.queue.push({ header: entry.header, record: entry.record, metricName: entry.metricName,
|
|
293
|
+
metrics: metricProjection(entry.record.category.event, entry.metrics), extra: extraProjection(entry.extra), attempts: 0, crashFile: file });
|
|
294
|
+
}
|
|
203
295
|
} catch { /* A partial crash write cannot prevent startup. */ }
|
|
204
296
|
}
|
|
205
297
|
} catch { /* First startup or unavailable storage. */ }
|
|
@@ -229,12 +321,81 @@ export class RuntimeLogReporter {
|
|
|
229
321
|
record(level, component, event, fields = {}) {
|
|
230
322
|
try {
|
|
231
323
|
if (this.#closed) return false;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
324
|
+
return this.#enqueue(this.#logEntry(level, component, event, fields));
|
|
325
|
+
} catch { return false; }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#logEntry(level, component, event, fields) {
|
|
329
|
+
let safeFields;
|
|
330
|
+
try { safeFields = projectFields(fields); } catch { safeFields = {}; }
|
|
331
|
+
if (OBSERVATION_EVENTS.has(event)) safeFields.observation_id = randomUUID();
|
|
332
|
+
const severity = ["info", "warn", "error", "fatal"].includes(level) ? level : "info";
|
|
333
|
+
// Keep arbitrary upstream error codes out of trusted-device metric dimensions.
|
|
334
|
+
const trustedDevice = TRUSTED_DEVICE_EVENTS.has(event);
|
|
335
|
+
const dimensions = trustedDevice ? {} : Object.fromEntries(["stage", "error_class", "error_code", "error_kind", "outcome"]
|
|
336
|
+
.filter(key => safeFields[key]).map(key => [key, safeFields[key]]));
|
|
337
|
+
if (OBSERVATION_EVENTS.has(event)) {
|
|
338
|
+
for (const [key, values] of Object.entries(trustedDevice ? TRUSTED_DEVICE_DIMENSIONS : OBSERVATION_DIMENSIONS)) {
|
|
339
|
+
if (safeFields[key]) dimensions[key] = values.includes(safeFields[key]) ? safeFields[key] : "unknown";
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
...this.#entry(severity, component, event, safeFields, this.#identity, dimensions, true),
|
|
344
|
+
metricName: keepLegacyMetric(event, safeFields, this.#role, severity) ? "codem_runtime_diagnostic" : null,
|
|
345
|
+
metrics: metricProjection(event, safeFields), extra: extraProjection(safeFields)
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
recordLifecycle(event, fields = {}, { persist = false } = {}) {
|
|
350
|
+
try {
|
|
351
|
+
if (this.#closed || !Object.hasOwn(LIFECYCLE_COMPONENTS, event)) return false;
|
|
352
|
+
const unstable = fields.stability === "unstable" || fields.outcome === "failed" || fields.outcome === "conflicting";
|
|
353
|
+
const hasDetail = fields.detail ?? (persist || unstable || fields.outcome === "rejected" || fields.outcome === "unknown");
|
|
354
|
+
const severity = persist ? "fatal" : unstable ? "error" : "info";
|
|
355
|
+
const dimensions = {
|
|
356
|
+
stability: unstable ? "unstable" : fields.outcome === "unknown" ? "unknown" : "stable",
|
|
357
|
+
has_detail: hasDetail ? "1" : "0", replay: "0"
|
|
358
|
+
};
|
|
359
|
+
for (const [key, values] of Object.entries(LIFECYCLE_DIMENSIONS)) {
|
|
360
|
+
if (fields[key] != null && values.includes(String(fields[key]))) dimensions[key] = String(fields[key]);
|
|
361
|
+
}
|
|
362
|
+
const errorCode = lifecycleErrorCode(fields);
|
|
363
|
+
if (errorCode) dimensions.error_code = errorCode;
|
|
364
|
+
if (fields.command_type) dimensions.command_type = OBSERVATION_DIMENSIONS.command_type.includes(fields.command_type) ? fields.command_type : "other";
|
|
365
|
+
const metrics = Object.fromEntries(LIFECYCLE_METRICS
|
|
366
|
+
.filter(key => typeof fields[key] === "number" && Number.isFinite(fields[key]) && fields[key] >= 0)
|
|
367
|
+
.map(key => [key, Math.round(fields[key])]));
|
|
368
|
+
if (event !== "runtime_state") {
|
|
369
|
+
metrics.count = event === "delivery_settled" && Number.isSafeInteger(fields.count) && fields.count > 0 ? fields.count : 1;
|
|
370
|
+
}
|
|
371
|
+
const identity = {
|
|
372
|
+
...this.#identity,
|
|
373
|
+
...(Object.hasOwn(fields, "tenantId") ? { tenantId: token(fields.tenantId) } : {})
|
|
374
|
+
};
|
|
375
|
+
const { replay: _replay, ...logDimensions } = dimensions;
|
|
376
|
+
const summary = hasDetail ? lifecycleErrorSummary(dimensions, fields) : "";
|
|
377
|
+
const { reason: _reason, error_code: _errorCode, ...projected } = projectFields(fields, false);
|
|
378
|
+
const safeFields = {
|
|
379
|
+
...projected, ...logDimensions, ...metrics,
|
|
380
|
+
process_instance_id: this.#instanceId,
|
|
381
|
+
...(event === "daemon_startup" ? { startup_id: this.#instanceId } : {}),
|
|
382
|
+
observation_id: randomUUID(),
|
|
383
|
+
...(summary ? { error_summary: summary } : {})
|
|
384
|
+
};
|
|
385
|
+
const extra = Object.fromEntries(LIFECYCLE_EXTRA_FIELDS
|
|
386
|
+
.filter(key => token(safeFields[key])).map(key => [key, safeFields[key]]));
|
|
387
|
+
if (summary) extra.error_summary = summary;
|
|
388
|
+
const entry = {
|
|
389
|
+
...this.#entry(severity, LIFECYCLE_COMPONENTS[event], event, safeFields, identity, dimensions, hasDetail),
|
|
390
|
+
metricName: LIFECYCLE_EVENT_NAME, metrics, extra
|
|
391
|
+
};
|
|
392
|
+
if (persist && hasDetail) this.#saveCrash(entry);
|
|
393
|
+
return this.#enqueue(entry);
|
|
394
|
+
} catch { return false; }
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
#entry(severity, component, event, safeFields, identity, dimensions, includeLog) {
|
|
398
|
+
const category = {
|
|
238
399
|
...workerRuntimeAttributes(),
|
|
239
400
|
client_kind: "cli", runtime_role: this.#role, reporter: "codem_js", log_schema: "1",
|
|
240
401
|
component: token(component) ?? "unknown", event: token(event) ?? "unknown", severity,
|
|
@@ -242,16 +403,8 @@ export class RuntimeLogReporter {
|
|
|
242
403
|
diagnostic_id: this.#diagnosticId,
|
|
243
404
|
...(identity.tenantId ? { tenant_id: identity.tenantId } : {}),
|
|
244
405
|
...(identity.serverEnvironment ? { server_environment: identity.serverEnvironment } : {}),
|
|
245
|
-
...
|
|
246
|
-
|
|
247
|
-
if (OBSERVATION_EVENTS.has(category.event)) {
|
|
248
|
-
const trustedDevice = TRUSTED_DEVICE_EVENTS.has(category.event);
|
|
249
|
-
// Keep arbitrary upstream error codes in logs, not metric dimensions.
|
|
250
|
-
if (trustedDevice) for (const key of ["stage", "error_class", "error_code", "error_kind", "outcome"]) delete category[key];
|
|
251
|
-
for (const [key, values] of Object.entries(trustedDevice ? TRUSTED_DEVICE_DIMENSIONS : OBSERVATION_DIMENSIONS)) {
|
|
252
|
-
if (safeFields[key]) category[key] = values.includes(safeFields[key]) ? safeFields[key] : "unknown";
|
|
253
|
-
}
|
|
254
|
-
}
|
|
406
|
+
...dimensions
|
|
407
|
+
};
|
|
255
408
|
const header = {
|
|
256
409
|
aid: SLARDAR_PC_AID,
|
|
257
410
|
device_id: identity.deviceId ?? this.#instanceId,
|
|
@@ -261,31 +414,35 @@ export class RuntimeLogReporter {
|
|
|
261
414
|
os: this.#platform, os_version: this.#osVersion
|
|
262
415
|
};
|
|
263
416
|
const timestamp = Date.now();
|
|
264
|
-
const payload = {
|
|
417
|
+
const payload = includeLog ? {
|
|
265
418
|
schema_version: 1, timestamp, component: category.component, event: category.event, level: severity,
|
|
266
419
|
process_instance_id: this.#instanceId, diagnostic_id: this.#diagnosticId, pid: process.pid,
|
|
267
420
|
...(identity.tenantId ? { tenant_id: identity.tenantId } : {}),
|
|
268
421
|
fields: safeFields
|
|
269
|
-
};
|
|
422
|
+
} : undefined;
|
|
270
423
|
const record = {
|
|
271
424
|
app_version: this.#version, app_minor_version: "", build_id: "",
|
|
272
425
|
process_type: `codem-${this.#role}`, session_id: "", timestamp, category,
|
|
273
426
|
log_type: "log", log_level: severity === "fatal" ? "error" : severity,
|
|
274
|
-
log_content: `CodeM diagnostic ${severity} ${category.component}/${category.event}\n${JSON.stringify(payload)}`
|
|
427
|
+
...(payload ? { log_content: `CodeM diagnostic ${severity} ${category.component}/${category.event}\n${JSON.stringify(payload)}` } : {})
|
|
275
428
|
};
|
|
429
|
+
const criticalSettlement = event === "delivery_settled" && dimensions.critical === "true";
|
|
276
430
|
const priority = severity === "fatal" ? 3 :
|
|
277
431
|
["resumed", "settled"].includes(safeFields.stage) || (severity === "error" && !safeFields.suppressed_count) ? 2 :
|
|
278
|
-
severity === "info" ? 0 : 1;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
432
|
+
severity === "info" && !criticalSettlement ? 0 : 1;
|
|
433
|
+
return { header, record, includeLog, priority, attempts: 0, crashFile: undefined };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
#enqueue(entry) {
|
|
437
|
+
const queue = this.#live.queue;
|
|
438
|
+
if (queue.length >= CAPACITY) {
|
|
439
|
+
const minimum = Math.min(...queue.map(item => item.priority));
|
|
440
|
+
if (minimum > entry.priority) { this.#dropped += 1; return false; }
|
|
441
|
+
queue.splice(queue.findIndex(item => item.priority === minimum), 1);
|
|
442
|
+
this.#dropped += 1;
|
|
443
|
+
}
|
|
444
|
+
queue.push(entry);
|
|
445
|
+
return true;
|
|
289
446
|
}
|
|
290
447
|
|
|
291
448
|
async flush({ timeoutMs = REQUEST_TIMEOUT_MS } = {}) {
|
|
@@ -318,7 +475,7 @@ export class RuntimeLogReporter {
|
|
|
318
475
|
// Run both lanes concurrently: old replay cannot delay live sends. Waiting
|
|
319
476
|
// for both within one budget also gives short-lived invocations a chance
|
|
320
477
|
// to finish replay; a timeout leaves its original crash file for next time.
|
|
321
|
-
const replay = this.#flushLane(this.#replay, REQUEST_TIMEOUT_MS);
|
|
478
|
+
const replay = this.#restoring.then(() => this.#flushLane(this.#replay, REQUEST_TIMEOUT_MS));
|
|
322
479
|
let timer;
|
|
323
480
|
try {
|
|
324
481
|
await Promise.race([
|
|
@@ -349,16 +506,27 @@ export class RuntimeLogReporter {
|
|
|
349
506
|
const first = lane.queue[0];
|
|
350
507
|
const key = JSON.stringify(first.header);
|
|
351
508
|
const batch = [];
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
509
|
+
const data = [];
|
|
510
|
+
let bytes = 0;
|
|
511
|
+
while (batch.length < BATCH_SIZE && lane.queue.length && JSON.stringify(lane.queue[0].header) === key && Boolean(lane.queue[0].crashFile) === Boolean(first.crashFile)) {
|
|
512
|
+
const { record, metrics, extra, metricName = "codem_runtime_diagnostic", includeLog = true } = lane.queue[0];
|
|
513
|
+
const records = includeLog ? [record] : [];
|
|
514
|
+
if (metricName) records.push({
|
|
515
|
+
app_version: record.app_version, app_minor_version: "", build_id: "",
|
|
516
|
+
process_type: record.process_type, session_id: "", timestamp: record.timestamp,
|
|
517
|
+
log_type: "flexible", event_name: metricName, category: record.category,
|
|
518
|
+
metrics: metrics ?? { count: 1 }, ...(extra ? { extra } : {})
|
|
519
|
+
});
|
|
520
|
+
const entryBytes = Buffer.byteLength(JSON.stringify(records));
|
|
521
|
+
if (batch.length && bytes + entryBytes > MAX_BATCH_BYTES) break;
|
|
522
|
+
bytes += entryBytes;
|
|
523
|
+
batch.push(lane.queue.shift());
|
|
524
|
+
data.push(...records);
|
|
525
|
+
}
|
|
358
526
|
try {
|
|
359
527
|
await Promise.race([Promise.resolve().then(() => this.#send({ header: first.header, data }, { signal: controller.signal })), deadline]);
|
|
360
528
|
for (const entry of batch) {
|
|
361
|
-
if (entry.crashFile)
|
|
529
|
+
if (entry.crashFile) await fs.promises.unlink(entry.crashFile).catch(() => {});
|
|
362
530
|
}
|
|
363
531
|
} catch {
|
|
364
532
|
this.#uploadFailures += 1;
|
|
@@ -415,15 +583,25 @@ export function recordRuntimeLog(level, component, event, fields = {}) {
|
|
|
415
583
|
try { return active?.record(level, component, event, fields) ?? false; } catch { return false; }
|
|
416
584
|
}
|
|
417
585
|
|
|
586
|
+
// An explicit result or state observation. This never waits for the collector;
|
|
587
|
+
// persist is reserved for rare terminal failures that can outlive this process.
|
|
588
|
+
export function recordBridgeLifecycle(event, fields, options = {}) {
|
|
589
|
+
try { return active?.recordLifecycle(event, fields, options) ?? false; } catch { return false; }
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export function flushRuntimeLogsInBackground() {
|
|
593
|
+
if (active) setImmediate(() => { void active?.flush().catch(() => {}); }).unref?.();
|
|
594
|
+
}
|
|
595
|
+
|
|
418
596
|
export function persistRuntimeCrash(component, event, fields) {
|
|
419
597
|
try { active?.persistCrash(component, event, fields); } catch { /* best effort */ }
|
|
420
598
|
}
|
|
421
599
|
|
|
422
600
|
// A failure that ends a short-lived process: persist first, then try to deliver
|
|
423
601
|
// within one upload budget, so an immediate exit cannot lose the count.
|
|
424
|
-
export
|
|
602
|
+
export function reportRuntimeFailure(component, event, fields = {}) {
|
|
425
603
|
try { active?.persistCrash(component, event, fields, { deliverNow: true }); } catch { /* best effort */ }
|
|
426
|
-
|
|
604
|
+
flushRuntimeLogsInBackground();
|
|
427
605
|
}
|
|
428
606
|
|
|
429
607
|
export async function flushRuntimeLogs(budgetMs = 200) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import https from "node:https";
|
|
2
|
-
import {
|
|
2
|
+
import { gzip } from "node:zlib";
|
|
3
|
+
import { promisify } from "node:util";
|
|
3
4
|
|
|
4
5
|
// PC Custom Log / Custom Event wire format, verified against Parfait 1.7.0.0's
|
|
5
6
|
// public IUploader callback. This is not the Slardar Web/BID protocol. Keep the
|
|
@@ -10,10 +11,13 @@ export const SLARDAR_PC_AID = "1880";
|
|
|
10
11
|
// from the native Parfait SDK. Do not advertise a native SDK we do not run.
|
|
11
12
|
export const SLARDAR_PC_REPORTER_VERSION = "1.0.0.0";
|
|
12
13
|
const ENDPOINT = `https://pc-mon.zijieapi.com/monitor_pc/collect/api/pc_log?aid=${SLARDAR_PC_AID}`;
|
|
14
|
+
const compress = promisify(gzip);
|
|
13
15
|
|
|
14
|
-
export function sendSlardarPcBatch(payload, { signal, request = https.request } = {}) {
|
|
16
|
+
export async function sendSlardarPcBatch(payload, { signal, request = https.request } = {}) {
|
|
17
|
+
signal?.throwIfAborted();
|
|
18
|
+
const body = await compress(Buffer.from(JSON.stringify(payload)));
|
|
19
|
+
signal?.throwIfAborted();
|
|
15
20
|
return new Promise((resolve, reject) => {
|
|
16
|
-
const body = gzipSync(Buffer.from(JSON.stringify(payload)));
|
|
17
21
|
const req = request(ENDPOINT, {
|
|
18
22
|
method: "POST",
|
|
19
23
|
signal,
|