@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 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
+ }
@@ -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
- runtimeRoot = path.join(familyRoot, 'versions', pointer.version);
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
- const marker = await readJSON(markerPath);
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
- // No overwrite: concurrent first starts must agree before either starts work.
172
- try { await fs.link(temporary, markerPath); } catch (error) { if (error.code !== 'EEXIST') throw error; }
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-wfqa.100103558174.0.gc09ffa9",
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-wf-gray-f1bfb81f-1107-c6520c0-85edbe7.100103552606.0.g85edbe7",
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-wfqa.100103558174.0.gc09ffa9",
39
- "@lark-codem/codem-cli-darwin-x64": "0.1.269-wfqa.100103558174.0.gc09ffa9",
40
- "@lark-codem/codem-cli-linux-arm64": "0.1.269-wfqa.100103558174.0.gc09ffa9",
41
- "@lark-codem/codem-cli-linux-x64": "0.1.269-wfqa.100103558174.0.gc09ffa9",
42
- "@lark-codem/codem-cli-win32-arm64": "0.1.269-wfqa.100103558174.0.gc09ffa9",
43
- "@lark-codem/codem-cli-win32-x64": "0.1.269-wfqa.100103558174.0.gc09ffa9"
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"
@@ -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
- if (!this.record("fatal", component, event, fields)) return;
181
- entry = this.#live.queue.pop();
182
- if (!this.#crashDirectory) return;
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 file = path.join(this.#crashDirectory, `js-${randomUUID()}.json`);
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
- crashFile = file;
189
- } catch { /* Crash diagnostics must preserve the original exit behavior. */ } finally {
190
- if (deliverNow && entry) this.#replay.queue.push({ ...entry, attempts: 0, ...(crashFile ? { crashFile } : {}) });
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.readdirSync(this.#crashDirectory).filter(name => /^js-[a-f0-9-]+\.json$/u.test(name)).slice(0, 32)) {
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
- if (fs.lstatSync(file).isSymbolicLink() || fs.statSync(file).size > 65536) continue;
200
- const entry = JSON.parse(fs.readFileSync(file, "utf8"));
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.#replay.queue.push({ header: entry.header, record: entry.record, metrics: metricProjection(entry.record.category.event, entry.metrics), extra: extraProjection(entry.extra), attempts: 0, crashFile: file });
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
- let safeFields;
233
- try { safeFields = projectFields(fields); } catch { safeFields = {}; }
234
- if (OBSERVATION_EVENTS.has(event)) safeFields.observation_id = randomUUID();
235
- const severity = ["info", "warn", "error", "fatal"].includes(level) ? level : "info";
236
- const identity = { ...this.#identity };
237
- const category = {
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
- ...Object.fromEntries(["stage", "error_class", "error_code", "error_kind", "outcome"].filter((key) => safeFields[key]).map((key) => [key, safeFields[key]]))
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
- if (this.#live.queue.length >= CAPACITY) {
280
- const minimum = Math.min(...this.#live.queue.map(entry => entry.priority ?? (entry.record.log_level === "info" ? 0 : 1)));
281
- if (minimum > priority) { this.#dropped += 1; return false; }
282
- const expendable = this.#live.queue.findIndex(entry => (entry.priority ?? (entry.record.log_level === "info" ? 0 : 1)) === minimum);
283
- this.#live.queue.splice(expendable, 1);
284
- this.#dropped += 1;
285
- }
286
- this.#live.queue.push({ header, record, metrics: metricProjection(category.event, safeFields), extra: extraProjection(safeFields), priority, attempts: 0 });
287
- return true;
288
- } catch { return false; }
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
- while (batch.length < BATCH_SIZE && lane.queue.length && JSON.stringify(lane.queue[0].header) === key && Boolean(lane.queue[0].crashFile) === Boolean(first.crashFile)) batch.push(lane.queue.shift());
353
- const data = batch.flatMap(({ record, metrics, extra }) => [record, {
354
- app_version: record.app_version, app_minor_version: "", build_id: "",
355
- process_type: record.process_type, session_id: "", timestamp: record.timestamp,
356
- log_type: "flexible", event_name: "codem_runtime_diagnostic", category: record.category, metrics: metrics ?? { count: 1 }, ...(extra ? { extra } : {})
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) { try { fs.unlinkSync(entry.crashFile); } catch { /* Replay may duplicate after a crash or concurrent startup. */ } }
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 async function reportRuntimeFailure(component, event, fields = {}, budgetMs = 3_000) {
602
+ export function reportRuntimeFailure(component, event, fields = {}) {
425
603
  try { active?.persistCrash(component, event, fields, { deliverNow: true }); } catch { /* best effort */ }
426
- await flushRuntimeLogs(budgetMs);
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 { gzipSync } from "node:zlib";
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,