@codemem/opencode-plugin 0.47.0-beta.2 → 0.47.0-beta.4

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.
@@ -0,0 +1,220 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { promisify } from "node:util";
7
+
8
+ const executeFile = (...args) => promisify(execFile)(...args);
9
+ const PROBE_PATH = fileURLToPath(new URL("./cli-runner-probe.js", import.meta.url));
10
+ const MAX_OUTPUT_BYTES = 16 * 1024;
11
+ const PROBE_TIMEOUT_MS = 5_000;
12
+ const BOOTSTRAP_TIMEOUT_MS = 60_000;
13
+ const RETRY_DELAY_MS = 30_000;
14
+
15
+ const readManifest = (path) => {
16
+ if (statSync(path).size > 64 * 1024) throw new Error("manifest_unavailable");
17
+ return JSON.parse(readFileSync(path, "utf8"));
18
+ };
19
+
20
+ const isContainedPath = (root, path) => {
21
+ const child = relative(root, path);
22
+ return !!child && child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child);
23
+ };
24
+
25
+ const containedFile = (root, path) => {
26
+ const canonicalPath = realpathSync(path);
27
+ if (!isContainedPath(realpathSync(root), canonicalPath) || !statSync(canonicalPath).isFile()) {
28
+ throw new Error("entrypoint_unavailable");
29
+ }
30
+ return canonicalPath;
31
+ };
32
+
33
+ const fileStamp = (path) => {
34
+ const canonicalPath = realpathSync(path);
35
+ const stat = statSync(canonicalPath);
36
+ if (!stat.isFile()) throw new Error("entrypoint_unavailable");
37
+ return [canonicalPath, stat.dev, stat.ino, stat.size, stat.mtimeMs, stat.ctimeMs];
38
+ };
39
+
40
+ const candidateStamp = (candidate) => JSON.stringify(candidate.files.map(fileStamp));
41
+
42
+ export const isCachedCliCurrent = (candidate) => {
43
+ try {
44
+ return candidateStamp(candidate) === candidate.stamp;
45
+ } catch {
46
+ return false;
47
+ }
48
+ };
49
+
50
+ const readEmbeddings = (entrypoint, version) => {
51
+ // Use Node's lookup paths without requiring the package's private manifest
52
+ // subpath to be exported. Exclude CJS global folders, which ESM cannot import.
53
+ const paths = createRequire(entrypoint).resolve.paths("@codemem/embeddings") || [];
54
+ const manifestPath = paths.filter((path) => isContainedPath(dirname(path), entrypoint))
55
+ .map((path) => join(path, "@codemem/embeddings/package.json")).find(existsSync);
56
+ const manifest = readManifest(manifestPath);
57
+ if (manifest.name !== "@codemem/embeddings" || manifest.version !== version) {
58
+ throw new Error("embeddings_unavailable");
59
+ }
60
+ const exported = manifest.exports?.["."]?.import;
61
+ if (typeof exported !== "string") throw new Error("embeddings_unavailable");
62
+ const entry = containedFile(dirname(manifestPath), resolve(dirname(manifestPath), exported));
63
+ return [manifestPath, entry];
64
+ };
65
+
66
+ const readCandidate = (manifestPath, { nodePath, executable, expectedVersion }) => {
67
+ const manifest = readManifest(manifestPath);
68
+ if (manifest.name !== "codemem" || typeof manifest.version !== "string"
69
+ || typeof manifest.bin?.codemem !== "string") throw new Error("package_unavailable");
70
+ if (expectedVersion && manifest.version !== expectedVersion) throw new Error("version_mismatch");
71
+ const entrypoint = containedFile(dirname(manifestPath), resolve(dirname(manifestPath), manifest.bin.codemem));
72
+ // POSIX package bins are symlinks. Windows command shims are not JS files;
73
+ // resolve their package metadata instead of parsing or executing the shim.
74
+ if (executable && !executable.endsWith(".cmd") && realpathSync(executable) !== entrypoint) {
75
+ throw new Error("entrypoint_mismatch");
76
+ }
77
+ const files = [nodePath, manifestPath, entrypoint];
78
+ if (executable) files.push(executable);
79
+ if (expectedVersion) files.push(...readEmbeddings(entrypoint, expectedVersion));
80
+ const candidate = { command: realpathSync(nodePath), args: [entrypoint], version: manifest.version, files,
81
+ manifestPath, executable };
82
+ return { ...candidate, stamp: candidateStamp(candidate) };
83
+ };
84
+
85
+ const pathCandidateManifests = ({ path, selectedOnly }) => {
86
+ const candidates = [];
87
+ for (const directory of String(path || "").slice(0, 32_768).split(delimiter).slice(0, 128)) {
88
+ if (!isAbsolute(directory)) continue;
89
+ const executable = join(directory, process.platform === "win32" ? "codemem.cmd" : "codemem");
90
+ if (selectedOnly && !existsSync(executable)) continue;
91
+ try {
92
+ const anchor = realpathSync(executable);
93
+ const manifestPath = createRequire(anchor).resolve("codemem/package.json");
94
+ candidates.push({ manifestPath, executable });
95
+ } catch { /* Check only named executable files, never enumerate cache directories. */ }
96
+ // Do not bypass a selected shim for another CLI later on PATH or in the
97
+ // plugin's dependencies. Keep its legacy invocation if it cannot be resolved.
98
+ if (selectedOnly) return candidates;
99
+ }
100
+ return candidates;
101
+ };
102
+
103
+ const candidateManifests = ({ path, bootstrap, selectedOnly }) => {
104
+ const candidates = pathCandidateManifests({ path, selectedOnly });
105
+ if (!bootstrap && !selectedOnly) {
106
+ try { candidates.push({ manifestPath: createRequire(import.meta.url).resolve("codemem/package.json") }); }
107
+ catch { /* The plugin need not depend on the CLI package. */ }
108
+ }
109
+ return candidates;
110
+ };
111
+
112
+ export const probeCliCandidate = async ({ path, nodePath, expectedVersion, bootstrap = false, selectedOnly = false }) => {
113
+ const deadline = Date.now() + 4_000;
114
+ for (const location of candidateManifests({ path, bootstrap, selectedOnly }).slice(0, 8)) {
115
+ const remaining = deadline - Date.now();
116
+ if (remaining <= 0) break;
117
+ try {
118
+ const candidate = readCandidate(location.manifestPath, { nodePath, expectedVersion, executable: location.executable });
119
+ const result = await executeFile(candidate.command, [...candidate.args, "--version"], {
120
+ timeout: remaining, maxBuffer: MAX_OUTPUT_BYTES, killSignal: "SIGKILL",
121
+ });
122
+ if (result.stdout.trim() === candidate.version && isCachedCliCurrent(candidate)) return candidate;
123
+ } catch { /* An unusable candidate must not select an unverified command. */ }
124
+ }
125
+ return null;
126
+ };
127
+
128
+ const runProbe = async ({ command, args, cwd, env, timeout, execute, expectedVersion }) => {
129
+ try {
130
+ const result = await execute(command, args, {
131
+ cwd, env, timeout, maxBuffer: MAX_OUTPUT_BYTES, killSignal: "SIGKILL",
132
+ });
133
+ const candidate = JSON.parse(result.stdout);
134
+ if (!candidate || !isAbsolute(candidate.command) || candidate.args?.length !== 1
135
+ || !isAbsolute(candidate.args[0]) || !Array.isArray(candidate.files)
136
+ || candidate.files.length > 6 || !isCachedCliCurrent(candidate)) return null;
137
+ const verified = readCandidate(candidate.manifestPath, {
138
+ nodePath: candidate.command, executable: candidate.executable, expectedVersion,
139
+ });
140
+ if (verified.version !== candidate.version || verified.args[0] !== candidate.args[0]
141
+ || verified.stamp !== candidate.stamp) return null;
142
+ return verified;
143
+ } catch {
144
+ return null;
145
+ }
146
+ };
147
+
148
+ const warnOnce = (warn) => {
149
+ let warned = false;
150
+ return (reason) => {
151
+ if (warned) return;
152
+ warned = true;
153
+ try { void Promise.resolve(warn(reason)).catch(() => {}); } catch { /* best effort */ }
154
+ };
155
+ };
156
+
157
+ const resolveDefaultCli = async ({ runner, pin, cwd, env, isCompatibleVersion, warn, execute }) => {
158
+ const probe = (command, args, timeout, expectedVersion) =>
159
+ runProbe({ command, args, timeout, cwd, env, execute, expectedVersion });
160
+ const probeArgs = [PROBE_PATH];
161
+ if (runner === "codemem") probeArgs.push("--selected");
162
+ const installed = await probe("node", probeArgs, PROBE_TIMEOUT_MS);
163
+ if (installed && isCompatibleVersion(installed.version, pin)) return installed;
164
+ if (installed) warn("channel_or_version_mismatch");
165
+ // A compatible PATH command may be a version-manager or package-manager shim.
166
+ // Failure to optimize it must not replace the selected backend with the pin.
167
+ if (runner !== "npx") return null;
168
+ // npm exec documents the requested packages' bins on child PATH. The shipped
169
+ // probe reads that PATH; it does not infer npm's private cache layout.
170
+ const bootstrapped = await probe("npx", [
171
+ "-y", "--package", `codemem@${pin}`, "--package", `@codemem/embeddings@${pin}`,
172
+ "node", PROBE_PATH, pin,
173
+ ], BOOTSTRAP_TIMEOUT_MS, pin);
174
+ if (bootstrapped?.version === pin) return bootstrapped;
175
+ warn("resolution_unavailable");
176
+ return null;
177
+ };
178
+
179
+ export const createCachedCliRunner = ({
180
+ runner, runnerArgs, explicit, pin, cwd, env = process.env,
181
+ isCompatibleVersion, warn = () => {}, now = Date.now, execute = executeFile,
182
+ }) => {
183
+ let cached = null;
184
+ let pending = null;
185
+ let retryAt = 0;
186
+ let generation = 0;
187
+ const report = warnOnce(warn);
188
+
189
+ const invalidate = () => { cached = null; pending = null; retryAt = 0; generation += 1; };
190
+ const resolveInvocation = async () => {
191
+ if (pending) return pending;
192
+ if (cached && isCachedCliCurrent(cached)) return cached;
193
+ cached = null;
194
+ if (now() < retryAt) return null;
195
+ const startedGeneration = generation;
196
+ const resolution = resolveDefaultCli({ runner, pin, cwd, env, isCompatibleVersion, warn: report, execute })
197
+ .catch(() => null)
198
+ .then((candidate) => {
199
+ if (startedGeneration !== generation) return null;
200
+ cached = candidate;
201
+ if (!candidate) retryAt = now() + RETRY_DELAY_MS;
202
+ return candidate;
203
+ })
204
+ .finally(() => { if (pending === resolution) pending = null; });
205
+ pending = resolution;
206
+ return resolution;
207
+ };
208
+
209
+ const command = (args) => {
210
+ if (explicit) return [runner, ...runnerArgs, ...args];
211
+ return resolveInvocation().then((invocation) => {
212
+ if (!invocation) {
213
+ if (runner === "codemem") return [runner, ...runnerArgs, ...args];
214
+ return null;
215
+ }
216
+ return [invocation.command, ...invocation.args, ...args];
217
+ });
218
+ };
219
+ return { command, invalidate };
220
+ };
@@ -0,0 +1,21 @@
1
+ import { probeCliCandidate } from "./cached-cli-runner.js";
2
+
3
+ // This script runs under the PATH-selected Node, not the plugin host executable.
4
+ // npm exec supplies its package bins through PATH on the one bootstrap call.
5
+ const main = async () => {
6
+ if (process.release?.name !== "node" || process.versions.bun) return;
7
+ const [major, minor] = process.versions.node.split(".").map(Number);
8
+ if (major < 24 || (major === 24 && minor < 15)) return;
9
+ const selectedOnly = process.argv[2] === "--selected";
10
+ const expectedVersion = selectedOnly ? undefined : process.argv[2];
11
+ const candidate = await probeCliCandidate({
12
+ path: process.env.PATH,
13
+ nodePath: process.execPath,
14
+ expectedVersion,
15
+ selectedOnly,
16
+ bootstrap: expectedVersion !== undefined,
17
+ });
18
+ if (candidate) process.stdout.write(JSON.stringify(candidate));
19
+ };
20
+
21
+ main().catch(() => { process.exitCode = 1; });
@@ -0,0 +1,75 @@
1
+ const REASONS = [
2
+ "success", "disabled", "backoff", "profile_malformed", "protocol_mismatch",
3
+ "database_mismatch", "identity_mismatch", "contract_unsupported",
4
+ "request_rejected", "authorization_failure", "policy_failure",
5
+ "endpoint_unavailable", "server_failure", "unexpected_status",
6
+ "malformed_response", "timeout", "connection_failure", "unknown",
7
+ ];
8
+ const OPERATIONS = ["pack", "prompt-pack-ledger"];
9
+ const RUNNERS = ["viewer", "none", "codemem", "npx", "node", "uvx", "uv", "custom"];
10
+ const STAGES = ["skipped", "profile", "request"];
11
+ const MAX_COUNT = 1_000_000;
12
+ const MAX_ELAPSED_MS = 86_400_000;
13
+ const LOG_INTERVAL_MS = 30_000;
14
+
15
+ const increment = (count) => Math.min(MAX_COUNT, count + 1);
16
+ const boundedElapsed = (value) => Number.isFinite(value)
17
+ ? Math.min(MAX_ELAPSED_MS, Math.max(0, Math.round(value)))
18
+ : 0;
19
+
20
+ const safeFields = (input) => ({
21
+ stage: STAGES.includes(input.stage) ? input.stage : "skipped",
22
+ elapsed_ms: boundedElapsed(input.elapsed_ms),
23
+ runner: RUNNERS.includes(input.runner) ? input.runner : "custom",
24
+ disposition: ["fallback", "local_fallback", "terminal"].includes(input.disposition)
25
+ ? input.disposition : "unknown",
26
+ status_class: ["2xx", "3xx", "4xx", "5xx"].includes(input.status_class)
27
+ ? input.status_class : "none",
28
+ });
29
+
30
+ // Only fixed categories and bounded numbers cross this boundary. No request,
31
+ // response, error, runner path, or identity is retained, even in suppressed logs.
32
+ export const createPromptTransportDiagnostics = ({ logLine, now = () => performance.now() }) => {
33
+ const counters = Object.fromEntries(OPERATIONS.map((operation) => [
34
+ operation, Object.fromEntries(REASONS.map((reason) => [reason, 0])),
35
+ ]));
36
+ let suppressed = 0;
37
+ let nextLogAt = -Infinity;
38
+ let writing = false;
39
+
40
+ const snapshot = () => ({
41
+ counters: Object.fromEntries(OPERATIONS.map((operation) => [operation, { ...counters[operation] }])),
42
+ suppressed,
43
+ });
44
+
45
+ const record = (input) => {
46
+ if (!OPERATIONS.includes(input.operation)) return;
47
+ const reason = REASONS.includes(input.reason) ? input.reason : "unknown";
48
+ const counts = counters[input.operation];
49
+ counts[reason] = increment(counts[reason]);
50
+ if (reason === "success") return;
51
+ const timestamp = now();
52
+ if (writing || timestamp < nextLogAt) {
53
+ suppressed = increment(suppressed);
54
+ return;
55
+ }
56
+ nextLogAt = timestamp + LOG_INTERVAL_MS;
57
+ const entry = {
58
+ operation: input.operation,
59
+ reason,
60
+ ...safeFields(input),
61
+ ...snapshot(),
62
+ };
63
+ suppressed = 0;
64
+ writing = true;
65
+ try {
66
+ void Promise.resolve(logLine(`inject.transport ${JSON.stringify(entry)}`))
67
+ .catch(() => {})
68
+ .finally(() => { writing = false; });
69
+ } catch {
70
+ writing = false;
71
+ }
72
+ };
73
+
74
+ return { record, snapshot };
75
+ };
@@ -4,6 +4,7 @@ import { existsSync, readFileSync } from "node:fs";
4
4
  import { appendFile, mkdir } from "node:fs/promises";
5
5
  import { homedir } from "node:os";
6
6
  import { basename, dirname, join, posix, resolve, win32 } from "node:path";
7
+ import { createCachedCliRunner } from "./cached-cli-runner.js";
7
8
  import {
8
9
  isVersionAtLeast,
9
10
  parseBackendUpdatePolicy,
@@ -12,6 +13,7 @@ import {
12
13
  resolveUpgradeGuidance,
13
14
  } from "./compat.js";
14
15
  import { V2_ADAPTER_DIAGNOSTICS } from "./host-contract.js";
16
+ import { createPromptTransportDiagnostics } from "./prompt-transport-diagnostics.js";
15
17
  import {
16
18
  classifyRawEventTransportCause,
17
19
  createRawEventDelivery,
@@ -30,7 +32,7 @@ import {
30
32
 
31
33
  const TRUTHY_VALUES = ["1", "true", "yes"];
32
34
  const DISABLED_VALUES = ["0", "false", "off"];
33
- const PINNED_BACKEND_VERSION = "0.47.0-beta.2";
35
+ const PINNED_BACKEND_VERSION = "0.47.0-beta.4";
34
36
  const COMPAT_CHECK_DELAY_MS = 1500;
35
37
  const COMPAT_CHECK_CACHE_TTL_MS = 5 * 60 * 1000;
36
38
  const MAX_UPDATE_STATUS_BYTES = 16 * 1024;
@@ -939,6 +941,84 @@ const isViewerPolicyOrAuthFailurePayload = (payload) => {
939
941
  ].includes(code);
940
942
  };
941
943
 
944
+ const diagnosticTimeoutReason = (signal, fallback) =>
945
+ signal?.aborted && signal.reason?.name === "TimeoutError" ? "timeout" : fallback;
946
+
947
+ const diagnosticStatusClass = (status) =>
948
+ Number.isInteger(status) ? `${Math.floor(status / 100)}xx` : "none";
949
+
950
+ const viewerHttpDiagnosticReason = ({ operation, status, body, signal }) => {
951
+ // A timeout is reported only when our own deadline fired, never from raw
952
+ // exception text or merely observing that a CLI fallback was selected.
953
+ if (diagnosticTimeoutReason(signal) === "timeout") return "timeout";
954
+ if (isViewerDbMismatchPayload(body)) return "database_mismatch";
955
+ if (isViewerIdentityMismatchPayload(body)) return "identity_mismatch";
956
+ if (isViewerContractUnsupportedPayload(body)) return "contract_unsupported";
957
+ if (operation === "prompt-pack-ledger" && isValidLedgerFailureHttpPayload(body)) {
958
+ return "request_rejected";
959
+ }
960
+ if (status === 401 || status === 403) return "authorization_failure";
961
+ if (isViewerPolicyOrAuthFailurePayload(body)) return "policy_failure";
962
+ if (isViewerInvalidRequestPayload(body)) {
963
+ return "request_rejected";
964
+ }
965
+ if (status === 404 || status === 405) return "endpoint_unavailable";
966
+ if (Number.isInteger(status) && status >= 500) return "server_failure";
967
+ if (Number.isInteger(status)) return "unexpected_status";
968
+ return "connection_failure";
969
+ };
970
+
971
+ const viewerProfileFailureKind = (profileBody, dbPath, identityTarget) => {
972
+ if (!isRecord(profileBody) || profileBody.service !== "codemem-viewer") return "profile_malformed";
973
+ const range = normalizePromptTransportProtocolRange(
974
+ profileBody.protocol_version, profileBody.min_supported_protocol_version,
975
+ );
976
+ if (!range) return "protocol_malformed";
977
+ if (!arePromptTransportProtocolRangesCompatible(PROMPT_TRANSPORT_PROTOCOL_RANGE, range)) {
978
+ return "protocol_range_mismatch";
979
+ }
980
+ if (profileBody.db_path !== dbPath) return "database_mismatch";
981
+ if (canonicalJson(profileBody.identity_target) !== canonicalJson(identityTarget)) {
982
+ return "runtime_identity_mismatch";
983
+ }
984
+ return null;
985
+ };
986
+
987
+ const classifyViewerProfileFailure = (profileBody, { operation, dbPath, identityTarget }) => {
988
+ const kind = viewerProfileFailureKind(profileBody, dbPath, identityTarget);
989
+ if (!kind) return null;
990
+ const details = {
991
+ profile_malformed: ["profile malformed", "profile_malformed"],
992
+ protocol_malformed: ["protocol range malformed", "profile_malformed"],
993
+ protocol_range_mismatch: ["protocol range unsupported", "protocol_mismatch"],
994
+ database_mismatch: ["database mismatch", "database_mismatch"],
995
+ runtime_identity_mismatch: ["runtime identity mismatch", "identity_mismatch"],
996
+ };
997
+ const [description, reason] = details[kind];
998
+ return {
999
+ classification: viewerFailureClassification(
1000
+ `${operation} viewer ${description}`, classifyPromptTransportFailure({ kind }),
1001
+ ),
1002
+ reason,
1003
+ };
1004
+ };
1005
+
1006
+ const createPromptTransportResult = ({ operation, runner, diagnostics, viewerOnly, timing }) =>
1007
+ (result, reason, status = null) => {
1008
+ let selectedRunner = "viewer";
1009
+ if (!result.ok) {
1010
+ selectedRunner = result.classification.retryable && !viewerOnly ? runner : "none";
1011
+ }
1012
+ diagnostics.record({
1013
+ operation, reason, stage: timing.stage,
1014
+ elapsed_ms: performance.now() - timing.startedAt,
1015
+ status_class: diagnosticStatusClass(status),
1016
+ runner: selectedRunner,
1017
+ disposition: result.classification?.disposition,
1018
+ });
1019
+ return result;
1020
+ };
1021
+
942
1022
  const viewerFailureClassification = (cause, disposition, kind = null) => ({
943
1023
  cause,
944
1024
  disposition,
@@ -2171,6 +2251,27 @@ const buildRunnerArgs = ({ runner, runnerFrom, runnerFromExplicit }) => {
2171
2251
  return runnerFromExplicit ? [runnerFrom] : [];
2172
2252
  };
2173
2253
 
2254
+ const resolveRuntimeRunner = ({ cwd, log }) => {
2255
+ const runner = detectRunner({ cwd, envRunner: process.env.CODEMEM_RUNNER });
2256
+ const runnerFromExplicit = Boolean(String(process.env.CODEMEM_RUNNER_FROM || "").trim());
2257
+ const runnerFrom = process.env.CODEMEM_RUNNER_FROM || cwd;
2258
+ const runnerArgs = buildRunnerArgs({ runner, runnerFrom, runnerFromExplicit });
2259
+ const cliRunner = createCachedCliRunner({
2260
+ runner, runnerArgs, pin: PINNED_BACKEND_VERSION, cwd,
2261
+ explicit: Object.hasOwn(process.env, "CODEMEM_RUNNER") || Object.hasOwn(process.env, "CODEMEM_RUNNER_FROM"),
2262
+ isCompatibleVersion: isSameChannelVersionAtLeast,
2263
+ warn: (reason) => log("warn", "codemem default CLI runner could not be reused", { reason }),
2264
+ });
2265
+ return { runner, runnerArgs, runnerFrom, runnerFromExplicit, cliRunner };
2266
+ };
2267
+
2268
+ const createViewerCliArgs = ({ viewerHost, viewerPort, viewerDbPath, viewerConfigPath }) => (action) => {
2269
+ const args = ["serve", action, "--host", viewerHost, "--port", viewerPort];
2270
+ if (String(viewerDbPath || "").trim()) args.push("--db-path", viewerDbPath);
2271
+ if (String(viewerConfigPath || "").trim()) args.push("--config", viewerConfigPath);
2272
+ return args;
2273
+ };
2274
+
2174
2275
  export const createCodememRuntime = async ({ location, host, hostGeneration = null }) => {
2175
2276
  const { project, directory, worktree } = location;
2176
2277
  const hostLog = typeof host?.log === "function" ? host.log : async () => {};
@@ -2203,6 +2304,7 @@ export const createCodememRuntime = async ({ location, host, hostGeneration = nu
2203
2304
  const errorLogPath = resolveErrorLogPath(cwd, process.env.HOME);
2204
2305
  const logLine = createLogLine(logPath);
2205
2306
  const errorLogLine = createLogLine(errorLogPath);
2307
+ const promptTransportDiagnostics = createPromptTransportDiagnostics({ logLine: errorLogLine });
2206
2308
  const log = createDebugLogger({
2207
2309
  debug,
2208
2310
  host: { log: hostLog },
@@ -2225,13 +2327,7 @@ export const createCodememRuntime = async ({ location, host, hostGeneration = nu
2225
2327
  return null;
2226
2328
  }
2227
2329
 
2228
- const runner = detectRunner({
2229
- cwd,
2230
- envRunner: process.env.CODEMEM_RUNNER,
2231
- });
2232
- const runnerFromExplicit = Boolean(String(process.env.CODEMEM_RUNNER_FROM || "").trim());
2233
- const runnerFrom = process.env.CODEMEM_RUNNER_FROM || cwd;
2234
- const runnerArgs = buildRunnerArgs({ runner, runnerFrom, runnerFromExplicit });
2330
+ const { runner, runnerArgs, runnerFrom, runnerFromExplicit, cliRunner } = resolveRuntimeRunner({ cwd, log });
2235
2331
  const viewerEnabled = envNotDisabled(process.env.CODEMEM_VIEWER || "1");
2236
2332
  const viewerAutoStart = envNotDisabled(
2237
2333
  process.env.CODEMEM_VIEWER_AUTO || "1"
@@ -2361,16 +2457,7 @@ export const createCodememRuntime = async ({ location, host, hostGeneration = nu
2361
2457
  return `${Date.now()}-${Math.random()}`;
2362
2458
  };
2363
2459
 
2364
- const buildViewerCliArgs = (action) => {
2365
- const args = ["serve", action, "--host", viewerHost, "--port", viewerPort];
2366
- if (String(viewerDbPath || "").trim()) {
2367
- args.push("--db-path", viewerDbPath);
2368
- }
2369
- if (String(viewerConfigPath || "").trim()) {
2370
- args.push("--config", viewerConfigPath);
2371
- }
2372
- return args;
2373
- };
2460
+ const buildViewerCliArgs = createViewerCliArgs({ viewerHost, viewerPort, viewerDbPath, viewerConfigPath });
2374
2461
 
2375
2462
  // Session context tracking for comprehensive memories
2376
2463
  const createSessionContext = () => ({
@@ -2702,9 +2789,10 @@ export const createCodememRuntime = async ({ location, host, hostGeneration = nu
2702
2789
  return;
2703
2790
  }
2704
2791
  const viewerArgs = buildViewerCliArgs("start");
2705
- const cmd = [runner, ...runnerArgs, ...viewerArgs];
2706
- logLine(`auto-starting viewer: ${cmd.join(" ")}`).catch(() => {});
2707
2792
  try {
2793
+ const cmd = await cliRunner.command(viewerArgs);
2794
+ if (!cmd || !runtimeActive) { viewerStartInFlight = false; return; }
2795
+ logLine("auto-starting viewer").catch(() => {});
2708
2796
  const child = nodeSpawn(cmd[0], cmd.slice(1), {
2709
2797
  cwd,
2710
2798
  env: process.env,
@@ -2800,8 +2888,20 @@ export const createCodememRuntime = async ({ location, host, hostGeneration = nu
2800
2888
  });
2801
2889
  };
2802
2890
 
2803
- const runCli = async (args, options = {}) =>
2804
- runCommand([runner, ...runnerArgs, ...args], options);
2891
+ const runCli = (args, options = {}) => {
2892
+ const command = cliRunner.command(args);
2893
+ if (Array.isArray(command)) return runCommand(command, options);
2894
+ return command.then((cmd) => {
2895
+ if (!cmd) return { exitCode: 1, stdout: "", stderr: "CLI runner unavailable" };
2896
+ return runCommand(cmd, options);
2897
+ });
2898
+ };
2899
+
2900
+ const installCliUpdate = async (command) => {
2901
+ const result = await runCommand(command, { env: resolveAutoUpdateEnvironment(), timeoutMs: 480_000 });
2902
+ if (result?.exitCode === 0) cliRunner.invalidate();
2903
+ return result;
2904
+ };
2805
2905
 
2806
2906
  rawEventDelivery = createRawEventDelivery({
2807
2907
  backoffMs: rawEventsBackoffMs,
@@ -2830,32 +2930,39 @@ export const createCodememRuntime = async ({ location, host, hostGeneration = nu
2830
2930
  spoolMaxEntries: rawEventSpoolMaxEntries,
2831
2931
  });
2832
2932
 
2833
- const postViewerJson = async ({ url, operation, payload, validate }) => {
2933
+ const postViewerJson = async ({ url, operation, payload, validate, viewerOnly = false }) => {
2934
+ const timing = { startedAt: performance.now(), stage: "skipped" };
2935
+ let signal;
2936
+ const finish = createPromptTransportResult({
2937
+ operation, runner, diagnostics: promptTransportDiagnostics, viewerOnly, timing,
2938
+ });
2834
2939
  if (!viewerEnabled) {
2835
- return {
2940
+ return finish({
2836
2941
  ok: false,
2837
2942
  classification: viewerFailureClassification(
2838
2943
  `${operation} viewer transport disabled`,
2839
2944
  "fallback",
2840
2945
  ),
2841
- };
2946
+ }, "disabled");
2842
2947
  }
2843
2948
  if (Date.now() < promptPackTransportUnavailableUntil) {
2844
- return {
2949
+ return finish({
2845
2950
  ok: false,
2846
2951
  classification: viewerFailureClassification(
2847
2952
  `${operation} viewer transport in backoff`,
2848
2953
  "fallback",
2849
2954
  ),
2850
- };
2955
+ }, "backoff");
2851
2956
  }
2852
2957
 
2853
2958
  let response;
2854
2959
  try {
2960
+ timing.stage = "profile";
2961
+ signal = AbortSignal.timeout(promptPackHttpTimeout);
2855
2962
  const profileResponse = await fetch(promptPackProfileUrl, {
2856
2963
  method: "GET",
2857
2964
  redirect: "manual",
2858
- signal: AbortSignal.timeout(promptPackHttpTimeout),
2965
+ signal,
2859
2966
  });
2860
2967
  let profileBody;
2861
2968
  try {
@@ -2872,65 +2979,34 @@ export const createCodememRuntime = async ({ location, host, hostGeneration = nu
2872
2979
  if (classification.retryable) {
2873
2980
  promptPackTransportUnavailableUntil = Date.now() + Math.max(1000, rawEventsBackoffMs);
2874
2981
  }
2875
- return { ok: false, classification };
2876
- }
2877
- const viewerProtocolRange = isRecord(profileBody)
2878
- ? normalizePromptTransportProtocolRange(
2879
- profileBody.protocol_version,
2880
- profileBody.min_supported_protocol_version,
2881
- )
2882
- : null;
2883
- let profileFailure = null;
2884
- if (!isRecord(profileBody) || profileBody.service !== "codemem-viewer") {
2885
- profileFailure = viewerFailureClassification(
2886
- `${operation} viewer profile malformed`,
2887
- classifyPromptTransportFailure({ kind: "profile_malformed" }),
2888
- );
2889
- } else if (!viewerProtocolRange) {
2890
- profileFailure = viewerFailureClassification(
2891
- `${operation} viewer protocol range malformed`,
2892
- classifyPromptTransportFailure({ kind: "profile_malformed" }),
2893
- );
2894
- } else if (!arePromptTransportProtocolRangesCompatible(
2895
- PROMPT_TRANSPORT_PROTOCOL_RANGE,
2896
- viewerProtocolRange,
2897
- )) {
2898
- profileFailure = viewerFailureClassification(
2899
- `${operation} viewer protocol range unsupported`,
2900
- classifyPromptTransportFailure({ kind: "protocol_range_mismatch" }),
2901
- );
2902
- } else if (profileBody.db_path !== promptPackDbPath) {
2903
- profileFailure = viewerFailureClassification(
2904
- `${operation} viewer database mismatch`,
2905
- classifyPromptTransportFailure({ kind: "database_mismatch" }),
2906
- );
2907
- } else if (
2908
- canonicalJson(profileBody.identity_target) !== canonicalJson(promptPackIdentityTarget)
2909
- ) {
2910
- profileFailure = viewerFailureClassification(
2911
- `${operation} viewer runtime identity mismatch`,
2912
- classifyPromptTransportFailure({ kind: "runtime_identity_mismatch" }),
2913
- );
2982
+ return finish({ ok: false, classification }, viewerHttpDiagnosticReason({
2983
+ operation: `${operation} profile`, status: profileResponse.status, body: profileBody, signal,
2984
+ }), profileResponse.status);
2914
2985
  }
2986
+ const profileFailure = classifyViewerProfileFailure(profileBody, {
2987
+ operation, dbPath: promptPackDbPath, identityTarget: promptPackIdentityTarget,
2988
+ });
2915
2989
  if (profileFailure) {
2916
2990
  promptPackTransportUnavailableUntil = Date.now() + Math.max(1000, rawEventsBackoffMs);
2917
- return {
2991
+ return finish({
2918
2992
  ok: false,
2919
- classification: profileFailure,
2920
- };
2993
+ classification: profileFailure.classification,
2994
+ }, diagnosticTimeoutReason(signal, profileFailure.reason), profileResponse.status);
2921
2995
  }
2996
+ timing.stage = "request";
2997
+ signal = AbortSignal.timeout(promptPackHttpTimeout);
2922
2998
  response = await fetch(url, {
2923
2999
  method: "POST",
2924
3000
  redirect: "manual",
2925
3001
  headers: { "Content-Type": "application/json" },
2926
3002
  body: JSON.stringify(payload),
2927
- signal: AbortSignal.timeout(promptPackHttpTimeout),
3003
+ signal,
2928
3004
  });
2929
3005
  } catch (error) {
2930
3006
  const classification = classifyViewerHttpFailure({ operation, error });
2931
3007
  promptPackTransportUnavailableUntil =
2932
3008
  Date.now() + Math.max(1000, rawEventsBackoffMs);
2933
- return { ok: false, classification };
3009
+ return finish({ ok: false, classification }, viewerHttpDiagnosticReason({ operation, signal }));
2934
3010
  }
2935
3011
 
2936
3012
  let body;
@@ -2950,18 +3026,20 @@ export const createCodememRuntime = async ({ location, host, hostGeneration = nu
2950
3026
  promptPackTransportUnavailableUntil =
2951
3027
  Date.now() + Math.max(1000, rawEventsBackoffMs);
2952
3028
  }
2953
- return { ok: false, classification };
3029
+ return finish({ ok: false, classification }, viewerHttpDiagnosticReason({
3030
+ operation, status: response.status, body, signal,
3031
+ }), response.status);
2954
3032
  }
2955
3033
 
2956
3034
  if (!validate(body)) {
2957
3035
  const classification = classifyViewerHttpFailure({ operation, malformed: true });
2958
3036
  promptPackTransportUnavailableUntil =
2959
3037
  Date.now() + Math.max(1000, rawEventsBackoffMs);
2960
- return { ok: false, classification };
3038
+ return finish({ ok: false, classification }, diagnosticTimeoutReason(signal, "malformed_response"), response.status);
2961
3039
  }
2962
3040
 
2963
3041
  promptPackTransportUnavailableUntil = 0;
2964
- return { ok: true, body };
3042
+ return finish({ ok: true, body }, "success", response.status);
2965
3043
  };
2966
3044
 
2967
3045
  const runPromptPackLedger = async (payload, { viewerOnly = false } = {}) => {
@@ -2973,6 +3051,7 @@ export const createCodememRuntime = async ({ location, host, hostGeneration = nu
2973
3051
  const httpResult = await postViewerJson({
2974
3052
  url: promptPackLedgerUrl,
2975
3053
  operation: "prompt-pack-ledger",
3054
+ viewerOnly,
2976
3055
  payload: viewerPayload,
2977
3056
  validate: isValidLedgerHttpPayload,
2978
3057
  });
@@ -3152,10 +3231,7 @@ export const createCodememRuntime = async ({ location, host, hostGeneration = nu
3152
3231
  return;
3153
3232
  }
3154
3233
  await logLine(`compat.auto_update_start cmd=${autoPlan.commandText}`);
3155
- const updateResult = await runCommand(autoPlan.command, {
3156
- env: resolveAutoUpdateEnvironment(),
3157
- timeoutMs: 480_000,
3158
- });
3234
+ const updateResult = await installCliUpdate(autoPlan.command);
3159
3235
  if (updateResult?.exitCode === 0) {
3160
3236
  await logLine(
3161
3237
  `compat.auto_update_result exit=${updateResult?.exitCode ?? "unknown"} stderr=${redactLog(
@@ -3234,10 +3310,7 @@ export const createCodememRuntime = async ({ location, host, hostGeneration = nu
3234
3310
  });
3235
3311
  if (autoPlan.allowed) {
3236
3312
  await logLine(`release.auto_update_start cmd=${autoPlan.commandText}`);
3237
- const installation = await runCommand(autoPlan.command, {
3238
- env: resolveAutoUpdateEnvironment(),
3239
- timeoutMs: 480_000,
3240
- });
3313
+ const installation = await installCliUpdate(autoPlan.command);
3241
3314
  if (installation?.exitCode === 0) {
3242
3315
  const verification = await runCli(["version"]);
3243
3316
  const installedVersion = (verification?.stdout || "").trim();
@@ -4757,6 +4830,9 @@ export const __testUtils = {
4757
4830
  arePromptTransportProtocolRangesCompatible,
4758
4831
  classifyPromptTransportFailure,
4759
4832
  classifyViewerHttpFailure,
4833
+ viewerHttpDiagnosticReason,
4834
+ classifyViewerProfileFailure,
4835
+ createPromptTransportResult,
4760
4836
  isValidPackHttpPayload,
4761
4837
  isValidLedgerHttpPayload,
4762
4838
  buildPackHttpBody,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemem/opencode-plugin",
3
- "version": "0.47.0-beta.2",
3
+ "version": "0.47.0-beta.4",
4
4
  "description": "CodeMem plugin for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -39,6 +39,7 @@
39
39
  "@opencode/plugin": "2.0.24"
40
40
  },
41
41
  "devDependencies": {
42
+ "codemem": "^0.47.0-beta.4",
42
43
  "@opencode/cli": "2.0.24",
43
44
  "@types/node": "^24.12.4",
44
45
  "vitest": "^4.1.11"
@@ -72,6 +73,7 @@
72
73
  "scripts": {
73
74
  "test": "pnpm exec vitest run",
74
75
  "test:opencode-v2-contract": "node ./scripts/packed-v2-host-smoke.mjs",
75
- "test:packed-artifact": "pnpm --filter codemem build && node ./scripts/packed-artifact-smoke.mjs"
76
+ "test:packed-artifact": "pnpm --filter codemem build && node ./scripts/packed-artifact-smoke.mjs",
77
+ "test:packed-artifact:prepared": "node ./scripts/packed-artifact-smoke.mjs"
76
78
  }
77
79
  }