@lark-codem/codem-cli 0.1.269-dev.100103562624.0.geaa0d89 → 0.1.269-dev.100103619152.0.ge5b2cdd

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/codem.js CHANGED
@@ -145,6 +145,19 @@ function explainWindowsFatalStatus(code) {
145
145
  ].join("\n");
146
146
  }
147
147
 
148
+ // Source/development and npm entrypoints select before invoking business code.
149
+ // The compiled entry has the same guard for managed/direct binary installations.
150
+ if (process.env.CODEM_WORKER_RUNTIME !== undefined) {
151
+ const { isWorkerRuntimeStartup } = await import("./worker-runtime.js");
152
+ if (isWorkerRuntimeStartup()) {
153
+ const bootstrap = await import("../dist/src/worker-runtime-bootstrap.js").catch(() => null);
154
+ if (bootstrap) {
155
+ try { process.exitCode = await bootstrap.runWorkerRuntimeBootstrap(); }
156
+ catch (error) { writeStderrSync(`worker runtime startup failed: ${error.message}`); process.exitCode = 1; }
157
+ process.exit(process.exitCode ?? 0);
158
+ }
159
+ }
160
+ }
148
161
  const binaryPath = resolvePlatformBinary();
149
162
 
150
163
  if (binaryPath) {
@@ -0,0 +1,45 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+
4
+ export function resolveEffectiveLincoHome({
5
+ env = process.env, homeDir = os.homedir(), workingDirectory = process.cwd(), platform = process.platform
6
+ } = {}) {
7
+ const pathApi = platform === "win32" ? path.win32 : path;
8
+ const configured = Object.hasOwn(env ?? {}, "LINCO_HOME")
9
+ ? String(env.LINCO_HOME ?? "") : pathApi.join(String(homeDir), ".codem");
10
+ return pathApi.resolve(String(workingDirectory), configured);
11
+ }
12
+
13
+ // Mirror Core sessions_root_for, including present-but-empty path overrides.
14
+ // Sidecars are siblings of the session root, never sandbox CODEM_STATE_HOME.
15
+ export function resolveLincoSessionsRoot(options = {}) {
16
+ const { env = process.env, workingDirectory = process.cwd(), platform = process.platform } = options;
17
+ const pathApi = platform === "win32" ? path.win32 : path;
18
+ return Object.hasOwn(env ?? {}, "LINCO_SESSIONS_ROOT")
19
+ ? pathApi.resolve(String(workingDirectory), String(env.LINCO_SESSIONS_ROOT ?? ""))
20
+ : pathApi.join(options.effectiveLincoHome ?? resolveEffectiveLincoHome(options), "sessions");
21
+ }
22
+
23
+ export function resolveLincoHostContextRoot(options = {}) {
24
+ const pathApi = options.platform === "win32" ? path.win32 : path;
25
+ return pathApi.join(pathApi.dirname(resolveLincoSessionsRoot(options)), "host-context");
26
+ }
27
+
28
+ export function sameRecoveryPath(retained, current, platform = process.platform) {
29
+ if (typeof retained !== "string" || typeof current !== "string") return false;
30
+ if (platform !== "win32" || retained === current) return retained === current;
31
+ // Rust canonicalize retains the extended-length drive/UNC prefix; Node
32
+ // realpath removes it. Strip only that representation difference, preserving
33
+ // case and literal segments rather than resolving untrusted binding paths.
34
+ const identity = value => {
35
+ if (value.toLowerCase().startsWith("\\\\?\\unc\\")) value = `\\\\${value.slice(8)}`;
36
+ else if (value.startsWith("\\\\?\\")) value = value.slice(4);
37
+ const root = path.win32.parse(value).root;
38
+ if (!path.win32.isAbsolute(value) || root.length <= 1) return null;
39
+ // Namespaced UNC roots may omit the trailing separator. Do not normalize
40
+ // non-root segments: verbatim paths must not acquire lexical aliases.
41
+ return value === root && !value.endsWith("\\") ? `${value}\\` : value;
42
+ };
43
+ const retainedIdentity = identity(retained);
44
+ return retainedIdentity !== null && retainedIdentity === identity(current);
45
+ }
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
+ }
@@ -0,0 +1,294 @@
1
+ // Startup-only routing. Keep this module independent of Bridge business imports:
2
+ // it is also executed by container entrypoints before either Bridge is loaded.
3
+ import { spawn } from 'node:child_process';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import fs from 'node:fs/promises';
6
+ import { constants as fsConstants } from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { pathToFileURL } from 'node:url';
10
+ import { setTimeout as delay } from 'node:timers/promises';
11
+ import { applyHostedWindowsChildOptions } from './platform-child-process.js';
12
+ import { resolveEffectiveLincoHome, resolveLincoSessionsRoot, sameRecoveryPath } from './linco-paths.js';
13
+ import { observeWorkerRuntime } from './worker-runtime-telemetry.js';
14
+
15
+ export const WORKER_HOST_HOME = process.env.CODEM_WORKER_HOST_HOME || path.join(os.homedir(), '.codem');
16
+ export const WORKER_RUNTIME_ROOT = path.join(WORKER_HOST_HOME, 'worker-runtimes');
17
+ export const EXACT_RUNTIME_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
18
+ const families = ['stable', 'current'];
19
+
20
+
21
+ async function readJSON(file) { return JSON.parse(await fs.readFile(file, 'utf8')); }
22
+ async function confinedFile(root, relative) {
23
+ if (typeof relative !== 'string' || !relative || path.isAbsolute(relative)) throw new Error('invalid runtime path');
24
+ const realRoot = await fs.realpath(root);
25
+ const resolved = await fs.realpath(path.resolve(realRoot, relative));
26
+ const inside = path.relative(realRoot, resolved);
27
+ if (!inside || inside === '..' || inside.startsWith(`..${path.sep}`) || path.isAbsolute(inside)) throw new Error('runtime path escapes pair');
28
+ if (!(await fs.stat(resolved)).isFile()) throw new Error('runtime entry is not a file');
29
+ return resolved;
30
+ }
31
+
32
+ // Only installers write these immutable pair manifests. No npm resolution or
33
+ // floating global/current Core lookup happens in the execution process.
34
+ export async function readWorkerRuntime(root, family, { version = null, generation = null } = {}) {
35
+ if (!families.includes(family)) throw new Error('invalid worker runtime');
36
+ const familyRoot = path.join(root, family);
37
+ let pointerChannel = null;
38
+ let runtimeRoot = version ? path.join(familyRoot, 'versions', version) : familyRoot;
39
+ if (version && !EXACT_RUNTIME_VERSION.test(version)) throw new Error('invalid runtime version');
40
+ if (!version) try {
41
+ const pointer = await readJSON(path.join(familyRoot, 'current.json'));
42
+ if (!EXACT_RUNTIME_VERSION.test(pointer.version)) throw new Error('invalid runtime pointer');
43
+ pointerChannel = pointer.channel;
44
+ version = pointer.version;
45
+ generation = pointer.generation ?? null;
46
+ runtimeRoot = path.join(familyRoot, 'versions', version);
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');
54
+ const manifest = await readJSON(path.join(runtimeRoot, 'runtime-manifest.json'));
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');
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');
58
+ const cliEntry = await confinedFile(runtimeRoot, manifest.cliEntry);
59
+ const corePath = await confinedFile(runtimeRoot, manifest.coreBinaryPath);
60
+ return { ...manifest, ...(pointerChannel ? { channel: pointerChannel } : {}), generation, root: runtimeRoot, cliEntry, corePath };
61
+ }
62
+
63
+ function routeIdentity(env) {
64
+ const values = [env.CODEM_SANDBOX_TENANT_KEY, env.CODEM_SANDBOX_PROJECT_KEY, env.CODEM_SANDBOX_SESSION_ID];
65
+ if (values.some(value => typeof value !== 'string' || !value.trim() || value.length > 1024 || /[\0\r\n]/.test(value))) throw new Error('worker runtime requires trusted tenant/project/session');
66
+ const scope = createHash('sha256').update(JSON.stringify(values)).digest('hex');
67
+ const lincoHome = resolveEffectiveLincoHome({ env });
68
+ const sessions = resolveLincoSessionsRoot({ env, effectiveLincoHome: lincoHome });
69
+ const directory = path.join(path.dirname(sessions), 'worker-runtime-routes');
70
+ const dispatch = env.CODEM_SANDBOX_DISPATCH_ID;
71
+ if (typeof dispatch !== 'string' || !dispatch.trim() || dispatch.length > 1024 || /[\0\r\n]/.test(dispatch)) throw new Error('worker runtime requires trusted dispatch');
72
+ const executionScope = createHash('sha256').update(JSON.stringify([scope, dispatch])).digest('hex');
73
+ return { scope, lincoHome, sessions, markerPath: path.join(directory, `${scope}.json`), executionScope,
74
+ executionPath: path.join(directory, `${scope}-${executionScope}.json`) };
75
+ }
76
+
77
+ // This is only architecture routing, not a second checkpoint validator. Source
78
+ // adoption is committed to the original Root before the new main Agent starts
79
+ // (Core resume_prebound / SourceTaskAttached). Core still owns full replay,
80
+ // identity, compatibility and the existing recovery locks.
81
+ async function retainedRootMatches(lincoHome, metadata, file, tos) {
82
+ const runId = metadata?.run_id;
83
+ if (typeof runId !== 'string' || !runId) return true;
84
+ const bindingPath = path.join(lincoHome, 'workflow-runs', 'locks', `${createHash('sha256').update(runId).digest('hex')}.lock`);
85
+ let handle;
86
+ try { handle = await fs.open(bindingPath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0) | (fsConstants.O_NONBLOCK ?? 0)); }
87
+ catch (error) { if (error.code === 'ENOENT') return true; throw error; }
88
+ let retained;
89
+ try {
90
+ const limit = 4 * 1024 * 1024;
91
+ const stat = await handle.stat();
92
+ if (!stat.isFile()) throw new Error('workflow recovery binding is not a regular file');
93
+ if (stat.size > limit) throw new Error('workflow recovery binding exceeds replay limit');
94
+ const chunks = [], buffer = Buffer.allocUnsafe(64 * 1024);
95
+ let size = 0;
96
+ for (;;) {
97
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
98
+ if (!bytesRead) break;
99
+ size += bytesRead;
100
+ if (size > limit) throw new Error('workflow recovery binding exceeds replay limit');
101
+ chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
102
+ }
103
+ try { retained = JSON.parse(Buffer.concat(chunks).toString('utf8')); }
104
+ catch { throw new Error('invalid retained workflow binding'); }
105
+ } finally { await handle.close(); }
106
+ // The retained binding, not today's sandbox type, froze the Root layout.
107
+ // Core still validates its complete contents and canonical workspace later.
108
+ const retainedTos = retained.storage_profile === 'tos_v1';
109
+ if (!retainedTos && retained.storage_profile !== undefined) throw new Error('unsupported retained workflow storage profile');
110
+ if (retainedTos !== tos || retained.initial_state?.metadata?.run_id !== runId ||
111
+ retained.initial_state?.metadata?.root_session_id !== metadata.root_session_id) return false;
112
+ if (typeof retained.root_session_path !== 'string' || !path.isAbsolute(retained.root_session_path)) throw new Error('invalid retained workflow root path');
113
+ return sameRecoveryPath(retained.root_session_path, await fs.realpath(file));
114
+ }
115
+
116
+ async function historicalFamily(sessions, session, lincoHome) {
117
+ if (!session || /[/\\\0]/.test(session) || session === '.' || session === '..') throw new Error('invalid recovery session');
118
+ let directories;
119
+ try { directories = await fs.readdir(sessions, { withFileTypes: true }); }
120
+ catch (error) { if (error.code === 'ENOENT') return null; throw error; }
121
+ let found = false;
122
+ for (const entry of directories) {
123
+ if (!entry.isDirectory()) continue;
124
+ for (const layout of ['', 'workflow-tos-v1']) {
125
+ const file = path.join(sessions, entry.name, layout, `${session}.jsonl`);
126
+ let handle;
127
+ try { handle = await fs.open(file, 'r'); }
128
+ catch (error) { if (error.code === 'ENOENT') continue; throw error; }
129
+ try {
130
+ // Same replay/record bounds as WorkflowStoreLimits; never load all Sessions.
131
+ if ((await handle.stat()).size > 64 * 1024 * 1024) throw new Error('workflow recovery history exceeds replay limit');
132
+ let pending = Buffer.alloc(0), root = null;
133
+ for await (const chunk of handle.createReadStream()) {
134
+ pending = Buffer.concat([pending, chunk]);
135
+ let newline;
136
+ while ((newline = pending.indexOf(10)) !== -1) {
137
+ if (newline > 4 * 1024 * 1024) throw new Error('workflow recovery record exceeds replay limit');
138
+ let value;
139
+ try { value = JSON.parse(pending.subarray(0, newline).toString('utf8')); }
140
+ catch { throw new Error('invalid workflow recovery history'); }
141
+ pending = pending.subarray(newline + 1);
142
+ if (root === null) {
143
+ if (value.type !== 'workflow_metadata' || value.record?.initial_state?.metadata?.root_session_id !== session) { root = false; break; }
144
+ if (!await retainedRootMatches(lincoHome, value.record.initial_state.metadata, file, layout === 'workflow-tos-v1')) { root = false; break; }
145
+ root = found = true;
146
+ if (value.record.local_execution || value.record.initialization?.local_execution) return 'current';
147
+ }
148
+ if (root && value.type === 'workflow_local' && value.record?.event?.kind === 'source_task_attached') return 'current';
149
+ }
150
+ if (root === false) break;
151
+ if (pending.length > 4 * 1024 * 1024) throw new Error('workflow recovery record exceeds replay limit');
152
+ }
153
+ } finally { await handle.close(); }
154
+ }
155
+ }
156
+ return found ? 'stable' : null;
157
+ }
158
+
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;
163
+ const temporary = `${markerPath}.${randomUUID()}.tmp`;
164
+ try {
165
+ await fs.mkdir(path.dirname(markerPath), { recursive: true, mode: 0o700 });
166
+ await fs.writeFile(temporary, `${JSON.stringify({ schema: 1, scope, family: 'current' })}\n`, { mode: 0o600, flag: 'wx' });
167
+ await fs.rename(temporary, markerPath);
168
+ } finally { await fs.rm(temporary, { force: true }); }
169
+ }
170
+ async function readRoute(markerPath, scope) {
171
+ try {
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);
182
+ if (marker.schema !== 1 || marker.scope !== scope || !families.includes(marker.family)) throw new Error('identity or schema mismatch');
183
+ return marker.family;
184
+ } catch (error) {
185
+ if (error.code === 'ENOENT') return null;
186
+ throw new Error('invalid runtime route', { cause: error });
187
+ }
188
+ }
189
+ async function recordRoute(markerPath, scope, family) {
190
+ await fs.mkdir(path.dirname(markerPath), { recursive: true, mode: 0o700 });
191
+ const temporary = `${markerPath}.${randomUUID()}.tmp`;
192
+ try {
193
+ await fs.writeFile(temporary, `${JSON.stringify({ schema: 1, scope, family })}\n`, { mode: 0o600, flag: 'wx' });
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
+ }
203
+ } finally { await fs.rm(temporary, { force: true }); }
204
+ return readRoute(markerPath, scope);
205
+ }
206
+
207
+ export async function selectWorkerRuntime({ env = process.env, root = env.CODEM_WORKER_RUNTIME_ROOT || WORKER_RUNTIME_ROOT, loadRuntime = readWorkerRuntime, currentChannel = null } = {}) {
208
+ const requested = env.CODEM_WORKER_RUNTIME;
209
+ if (!families.includes(requested)) throw new Error('invalid worker runtime');
210
+ const { scope, lincoHome, sessions, markerPath, executionScope, executionPath } = routeIdentity(env);
211
+ let actual = await readRoute(markerPath, scope);
212
+ const restoring = env.CODEM_TASK_MODE === 'resume';
213
+ const upgradeFrom = env.CODEM_WORKER_RUNTIME_UPGRADE_FROM;
214
+ if (upgradeFrom !== undefined && (upgradeFrom !== 'stable' || !restoring || requested !== 'current')) throw new Error('invalid worker runtime upgrade');
215
+ if (restoring && actual !== 'current') {
216
+ const historical = await historicalFamily(sessions, env.CODEM_SANDBOX_SESSION_ID, lincoHome);
217
+ if (historical === 'current') {
218
+ await promoteRoute(markerPath, scope);
219
+ actual = 'current';
220
+ }
221
+ }
222
+ const frozen = await readRoute(executionPath, executionScope);
223
+ if (frozen === 'stable' && actual === 'current') throw new Error('execution runtime conflicts with retained current state');
224
+ const upgrading = restoring && actual === 'stable' && requested === 'current' && !frozen;
225
+ const reasons = [];
226
+ const load = async family => {
227
+ try {
228
+ const runtime = await loadRuntime(root, family);
229
+ // A retained alpha pair can recover its own Session, but cannot serve a new
230
+ // latest-channel Session after a failed channel switch.
231
+ if ((!actual || upgrading) && !frozen && family === 'current' && currentChannel !== null) {
232
+ const channel = typeof currentChannel === 'function' ? await currentChannel() : currentChannel;
233
+ if (runtime.channel !== channel) throw new Error('worker runtime channel unavailable');
234
+ }
235
+ return runtime;
236
+ } catch { reasons.push(`${family}_unavailable`); return null; }
237
+ };
238
+ let runtime = await load(frozen || (actual === 'current' ? 'current' : restoring ? requested : actual || requested));
239
+ if (!frozen && actual !== 'current' && !runtime && requested === 'current') runtime = await load('stable');
240
+ if (!runtime) throw new Error(`${actual ? 'required ' : ''}runtime unavailable: ${reasons.join(',')}`);
241
+ if (!actual) {
242
+ actual = await recordRoute(markerPath, scope, runtime.family);
243
+ if (actual !== runtime.family) {
244
+ runtime = await load(actual);
245
+ if (!runtime) throw new Error('required runtime unavailable after concurrent selection');
246
+ }
247
+ }
248
+ // Freeze fallback per Dispatch, independently of the monotonic Session family.
249
+ // A later user recovery has a new Dispatch; command retries keep this choice.
250
+ const executionFamily = await recordRoute(executionPath, executionScope, runtime.family);
251
+ if (executionFamily !== runtime.family) {
252
+ if (executionFamily === 'stable' && await readRoute(markerPath, scope) === 'current') throw new Error('execution runtime conflicts with retained current state');
253
+ runtime = await load(executionFamily);
254
+ if (!runtime) throw new Error('required runtime unavailable after concurrent execution selection');
255
+ }
256
+ if (restoring && actual === 'stable' && runtime.family === 'current') await promoteRoute(markerPath, scope);
257
+ return { ...runtime, runtimeRoot: root, lincoHome, sessionsRoot: sessions, requested, markerPath, fallbackReason: requested !== runtime.family ? reasons.join(',') || 'session_family' : null };
258
+ }
259
+
260
+ export function launchWorkerRuntime(runtime, { args = process.argv.slice(2), env = process.env, nodePath = process.execPath, invocation = null, log = console.error } = {}) {
261
+ log(JSON.stringify({ event: 'worker_runtime_selected', requested_family: runtime.requested, actual_family: runtime.family, fallback_reason: runtime.fallbackReason,
262
+ bridge_version: runtime.cliVersion, core_version: runtime.coreVersion, pair: `${runtime.cliVersion}/${runtime.coreVersion}`, dispatch_id: env.CODEM_SANDBOX_DISPATCH_ID, session_id: env.CODEM_SANDBOX_SESSION_ID }));
263
+ return new Promise((resolve, reject) => {
264
+ const launch = invocation || { command: nodePath, args: [runtime.cliEntry, ...args] };
265
+ const child = spawn(launch.command, launch.args, applyHostedWindowsChildOptions({ ...launch.options, stdio: 'inherit', env: { ...env,
266
+ // Keep relative/empty overrides frozen even if business startup changes cwd.
267
+ ...(runtime.lincoHome ? { LINCO_HOME: runtime.lincoHome } : {}),
268
+ ...(runtime.sessionsRoot ? { LINCO_SESSIONS_ROOT: runtime.sessionsRoot } : {}),
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,
270
+ CODEM_PLUGIN_NODE: env.CODEM_PLUGIN_NODE || nodePath,
271
+ CODEM_SKIP_UPDATE_CHECK: '1', CODEM_DISABLE_DAEMON_AUTO_UPDATE: '1' } }));
272
+ const stopObservation=observeWorkerRuntime(runtime,{env,args});
273
+ const signals = ['SIGTERM', 'SIGINT', 'SIGHUP'];
274
+ const handlers = signals.map(signal => () => { if (child.exitCode === null && child.signalCode === null) { try { child.kill(signal); } catch {} } });
275
+ signals.forEach((signal, index) => process.on(signal, handlers[index]));
276
+ const cleanup = () => {stopObservation();signals.forEach((signal, index) => process.off(signal, handlers[index]));};
277
+ child.once('error', error => { cleanup(); reject(error); });
278
+ child.once('exit', (code, signal) => { cleanup(); resolve(code ?? (signal ? 128 + (os.constants.signals[signal] || 1) : 1)); });
279
+ });
280
+ }
281
+
282
+ export function isWorkerRuntimeStartup(args = process.argv.slice(2), env = process.env) {
283
+ return env.CODEM_WORKER_RUNTIME !== undefined && env.CODEM_WORKER_RUNTIME_READY !== '1' && args[0] === 'daemon' && args.some(arg => arg === '--sandbox' || arg.startsWith('--sandbox='));
284
+ }
285
+
286
+ // Container entrypoints invoke this directly, with a baked immutable root.
287
+ async function runStandalone() {
288
+ try {
289
+ const runtime = await selectWorkerRuntime();
290
+ process.exitCode = await launchWorkerRuntime(runtime, { nodePath: process.execPath });
291
+ } catch (error) { console.error(`worker runtime startup failed: ${error.message}`); process.exitCode = 1; }
292
+ }
293
+
294
+ if (typeof Bun === 'undefined' && process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) void runStandalone();
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@lark-codem/codem-cli",
3
- "version": "0.1.269-dev.100103562624.0.geaa0d89",
3
+ "version": "0.1.269-dev.100103619152.0.ge5b2cdd",
4
4
  "codemServiceManagementProtocol": 3,
5
+ "codemWorkerRuntimeProtocol": 1,
5
6
  "codemProfileProtocol": 1,
6
7
  "type": "module",
7
8
  "description": "CodeM local CLI for Feishu-driven coding sessions",
@@ -29,17 +30,17 @@
29
30
  "access": "public"
30
31
  },
31
32
  "dependencies": {
32
- "@lark-codem/codem-core": "0.8.86-dev.100103562272.0.ga7d1943",
33
+ "@lark-codem/codem-core": "0.8.86-dev.100103616567.0.g0b3442f",
33
34
  "zod": "3.25.76",
34
35
  "@lark-codem/lark-cli": "0.1.1"
35
36
  },
36
37
  "optionalDependencies": {
37
- "@lark-codem/codem-cli-darwin-arm64": "0.1.269-dev.100103562624.0.geaa0d89",
38
- "@lark-codem/codem-cli-darwin-x64": "0.1.269-dev.100103562624.0.geaa0d89",
39
- "@lark-codem/codem-cli-linux-arm64": "0.1.269-dev.100103562624.0.geaa0d89",
40
- "@lark-codem/codem-cli-linux-x64": "0.1.269-dev.100103562624.0.geaa0d89",
41
- "@lark-codem/codem-cli-win32-arm64": "0.1.269-dev.100103562624.0.geaa0d89",
42
- "@lark-codem/codem-cli-win32-x64": "0.1.269-dev.100103562624.0.geaa0d89"
38
+ "@lark-codem/codem-cli-darwin-arm64": "0.1.269-dev.100103619152.0.ge5b2cdd",
39
+ "@lark-codem/codem-cli-darwin-x64": "0.1.269-dev.100103619152.0.ge5b2cdd",
40
+ "@lark-codem/codem-cli-linux-arm64": "0.1.269-dev.100103619152.0.ge5b2cdd",
41
+ "@lark-codem/codem-cli-linux-x64": "0.1.269-dev.100103619152.0.ge5b2cdd",
42
+ "@lark-codem/codem-cli-win32-arm64": "0.1.269-dev.100103619152.0.ge5b2cdd",
43
+ "@lark-codem/codem-cli-win32-x64": "0.1.269-dev.100103619152.0.ge5b2cdd"
43
44
  },
44
45
  "engines": {
45
46
  "node": ">=18"
@@ -1,3 +1,4 @@
1
+ import { workerRuntimeAttributes } from "./worker-runtime-observation.js";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import os from "node:os";
3
4
  import fs from "node:fs";
@@ -10,7 +11,47 @@ const CAPACITY = 256;
10
11
  const BATCH_SIZE = 32;
11
12
  const INTERVAL_MS = 10_000;
12
13
  const REQUEST_TIMEOUT_MS = 3_000;
13
- const OBSERVATION_EVENTS = new Set(["command_delivery_observed", "command_admission", "runtime_capability_probe", "command_ack", "command_observation_closed", "diagnostic_reporter_health"]);
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
+ };
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"]);
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"];
48
+ const TRUSTED_DEVICE_DIMENSIONS = {
49
+ phase: TRUSTED_DEVICE_PHASES,
50
+ operation: ["create", "renew", "recovery", "platform_identity", "network_interfaces", "network_ports"],
51
+ outcome: ["success", "partial", "failed", "ready", "not_registered", "no_auth", "reset", "blocked", "invalid", "verified", "not_required", "absent", "expired"],
52
+ failure_kind: ["none", "timeout", "exec_failed", "invalid_json", "empty_identity", "read_failed", "authorization", "unknown"]
53
+ };
54
+ const OBSERVATION_EVENTS = new Set(["command_delivery_observed", "command_admission", "runtime_capability_probe", "command_ack", "command_observation_closed", "diagnostic_reporter_health", ...TRUSTED_DEVICE_EVENTS]);
14
55
  const OBSERVATION_DIMENSIONS = {
15
56
  phase: ["resolve_binary", "build_env", "host_capabilities", "parse_capabilities", "strict_resume", "verify_binding", "prepare", "persist_state", "trusted_device", "request", "validate_response", "apply_response"],
16
57
  reason: ["runtime_unavailable", "runtime_environment_failed", "capability_probe_failed", "runtime_binary_changed", "strict_resume_unsupported", "turn_context_unsupported", "active_run_protocol_mismatch", "trusted_device_blocked", "shutting_down_handoff", "handoff_target_not_active", "duplicate_inflight", "handler_inflight", "prepare_exception_unknown", "capacity", "stopped", "replaced", "scope_changed", "queue_removed", "local_rejected"],
@@ -22,6 +63,12 @@ const OBSERVATION_DIMENSIONS = {
22
63
  timing_origin: ["observed", "restored_unknown"]
23
64
  };
24
65
  const OBSERVATION_METRICS = {
66
+ host_identity_collection: ["duration_ms", "field_error_count"],
67
+ host_identity_command: ["duration_ms", "timeout_ms"],
68
+ host_session_attempt: ["elapsed_ms", "next_retry_ms", "consecutive_failures", "session_remaining_ms", ...TRUSTED_DEVICE_PHASES.map(phase => `${phase}_duration_ms`)],
69
+ host_session_expired: ["expired_for_ms", "consecutive_failures", "next_retry_ms"],
70
+ trusted_device_admission_blocked: ["elapsed_ms", "blocked_response_count"],
71
+ trusted_device_admission_recovered: ["elapsed_ms", "blocked_response_count"],
25
72
  command_delivery_observed: ["attempt_delta", "suppressed_count"],
26
73
  command_admission: ["attempt_delta", "suppressed_count", "deferred_age_ms", "deferred_duration_ms"],
27
74
  runtime_capability_probe: ["probe_duration_ms"],
@@ -29,24 +76,29 @@ const OBSERVATION_METRICS = {
29
76
  command_observation_closed: ["omitted_detail_count", "pending_command_ack_count", "deferred_count"],
30
77
  diagnostic_reporter_health: ["dropped_delta", "upload_failure_delta", "pending_event_count"]
31
78
  };
32
- const EXTRA_FIELDS = ["observation_id", "runtime_client_instance_id", "command_id", "delivery_id", "probe_id", "sync_id", "transport_request_id"];
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"];
33
81
  const STRING_FIELDS = new Set([
34
82
  ...EXTRA_FIELDS, "command_type", "ack_status", "ack_outcome", "readback_scope", "timing_origin", "required_capability",
35
83
  "request_id", "command_id", "session_id", "task_id", "dispatch_id", "event_id", "sync_id",
36
84
  "process_instance_id", "run_id", "stage", "phase", "status", "outcome", "reason", "action",
37
85
  "error_class", "error_code", "error_kind", "error_type", "code", "signal", "stop_reason",
86
+ "first_error", "retry_source", "cleanup_result",
38
87
  "terminal_event", "event_type", "actor_type", "source", "trigger", "hook_id", "hook_event",
39
88
  "target_version", "cli_version", "core_version", "version", "previous_state", "state",
40
- "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"
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",
90
+ "operation_id", "entrypoint"
41
91
  ]);
42
92
  const VALUE_FIELDS = new Set([
93
+ "session_retained",
43
94
  ...new Set(Object.values(OBSERVATION_METRICS).flat()),
44
95
  "pid", "exit_code", "duration_ms", "elapsed_ms", "wait_ms", "run_ms", "total_ms", "took_ms",
45
- "attempt", "attempts", "attempt_count", "retryable", "http_status", "active_task_count",
96
+ "attempt", "attempts", "attempt_count", "retryable", "http_status", "errno", "active_task_count",
46
97
  "pending_event_count", "pending_command_ack_count", "next_retry_ms", "consecutive_failures",
47
98
  "outage_duration_ms", "startup_duration_ms", "failed_attempts", "active_runs", "completed",
48
99
  "delivered", "result_delivered", "queued_behind_existing", "expected", "timed_out",
49
- "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", "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
50
102
  ]);
51
103
 
52
104
  function token(value) {
@@ -72,7 +124,7 @@ function extraProjection(fields = {}) {
72
124
  return Object.fromEntries(EXTRA_FIELDS.filter(key => token(fields?.[key])).map(key => [key, token(fields[key])]));
73
125
  }
74
126
 
75
- function projectFields(input) {
127
+ function projectFields(input, fingerprintErrors = true) {
76
128
  const result = {};
77
129
  for (const [key, value] of Object.entries(input ?? {}).slice(0, 100)) {
78
130
  const name = fieldName(key);
@@ -83,6 +135,7 @@ function projectFields(input) {
83
135
  result[name] = value;
84
136
  }
85
137
  }
138
+ if (!fingerprintErrors) return result;
86
139
  const error = input?.error ?? input?.terminalError;
87
140
  const fingerprint = input?.error_fingerprint ?? input?.errorFingerprint;
88
141
  if (typeof fingerprint === "string" && /^[a-f0-9]{24}$/u.test(fingerprint)) result.error_fingerprint = fingerprint;
@@ -105,6 +158,36 @@ function projectFields(input) {
105
158
  return result;
106
159
  }
107
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
+
108
191
  export function runtimeLogIdentity(config) {
109
192
  const oauth = optional(() => config?.auth?.oauth);
110
193
  return {
@@ -117,6 +200,15 @@ export function runtimeLogIdentity(config) {
117
200
  };
118
201
  }
119
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
+
120
212
  export class RuntimeLogReporter {
121
213
  #live = { queue: [], inflight: undefined, abort: undefined };
122
214
  #replay = { queue: [], inflight: undefined, abort: undefined };
@@ -136,6 +228,8 @@ export class RuntimeLogReporter {
136
228
  #platform;
137
229
  #osVersion;
138
230
  #crashDirectory;
231
+ #restoring;
232
+ #savedDuringRestore = new Set();
139
233
 
140
234
  constructor({ role = "cli", version = "unknown", send = sendSlardarPcBatch, instanceId = randomUUID(), diagnosticId: supportId = diagnosticId(), platform = process.platform, osVersion = os.release(), crashDirectory } = {}) {
141
235
  this.#send = send;
@@ -148,7 +242,7 @@ export class RuntimeLogReporter {
148
242
  this.#osVersion = osVersion;
149
243
  this.#timer = setInterval(() => { void this.flush(); }, INTERVAL_MS);
150
244
  this.#timer.unref?.();
151
- this.#restoreCrashes();
245
+ this.#restoring = this.#restoreCrashes().finally(() => { this.#savedDuringRestore = undefined; });
152
246
  }
153
247
 
154
248
  // Crash files hold already-projected records and the identity at failure time.
@@ -156,33 +250,48 @@ export class RuntimeLogReporter {
156
250
  // deliverNow also queues the record for this process's next flush. A
157
251
  // successful send deletes its file; otherwise the next process replays it.
158
252
  persistCrash(component, event, fields = {}, { deliverNow = false } = {}) {
159
- let entry;
160
- let crashFile;
161
253
  try {
162
254
  if (this.#closed || (!this.#crashDirectory && !deliverNow)) return;
163
- if (!this.record("fatal", component, event, fields)) return;
164
- entry = this.#live.queue.pop();
165
- 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 {
166
264
  fs.mkdirSync(this.#crashDirectory, { recursive: true, mode: 0o700 });
167
265
  const names = fs.readdirSync(this.#crashDirectory).filter(name => /^js-[a-f0-9-]+\.json$/u.test(name));
168
266
  if (names.length >= 32) return;
169
- const file = path.join(this.#crashDirectory, `js-${randomUUID()}.json`);
267
+ const name = `js-${randomUUID()}.json`;
268
+ const file = path.join(this.#crashDirectory, name);
170
269
  fs.writeFileSync(file, JSON.stringify(entry), { flag: "wx", mode: 0o600 });
171
- crashFile = file;
172
- } catch { /* Crash diagnostics must preserve the original exit behavior. */ } finally {
173
- if (deliverNow && entry) this.#replay.queue.push({ ...entry, attempts: 0, ...(crashFile ? { crashFile } : {}) });
174
- }
270
+ this.#savedDuringRestore?.add(name);
271
+ entry.crashFile = file;
272
+ } catch { /* Local storage can be full or unavailable. */ }
175
273
  }
176
274
 
177
- #restoreCrashes() {
275
+ async #restoreCrashes() {
276
+ if (!this.#crashDirectory) return;
178
277
  try {
179
- 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;
180
281
  const file = path.join(this.#crashDirectory, name);
181
282
  try {
182
- if (fs.lstatSync(file).isSymbolicLink() || fs.statSync(file).size > 65536) continue;
183
- 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"));
184
286
  if (entry?.record?.category?.reporter !== "codem_js" || entry?.record?.log_type !== "log" || !entry.header) continue;
185
- 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
+ }
186
295
  } catch { /* A partial crash write cannot prevent startup. */ }
187
296
  }
188
297
  } catch { /* First startup or unavailable storage. */ }
@@ -212,25 +321,90 @@ export class RuntimeLogReporter {
212
321
  record(level, component, event, fields = {}) {
213
322
  try {
214
323
  if (this.#closed) return false;
215
- let safeFields;
216
- try { safeFields = projectFields(fields); } catch { safeFields = {}; }
217
- if (OBSERVATION_EVENTS.has(event)) safeFields.observation_id = randomUUID();
218
- const severity = ["info", "warn", "error", "fatal"].includes(level) ? level : "info";
219
- const identity = { ...this.#identity };
220
- 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 = {
399
+ ...workerRuntimeAttributes(),
221
400
  client_kind: "cli", runtime_role: this.#role, reporter: "codem_js", log_schema: "1",
222
401
  component: token(component) ?? "unknown", event: token(event) ?? "unknown", severity,
223
402
  identity_source: identity.deviceId ? "codem" : "process",
224
403
  diagnostic_id: this.#diagnosticId,
225
404
  ...(identity.tenantId ? { tenant_id: identity.tenantId } : {}),
226
405
  ...(identity.serverEnvironment ? { server_environment: identity.serverEnvironment } : {}),
227
- ...Object.fromEntries(["stage", "error_class", "error_code", "error_kind", "outcome"].filter((key) => safeFields[key]).map((key) => [key, safeFields[key]]))
228
- };
229
- if (OBSERVATION_EVENTS.has(category.event)) {
230
- for (const [key, values] of Object.entries(OBSERVATION_DIMENSIONS)) {
231
- if (safeFields[key]) category[key] = values.includes(safeFields[key]) ? safeFields[key] : "unknown";
232
- }
233
- }
406
+ ...dimensions
407
+ };
234
408
  const header = {
235
409
  aid: SLARDAR_PC_AID,
236
410
  device_id: identity.deviceId ?? this.#instanceId,
@@ -240,31 +414,35 @@ export class RuntimeLogReporter {
240
414
  os: this.#platform, os_version: this.#osVersion
241
415
  };
242
416
  const timestamp = Date.now();
243
- const payload = {
417
+ const payload = includeLog ? {
244
418
  schema_version: 1, timestamp, component: category.component, event: category.event, level: severity,
245
419
  process_instance_id: this.#instanceId, diagnostic_id: this.#diagnosticId, pid: process.pid,
246
420
  ...(identity.tenantId ? { tenant_id: identity.tenantId } : {}),
247
421
  fields: safeFields
248
- };
422
+ } : undefined;
249
423
  const record = {
250
424
  app_version: this.#version, app_minor_version: "", build_id: "",
251
425
  process_type: `codem-${this.#role}`, session_id: "", timestamp, category,
252
426
  log_type: "log", log_level: severity === "fatal" ? "error" : severity,
253
- 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)}` } : {})
254
428
  };
429
+ const criticalSettlement = event === "delivery_settled" && dimensions.critical === "true";
255
430
  const priority = severity === "fatal" ? 3 :
256
431
  ["resumed", "settled"].includes(safeFields.stage) || (severity === "error" && !safeFields.suppressed_count) ? 2 :
257
- severity === "info" ? 0 : 1;
258
- if (this.#live.queue.length >= CAPACITY) {
259
- const minimum = Math.min(...this.#live.queue.map(entry => entry.priority ?? (entry.record.log_level === "info" ? 0 : 1)));
260
- if (minimum > priority) { this.#dropped += 1; return false; }
261
- const expendable = this.#live.queue.findIndex(entry => (entry.priority ?? (entry.record.log_level === "info" ? 0 : 1)) === minimum);
262
- this.#live.queue.splice(expendable, 1);
263
- this.#dropped += 1;
264
- }
265
- this.#live.queue.push({ header, record, metrics: metricProjection(category.event, safeFields), extra: extraProjection(safeFields), priority, attempts: 0 });
266
- return true;
267
- } 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;
268
446
  }
269
447
 
270
448
  async flush({ timeoutMs = REQUEST_TIMEOUT_MS } = {}) {
@@ -297,7 +475,7 @@ export class RuntimeLogReporter {
297
475
  // Run both lanes concurrently: old replay cannot delay live sends. Waiting
298
476
  // for both within one budget also gives short-lived invocations a chance
299
477
  // to finish replay; a timeout leaves its original crash file for next time.
300
- const replay = this.#flushLane(this.#replay, REQUEST_TIMEOUT_MS);
478
+ const replay = this.#restoring.then(() => this.#flushLane(this.#replay, REQUEST_TIMEOUT_MS));
301
479
  let timer;
302
480
  try {
303
481
  await Promise.race([
@@ -328,16 +506,27 @@ export class RuntimeLogReporter {
328
506
  const first = lane.queue[0];
329
507
  const key = JSON.stringify(first.header);
330
508
  const batch = [];
331
- 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());
332
- const data = batch.flatMap(({ record, metrics, extra }) => [record, {
333
- app_version: record.app_version, app_minor_version: "", build_id: "",
334
- process_type: record.process_type, session_id: "", timestamp: record.timestamp,
335
- log_type: "flexible", event_name: "codem_runtime_diagnostic", category: record.category, metrics: metrics ?? { count: 1 }, ...(extra ? { extra } : {})
336
- }]);
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
+ }
337
526
  try {
338
527
  await Promise.race([Promise.resolve().then(() => this.#send({ header: first.header, data }, { signal: controller.signal })), deadline]);
339
528
  for (const entry of batch) {
340
- 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(() => {});
341
530
  }
342
531
  } catch {
343
532
  this.#uploadFailures += 1;
@@ -394,15 +583,25 @@ export function recordRuntimeLog(level, component, event, fields = {}) {
394
583
  try { return active?.record(level, component, event, fields) ?? false; } catch { return false; }
395
584
  }
396
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
+
397
596
  export function persistRuntimeCrash(component, event, fields) {
398
597
  try { active?.persistCrash(component, event, fields); } catch { /* best effort */ }
399
598
  }
400
599
 
401
600
  // A failure that ends a short-lived process: persist first, then try to deliver
402
601
  // within one upload budget, so an immediate exit cannot lose the count.
403
- export async function reportRuntimeFailure(component, event, fields = {}, budgetMs = 3_000) {
602
+ export function reportRuntimeFailure(component, event, fields = {}) {
404
603
  try { active?.persistCrash(component, event, fields, { deliverNow: true }); } catch { /* best effort */ }
405
- await flushRuntimeLogs(budgetMs);
604
+ flushRuntimeLogsInBackground();
406
605
  }
407
606
 
408
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,
@@ -0,0 +1,10 @@
1
+ const exactVersion = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
2
+
3
+ // Low-cardinality process context shared by existing logs, metric categories,
4
+ // and tracing. Ordinary personal-Bot processes have no worker attribution.
5
+ export function workerRuntimeAttributes(env = process.env) {
6
+ if (env.CODEM_WORKER_RUNTIME_READY !== '1' || !['stable', 'current'].includes(env.CODEM_WORKER_RUNTIME_ACTUAL)) return {};
7
+ return { worker_runtime_family: env.CODEM_WORKER_RUNTIME_ACTUAL,
8
+ ...(exactVersion.test(env.CODEM_WORKER_CLI_VERSION || '') ? { worker_cli_version: env.CODEM_WORKER_CLI_VERSION } : {}),
9
+ ...(exactVersion.test(env.CODEM_WORKER_CORE_VERSION || '') ? { worker_core_version: env.CODEM_WORKER_CORE_VERSION } : {}) };
10
+ }