@codemem/opencode-plugin 0.47.0-beta.2 → 0.47.0-beta.3
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
|
+
};
|
package/.opencode/lib/runtime.js
CHANGED
|
@@ -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.
|
|
35
|
+
const PINNED_BACKEND_VERSION = "0.47.0-beta.3";
|
|
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 =
|
|
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 = (
|
|
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 =
|
|
2804
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|