@codemem/opencode-plugin 0.40.2 → 0.42.0
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/.opencode/lib/compat.js +31 -11
- package/.opencode/plugins/codemem.js +881 -113
- package/README.md +7 -0
- package/index.js +5 -2
- package/package.json +2 -2
package/.opencode/lib/compat.js
CHANGED
|
@@ -106,9 +106,17 @@ const isPinnedGitSource = (runnerFrom) => {
|
|
|
106
106
|
}
|
|
107
107
|
};
|
|
108
108
|
|
|
109
|
-
export const resolveAutoUpdatePlan = ({ runner, runnerFrom }) => {
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
export const resolveAutoUpdatePlan = ({ runner, runnerFrom, runnerFromExplicit = false }) => {
|
|
110
|
+
const normalizedRunner = String(runner || "").trim();
|
|
111
|
+
const source = String(runnerFrom || "").trim();
|
|
112
|
+
if (isPinnedGitSource(source) || /^codemem@(?!latest$|next$|\*$)[^\s]+$/i.test(source)) {
|
|
113
|
+
return {
|
|
114
|
+
allowed: false,
|
|
115
|
+
reason: "pinned-source",
|
|
116
|
+
command: null,
|
|
117
|
+
commandText: null,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
112
120
|
|
|
113
121
|
if (normalizedRunner === "node") {
|
|
114
122
|
return {
|
|
@@ -119,8 +127,16 @@ export const resolveAutoUpdatePlan = ({ runner, runnerFrom }) => {
|
|
|
119
127
|
};
|
|
120
128
|
}
|
|
121
129
|
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
if (normalizedRunner === "npx") {
|
|
131
|
+
if (!runnerFromExplicit || !/^(?:codemem|codemem@(?:latest|next|\*))$/i.test(source)) {
|
|
132
|
+
return {
|
|
133
|
+
allowed: false,
|
|
134
|
+
reason: !runnerFromExplicit ? "implicit-pinned-source" : "custom-source",
|
|
135
|
+
command: null,
|
|
136
|
+
commandText: null,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
124
140
|
allowed: true,
|
|
125
141
|
reason: null,
|
|
126
142
|
command: ["npm", "install", "-g", "codemem@latest"],
|
|
@@ -146,10 +162,14 @@ export const resolveAutoUpdatePlan = ({ runner, runnerFrom }) => {
|
|
|
146
162
|
};
|
|
147
163
|
}
|
|
148
164
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
165
|
+
if (normalizedRunner === "codemem") {
|
|
166
|
+
return {
|
|
167
|
+
allowed: true,
|
|
168
|
+
reason: null,
|
|
169
|
+
command: ["npm", "install", "-g", "codemem@latest"],
|
|
170
|
+
commandText: "npm install -g codemem@latest",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { allowed: false, reason: "unknown-runner", command: null, commandText: null };
|
|
155
175
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, posix, resolve, win32 } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
4
5
|
import { createHash } from "node:crypto";
|
|
5
6
|
import { spawn as nodeSpawn, execSync } from "node:child_process";
|
|
6
7
|
import { tool } from "@opencode-ai/plugin";
|
|
@@ -15,16 +16,48 @@ import {
|
|
|
15
16
|
|
|
16
17
|
const TRUTHY_VALUES = ["1", "true", "yes"];
|
|
17
18
|
const DISABLED_VALUES = ["0", "false", "off"];
|
|
18
|
-
const PINNED_BACKEND_VERSION = "0.
|
|
19
|
+
const PINNED_BACKEND_VERSION = "0.42.0";
|
|
19
20
|
const COMPAT_CHECK_DELAY_MS = 1500;
|
|
20
21
|
const COMPAT_CHECK_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
22
|
+
const MAX_UPDATE_STATUS_BYTES = 16 * 1024;
|
|
23
|
+
const MAX_UPDATE_ACTION_CHARS = 1000;
|
|
24
|
+
const MAX_UPDATE_VERSION_CHARS = 128;
|
|
25
|
+
const STABLE_RELEASE_VERSION =
|
|
26
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
21
27
|
const CODEMEM_CONTEXT_PART_ID_PREFIX = "codemem-context-";
|
|
22
28
|
const MAX_MESSAGE_INJECTION_CACHE_SESSIONS = 20;
|
|
23
29
|
const MAX_MESSAGE_INJECTION_CACHE_MESSAGES = 100;
|
|
24
30
|
const COMPACTION_INJECTION_SKIP_TTL_MS = 30 * 1000;
|
|
25
31
|
const MAX_WORKING_SET_PATH_CHARS = 400;
|
|
32
|
+
const VIEWER_HEALTH_CHECK_INTERVAL_MS = 60_000;
|
|
33
|
+
const VIEWER_HEALTH_TIMEOUT_MS = 5_000;
|
|
34
|
+
const VIEWER_HEALTH_RESTART_THRESHOLD = 3;
|
|
35
|
+
const VIEWER_HEALTH_RESTART_COOLDOWN_MS = 5 * 60_000;
|
|
36
|
+
const RAW_EVENTS_STATUS_TIMEOUT_MS = 5_000;
|
|
26
37
|
|
|
27
38
|
let compatCheckCache = null;
|
|
39
|
+
const notifiedReleaseVersions = new Set();
|
|
40
|
+
|
|
41
|
+
// Release an unread response body without surfacing cancellation failures.
|
|
42
|
+
const discardResponseBody = (response) => {
|
|
43
|
+
try {
|
|
44
|
+
const cancelled = response?.body?.cancel?.();
|
|
45
|
+
if (cancelled && typeof cancelled.catch === "function") {
|
|
46
|
+
cancelled.catch(() => {});
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// Best effort — a locked or already-errored stream is fine to abandon.
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Bounded ingest-availability preflight: a hung viewer socket must not stall
|
|
54
|
+
// raw-event delivery indefinitely. Failures fall into the existing stream
|
|
55
|
+
// backoff + CLI enqueue fallback path.
|
|
56
|
+
const fetchRawEventsStatus = (url, fetchFn = fetch) =>
|
|
57
|
+
fetchFn(url, {
|
|
58
|
+
method: "GET",
|
|
59
|
+
signal: AbortSignal.timeout(RAW_EVENTS_STATUS_TIMEOUT_MS),
|
|
60
|
+
});
|
|
28
61
|
|
|
29
62
|
const normalizeEnvValue = (value) => (value || "").toLowerCase();
|
|
30
63
|
const envHasValue = (value, truthyValues) =>
|
|
@@ -32,6 +65,149 @@ const envHasValue = (value, truthyValues) =>
|
|
|
32
65
|
const envNotDisabled = (value) =>
|
|
33
66
|
!DISABLED_VALUES.includes(normalizeEnvValue(value));
|
|
34
67
|
|
|
68
|
+
const createViewerHealthMonitor = ({
|
|
69
|
+
viewerHealthUrl,
|
|
70
|
+
legacyStatusUrl,
|
|
71
|
+
isActive,
|
|
72
|
+
restartViewer,
|
|
73
|
+
logLine,
|
|
74
|
+
fetchFn = fetch,
|
|
75
|
+
now = Date.now,
|
|
76
|
+
setIntervalFn = setInterval,
|
|
77
|
+
clearIntervalFn = clearInterval,
|
|
78
|
+
timeoutSignal = (timeoutMs) => AbortSignal.timeout(timeoutMs),
|
|
79
|
+
}) => {
|
|
80
|
+
let timer = null;
|
|
81
|
+
let consecutiveFailures = 0;
|
|
82
|
+
let lastRestartAttempt = 0;
|
|
83
|
+
|
|
84
|
+
const boundedFetch = (url) => fetchFn(url, {
|
|
85
|
+
method: "GET",
|
|
86
|
+
signal: timeoutSignal(VIEWER_HEALTH_TIMEOUT_MS),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const probe = async () => {
|
|
90
|
+
let response;
|
|
91
|
+
try {
|
|
92
|
+
response = await boundedFetch(viewerHealthUrl);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return { live: false, detail: `error: ${String(error).slice(0, 200)}` };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (response.status === 404) {
|
|
98
|
+
discardResponseBody(response);
|
|
99
|
+
try {
|
|
100
|
+
// Old-viewer compatibility: released viewers serving this route
|
|
101
|
+
// always include the `ingest` availability object, so require that
|
|
102
|
+
// identifying evidence rather than trusting any 2xx from an
|
|
103
|
+
// arbitrary local service.
|
|
104
|
+
const fallbackResponse = await boundedFetch(legacyStatusUrl);
|
|
105
|
+
if (!fallbackResponse.ok) {
|
|
106
|
+
discardResponseBody(fallbackResponse);
|
|
107
|
+
return { live: false, detail: `fallback status=${fallbackResponse.status}` };
|
|
108
|
+
}
|
|
109
|
+
const fallbackPayload = await fallbackResponse.json();
|
|
110
|
+
const looksLikeViewer =
|
|
111
|
+
fallbackPayload &&
|
|
112
|
+
typeof fallbackPayload === "object" &&
|
|
113
|
+
fallbackPayload.ingest &&
|
|
114
|
+
typeof fallbackPayload.ingest === "object";
|
|
115
|
+
return looksLikeViewer
|
|
116
|
+
? { live: true }
|
|
117
|
+
: { live: false, detail: "fallback unexpected payload" };
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return { live: false, detail: `fallback error: ${String(error).slice(0, 200)}` };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
// Release the unread body so a persistently failing viewer does not
|
|
125
|
+
// pin connections across the 60s monitor interval.
|
|
126
|
+
discardResponseBody(response);
|
|
127
|
+
return { live: false, detail: `status=${response.status}` };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let payload;
|
|
131
|
+
try {
|
|
132
|
+
payload = await response.json();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return { live: false, detail: `invalid JSON: ${String(error).slice(0, 200)}` };
|
|
135
|
+
}
|
|
136
|
+
if (!payload || typeof payload !== "object" || payload.service !== "codemem-viewer") {
|
|
137
|
+
return { live: false, detail: "unexpected service" };
|
|
138
|
+
}
|
|
139
|
+
return { live: true };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const check = async () => {
|
|
143
|
+
if (!isActive()) return;
|
|
144
|
+
const result = await probe();
|
|
145
|
+
if (result.live) {
|
|
146
|
+
if (consecutiveFailures > 0) {
|
|
147
|
+
await logLine(`viewer.health recovered after ${consecutiveFailures} failure(s)`);
|
|
148
|
+
}
|
|
149
|
+
consecutiveFailures = 0;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
consecutiveFailures += 1;
|
|
154
|
+
await logLine(
|
|
155
|
+
`viewer.health check failed (${result.detail}, consecutive=${consecutiveFailures})`
|
|
156
|
+
);
|
|
157
|
+
if (
|
|
158
|
+
consecutiveFailures < VIEWER_HEALTH_RESTART_THRESHOLD ||
|
|
159
|
+
now() - lastRestartAttempt < VIEWER_HEALTH_RESTART_COOLDOWN_MS
|
|
160
|
+
) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Re-check after the awaited probe: a stop requested while the probe was
|
|
165
|
+
// in flight must not be undone by a restart.
|
|
166
|
+
if (!isActive()) return;
|
|
167
|
+
|
|
168
|
+
lastRestartAttempt = now();
|
|
169
|
+
await logLine(`viewer.health restarting viewer after ${consecutiveFailures} consecutive failures`);
|
|
170
|
+
try {
|
|
171
|
+
const restartResult = await restartViewer();
|
|
172
|
+
const restarted = restartResult?.exitCode === 0;
|
|
173
|
+
await logLine(
|
|
174
|
+
`viewer.health restart ${restarted ? "succeeded" : "failed"} (exit=${restartResult?.exitCode ?? "unknown"})`
|
|
175
|
+
);
|
|
176
|
+
if (restarted) consecutiveFailures = 0;
|
|
177
|
+
} catch (error) {
|
|
178
|
+
await logLine(`viewer.health restart error: ${String(error).slice(0, 200)}`);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const start = () => {
|
|
183
|
+
if (timer) return;
|
|
184
|
+
consecutiveFailures = 0;
|
|
185
|
+
timer = setIntervalFn(() => {
|
|
186
|
+
check().catch(() => {});
|
|
187
|
+
}, VIEWER_HEALTH_CHECK_INTERVAL_MS);
|
|
188
|
+
if (timer?.unref) timer.unref();
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const stop = () => {
|
|
192
|
+
if (timer) {
|
|
193
|
+
clearIntervalFn(timer);
|
|
194
|
+
timer = null;
|
|
195
|
+
}
|
|
196
|
+
consecutiveFailures = 0;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
check,
|
|
201
|
+
start,
|
|
202
|
+
stop,
|
|
203
|
+
state: () => ({
|
|
204
|
+
consecutiveFailures,
|
|
205
|
+
lastRestartAttempt,
|
|
206
|
+
running: timer !== null,
|
|
207
|
+
}),
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
|
|
35
211
|
const resolveInjectSurface = (value) => {
|
|
36
212
|
const normalized = String(value || "message").trim().toLowerCase();
|
|
37
213
|
if (normalized === "system") {
|
|
@@ -176,6 +352,40 @@ const clearCompatCheckCache = () => {
|
|
|
176
352
|
compatCheckCache = null;
|
|
177
353
|
};
|
|
178
354
|
|
|
355
|
+
const isStableReleaseVersion = (value) => {
|
|
356
|
+
if (typeof value !== "string" || value.length > MAX_UPDATE_VERSION_CHARS) return false;
|
|
357
|
+
const match = STABLE_RELEASE_VERSION.exec(value);
|
|
358
|
+
return Boolean(match && match.slice(1, 4).map(Number).every(Number.isSafeInteger));
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const parseReleaseNotification = (result) => {
|
|
362
|
+
if (result?.exitCode !== 0) return null;
|
|
363
|
+
const stdout = String(result?.stdout || "").trim();
|
|
364
|
+
if (!stdout || Buffer.byteLength(stdout, "utf8") > MAX_UPDATE_STATUS_BYTES) return null;
|
|
365
|
+
try {
|
|
366
|
+
const status = JSON.parse(stdout);
|
|
367
|
+
if (!status || typeof status !== "object" || Array.isArray(status)) return null;
|
|
368
|
+
if (status.update_available !== true) return null;
|
|
369
|
+
if (!isStableReleaseVersion(status.latest_version)) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
if (
|
|
373
|
+
typeof status.recommended_action !== "string"
|
|
374
|
+
|| !status.recommended_action.trim()
|
|
375
|
+
|| status.recommended_action.length > MAX_UPDATE_ACTION_CHARS
|
|
376
|
+
) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
latestVersion: status.latest_version,
|
|
381
|
+
recommendedAction: status.recommended_action.trim(),
|
|
382
|
+
autoUpdateEligible: status.auto_update_eligible === true,
|
|
383
|
+
};
|
|
384
|
+
} catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
179
389
|
const createLogLine = (logPath) => async (line) => {
|
|
180
390
|
if (!logPath) {
|
|
181
391
|
return;
|
|
@@ -413,6 +623,31 @@ const parsePackMetrics = (stdout) => {
|
|
|
413
623
|
}
|
|
414
624
|
};
|
|
415
625
|
|
|
626
|
+
const buildViewerIdentityTarget = (env = process.env, cwd = process.cwd()) => {
|
|
627
|
+
const normalizeIdentityPath = (value) => {
|
|
628
|
+
const trimmed = String(value || "").trim();
|
|
629
|
+
if (!trimmed) return null;
|
|
630
|
+
const expanded = trimmed.startsWith("~/")
|
|
631
|
+
? join(env.HOME?.trim() || homedir(), trimmed.slice(2))
|
|
632
|
+
: trimmed;
|
|
633
|
+
return resolve(cwd, expanded);
|
|
634
|
+
};
|
|
635
|
+
return {
|
|
636
|
+
device_id: env.CODEMEM_DEVICE_ID?.trim() || null,
|
|
637
|
+
actor_id_present: Object.hasOwn(env, "CODEMEM_ACTOR_ID"),
|
|
638
|
+
actor_id: env.CODEMEM_ACTOR_ID?.trim() || null,
|
|
639
|
+
config_path: normalizeIdentityPath(env.CODEMEM_CONFIG),
|
|
640
|
+
runtime_root: normalizeIdentityPath(env.CODEMEM_RUNTIME_ROOT),
|
|
641
|
+
workspace_id: env.CODEMEM_WORKSPACE_ID?.trim() || null,
|
|
642
|
+
home_dir: normalizeIdentityPath(env.HOME || homedir()),
|
|
643
|
+
pack_compression: env.CODEMEM_PACK_COMPRESSION?.trim() || null,
|
|
644
|
+
embedding_disabled: ["1", "true", "yes"].includes(
|
|
645
|
+
String(env.CODEMEM_EMBEDDING_DISABLED || "").toLowerCase(),
|
|
646
|
+
),
|
|
647
|
+
embedding_model: env.CODEMEM_EMBEDDING_MODEL || "Xenova/bge-small-en-v1.5",
|
|
648
|
+
};
|
|
649
|
+
};
|
|
650
|
+
|
|
416
651
|
const parsePackOutput = (result) => {
|
|
417
652
|
const succeeded = result?.exitCode === 0;
|
|
418
653
|
let payload = null;
|
|
@@ -445,6 +680,229 @@ const parsePackOutput = (result) => {
|
|
|
445
680
|
};
|
|
446
681
|
};
|
|
447
682
|
|
|
683
|
+
const isRecord = (value) => value != null && typeof value === "object" && !Array.isArray(value);
|
|
684
|
+
|
|
685
|
+
const isValidPackHttpPayload = (payload) => {
|
|
686
|
+
if (!isRecord(payload) || typeof payload.pack_text !== "string" || !isRecord(payload.metrics)) {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
const itemCount = payload.metrics.total_items;
|
|
690
|
+
if (!Number.isInteger(itemCount) || itemCount < 0) {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
if (
|
|
694
|
+
payload.ledger_artifact_fingerprint != null
|
|
695
|
+
&& !/^[0-9a-f]{64}$/i.test(payload.ledger_artifact_fingerprint)
|
|
696
|
+
) {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
if (payload.ledger_outcome != null) {
|
|
700
|
+
return isRecord(payload.ledger_outcome)
|
|
701
|
+
&& payload.ledger_outcome.ok === false
|
|
702
|
+
&& payload.ledger_outcome.errorCode === "retrieval_ledger_write_failed"
|
|
703
|
+
&& payload.ledger_outcome.reason === "idempotency_conflict";
|
|
704
|
+
}
|
|
705
|
+
return true;
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const isValidLedgerHttpPayload = (payload) => isRecord(payload) && payload.ok === true;
|
|
709
|
+
|
|
710
|
+
const isValidLedgerFailureHttpPayload = (payload) =>
|
|
711
|
+
isRecord(payload)
|
|
712
|
+
&& payload.ok === false
|
|
713
|
+
&& (
|
|
714
|
+
payload.errorCode === "retrieval_ledger_write_failed"
|
|
715
|
+
|| payload.errorCode === "retrieval_ledger_delivery_write_failed"
|
|
716
|
+
)
|
|
717
|
+
&& typeof payload.reason === "string";
|
|
718
|
+
|
|
719
|
+
const isViewerDbMismatchPayload = (payload) =>
|
|
720
|
+
isRecord(payload)
|
|
721
|
+
&& isRecord(payload.error)
|
|
722
|
+
&& payload.error.code === "viewer_db_mismatch";
|
|
723
|
+
|
|
724
|
+
const isViewerIdentityMismatchPayload = (payload) =>
|
|
725
|
+
isRecord(payload)
|
|
726
|
+
&& isRecord(payload.error)
|
|
727
|
+
&& payload.error.code === "viewer_identity_mismatch";
|
|
728
|
+
|
|
729
|
+
const isViewerContractUnsupportedPayload = (payload) =>
|
|
730
|
+
isRecord(payload)
|
|
731
|
+
&& isRecord(payload.error)
|
|
732
|
+
&& payload.error.code === "viewer_contract_unsupported";
|
|
733
|
+
|
|
734
|
+
const isViewerInvalidRequestPayload = (payload) =>
|
|
735
|
+
isRecord(payload)
|
|
736
|
+
&& isRecord(payload.error)
|
|
737
|
+
&& payload.error.code === "invalid_request";
|
|
738
|
+
|
|
739
|
+
// Dependency-free port of @codemem/core prompt-transport semantics. Keep the
|
|
740
|
+
// range and classifier parity pinned by the plugin injection tests.
|
|
741
|
+
const PROMPT_TRANSPORT_PROTOCOL_RANGE = Object.freeze({
|
|
742
|
+
minSupportedProtocolVersion: 1,
|
|
743
|
+
protocolVersion: 1,
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
const normalizePromptTransportProtocolRange = (
|
|
747
|
+
protocolVersion,
|
|
748
|
+
minSupportedProtocolVersion = undefined,
|
|
749
|
+
) => {
|
|
750
|
+
if (!Number.isSafeInteger(protocolVersion) || protocolVersion < 1) return null;
|
|
751
|
+
const minimum = minSupportedProtocolVersion === undefined
|
|
752
|
+
? protocolVersion
|
|
753
|
+
: minSupportedProtocolVersion;
|
|
754
|
+
if (!Number.isSafeInteger(minimum) || minimum < 1 || minimum > protocolVersion) return null;
|
|
755
|
+
return { minSupportedProtocolVersion: minimum, protocolVersion };
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
const arePromptTransportProtocolRangesCompatible = (left, right) =>
|
|
759
|
+
left.minSupportedProtocolVersion <= right.protocolVersion
|
|
760
|
+
&& right.minSupportedProtocolVersion <= left.protocolVersion;
|
|
761
|
+
|
|
762
|
+
const classifyPromptTransportFailure = ({ kind, compatibleProfile = false }) => {
|
|
763
|
+
if (kind === "database_mismatch" || kind === "runtime_identity_mismatch") {
|
|
764
|
+
return "local_fallback";
|
|
765
|
+
}
|
|
766
|
+
if (kind === "invalid_request") {
|
|
767
|
+
return compatibleProfile ? "terminal" : "fallback";
|
|
768
|
+
}
|
|
769
|
+
if (kind === "policy_failure" || kind === "authorization_failure") {
|
|
770
|
+
return "terminal";
|
|
771
|
+
}
|
|
772
|
+
if (kind === "viewer_contract_unsupported") {
|
|
773
|
+
return compatibleProfile ? "terminal" : "fallback";
|
|
774
|
+
}
|
|
775
|
+
return "fallback";
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
const isViewerPolicyOrAuthFailurePayload = (payload) => {
|
|
779
|
+
const code = isRecord(payload) && isRecord(payload.error)
|
|
780
|
+
? payload.error.code
|
|
781
|
+
: isRecord(payload)
|
|
782
|
+
? payload.error
|
|
783
|
+
: null;
|
|
784
|
+
return typeof code === "string" && [
|
|
785
|
+
"authorization_failed",
|
|
786
|
+
"forbidden",
|
|
787
|
+
"policy_denied",
|
|
788
|
+
"policy_disabled",
|
|
789
|
+
"unauthorized",
|
|
790
|
+
].includes(code);
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
const viewerFailureClassification = (cause, disposition) => ({
|
|
794
|
+
cause,
|
|
795
|
+
disposition,
|
|
796
|
+
retryable: disposition !== "terminal",
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
const classifyViewerHttpFailure = ({
|
|
800
|
+
operation,
|
|
801
|
+
status = null,
|
|
802
|
+
error = null,
|
|
803
|
+
malformed = false,
|
|
804
|
+
body = null,
|
|
805
|
+
compatibleProfile = false,
|
|
806
|
+
}) => {
|
|
807
|
+
if (malformed) {
|
|
808
|
+
return viewerFailureClassification(
|
|
809
|
+
`${operation} returned malformed success`,
|
|
810
|
+
classifyPromptTransportFailure({ kind: "malformed_response" }),
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
if (isViewerDbMismatchPayload(body)) {
|
|
814
|
+
return viewerFailureClassification(
|
|
815
|
+
`${operation} viewer database mismatch`,
|
|
816
|
+
classifyPromptTransportFailure({ kind: "database_mismatch" }),
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
if (isViewerIdentityMismatchPayload(body)) {
|
|
820
|
+
return viewerFailureClassification(
|
|
821
|
+
`${operation} viewer runtime identity mismatch`,
|
|
822
|
+
classifyPromptTransportFailure({ kind: "runtime_identity_mismatch" }),
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
if (isViewerContractUnsupportedPayload(body)) {
|
|
826
|
+
return viewerFailureClassification(
|
|
827
|
+
`${operation} viewer contract unsupported`,
|
|
828
|
+
classifyPromptTransportFailure({
|
|
829
|
+
kind: "viewer_contract_unsupported",
|
|
830
|
+
compatibleProfile,
|
|
831
|
+
}),
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
if (operation === "prompt-pack-ledger" && isValidLedgerFailureHttpPayload(body)) {
|
|
835
|
+
return viewerFailureClassification(`${operation} request rejected (${status})`, "terminal");
|
|
836
|
+
}
|
|
837
|
+
if (status === 401 || status === 403 || isViewerPolicyOrAuthFailurePayload(body)) {
|
|
838
|
+
return viewerFailureClassification(
|
|
839
|
+
`${operation} policy or authorization failure (${status})`,
|
|
840
|
+
classifyPromptTransportFailure({
|
|
841
|
+
kind: status === 401 || status === 403 ? "authorization_failure" : "policy_failure",
|
|
842
|
+
}),
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
if (isViewerInvalidRequestPayload(body)) {
|
|
846
|
+
return viewerFailureClassification(
|
|
847
|
+
`${operation} request rejected (${status})`,
|
|
848
|
+
classifyPromptTransportFailure({ kind: "invalid_request", compatibleProfile }),
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
if (status === 404 || status === 405) {
|
|
852
|
+
return viewerFailureClassification(`${operation} endpoint unavailable (${status})`, "fallback");
|
|
853
|
+
}
|
|
854
|
+
if (Number.isInteger(status) && status >= 500) {
|
|
855
|
+
return viewerFailureClassification(`${operation} server failure (${status})`, "fallback");
|
|
856
|
+
}
|
|
857
|
+
if (Number.isInteger(status)) {
|
|
858
|
+
return viewerFailureClassification(`${operation} unexpected response (${status})`, "fallback");
|
|
859
|
+
}
|
|
860
|
+
const diagnostic = [
|
|
861
|
+
error?.name,
|
|
862
|
+
error?.code,
|
|
863
|
+
error?.cause?.code,
|
|
864
|
+
error?.message,
|
|
865
|
+
error,
|
|
866
|
+
].filter(Boolean).join(" ");
|
|
867
|
+
const timedOut = /AbortError|TimeoutError|timeout|timed out|ETIMEDOUT/i.test(diagnostic);
|
|
868
|
+
return viewerFailureClassification(
|
|
869
|
+
timedOut ? `${operation} request timeout` : `${operation} connection failed`,
|
|
870
|
+
"fallback",
|
|
871
|
+
);
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
const buildPackHttpBody = ({
|
|
875
|
+
query,
|
|
876
|
+
filesModified,
|
|
877
|
+
injectLimit,
|
|
878
|
+
injectTokenBudget,
|
|
879
|
+
projectName,
|
|
880
|
+
cwd,
|
|
881
|
+
dbPath,
|
|
882
|
+
identityTarget,
|
|
883
|
+
attempt,
|
|
884
|
+
}) => ({
|
|
885
|
+
context: query,
|
|
886
|
+
limit: injectLimit !== null && Number.isFinite(injectLimit) && injectLimit > 0
|
|
887
|
+
? Math.trunc(injectLimit)
|
|
888
|
+
: 10,
|
|
889
|
+
token_budget:
|
|
890
|
+
injectTokenBudget !== null
|
|
891
|
+
&& Number.isFinite(injectTokenBudget)
|
|
892
|
+
&& injectTokenBudget > 0
|
|
893
|
+
? Math.trunc(injectTokenBudget)
|
|
894
|
+
: null,
|
|
895
|
+
...(projectName ? { project: projectName } : {}),
|
|
896
|
+
...(cwd ? { cwd } : {}),
|
|
897
|
+
db_path: dbPath,
|
|
898
|
+
identity_target: identityTarget,
|
|
899
|
+
working_set_files: Array.from(filesModified || [])
|
|
900
|
+
.slice(-8)
|
|
901
|
+
.map((value) => String(value || "").trim())
|
|
902
|
+
.filter((value) => value && value.length <= MAX_WORKING_SET_PATH_CHARS),
|
|
903
|
+
attempt,
|
|
904
|
+
});
|
|
905
|
+
|
|
448
906
|
const canonicalJson = (value) => {
|
|
449
907
|
if (Array.isArray(value)) {
|
|
450
908
|
return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
@@ -491,7 +949,7 @@ const applyInjectedContextToOutput = async ({
|
|
|
491
949
|
// lastPromptText hadn't yet been captured by the time
|
|
492
950
|
// experimental.chat.system.transform fired) would silently re-serve
|
|
493
951
|
// the first turn's pack. Recompute on every call instead — the chat
|
|
494
|
-
// path tolerates the
|
|
952
|
+
// path tolerates the transport round trip, and correctness beats the
|
|
495
953
|
// O(1) hit when the cache key isn't tied to the prompt that produced
|
|
496
954
|
// the pack.
|
|
497
955
|
const query = resolveInjectQuery();
|
|
@@ -1264,7 +1722,7 @@ const buildRunnerArgs = ({ runner, runnerFrom, runnerFromExplicit }) => {
|
|
|
1264
1722
|
return runnerFromExplicit ? [runnerFrom] : [];
|
|
1265
1723
|
};
|
|
1266
1724
|
|
|
1267
|
-
export const
|
|
1725
|
+
export const CodememPlugin = async ({
|
|
1268
1726
|
project,
|
|
1269
1727
|
client,
|
|
1270
1728
|
directory,
|
|
@@ -1323,6 +1781,14 @@ export const OpencodeMemPlugin = async ({
|
|
|
1323
1781
|
const viewerHost = process.env.CODEMEM_VIEWER_HOST || "127.0.0.1";
|
|
1324
1782
|
const viewerPort = process.env.CODEMEM_VIEWER_PORT || "38888";
|
|
1325
1783
|
const viewerDbPath = process.env.CODEMEM_DB || "";
|
|
1784
|
+
const expandedViewerDbPath = viewerDbPath.startsWith("~/")
|
|
1785
|
+
? join(process.env.HOME?.trim() || homedir(), viewerDbPath.slice(2))
|
|
1786
|
+
: viewerDbPath;
|
|
1787
|
+
const promptPackDbPath = resolve(
|
|
1788
|
+
cwd,
|
|
1789
|
+
expandedViewerDbPath || join(homedir(), ".codemem", "mem.sqlite"),
|
|
1790
|
+
);
|
|
1791
|
+
const promptPackIdentityTarget = buildViewerIdentityTarget(process.env, cwd);
|
|
1326
1792
|
const viewerConfigPath = process.env.CODEMEM_CONFIG || "";
|
|
1327
1793
|
// A malformed value (e.g. "abc", "", "-1") falls back to the 20s default
|
|
1328
1794
|
// instead of NaN, which would silently disable the timeout. An explicit "0"
|
|
@@ -1332,6 +1798,8 @@ export const OpencodeMemPlugin = async ({
|
|
|
1332
1798
|
const rawCommandTimeout = String(process.env.CODEMEM_PLUGIN_CMD_TIMEOUT ?? "").trim();
|
|
1333
1799
|
const commandTimeout =
|
|
1334
1800
|
rawCommandTimeout === "0" ? 0 : parsePositiveInt(rawCommandTimeout, 20000);
|
|
1801
|
+
const promptPackHttpTimeout =
|
|
1802
|
+
parsePositiveInt(process.env.CODEMEM_INJECT_HTTP_MAX_TIME_S || "2", 2) * 1000;
|
|
1335
1803
|
const backendUpdatePolicy = parseBackendUpdatePolicy(
|
|
1336
1804
|
process.env.CODEMEM_BACKEND_UPDATE_POLICY || "notify"
|
|
1337
1805
|
);
|
|
@@ -1359,6 +1827,8 @@ export const OpencodeMemPlugin = async ({
|
|
|
1359
1827
|
let sessionStartedAt = null;
|
|
1360
1828
|
let activeSessionID = null;
|
|
1361
1829
|
let viewerStarted = false;
|
|
1830
|
+
let viewerStartInFlight = false;
|
|
1831
|
+
let compatibilityAutoUpdateAttempted = false;
|
|
1362
1832
|
let promptCounter = 0;
|
|
1363
1833
|
let skippedAttemptCounter = 0;
|
|
1364
1834
|
let lastPromptText = null;
|
|
@@ -1373,8 +1843,15 @@ export const OpencodeMemPlugin = async ({
|
|
|
1373
1843
|
const rawEventsEnabled = envNotDisabled(
|
|
1374
1844
|
process.env.CODEMEM_RAW_EVENTS || "1"
|
|
1375
1845
|
);
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1846
|
+
const viewerUrlHost = viewerHost.includes(":") && !viewerHost.startsWith("[")
|
|
1847
|
+
? `[${viewerHost}]`
|
|
1848
|
+
: viewerHost;
|
|
1849
|
+
const rawEventsUrl = `http://${viewerUrlHost}:${viewerPort}/api/raw-events`;
|
|
1850
|
+
const rawEventsStatusUrl = `http://${viewerUrlHost}:${viewerPort}/api/raw-events/status?limit=1`;
|
|
1851
|
+
const packUrl = `http://${viewerUrlHost}:${viewerPort}/api/pack`;
|
|
1852
|
+
const promptPackProfileUrl = `http://${viewerUrlHost}:${viewerPort}/api/prompt-pack-profile`;
|
|
1853
|
+
const promptPackLedgerUrl = `http://${viewerUrlHost}:${viewerPort}/api/prompt-pack-ledger`;
|
|
1854
|
+
const viewerHealthUrl = `http://${viewerUrlHost}:${viewerPort}/api/health`;
|
|
1378
1855
|
const rawEventsBackoffMs = parseNumber(
|
|
1379
1856
|
process.env.CODEMEM_RAW_EVENTS_BACKOFF_MS || "10000",
|
|
1380
1857
|
10000
|
|
@@ -1392,14 +1869,8 @@ export const OpencodeMemPlugin = async ({
|
|
|
1392
1869
|
let fallbackFailureNoted = false;
|
|
1393
1870
|
let lastStatusCheckAt = 0;
|
|
1394
1871
|
let lastStatusAvailable = true;
|
|
1872
|
+
let promptPackTransportUnavailableUntil = 0;
|
|
1395
1873
|
|
|
1396
|
-
// Viewer health-check state
|
|
1397
|
-
const HEALTH_CHECK_INTERVAL_MS = 60_000;
|
|
1398
|
-
const HEALTH_CONSECUTIVE_FAILURES_BEFORE_RESTART = 3;
|
|
1399
|
-
const HEALTH_RESTART_COOLDOWN_MS = 5 * 60_000;
|
|
1400
|
-
let healthCheckTimer = null;
|
|
1401
|
-
let healthConsecutiveFailures = 0;
|
|
1402
|
-
let healthLastRestartAttempt = 0;
|
|
1403
1874
|
const nextEventId = () => {
|
|
1404
1875
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
1405
1876
|
return crypto.randomUUID();
|
|
@@ -1525,8 +1996,10 @@ export const OpencodeMemPlugin = async ({
|
|
|
1525
1996
|
}
|
|
1526
1997
|
try {
|
|
1527
1998
|
if (now - lastStatusCheckAt >= Math.max(1000, rawEventsStatusCheckMs)) {
|
|
1528
|
-
const statusResp = await
|
|
1999
|
+
const statusResp = await fetchRawEventsStatus(rawEventsStatusUrl);
|
|
1529
2000
|
if (!statusResp.ok) {
|
|
2001
|
+
// Release the unread body before bailing into the backoff path.
|
|
2002
|
+
discardResponseBody(statusResp);
|
|
1530
2003
|
throw new Error(`raw-events status failed (${statusResp.status})`);
|
|
1531
2004
|
}
|
|
1532
2005
|
const statusJson = await statusResp.json();
|
|
@@ -1540,10 +2013,27 @@ export const OpencodeMemPlugin = async ({
|
|
|
1540
2013
|
const postResp = await fetch(rawEventsUrl, {
|
|
1541
2014
|
method: "POST",
|
|
1542
2015
|
headers: { "Content-Type": "application/json" },
|
|
1543
|
-
body: JSON.stringify(
|
|
2016
|
+
body: JSON.stringify({
|
|
2017
|
+
...body,
|
|
2018
|
+
db_path: promptPackDbPath,
|
|
2019
|
+
identity_target: promptPackIdentityTarget,
|
|
2020
|
+
}),
|
|
1544
2021
|
});
|
|
1545
2022
|
if (!postResp.ok) {
|
|
1546
|
-
|
|
2023
|
+
let responseBody = null;
|
|
2024
|
+
if (postResp.status === 409) {
|
|
2025
|
+
try {
|
|
2026
|
+
responseBody = await postResp.json();
|
|
2027
|
+
} catch {
|
|
2028
|
+
// Generic failure handling below remains the safe fallback.
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
const targetMismatch = isViewerDbMismatchPayload(responseBody)
|
|
2032
|
+
|| isViewerIdentityMismatchPayload(responseBody)
|
|
2033
|
+
|| isViewerContractUnsupportedPayload(responseBody);
|
|
2034
|
+
const postError = new Error(`raw-events post failed (${postResp.status})`);
|
|
2035
|
+
postError.viewerTargetMismatch = targetMismatch;
|
|
2036
|
+
throw postError;
|
|
1547
2037
|
}
|
|
1548
2038
|
streamUnavailableUntil = 0;
|
|
1549
2039
|
streamErrorNoted = false;
|
|
@@ -1566,6 +2056,7 @@ export const OpencodeMemPlugin = async ({
|
|
|
1566
2056
|
type,
|
|
1567
2057
|
viewerHost,
|
|
1568
2058
|
viewerPort,
|
|
2059
|
+
viewerTargetMismatch: err?.viewerTargetMismatch === true,
|
|
1569
2060
|
error: String(err),
|
|
1570
2061
|
},
|
|
1571
2062
|
});
|
|
@@ -1881,12 +2372,28 @@ export const OpencodeMemPlugin = async ({
|
|
|
1881
2372
|
return { usage, id: info.id };
|
|
1882
2373
|
};
|
|
1883
2374
|
|
|
1884
|
-
const startViewer = () => {
|
|
1885
|
-
if (!viewerEnabled || !viewerAutoStart || viewerStarted) {
|
|
2375
|
+
const startViewer = async () => {
|
|
2376
|
+
if (!viewerEnabled || !viewerAutoStart || viewerStarted || viewerStartInFlight) {
|
|
1886
2377
|
if (viewerStarted) logLine("viewer already started, skipping auto-start").catch(() => {});
|
|
1887
2378
|
return;
|
|
1888
2379
|
}
|
|
1889
|
-
|
|
2380
|
+
viewerStartInFlight = true;
|
|
2381
|
+
let existingViewer = false;
|
|
2382
|
+
try {
|
|
2383
|
+
const existing = await fetch(viewerHealthUrl, {
|
|
2384
|
+
method: "GET",
|
|
2385
|
+
redirect: "manual",
|
|
2386
|
+
signal: AbortSignal.timeout(1_000),
|
|
2387
|
+
});
|
|
2388
|
+
existingViewer = existing.ok;
|
|
2389
|
+
} catch {
|
|
2390
|
+
// No live viewer responded; proceed with the plugin-owned start.
|
|
2391
|
+
}
|
|
2392
|
+
if (existingViewer) {
|
|
2393
|
+
viewerStartInFlight = false;
|
|
2394
|
+
logLine("viewer already running, skipping plugin-owned auto-start").catch(() => {});
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
1890
2397
|
const viewerArgs = buildViewerCliArgs("start");
|
|
1891
2398
|
const cmd = [runner, ...runnerArgs, ...viewerArgs];
|
|
1892
2399
|
logLine(`auto-starting viewer: ${cmd.join(" ")}`).catch(() => {});
|
|
@@ -1897,18 +2404,24 @@ export const OpencodeMemPlugin = async ({
|
|
|
1897
2404
|
detached: true,
|
|
1898
2405
|
stdio: "ignore",
|
|
1899
2406
|
});
|
|
2407
|
+
child.once("spawn", () => {
|
|
2408
|
+
viewerStarted = true;
|
|
2409
|
+
viewerStartInFlight = false;
|
|
2410
|
+
startHealthCheck();
|
|
2411
|
+
});
|
|
1900
2412
|
child.on("error", (err) => {
|
|
2413
|
+
viewerStartInFlight = false;
|
|
1901
2414
|
logLine(`viewer spawn error: ${err.message}`).catch(() => {});
|
|
1902
2415
|
});
|
|
1903
2416
|
child.unref();
|
|
1904
2417
|
} catch (err) {
|
|
2418
|
+
viewerStartInFlight = false;
|
|
1905
2419
|
logLine(`viewer spawn failed: ${err}`).catch(() => {});
|
|
1906
2420
|
}
|
|
1907
|
-
startHealthCheck();
|
|
1908
2421
|
};
|
|
1909
2422
|
|
|
1910
2423
|
const runCommand = async (cmd, options = {}) => {
|
|
1911
|
-
const { stdinText = null } = options;
|
|
2424
|
+
const { stdinText = null, timeoutMs = commandTimeout } = options;
|
|
1912
2425
|
const [command, ...args] = cmd;
|
|
1913
2426
|
return new Promise((resolve) => {
|
|
1914
2427
|
const proc = nodeSpawn(command, args, {
|
|
@@ -1947,19 +2460,31 @@ export const OpencodeMemPlugin = async ({
|
|
|
1947
2460
|
return;
|
|
1948
2461
|
}
|
|
1949
2462
|
let timer = null;
|
|
1950
|
-
|
|
2463
|
+
let killTimer = null;
|
|
2464
|
+
let timedOut = false;
|
|
2465
|
+
let settled = false;
|
|
2466
|
+
const finish = (result) => {
|
|
2467
|
+
if (settled) return;
|
|
2468
|
+
settled = true;
|
|
2469
|
+
if (timer) clearTimeout(timer);
|
|
2470
|
+
if (killTimer) clearTimeout(killTimer);
|
|
2471
|
+
resolve(result);
|
|
2472
|
+
};
|
|
2473
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
1951
2474
|
timer = setTimeout(() => {
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2475
|
+
timedOut = true;
|
|
2476
|
+
try { proc.kill("SIGTERM"); } catch { /* ignore */ }
|
|
2477
|
+
killTimer = setTimeout(() => {
|
|
2478
|
+
try { proc.kill("SIGKILL"); } catch { /* ignore */ }
|
|
2479
|
+
}, 5_000);
|
|
2480
|
+
if (killTimer.unref) killTimer.unref();
|
|
2481
|
+
}, timeoutMs);
|
|
1955
2482
|
}
|
|
1956
2483
|
proc.once("exit", (exitCode) => {
|
|
1957
|
-
|
|
1958
|
-
resolve({ exitCode, stdout, stderr });
|
|
2484
|
+
finish({ exitCode: timedOut ? null : exitCode, stdout, stderr: timedOut ? "timeout" : stderr });
|
|
1959
2485
|
});
|
|
1960
2486
|
proc.once("error", (err) => {
|
|
1961
|
-
|
|
1962
|
-
resolve({ exitCode: 1, stdout: "", stderr: String(err) });
|
|
2487
|
+
finish({ exitCode: 1, stdout: "", stderr: String(err) });
|
|
1963
2488
|
});
|
|
1964
2489
|
});
|
|
1965
2490
|
};
|
|
@@ -1967,6 +2492,140 @@ export const OpencodeMemPlugin = async ({
|
|
|
1967
2492
|
const runCli = async (args, options = {}) =>
|
|
1968
2493
|
runCommand([runner, ...runnerArgs, ...args], options);
|
|
1969
2494
|
|
|
2495
|
+
const postViewerJson = async ({ url, operation, payload, validate }) => {
|
|
2496
|
+
if (!viewerEnabled) {
|
|
2497
|
+
return {
|
|
2498
|
+
ok: false,
|
|
2499
|
+
classification: viewerFailureClassification(
|
|
2500
|
+
`${operation} viewer transport disabled`,
|
|
2501
|
+
"fallback",
|
|
2502
|
+
),
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
if (Date.now() < promptPackTransportUnavailableUntil) {
|
|
2506
|
+
return {
|
|
2507
|
+
ok: false,
|
|
2508
|
+
classification: viewerFailureClassification(
|
|
2509
|
+
`${operation} viewer transport in backoff`,
|
|
2510
|
+
"fallback",
|
|
2511
|
+
),
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
let response;
|
|
2516
|
+
try {
|
|
2517
|
+
const profileResponse = await fetch(promptPackProfileUrl, {
|
|
2518
|
+
method: "GET",
|
|
2519
|
+
redirect: "manual",
|
|
2520
|
+
signal: AbortSignal.timeout(promptPackHttpTimeout),
|
|
2521
|
+
});
|
|
2522
|
+
let profileBody;
|
|
2523
|
+
try {
|
|
2524
|
+
profileBody = await profileResponse.json();
|
|
2525
|
+
} catch {
|
|
2526
|
+
profileBody = null;
|
|
2527
|
+
}
|
|
2528
|
+
if (!profileResponse.ok) {
|
|
2529
|
+
const classification = classifyViewerHttpFailure({
|
|
2530
|
+
operation: `${operation} profile`,
|
|
2531
|
+
status: profileResponse.status,
|
|
2532
|
+
body: profileBody,
|
|
2533
|
+
});
|
|
2534
|
+
if (classification.retryable) {
|
|
2535
|
+
promptPackTransportUnavailableUntil = Date.now() + Math.max(1000, rawEventsBackoffMs);
|
|
2536
|
+
}
|
|
2537
|
+
return { ok: false, classification };
|
|
2538
|
+
}
|
|
2539
|
+
const viewerProtocolRange = isRecord(profileBody)
|
|
2540
|
+
? normalizePromptTransportProtocolRange(
|
|
2541
|
+
profileBody.protocol_version,
|
|
2542
|
+
profileBody.min_supported_protocol_version,
|
|
2543
|
+
)
|
|
2544
|
+
: null;
|
|
2545
|
+
let profileFailure = null;
|
|
2546
|
+
if (!isRecord(profileBody) || profileBody.service !== "codemem-viewer") {
|
|
2547
|
+
profileFailure = viewerFailureClassification(
|
|
2548
|
+
`${operation} viewer profile malformed`,
|
|
2549
|
+
classifyPromptTransportFailure({ kind: "profile_malformed" }),
|
|
2550
|
+
);
|
|
2551
|
+
} else if (!viewerProtocolRange) {
|
|
2552
|
+
profileFailure = viewerFailureClassification(
|
|
2553
|
+
`${operation} viewer protocol range malformed`,
|
|
2554
|
+
classifyPromptTransportFailure({ kind: "profile_malformed" }),
|
|
2555
|
+
);
|
|
2556
|
+
} else if (!arePromptTransportProtocolRangesCompatible(
|
|
2557
|
+
PROMPT_TRANSPORT_PROTOCOL_RANGE,
|
|
2558
|
+
viewerProtocolRange,
|
|
2559
|
+
)) {
|
|
2560
|
+
profileFailure = viewerFailureClassification(
|
|
2561
|
+
`${operation} viewer protocol range unsupported`,
|
|
2562
|
+
classifyPromptTransportFailure({ kind: "protocol_range_mismatch" }),
|
|
2563
|
+
);
|
|
2564
|
+
} else if (profileBody.db_path !== promptPackDbPath) {
|
|
2565
|
+
profileFailure = viewerFailureClassification(
|
|
2566
|
+
`${operation} viewer database mismatch`,
|
|
2567
|
+
classifyPromptTransportFailure({ kind: "database_mismatch" }),
|
|
2568
|
+
);
|
|
2569
|
+
} else if (
|
|
2570
|
+
canonicalJson(profileBody.identity_target) !== canonicalJson(promptPackIdentityTarget)
|
|
2571
|
+
) {
|
|
2572
|
+
profileFailure = viewerFailureClassification(
|
|
2573
|
+
`${operation} viewer runtime identity mismatch`,
|
|
2574
|
+
classifyPromptTransportFailure({ kind: "runtime_identity_mismatch" }),
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2577
|
+
if (profileFailure) {
|
|
2578
|
+
promptPackTransportUnavailableUntil = Date.now() + Math.max(1000, rawEventsBackoffMs);
|
|
2579
|
+
return {
|
|
2580
|
+
ok: false,
|
|
2581
|
+
classification: profileFailure,
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
response = await fetch(url, {
|
|
2585
|
+
method: "POST",
|
|
2586
|
+
redirect: "manual",
|
|
2587
|
+
headers: { "Content-Type": "application/json" },
|
|
2588
|
+
body: JSON.stringify(payload),
|
|
2589
|
+
signal: AbortSignal.timeout(promptPackHttpTimeout),
|
|
2590
|
+
});
|
|
2591
|
+
} catch (error) {
|
|
2592
|
+
const classification = classifyViewerHttpFailure({ operation, error });
|
|
2593
|
+
promptPackTransportUnavailableUntil =
|
|
2594
|
+
Date.now() + Math.max(1000, rawEventsBackoffMs);
|
|
2595
|
+
return { ok: false, classification };
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
let body;
|
|
2599
|
+
try {
|
|
2600
|
+
body = await response.json();
|
|
2601
|
+
} catch {
|
|
2602
|
+
body = null;
|
|
2603
|
+
}
|
|
2604
|
+
if (!response.ok) {
|
|
2605
|
+
const classification = classifyViewerHttpFailure({
|
|
2606
|
+
operation,
|
|
2607
|
+
status: response.status,
|
|
2608
|
+
body,
|
|
2609
|
+
compatibleProfile: true,
|
|
2610
|
+
});
|
|
2611
|
+
if (classification.retryable) {
|
|
2612
|
+
promptPackTransportUnavailableUntil =
|
|
2613
|
+
Date.now() + Math.max(1000, rawEventsBackoffMs);
|
|
2614
|
+
}
|
|
2615
|
+
return { ok: false, classification };
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
if (!validate(body)) {
|
|
2619
|
+
const classification = classifyViewerHttpFailure({ operation, malformed: true });
|
|
2620
|
+
promptPackTransportUnavailableUntil =
|
|
2621
|
+
Date.now() + Math.max(1000, rawEventsBackoffMs);
|
|
2622
|
+
return { ok: false, classification };
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
promptPackTransportUnavailableUntil = 0;
|
|
2626
|
+
return { ok: true, body };
|
|
2627
|
+
};
|
|
2628
|
+
|
|
1970
2629
|
const attemptMetadata = (identity, sessionID = null, promptNumber = promptCounter) => ({
|
|
1971
2630
|
attempt_id: identity.attemptId,
|
|
1972
2631
|
started_at: (() => {
|
|
@@ -1988,10 +2647,39 @@ export const OpencodeMemPlugin = async ({
|
|
|
1988
2647
|
});
|
|
1989
2648
|
|
|
1990
2649
|
const runPromptPackLedger = async (payload) => {
|
|
2650
|
+
const viewerPayload = {
|
|
2651
|
+
...payload,
|
|
2652
|
+
db_path: promptPackDbPath,
|
|
2653
|
+
identity_target: promptPackIdentityTarget,
|
|
2654
|
+
};
|
|
2655
|
+
const httpResult = await postViewerJson({
|
|
2656
|
+
url: promptPackLedgerUrl,
|
|
2657
|
+
operation: "prompt-pack-ledger",
|
|
2658
|
+
payload: viewerPayload,
|
|
2659
|
+
validate: isValidLedgerHttpPayload,
|
|
2660
|
+
});
|
|
2661
|
+
if (httpResult.ok) {
|
|
2662
|
+
return {
|
|
2663
|
+
exitCode: 0,
|
|
2664
|
+
stdout: JSON.stringify(httpResult.body),
|
|
2665
|
+
stderr: "",
|
|
2666
|
+
transport: "viewer",
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
const { cause, retryable } = httpResult.classification;
|
|
2671
|
+
await logLine(
|
|
2672
|
+
`inject.ledger.http_error cause=${JSON.stringify(redactLog(cause, 200))} retryable=${retryable}`
|
|
2673
|
+
);
|
|
2674
|
+
if (!retryable) {
|
|
2675
|
+
return { exitCode: 1, stdout: "", stderr: cause, transport: "viewer" };
|
|
2676
|
+
}
|
|
2677
|
+
|
|
1991
2678
|
try {
|
|
1992
|
-
|
|
2679
|
+
const result = await runCli(["prompt-pack-ledger"], {
|
|
1993
2680
|
stdinText: JSON.stringify(payload),
|
|
1994
2681
|
});
|
|
2682
|
+
return { ...result, transport: "cli" };
|
|
1995
2683
|
} catch {
|
|
1996
2684
|
return null;
|
|
1997
2685
|
}
|
|
@@ -2077,7 +2765,7 @@ export const OpencodeMemPlugin = async ({
|
|
|
2077
2765
|
if (!viewerEnabled || !viewerAutoStart || !viewerStarted) {
|
|
2078
2766
|
return { attempted: false, ok: false };
|
|
2079
2767
|
}
|
|
2080
|
-
const restartResult = await runCli(buildViewerCliArgs("restart"));
|
|
2768
|
+
const restartResult = await runCli(buildViewerCliArgs("restart"), { timeoutMs: 60_000 });
|
|
2081
2769
|
if (restartResult?.exitCode === 0) {
|
|
2082
2770
|
await logLine("compat.auto_update_viewer_restart ok");
|
|
2083
2771
|
return { attempted: true, ok: true };
|
|
@@ -2157,12 +2845,14 @@ export const OpencodeMemPlugin = async ({
|
|
|
2157
2845
|
`compat.version_mismatch current=${currentVersion} required=${minVersion} mode=${guidance.mode} note=${redactLog(guidance.note)}`
|
|
2158
2846
|
);
|
|
2159
2847
|
|
|
2160
|
-
const autoPlan = resolveAutoUpdatePlan({ runner, runnerFrom });
|
|
2161
2848
|
if (backendUpdatePolicy === "auto") {
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2849
|
+
compatibilityAutoUpdateAttempted = true;
|
|
2850
|
+
await logLine("compat.auto_update_start cmd=codemem update install --json");
|
|
2851
|
+
const updateResult = await runCli(
|
|
2852
|
+
["update", "install", "--json"],
|
|
2853
|
+
{ timeoutMs: 420_000 }
|
|
2854
|
+
);
|
|
2855
|
+
if (updateResult?.exitCode === 0) {
|
|
2166
2856
|
await logLine(
|
|
2167
2857
|
`compat.auto_update_result exit=${updateResult?.exitCode ?? "unknown"} stderr=${redactLog(
|
|
2168
2858
|
(updateResult?.stderr || "").trim()
|
|
@@ -2172,8 +2862,7 @@ export const OpencodeMemPlugin = async ({
|
|
|
2172
2862
|
const refreshedResult = await runCli(["version"]);
|
|
2173
2863
|
const refreshedVersion = (refreshedResult?.stdout || "").trim();
|
|
2174
2864
|
if (
|
|
2175
|
-
|
|
2176
|
-
&& refreshedResult?.exitCode === 0
|
|
2865
|
+
refreshedResult?.exitCode === 0
|
|
2177
2866
|
&& isVersionAtLeast(refreshedVersion, minVersion)
|
|
2178
2867
|
) {
|
|
2179
2868
|
writeCompatCheckCache(cacheKey, refreshedVersion);
|
|
@@ -2193,19 +2882,31 @@ export const OpencodeMemPlugin = async ({
|
|
|
2193
2882
|
}
|
|
2194
2883
|
return;
|
|
2195
2884
|
}
|
|
2196
|
-
|
|
2885
|
+
await logLine(
|
|
2886
|
+
`compat.auto_update_verification_failed current=${redactLog(refreshedVersion || "unknown")} required=${redactLog(minVersion)}`
|
|
2887
|
+
);
|
|
2197
2888
|
await showToast(
|
|
2198
|
-
`${message}. Auto-update
|
|
2889
|
+
`${message}. Auto-update completed, but the active CLI failed verification. Suggested action: ${guidance.action}`,
|
|
2199
2890
|
"warning"
|
|
2200
2891
|
);
|
|
2201
2892
|
return;
|
|
2202
2893
|
}
|
|
2203
|
-
|
|
2894
|
+
let installError = null;
|
|
2895
|
+
try {
|
|
2896
|
+
installError = JSON.parse((updateResult?.stdout || "").trim())?.error || null;
|
|
2897
|
+
} catch {
|
|
2898
|
+
// Non-JSON output is treated as an installation failure.
|
|
2899
|
+
}
|
|
2900
|
+
const failureReason = installError === "update_install_locked"
|
|
2901
|
+
? "another update is already running"
|
|
2902
|
+
: installError === "update_install_refused"
|
|
2903
|
+
? "not eligible"
|
|
2904
|
+
: "installation failed";
|
|
2204
2905
|
await logLine(
|
|
2205
|
-
`compat.auto_update_skipped reason=${
|
|
2906
|
+
`compat.auto_update_skipped reason=${installError || "update_install_failed"} exit=${updateResult?.exitCode ?? "unknown"} stderr=${redactLog((updateResult?.stderr || "").trim())}`
|
|
2206
2907
|
);
|
|
2207
2908
|
await showToast(
|
|
2208
|
-
`${message}. Auto-update skipped (${
|
|
2909
|
+
`${message}. Auto-update skipped (${failureReason}). Suggested action: ${guidance.action}`,
|
|
2209
2910
|
"warning"
|
|
2210
2911
|
);
|
|
2211
2912
|
return;
|
|
@@ -2214,6 +2915,53 @@ export const OpencodeMemPlugin = async ({
|
|
|
2214
2915
|
await showToast(`${message}. Suggested action: ${guidance.action}`, "warning");
|
|
2215
2916
|
};
|
|
2216
2917
|
|
|
2918
|
+
const checkForReleaseUpdate = async () => {
|
|
2919
|
+
if (backendUpdatePolicy === "off") return;
|
|
2920
|
+
const notification = parseReleaseNotification(
|
|
2921
|
+
await runCli(["update", "check", "--json"])
|
|
2922
|
+
);
|
|
2923
|
+
if (
|
|
2924
|
+
!notification
|
|
2925
|
+
|| notifiedReleaseVersions.has(notification.latestVersion)
|
|
2926
|
+
) {
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
notifiedReleaseVersions.add(notification.latestVersion);
|
|
2930
|
+
if (
|
|
2931
|
+
backendUpdatePolicy === "auto"
|
|
2932
|
+
&& notification.autoUpdateEligible
|
|
2933
|
+
&& !compatibilityAutoUpdateAttempted
|
|
2934
|
+
) {
|
|
2935
|
+
const autoPlan = resolveAutoUpdatePlan({ runner, runnerFrom, runnerFromExplicit });
|
|
2936
|
+
if (autoPlan.allowed) {
|
|
2937
|
+
const installation = await runCli(
|
|
2938
|
+
["update", "install", "--json"],
|
|
2939
|
+
{ timeoutMs: 420_000 }
|
|
2940
|
+
);
|
|
2941
|
+
if (installation?.exitCode === 0) {
|
|
2942
|
+
const viewerRestart = await restartViewerAfterAutoUpdate();
|
|
2943
|
+
await showToast(`Updated codemem to ${notification.latestVersion}.`, "success");
|
|
2944
|
+
if (viewerRestart.attempted && !viewerRestart.ok) {
|
|
2945
|
+
await showToast(
|
|
2946
|
+
"Backend updated, but viewer restart failed. Run `codemem serve restart`.",
|
|
2947
|
+
"warning"
|
|
2948
|
+
);
|
|
2949
|
+
}
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
await logLine(
|
|
2953
|
+
`release.auto_update_failed exit=${installation?.exitCode ?? "unknown"} stderr=${redactLog(
|
|
2954
|
+
(installation?.stderr || "").trim()
|
|
2955
|
+
)}`
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
await showToast(
|
|
2960
|
+
`codemem ${notification.latestVersion} is available. ${notification.recommendedAction}`,
|
|
2961
|
+
"warning"
|
|
2962
|
+
);
|
|
2963
|
+
};
|
|
2964
|
+
|
|
2217
2965
|
const resolveInjectQuery = (overrides = {}) => {
|
|
2218
2966
|
const firstPrompt = hasOwn(overrides, "firstPrompt")
|
|
2219
2967
|
? overrides.firstPrompt
|
|
@@ -2317,12 +3065,51 @@ export const OpencodeMemPlugin = async ({
|
|
|
2317
3065
|
injectTokenBudget,
|
|
2318
3066
|
internalLedger: true,
|
|
2319
3067
|
});
|
|
3068
|
+
const httpResult = await postViewerJson({
|
|
3069
|
+
url: packUrl,
|
|
3070
|
+
operation: "pack",
|
|
3071
|
+
payload: buildPackHttpBody({
|
|
3072
|
+
query,
|
|
3073
|
+
filesModified: sessionContext.filesModified,
|
|
3074
|
+
injectLimit,
|
|
3075
|
+
injectTokenBudget,
|
|
3076
|
+
projectName: normalizeProjectLabel(process.env.CODEMEM_PROJECT),
|
|
3077
|
+
cwd,
|
|
3078
|
+
dbPath: promptPackDbPath,
|
|
3079
|
+
identityTarget: promptPackIdentityTarget,
|
|
3080
|
+
attempt: metadata,
|
|
3081
|
+
}),
|
|
3082
|
+
validate: isValidPackHttpPayload,
|
|
3083
|
+
});
|
|
3084
|
+
if (httpResult.ok) {
|
|
3085
|
+
return {
|
|
3086
|
+
packArgs,
|
|
3087
|
+
result: {
|
|
3088
|
+
exitCode: 0,
|
|
3089
|
+
stdout: JSON.stringify(httpResult.body),
|
|
3090
|
+
stderr: "",
|
|
3091
|
+
transport: "viewer",
|
|
3092
|
+
},
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
const { cause, retryable } = httpResult.classification;
|
|
3097
|
+
await logLine(
|
|
3098
|
+
`inject.pack.http_error cause=${JSON.stringify(redactLog(cause, 200))} retryable=${retryable}`
|
|
3099
|
+
);
|
|
3100
|
+
if (!retryable) {
|
|
3101
|
+
return {
|
|
3102
|
+
packArgs,
|
|
3103
|
+
result: { exitCode: 1, stdout: "", stderr: cause, transport: "viewer" },
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
|
|
2320
3107
|
let result = await runCli(packArgs, { stdinText: JSON.stringify(metadata) });
|
|
2321
3108
|
if (rejectsInternalLedgerFlag(result)) {
|
|
2322
3109
|
packArgs = packArgs.filter((arg) => arg !== "--internal-ledger");
|
|
2323
3110
|
result = await runCli(packArgs);
|
|
2324
3111
|
}
|
|
2325
|
-
return { packArgs, result };
|
|
3112
|
+
return { packArgs, result: { ...result, transport: "cli" } };
|
|
2326
3113
|
};
|
|
2327
3114
|
let { packArgs, result } = await runPack();
|
|
2328
3115
|
let { packText, conflictPackText, metrics, itemCount, ledgerConflict } = parsePackOutput(result);
|
|
@@ -2333,7 +3120,7 @@ export const OpencodeMemPlugin = async ({
|
|
|
2333
3120
|
let repairFallbackUsed = false;
|
|
2334
3121
|
if (ledgerConflict) {
|
|
2335
3122
|
// A restarted plugin has no artifact cache to predict this conflict. The
|
|
2336
|
-
//
|
|
3123
|
+
// ledger marker is authoritative: never attribute delivery to the stale
|
|
2337
3124
|
// identity, and retry once with a fresh deterministic identity.
|
|
2338
3125
|
await log("warn", "codemem prompt-pack ledger conflict; retrying with fresh identity", {
|
|
2339
3126
|
sessionID,
|
|
@@ -2606,62 +3393,15 @@ export const OpencodeMemPlugin = async ({
|
|
|
2606
3393
|
await runCli(buildViewerCliArgs("stop"));
|
|
2607
3394
|
};
|
|
2608
3395
|
|
|
2609
|
-
const
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
await logLine(`viewer.health recovered after ${healthConsecutiveFailures} failure(s)`);
|
|
2619
|
-
}
|
|
2620
|
-
healthConsecutiveFailures = 0;
|
|
2621
|
-
return;
|
|
2622
|
-
}
|
|
2623
|
-
healthConsecutiveFailures++;
|
|
2624
|
-
await logLine(`viewer.health check failed (status=${resp.status}, consecutive=${healthConsecutiveFailures})`);
|
|
2625
|
-
} catch (err) {
|
|
2626
|
-
healthConsecutiveFailures++;
|
|
2627
|
-
await logLine(`viewer.health check error (consecutive=${healthConsecutiveFailures}): ${String(err).slice(0, 200)}`);
|
|
2628
|
-
}
|
|
2629
|
-
|
|
2630
|
-
if (
|
|
2631
|
-
healthConsecutiveFailures >= HEALTH_CONSECUTIVE_FAILURES_BEFORE_RESTART &&
|
|
2632
|
-
Date.now() - healthLastRestartAttempt >= HEALTH_RESTART_COOLDOWN_MS
|
|
2633
|
-
) {
|
|
2634
|
-
healthLastRestartAttempt = Date.now();
|
|
2635
|
-
await logLine(`viewer.health restarting viewer after ${healthConsecutiveFailures} consecutive failures`);
|
|
2636
|
-
try {
|
|
2637
|
-
const result = await runCli(buildViewerCliArgs("restart"));
|
|
2638
|
-
const ok = result?.exitCode === 0;
|
|
2639
|
-
await logLine(`viewer.health restart ${ok ? "succeeded" : "failed"} (exit=${result?.exitCode ?? "unknown"})`);
|
|
2640
|
-
if (ok) {
|
|
2641
|
-
healthConsecutiveFailures = 0;
|
|
2642
|
-
}
|
|
2643
|
-
} catch (restartErr) {
|
|
2644
|
-
await logLine(`viewer.health restart error: ${String(restartErr).slice(0, 200)}`);
|
|
2645
|
-
}
|
|
2646
|
-
}
|
|
2647
|
-
};
|
|
2648
|
-
|
|
2649
|
-
const startHealthCheck = () => {
|
|
2650
|
-
if (healthCheckTimer) return;
|
|
2651
|
-
healthConsecutiveFailures = 0;
|
|
2652
|
-
healthCheckTimer = setInterval(() => {
|
|
2653
|
-
checkViewerHealth().catch(() => {});
|
|
2654
|
-
}, HEALTH_CHECK_INTERVAL_MS);
|
|
2655
|
-
if (healthCheckTimer.unref) healthCheckTimer.unref();
|
|
2656
|
-
};
|
|
2657
|
-
|
|
2658
|
-
const stopHealthCheck = () => {
|
|
2659
|
-
if (healthCheckTimer) {
|
|
2660
|
-
clearInterval(healthCheckTimer);
|
|
2661
|
-
healthCheckTimer = null;
|
|
2662
|
-
}
|
|
2663
|
-
healthConsecutiveFailures = 0;
|
|
2664
|
-
};
|
|
3396
|
+
const viewerHealthMonitor = createViewerHealthMonitor({
|
|
3397
|
+
viewerHealthUrl,
|
|
3398
|
+
legacyStatusUrl: rawEventsStatusUrl,
|
|
3399
|
+
isActive: () => viewerStarted && viewerEnabled,
|
|
3400
|
+
restartViewer: () => runCli(buildViewerCliArgs("restart")),
|
|
3401
|
+
logLine,
|
|
3402
|
+
});
|
|
3403
|
+
const startHealthCheck = viewerHealthMonitor.start;
|
|
3404
|
+
const stopHealthCheck = viewerHealthMonitor.stop;
|
|
2665
3405
|
|
|
2666
3406
|
// Get version info (commit hash) for debugging
|
|
2667
3407
|
let version = "unknown";
|
|
@@ -2680,15 +3420,26 @@ export const OpencodeMemPlugin = async ({
|
|
|
2680
3420
|
|
|
2681
3421
|
await log("info", "codemem plugin initialized", { cwd, version });
|
|
2682
3422
|
await logLine(`plugin initialized cwd=${cwd} version=${version}`);
|
|
2683
|
-
startViewer();
|
|
2684
|
-
const
|
|
2685
|
-
void
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
)
|
|
2689
|
-
|
|
3423
|
+
void startViewer();
|
|
3424
|
+
const updateCheckTimer = setTimeout(() => {
|
|
3425
|
+
void (async () => {
|
|
3426
|
+
try {
|
|
3427
|
+
await verifyCliCompatibility();
|
|
3428
|
+
} catch (err) {
|
|
3429
|
+
await logLine(
|
|
3430
|
+
`compat.version_check_error message=${String(err?.message || err || "unknown")}`
|
|
3431
|
+
);
|
|
3432
|
+
}
|
|
3433
|
+
try {
|
|
3434
|
+
await checkForReleaseUpdate();
|
|
3435
|
+
} catch (err) {
|
|
3436
|
+
await logLine(
|
|
3437
|
+
`release.update_check_error message=${String(err?.message || err || "unknown")}`
|
|
3438
|
+
);
|
|
3439
|
+
}
|
|
3440
|
+
})();
|
|
2690
3441
|
}, COMPAT_CHECK_DELAY_MS);
|
|
2691
|
-
if (
|
|
3442
|
+
if (updateCheckTimer.unref) updateCheckTimer.unref();
|
|
2692
3443
|
|
|
2693
3444
|
const truncate = (value) => {
|
|
2694
3445
|
if (value === undefined || value === null) {
|
|
@@ -2937,9 +3688,9 @@ export const OpencodeMemPlugin = async ({
|
|
|
2937
3688
|
} inject_enabled=${injectEnabled} tui_toast=${Boolean(client.tui?.showToast)} query=${JSON.stringify(safeQuery)} first_prompt_len=${firstPromptLen} last_prompt_len=${lastPromptLen} project=${JSON.stringify(projectName)} files_modified=${filesModifiedCount}`
|
|
2938
3689
|
);
|
|
2939
3690
|
}
|
|
2940
|
-
// Without the old per-session cache, every transform call
|
|
2941
|
-
//
|
|
2942
|
-
//
|
|
3691
|
+
// Without the old per-session cache, every transform call rebuilds the
|
|
3692
|
+
// pack. Swallow rejections here so a single failed build (viewer failure,
|
|
3693
|
+
// CLI fallback crash, sqlite lock, or network blip)
|
|
2943
3694
|
// can't take down the chat path.
|
|
2944
3695
|
let applied = false;
|
|
2945
3696
|
try {
|
|
@@ -3217,7 +3968,7 @@ export const OpencodeMemPlugin = async ({
|
|
|
3217
3968
|
const stats = await runCli(["stats"]);
|
|
3218
3969
|
const recent = await runCli(["recent", "--limit", "5"]);
|
|
3219
3970
|
const lines = [
|
|
3220
|
-
`viewer: http://${
|
|
3971
|
+
`viewer: http://${viewerUrlHost}:${viewerPort}`,
|
|
3221
3972
|
`log: ${logPath || "disabled"}`,
|
|
3222
3973
|
];
|
|
3223
3974
|
if (stats.exitCode === 0 && stats.stdout.trim()) {
|
|
@@ -3262,9 +4013,16 @@ export const OpencodeMemPlugin = async ({
|
|
|
3262
4013
|
};
|
|
3263
4014
|
};
|
|
3264
4015
|
|
|
3265
|
-
export default
|
|
4016
|
+
export default CodememPlugin;
|
|
4017
|
+
|
|
4018
|
+
/**
|
|
4019
|
+
* @deprecated Use CodememPlugin.
|
|
4020
|
+
* Keep this reference-identical: OpenCode deduplicates plugin exports by identity.
|
|
4021
|
+
*/
|
|
4022
|
+
export const OpencodeMemPlugin = CodememPlugin;
|
|
3266
4023
|
export const __testUtils = {
|
|
3267
4024
|
PINNED_BACKEND_VERSION,
|
|
4025
|
+
fetchRawEventsStatus,
|
|
3268
4026
|
inferProjectFromCwd,
|
|
3269
4027
|
normalizeProjectLabel,
|
|
3270
4028
|
resolveProjectName,
|
|
@@ -3276,8 +4034,17 @@ export const __testUtils = {
|
|
|
3276
4034
|
redactPackCommand,
|
|
3277
4035
|
rejectsInternalLedgerFlag,
|
|
3278
4036
|
classifyFallbackCommandResult,
|
|
4037
|
+
PROMPT_TRANSPORT_PROTOCOL_RANGE,
|
|
4038
|
+
normalizePromptTransportProtocolRange,
|
|
4039
|
+
arePromptTransportProtocolRangesCompatible,
|
|
4040
|
+
classifyPromptTransportFailure,
|
|
4041
|
+
classifyViewerHttpFailure,
|
|
4042
|
+
isValidPackHttpPayload,
|
|
4043
|
+
isValidLedgerHttpPayload,
|
|
4044
|
+
buildPackHttpBody,
|
|
3279
4045
|
parsePackText,
|
|
3280
4046
|
parsePackMetrics,
|
|
4047
|
+
buildViewerIdentityTarget,
|
|
3281
4048
|
resolveInjectSurface,
|
|
3282
4049
|
applyInjectedContextToOutput,
|
|
3283
4050
|
applyInjectedContextToMessages,
|
|
@@ -3296,4 +4063,5 @@ export const __testUtils = {
|
|
|
3296
4063
|
buildRawEventEnvelope,
|
|
3297
4064
|
trimEventQueue,
|
|
3298
4065
|
parsePositiveInt,
|
|
4066
|
+
createViewerHealthMonitor,
|
|
3299
4067
|
};
|
package/README.md
CHANGED
|
@@ -21,6 +21,13 @@ Manual config also works. Add the package name to your OpenCode config:
|
|
|
21
21
|
|
|
22
22
|
OpenCode installs npm plugins automatically with Bun at startup.
|
|
23
23
|
|
|
24
|
+
## Exports
|
|
25
|
+
|
|
26
|
+
`CodememPlugin` is the canonical named export and the package default export.
|
|
27
|
+
`OpencodeMemPlugin` remains available as a deprecated, reference-identical alias
|
|
28
|
+
for compatibility with integrations created before the Codemem rename. No removal
|
|
29
|
+
version is currently scheduled.
|
|
30
|
+
|
|
24
31
|
## Documentation
|
|
25
32
|
|
|
26
33
|
- Repository: https://github.com/kunickiaj/codemem
|
package/index.js
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
export {
|
|
2
|
-
|
|
1
|
+
export {
|
|
2
|
+
CodememPlugin as default,
|
|
3
|
+
CodememPlugin,
|
|
4
|
+
OpencodeMemPlugin,
|
|
5
|
+
} from "./.opencode/plugins/codemem.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codemem/opencode-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "CodeMem plugin for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
".opencode/lib/"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@opencode-ai/plugin": "1.18.
|
|
21
|
+
"@opencode-ai/plugin": "1.18.18"
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|