@lark-codem/codem-cli 0.1.269-dev.100103647402.0.gd62de11 → 0.1.269-dev.100103656607.0.g3c6c983
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 +0 -13
- package/package.json +8 -9
- package/profile/runtime-logs.js +60 -240
- package/profile/slardar-pc-transport.js +3 -7
- package/bin/linco-paths.js +0 -45
- package/bin/tt-env.js +0 -171
- package/bin/worker-runtime-telemetry.js +0 -75
- package/bin/worker-runtime.js +0 -294
- package/profile/worker-runtime-observation.js +0 -10
package/bin/codem.js
CHANGED
|
@@ -145,19 +145,6 @@ 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
|
-
}
|
|
161
148
|
const binaryPath = resolvePlatformBinary();
|
|
162
149
|
|
|
163
150
|
if (binaryPath) {
|
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-codem/codem-cli",
|
|
3
|
-
"version": "0.1.269-dev.
|
|
3
|
+
"version": "0.1.269-dev.100103656607.0.g3c6c983",
|
|
4
4
|
"codemServiceManagementProtocol": 3,
|
|
5
|
-
"codemWorkerRuntimeProtocol": 1,
|
|
6
5
|
"codemProfileProtocol": 1,
|
|
7
6
|
"type": "module",
|
|
8
7
|
"description": "CodeM local CLI for Feishu-driven coding sessions",
|
|
@@ -30,17 +29,17 @@
|
|
|
30
29
|
"access": "public"
|
|
31
30
|
},
|
|
32
31
|
"dependencies": {
|
|
33
|
-
"@lark-codem/codem-core": "0.8.86-dev.
|
|
32
|
+
"@lark-codem/codem-core": "0.8.86-dev.100103656607.0.gd23c9f9",
|
|
34
33
|
"zod": "3.25.76",
|
|
35
34
|
"@lark-codem/lark-cli": "0.1.1"
|
|
36
35
|
},
|
|
37
36
|
"optionalDependencies": {
|
|
38
|
-
"@lark-codem/codem-cli-darwin-arm64": "0.1.269-dev.
|
|
39
|
-
"@lark-codem/codem-cli-darwin-x64": "0.1.269-dev.
|
|
40
|
-
"@lark-codem/codem-cli-linux-arm64": "0.1.269-dev.
|
|
41
|
-
"@lark-codem/codem-cli-linux-x64": "0.1.269-dev.
|
|
42
|
-
"@lark-codem/codem-cli-win32-arm64": "0.1.269-dev.
|
|
43
|
-
"@lark-codem/codem-cli-win32-x64": "0.1.269-dev.
|
|
37
|
+
"@lark-codem/codem-cli-darwin-arm64": "0.1.269-dev.100103656607.0.g3c6c983",
|
|
38
|
+
"@lark-codem/codem-cli-darwin-x64": "0.1.269-dev.100103656607.0.g3c6c983",
|
|
39
|
+
"@lark-codem/codem-cli-linux-arm64": "0.1.269-dev.100103656607.0.g3c6c983",
|
|
40
|
+
"@lark-codem/codem-cli-linux-x64": "0.1.269-dev.100103656607.0.g3c6c983",
|
|
41
|
+
"@lark-codem/codem-cli-win32-arm64": "0.1.269-dev.100103656607.0.g3c6c983",
|
|
42
|
+
"@lark-codem/codem-cli-win32-x64": "0.1.269-dev.100103656607.0.g3c6c983"
|
|
44
43
|
},
|
|
45
44
|
"engines": {
|
|
46
45
|
"node": ">=18"
|
package/profile/runtime-logs.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { workerRuntimeAttributes } from "./worker-runtime-observation.js";
|
|
2
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
2
|
import os from "node:os";
|
|
4
3
|
import fs from "node:fs";
|
|
@@ -11,43 +10,11 @@ const CAPACITY = 256;
|
|
|
11
10
|
const BATCH_SIZE = 32;
|
|
12
11
|
const INTERVAL_MS = 10_000;
|
|
13
12
|
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
|
-
};
|
|
46
13
|
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
14
|
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
15
|
const TRUSTED_DEVICE_DIMENSIONS = {
|
|
49
16
|
phase: TRUSTED_DEVICE_PHASES,
|
|
50
|
-
operation: ["create", "renew", "recovery", "platform_identity", "network_interfaces", "network_ports"
|
|
17
|
+
operation: ["create", "renew", "recovery", "platform_identity", "network_interfaces", "network_ports"],
|
|
51
18
|
outcome: ["success", "partial", "failed", "ready", "not_registered", "no_auth", "reset", "blocked", "invalid", "verified", "not_required", "absent", "expired"],
|
|
52
19
|
failure_kind: ["none", "timeout", "exec_failed", "invalid_json", "empty_identity", "read_failed", "authorization", "unknown"]
|
|
53
20
|
};
|
|
@@ -77,13 +44,11 @@ const OBSERVATION_METRICS = {
|
|
|
77
44
|
diagnostic_reporter_health: ["dropped_delta", "upload_failure_delta", "pending_event_count"]
|
|
78
45
|
};
|
|
79
46
|
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"];
|
|
81
47
|
const STRING_FIELDS = new Set([
|
|
82
48
|
...EXTRA_FIELDS, "command_type", "ack_status", "ack_outcome", "readback_scope", "timing_origin", "required_capability",
|
|
83
49
|
"request_id", "command_id", "session_id", "task_id", "dispatch_id", "event_id", "sync_id",
|
|
84
50
|
"process_instance_id", "run_id", "stage", "phase", "status", "outcome", "reason", "action",
|
|
85
51
|
"error_class", "error_code", "error_kind", "error_type", "code", "signal", "stop_reason",
|
|
86
|
-
"first_error", "retry_source", "cleanup_result",
|
|
87
52
|
"terminal_event", "event_type", "actor_type", "source", "trigger", "hook_id", "hook_event",
|
|
88
53
|
"target_version", "cli_version", "core_version", "version", "previous_state", "state",
|
|
89
54
|
"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",
|
|
@@ -93,12 +58,11 @@ const VALUE_FIELDS = new Set([
|
|
|
93
58
|
"session_retained",
|
|
94
59
|
...new Set(Object.values(OBSERVATION_METRICS).flat()),
|
|
95
60
|
"pid", "exit_code", "duration_ms", "elapsed_ms", "wait_ms", "run_ms", "total_ms", "took_ms",
|
|
96
|
-
"attempt", "attempts", "attempt_count", "retryable", "http_status", "
|
|
61
|
+
"attempt", "attempts", "attempt_count", "retryable", "http_status", "active_task_count",
|
|
97
62
|
"pending_event_count", "pending_command_ack_count", "next_retry_ms", "consecutive_failures",
|
|
98
63
|
"outage_duration_ms", "startup_duration_ms", "failed_attempts", "active_runs", "completed",
|
|
99
64
|
"delivered", "result_delivered", "queued_behind_existing", "expected", "timed_out",
|
|
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
|
|
65
|
+
"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"
|
|
102
66
|
]);
|
|
103
67
|
|
|
104
68
|
function token(value) {
|
|
@@ -124,7 +88,7 @@ function extraProjection(fields = {}) {
|
|
|
124
88
|
return Object.fromEntries(EXTRA_FIELDS.filter(key => token(fields?.[key])).map(key => [key, token(fields[key])]));
|
|
125
89
|
}
|
|
126
90
|
|
|
127
|
-
function projectFields(input
|
|
91
|
+
function projectFields(input) {
|
|
128
92
|
const result = {};
|
|
129
93
|
for (const [key, value] of Object.entries(input ?? {}).slice(0, 100)) {
|
|
130
94
|
const name = fieldName(key);
|
|
@@ -135,7 +99,6 @@ function projectFields(input, fingerprintErrors = true) {
|
|
|
135
99
|
result[name] = value;
|
|
136
100
|
}
|
|
137
101
|
}
|
|
138
|
-
if (!fingerprintErrors) return result;
|
|
139
102
|
const error = input?.error ?? input?.terminalError;
|
|
140
103
|
const fingerprint = input?.error_fingerprint ?? input?.errorFingerprint;
|
|
141
104
|
if (typeof fingerprint === "string" && /^[a-f0-9]{24}$/u.test(fingerprint)) result.error_fingerprint = fingerprint;
|
|
@@ -158,36 +121,6 @@ function projectFields(input, fingerprintErrors = true) {
|
|
|
158
121
|
return result;
|
|
159
122
|
}
|
|
160
123
|
|
|
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
|
-
|
|
191
124
|
export function runtimeLogIdentity(config) {
|
|
192
125
|
const oauth = optional(() => config?.auth?.oauth);
|
|
193
126
|
return {
|
|
@@ -200,15 +133,6 @@ export function runtimeLogIdentity(config) {
|
|
|
200
133
|
};
|
|
201
134
|
}
|
|
202
135
|
|
|
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
|
-
|
|
212
136
|
export class RuntimeLogReporter {
|
|
213
137
|
#live = { queue: [], inflight: undefined, abort: undefined };
|
|
214
138
|
#replay = { queue: [], inflight: undefined, abort: undefined };
|
|
@@ -228,8 +152,6 @@ export class RuntimeLogReporter {
|
|
|
228
152
|
#platform;
|
|
229
153
|
#osVersion;
|
|
230
154
|
#crashDirectory;
|
|
231
|
-
#restoring;
|
|
232
|
-
#savedDuringRestore = new Set();
|
|
233
155
|
|
|
234
156
|
constructor({ role = "cli", version = "unknown", send = sendSlardarPcBatch, instanceId = randomUUID(), diagnosticId: supportId = diagnosticId(), platform = process.platform, osVersion = os.release(), crashDirectory } = {}) {
|
|
235
157
|
this.#send = send;
|
|
@@ -242,7 +164,7 @@ export class RuntimeLogReporter {
|
|
|
242
164
|
this.#osVersion = osVersion;
|
|
243
165
|
this.#timer = setInterval(() => { void this.flush(); }, INTERVAL_MS);
|
|
244
166
|
this.#timer.unref?.();
|
|
245
|
-
this.#
|
|
167
|
+
this.#restoreCrashes();
|
|
246
168
|
}
|
|
247
169
|
|
|
248
170
|
// Crash files hold already-projected records and the identity at failure time.
|
|
@@ -250,48 +172,33 @@ export class RuntimeLogReporter {
|
|
|
250
172
|
// deliverNow also queues the record for this process's next flush. A
|
|
251
173
|
// successful send deletes its file; otherwise the next process replays it.
|
|
252
174
|
persistCrash(component, event, fields = {}, { deliverNow = false } = {}) {
|
|
175
|
+
let entry;
|
|
176
|
+
let crashFile;
|
|
253
177
|
try {
|
|
254
178
|
if (this.#closed || (!this.#crashDirectory && !deliverNow)) return;
|
|
255
|
-
|
|
256
|
-
this.#
|
|
257
|
-
if (
|
|
258
|
-
} catch { /* Crash diagnostics must preserve the original exit behavior. */ }
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
#saveCrash(entry) {
|
|
262
|
-
if (!this.#crashDirectory) return;
|
|
263
|
-
try {
|
|
179
|
+
if (!this.record("fatal", component, event, fields)) return;
|
|
180
|
+
entry = this.#live.queue.pop();
|
|
181
|
+
if (!this.#crashDirectory) return;
|
|
264
182
|
fs.mkdirSync(this.#crashDirectory, { recursive: true, mode: 0o700 });
|
|
265
183
|
const names = fs.readdirSync(this.#crashDirectory).filter(name => /^js-[a-f0-9-]+\.json$/u.test(name));
|
|
266
184
|
if (names.length >= 32) return;
|
|
267
|
-
const
|
|
268
|
-
const file = path.join(this.#crashDirectory, name);
|
|
185
|
+
const file = path.join(this.#crashDirectory, `js-${randomUUID()}.json`);
|
|
269
186
|
fs.writeFileSync(file, JSON.stringify(entry), { flag: "wx", mode: 0o600 });
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
187
|
+
crashFile = file;
|
|
188
|
+
} catch { /* Crash diagnostics must preserve the original exit behavior. */ } finally {
|
|
189
|
+
if (deliverNow && entry) this.#replay.queue.push({ ...entry, attempts: 0, ...(crashFile ? { crashFile } : {}) });
|
|
190
|
+
}
|
|
273
191
|
}
|
|
274
192
|
|
|
275
|
-
|
|
276
|
-
if (!this.#crashDirectory) return;
|
|
193
|
+
#restoreCrashes() {
|
|
277
194
|
try {
|
|
278
|
-
for (const name of
|
|
279
|
-
if (this.#closed) return;
|
|
280
|
-
if (this.#savedDuringRestore?.has(name)) continue;
|
|
195
|
+
for (const name of fs.readdirSync(this.#crashDirectory).filter(name => /^js-[a-f0-9-]+\.json$/u.test(name)).slice(0, 32)) {
|
|
281
196
|
const file = path.join(this.#crashDirectory, name);
|
|
282
197
|
try {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const entry = JSON.parse(await fs.promises.readFile(file, "utf8"));
|
|
198
|
+
if (fs.lstatSync(file).isSymbolicLink() || fs.statSync(file).size > 65536) continue;
|
|
199
|
+
const entry = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
286
200
|
if (entry?.record?.category?.reporter !== "codem_js" || entry?.record?.log_type !== "log" || !entry.header) continue;
|
|
287
|
-
|
|
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
|
-
}
|
|
201
|
+
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 });
|
|
295
202
|
} catch { /* A partial crash write cannot prevent startup. */ }
|
|
296
203
|
}
|
|
297
204
|
} catch { /* First startup or unavailable storage. */ }
|
|
@@ -321,90 +228,28 @@ export class RuntimeLogReporter {
|
|
|
321
228
|
record(level, component, event, fields = {}) {
|
|
322
229
|
try {
|
|
323
230
|
if (this.#closed) return false;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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(),
|
|
231
|
+
let safeFields;
|
|
232
|
+
try { safeFields = projectFields(fields); } catch { safeFields = {}; }
|
|
233
|
+
if (OBSERVATION_EVENTS.has(event)) safeFields.observation_id = randomUUID();
|
|
234
|
+
const severity = ["info", "warn", "error", "fatal"].includes(level) ? level : "info";
|
|
235
|
+
const identity = { ...this.#identity };
|
|
236
|
+
const category = {
|
|
400
237
|
client_kind: "cli", runtime_role: this.#role, reporter: "codem_js", log_schema: "1",
|
|
401
238
|
component: token(component) ?? "unknown", event: token(event) ?? "unknown", severity,
|
|
402
239
|
identity_source: identity.deviceId ? "codem" : "process",
|
|
403
240
|
diagnostic_id: this.#diagnosticId,
|
|
404
241
|
...(identity.tenantId ? { tenant_id: identity.tenantId } : {}),
|
|
405
242
|
...(identity.serverEnvironment ? { server_environment: identity.serverEnvironment } : {}),
|
|
406
|
-
...
|
|
407
|
-
|
|
243
|
+
...Object.fromEntries(["stage", "error_class", "error_code", "error_kind", "outcome"].filter((key) => safeFields[key]).map((key) => [key, safeFields[key]]))
|
|
244
|
+
};
|
|
245
|
+
if (OBSERVATION_EVENTS.has(category.event)) {
|
|
246
|
+
const trustedDevice = TRUSTED_DEVICE_EVENTS.has(category.event);
|
|
247
|
+
// Keep arbitrary upstream error codes in logs, not metric dimensions.
|
|
248
|
+
if (trustedDevice) for (const key of ["stage", "error_class", "error_code", "error_kind", "outcome"]) delete category[key];
|
|
249
|
+
for (const [key, values] of Object.entries(trustedDevice ? TRUSTED_DEVICE_DIMENSIONS : OBSERVATION_DIMENSIONS)) {
|
|
250
|
+
if (safeFields[key]) category[key] = values.includes(safeFields[key]) ? safeFields[key] : "unknown";
|
|
251
|
+
}
|
|
252
|
+
}
|
|
408
253
|
const header = {
|
|
409
254
|
aid: SLARDAR_PC_AID,
|
|
410
255
|
device_id: identity.deviceId ?? this.#instanceId,
|
|
@@ -414,35 +259,31 @@ export class RuntimeLogReporter {
|
|
|
414
259
|
os: this.#platform, os_version: this.#osVersion
|
|
415
260
|
};
|
|
416
261
|
const timestamp = Date.now();
|
|
417
|
-
const payload =
|
|
262
|
+
const payload = {
|
|
418
263
|
schema_version: 1, timestamp, component: category.component, event: category.event, level: severity,
|
|
419
264
|
process_instance_id: this.#instanceId, diagnostic_id: this.#diagnosticId, pid: process.pid,
|
|
420
265
|
...(identity.tenantId ? { tenant_id: identity.tenantId } : {}),
|
|
421
266
|
fields: safeFields
|
|
422
|
-
}
|
|
267
|
+
};
|
|
423
268
|
const record = {
|
|
424
269
|
app_version: this.#version, app_minor_version: "", build_id: "",
|
|
425
270
|
process_type: `codem-${this.#role}`, session_id: "", timestamp, category,
|
|
426
271
|
log_type: "log", log_level: severity === "fatal" ? "error" : severity,
|
|
427
|
-
|
|
272
|
+
log_content: `CodeM diagnostic ${severity} ${category.component}/${category.event}\n${JSON.stringify(payload)}`
|
|
428
273
|
};
|
|
429
|
-
const criticalSettlement = event === "delivery_settled" && dimensions.critical === "true";
|
|
430
274
|
const priority = severity === "fatal" ? 3 :
|
|
431
275
|
["resumed", "settled"].includes(safeFields.stage) || (severity === "error" && !safeFields.suppressed_count) ? 2 :
|
|
432
|
-
severity === "info"
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
444
|
-
queue.push(entry);
|
|
445
|
-
return true;
|
|
276
|
+
severity === "info" ? 0 : 1;
|
|
277
|
+
if (this.#live.queue.length >= CAPACITY) {
|
|
278
|
+
const minimum = Math.min(...this.#live.queue.map(entry => entry.priority ?? (entry.record.log_level === "info" ? 0 : 1)));
|
|
279
|
+
if (minimum > priority) { this.#dropped += 1; return false; }
|
|
280
|
+
const expendable = this.#live.queue.findIndex(entry => (entry.priority ?? (entry.record.log_level === "info" ? 0 : 1)) === minimum);
|
|
281
|
+
this.#live.queue.splice(expendable, 1);
|
|
282
|
+
this.#dropped += 1;
|
|
283
|
+
}
|
|
284
|
+
this.#live.queue.push({ header, record, metrics: metricProjection(category.event, safeFields), extra: extraProjection(safeFields), priority, attempts: 0 });
|
|
285
|
+
return true;
|
|
286
|
+
} catch { return false; }
|
|
446
287
|
}
|
|
447
288
|
|
|
448
289
|
async flush({ timeoutMs = REQUEST_TIMEOUT_MS } = {}) {
|
|
@@ -475,7 +316,7 @@ export class RuntimeLogReporter {
|
|
|
475
316
|
// Run both lanes concurrently: old replay cannot delay live sends. Waiting
|
|
476
317
|
// for both within one budget also gives short-lived invocations a chance
|
|
477
318
|
// to finish replay; a timeout leaves its original crash file for next time.
|
|
478
|
-
const replay = this.#
|
|
319
|
+
const replay = this.#flushLane(this.#replay, REQUEST_TIMEOUT_MS);
|
|
479
320
|
let timer;
|
|
480
321
|
try {
|
|
481
322
|
await Promise.race([
|
|
@@ -506,27 +347,16 @@ export class RuntimeLogReporter {
|
|
|
506
347
|
const first = lane.queue[0];
|
|
507
348
|
const key = JSON.stringify(first.header);
|
|
508
349
|
const batch = [];
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
}
|
|
350
|
+
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());
|
|
351
|
+
const data = batch.flatMap(({ record, metrics, extra }) => [record, {
|
|
352
|
+
app_version: record.app_version, app_minor_version: "", build_id: "",
|
|
353
|
+
process_type: record.process_type, session_id: "", timestamp: record.timestamp,
|
|
354
|
+
log_type: "flexible", event_name: "codem_runtime_diagnostic", category: record.category, metrics: metrics ?? { count: 1 }, ...(extra ? { extra } : {})
|
|
355
|
+
}]);
|
|
526
356
|
try {
|
|
527
357
|
await Promise.race([Promise.resolve().then(() => this.#send({ header: first.header, data }, { signal: controller.signal })), deadline]);
|
|
528
358
|
for (const entry of batch) {
|
|
529
|
-
if (entry.crashFile)
|
|
359
|
+
if (entry.crashFile) { try { fs.unlinkSync(entry.crashFile); } catch { /* Replay may duplicate after a crash or concurrent startup. */ } }
|
|
530
360
|
}
|
|
531
361
|
} catch {
|
|
532
362
|
this.#uploadFailures += 1;
|
|
@@ -583,25 +413,15 @@ export function recordRuntimeLog(level, component, event, fields = {}) {
|
|
|
583
413
|
try { return active?.record(level, component, event, fields) ?? false; } catch { return false; }
|
|
584
414
|
}
|
|
585
415
|
|
|
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
|
-
|
|
596
416
|
export function persistRuntimeCrash(component, event, fields) {
|
|
597
417
|
try { active?.persistCrash(component, event, fields); } catch { /* best effort */ }
|
|
598
418
|
}
|
|
599
419
|
|
|
600
420
|
// A failure that ends a short-lived process: persist first, then try to deliver
|
|
601
421
|
// within one upload budget, so an immediate exit cannot lose the count.
|
|
602
|
-
export function reportRuntimeFailure(component, event, fields = {}) {
|
|
422
|
+
export async function reportRuntimeFailure(component, event, fields = {}, budgetMs = 3_000) {
|
|
603
423
|
try { active?.persistCrash(component, event, fields, { deliverNow: true }); } catch { /* best effort */ }
|
|
604
|
-
|
|
424
|
+
await flushRuntimeLogs(budgetMs);
|
|
605
425
|
}
|
|
606
426
|
|
|
607
427
|
export async function flushRuntimeLogs(budgetMs = 200) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import https from "node:https";
|
|
2
|
-
import {
|
|
3
|
-
import { promisify } from "node:util";
|
|
2
|
+
import { gzipSync } from "node:zlib";
|
|
4
3
|
|
|
5
4
|
// PC Custom Log / Custom Event wire format, verified against Parfait 1.7.0.0's
|
|
6
5
|
// public IUploader callback. This is not the Slardar Web/BID protocol. Keep the
|
|
@@ -11,13 +10,10 @@ export const SLARDAR_PC_AID = "1880";
|
|
|
11
10
|
// from the native Parfait SDK. Do not advertise a native SDK we do not run.
|
|
12
11
|
export const SLARDAR_PC_REPORTER_VERSION = "1.0.0.0";
|
|
13
12
|
const ENDPOINT = `https://pc-mon.zijieapi.com/monitor_pc/collect/api/pc_log?aid=${SLARDAR_PC_AID}`;
|
|
14
|
-
const compress = promisify(gzip);
|
|
15
13
|
|
|
16
|
-
export
|
|
17
|
-
signal?.throwIfAborted();
|
|
18
|
-
const body = await compress(Buffer.from(JSON.stringify(payload)));
|
|
19
|
-
signal?.throwIfAborted();
|
|
14
|
+
export function sendSlardarPcBatch(payload, { signal, request = https.request } = {}) {
|
|
20
15
|
return new Promise((resolve, reject) => {
|
|
16
|
+
const body = gzipSync(Buffer.from(JSON.stringify(payload)));
|
|
21
17
|
const req = request(ENDPOINT, {
|
|
22
18
|
method: "POST",
|
|
23
19
|
signal,
|
package/bin/linco-paths.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// Observation only. This module runs before either business Bridge (including
|
|
2
|
-
// an unmodified stable CLI), and imports only stdlib and shared lane rules.
|
|
3
|
-
import http from 'node:http';
|
|
4
|
-
import https from 'node:https';
|
|
5
|
-
import { rewriteUrlForTtEnv, ttEnvHeadersFor } from './tt-env.js';
|
|
6
|
-
|
|
7
|
-
export function workerRuntimeTelemetryTarget({env=process.env,args=process.argv.slice(2)}={}) {
|
|
8
|
-
if (/^(1|true|yes|on)$/i.test(env.CODEM_TELEMETRY_DISABLED || '') || !env.CODEM_TOKEN || !env.CODEM_SANDBOX_DISPATCH_ID) return null;
|
|
9
|
-
// Mirror daemon's Commander options without importing business Bridge code:
|
|
10
|
-
// optional --sandbox [mode], required --server <url>, last occurrence wins.
|
|
11
|
-
let sandbox,serverFlag;
|
|
12
|
-
for(let index=0;index<args.length;index++) {
|
|
13
|
-
const arg=args[index];
|
|
14
|
-
if(arg==='--')break;
|
|
15
|
-
if(arg==='--sandbox') {
|
|
16
|
-
sandbox=args[index+1] && !args[index+1].startsWith('-') ? args[++index] : true;
|
|
17
|
-
} else if(arg.startsWith('--sandbox=')) {
|
|
18
|
-
sandbox=arg.slice('--sandbox='.length);
|
|
19
|
-
} else if(arg==='--server') {
|
|
20
|
-
if(!args[index+1] || args[index+1].startsWith('-'))return null;
|
|
21
|
-
serverFlag=args[++index];
|
|
22
|
-
} else if(arg.startsWith('--server=')) {
|
|
23
|
-
serverFlag=arg.slice('--server='.length);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
if(sandbox!==undefined && sandbox!==true && sandbox!=='native')return null;
|
|
27
|
-
const native=sandbox==='native';
|
|
28
|
-
// Native rejects --server and ignores legacy sandbox URL aliases. Cloud's
|
|
29
|
-
// --server default is CODEM_SERVER_URL, before the sandbox-config fallbacks.
|
|
30
|
-
if(native && serverFlag!==undefined)return null;
|
|
31
|
-
const candidates=native ? [env.CODEM_SERVER_URL,'https://codem.feishu.cn'] : [serverFlag===undefined ? env.CODEM_SERVER_URL : serverFlag,env.CODEM_SANDBOX_SERVER_URL,env.SANDBOX_SERVER_URL,env.SERVER_URL,'http://localhost:3000'];
|
|
32
|
-
const server=candidates.find(value=>typeof value==='string' && value.trim()).trim();
|
|
33
|
-
const url=new URL(rewriteUrlForTtEnv(server,env));
|
|
34
|
-
if (!['http:','https:'].includes(url.protocol) || url.username || url.password) return null;
|
|
35
|
-
url.pathname=`${url.pathname.replace(/\/+$/,'') || '/goapi/codem'}/sandbox/telemetry/workflow_metrics`;
|
|
36
|
-
url.search='';url.hash='';
|
|
37
|
-
return {url,headers:{'Content-Type':'application/json',Authorization:`Bearer sandbox_${env.CODEM_TOKEN}`,...ttEnvHeadersFor(url,env)}};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function reportWorkerRuntime(runtime,{env=process.env,args=process.argv.slice(2),signal,timeoutMS=750}={}) {
|
|
41
|
-
try {
|
|
42
|
-
const target=workerRuntimeTelemetryTarget({env,args});
|
|
43
|
-
if (!target || signal?.aborted) return false;
|
|
44
|
-
const body=JSON.stringify({worker_runtime:{dispatch_id:env.CODEM_SANDBOX_DISPATCH_ID,family:runtime.family,cli_version:runtime.cliVersion,core_version:runtime.coreVersion}});
|
|
45
|
-
return await new Promise(resolve=>{
|
|
46
|
-
let timer,finished=false;
|
|
47
|
-
const done=value=>{if(finished)return;finished=true;clearTimeout(timer);resolve(value);};
|
|
48
|
-
const request=(target.url.protocol==='https:'?https:http).request(target.url,{method:'POST',headers:{...target.headers,'Content-Length':Buffer.byteLength(body)},signal},response=>{
|
|
49
|
-
let payload='';
|
|
50
|
-
response.on('data',chunk=>{payload+=chunk;if(payload.length>4096){done(false);request.destroy();}});
|
|
51
|
-
response.once('error',()=>done(false));
|
|
52
|
-
response.once('end',()=>{try{done(response.statusCode===200 && JSON.parse(payload).accepted===true);}catch{done(false);}});
|
|
53
|
-
});
|
|
54
|
-
request.once('error',()=>done(false));
|
|
55
|
-
timer=setTimeout(()=>{done(false);request.destroy();},timeoutMS);
|
|
56
|
-
timer.unref();
|
|
57
|
-
request.end(body);
|
|
58
|
-
});
|
|
59
|
-
} catch { return false; }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function observeWorkerRuntime(runtime,options={}) {
|
|
63
|
-
const controller=new AbortController();
|
|
64
|
-
let timer;
|
|
65
|
-
// Native's dispatch token may only be materialized by the child's first
|
|
66
|
-
// normal Sync. Bounded retries do not acquire a lease or replay a task.
|
|
67
|
-
const delays=[1000,5000,30000];
|
|
68
|
-
const attempt=async index=>{
|
|
69
|
-
const accepted=await reportWorkerRuntime(runtime,{...options,signal:controller.signal});
|
|
70
|
-
if(accepted || controller.signal.aborted || index===delays.length)return;
|
|
71
|
-
timer=setTimeout(()=>void attempt(index+1),delays[index]);timer.unref();
|
|
72
|
-
};
|
|
73
|
-
void attempt(0);
|
|
74
|
-
return ()=>{clearTimeout(timer);controller.abort();};
|
|
75
|
-
}
|
package/bin/worker-runtime.js
DELETED
|
@@ -1,294 +0,0 @@
|
|
|
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();
|
|
@@ -1,10 +0,0 @@
|
|
|
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
|
-
}
|