@codemem/opencode-plugin 0.43.2 → 0.44.0-beta.1
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 +48 -14
- package/.opencode/lib/raw-event-spool.js +197 -0
- package/.opencode/plugins/codemem.js +534 -241
- package/package.json +5 -2
|
@@ -13,17 +13,26 @@ import {
|
|
|
13
13
|
resolveAutoUpdatePlan,
|
|
14
14
|
resolveUpgradeGuidance,
|
|
15
15
|
} from "../lib/compat.js";
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_DRAIN_LIMIT,
|
|
18
|
+
DEFAULT_MAX_ENTRIES,
|
|
19
|
+
RAW_EVENT_SPOOL_FULL_CODE,
|
|
20
|
+
loadRawEventSpoolEntries,
|
|
21
|
+
removeRawEventSpoolEntry,
|
|
22
|
+
resolveSpoolDirectory,
|
|
23
|
+
writeRawEventSpoolEntry,
|
|
24
|
+
} from "../lib/raw-event-spool.js";
|
|
16
25
|
|
|
17
26
|
const TRUTHY_VALUES = ["1", "true", "yes"];
|
|
18
27
|
const DISABLED_VALUES = ["0", "false", "off"];
|
|
19
|
-
const PINNED_BACKEND_VERSION = "0.
|
|
28
|
+
const PINNED_BACKEND_VERSION = "0.44.0-beta.1";
|
|
20
29
|
const COMPAT_CHECK_DELAY_MS = 1500;
|
|
21
30
|
const COMPAT_CHECK_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
22
31
|
const MAX_UPDATE_STATUS_BYTES = 16 * 1024;
|
|
23
32
|
const MAX_UPDATE_ACTION_CHARS = 1000;
|
|
24
33
|
const MAX_UPDATE_VERSION_CHARS = 128;
|
|
25
|
-
const
|
|
26
|
-
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
34
|
+
const RELEASE_VERSION =
|
|
35
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
27
36
|
const CODEMEM_CONTEXT_PART_ID_PREFIX = "codemem-context-";
|
|
28
37
|
const MAX_MESSAGE_INJECTION_CACHE_SESSIONS = 20;
|
|
29
38
|
const MAX_MESSAGE_INJECTION_CACHE_MESSAGES = 100;
|
|
@@ -34,10 +43,31 @@ const VIEWER_HEALTH_TIMEOUT_MS = 5_000;
|
|
|
34
43
|
const VIEWER_HEALTH_RESTART_THRESHOLD = 3;
|
|
35
44
|
const VIEWER_HEALTH_RESTART_COOLDOWN_MS = 5 * 60_000;
|
|
36
45
|
const RAW_EVENTS_STATUS_TIMEOUT_MS = 5_000;
|
|
46
|
+
const DEFAULT_EMBEDDING_MODEL = "Xenova/bge-small-en-v1.5";
|
|
47
|
+
const DEFAULT_EMBEDDING_REVISION = "ea104dacec62c0de699686887e3f920caeb4f3e3";
|
|
48
|
+
const PLUGIN_REGISTRATIONS_KEY = Symbol.for("codemem.opencode-plugin.registrations");
|
|
37
49
|
|
|
38
50
|
let compatCheckCache = null;
|
|
39
51
|
const notifiedReleaseVersions = new Set();
|
|
40
52
|
|
|
53
|
+
const claimPluginRegistration = (cwd) => {
|
|
54
|
+
let registrations = globalThis[PLUGIN_REGISTRATIONS_KEY];
|
|
55
|
+
if (!(registrations instanceof Map)) {
|
|
56
|
+
registrations = new Map();
|
|
57
|
+
globalThis[PLUGIN_REGISTRATIONS_KEY] = registrations;
|
|
58
|
+
}
|
|
59
|
+
const projectPath = resolve(cwd);
|
|
60
|
+
if (registrations.has(projectPath)) return null;
|
|
61
|
+
const registration = Symbol(projectPath);
|
|
62
|
+
registrations.set(projectPath, registration);
|
|
63
|
+
return () => {
|
|
64
|
+
if (registrations.get(projectPath) === registration) {
|
|
65
|
+
registrations.delete(projectPath);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
const rawEventSpoolDrainsInFlight = new Map();
|
|
70
|
+
|
|
41
71
|
// Release an unread response body without surfacing cancellation failures.
|
|
42
72
|
const discardResponseBody = (response) => {
|
|
43
73
|
try {
|
|
@@ -352,10 +382,74 @@ const clearCompatCheckCache = () => {
|
|
|
352
382
|
compatCheckCache = null;
|
|
353
383
|
};
|
|
354
384
|
|
|
355
|
-
const
|
|
356
|
-
if (typeof value !== "string" || value.length > MAX_UPDATE_VERSION_CHARS) return
|
|
357
|
-
const match =
|
|
358
|
-
|
|
385
|
+
const parseReleaseVersion = (value) => {
|
|
386
|
+
if (typeof value !== "string" || value.length > MAX_UPDATE_VERSION_CHARS) return null;
|
|
387
|
+
const match = RELEASE_VERSION.exec(value);
|
|
388
|
+
if (!match || !match.slice(1, 4).map(Number).every(Number.isSafeInteger)) return null;
|
|
389
|
+
const prerelease = match[4]?.split(".") || [];
|
|
390
|
+
if (prerelease.some((identifier) => /^0\d+$/.test(identifier))) return null;
|
|
391
|
+
return { core: match.slice(1, 4).map(Number), prerelease };
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const releaseChannelForVersion = (value) => {
|
|
395
|
+
const parsed = parseReleaseVersion(value);
|
|
396
|
+
if (!parsed) return null;
|
|
397
|
+
if (parsed.prerelease.length === 0) return "latest";
|
|
398
|
+
const channel = parsed.prerelease[0];
|
|
399
|
+
return channel === "alpha" || channel === "beta" || channel === "rc" ? channel : null;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const comparePrereleaseIdentifier = (left, right) => {
|
|
403
|
+
if (left === undefined) return right === undefined ? 0 : -1;
|
|
404
|
+
if (right === undefined) return 1;
|
|
405
|
+
if (left === right) return 0;
|
|
406
|
+
const leftNumeric = /^\d+$/.test(left);
|
|
407
|
+
const rightNumeric = /^\d+$/.test(right);
|
|
408
|
+
if (leftNumeric && rightNumeric && left.length !== right.length) {
|
|
409
|
+
return Math.sign(left.length - right.length);
|
|
410
|
+
}
|
|
411
|
+
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
|
|
412
|
+
return left < right ? -1 : 1;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const compareReleaseVersions = (left, right) => {
|
|
416
|
+
const leftVersion = parseReleaseVersion(left);
|
|
417
|
+
const rightVersion = parseReleaseVersion(right);
|
|
418
|
+
if (!leftVersion || !rightVersion) return null;
|
|
419
|
+
for (let index = 0; index < leftVersion.core.length; index += 1) {
|
|
420
|
+
const difference = leftVersion.core[index] - rightVersion.core[index];
|
|
421
|
+
if (difference !== 0) return Math.sign(difference);
|
|
422
|
+
}
|
|
423
|
+
if (leftVersion.prerelease.length === 0) {
|
|
424
|
+
return rightVersion.prerelease.length === 0 ? 0 : 1;
|
|
425
|
+
}
|
|
426
|
+
if (rightVersion.prerelease.length === 0) return -1;
|
|
427
|
+
const length = Math.max(leftVersion.prerelease.length, rightVersion.prerelease.length);
|
|
428
|
+
for (let index = 0; index < length; index += 1) {
|
|
429
|
+
const comparison = comparePrereleaseIdentifier(
|
|
430
|
+
leftVersion.prerelease[index],
|
|
431
|
+
rightVersion.prerelease[index],
|
|
432
|
+
);
|
|
433
|
+
if (comparison !== 0) return comparison;
|
|
434
|
+
}
|
|
435
|
+
return 0;
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const isSameChannelVersionAtLeast = (currentVersion, minimumVersion) => {
|
|
439
|
+
const currentChannel = releaseChannelForVersion(currentVersion);
|
|
440
|
+
if (!currentChannel || currentChannel !== releaseChannelForVersion(minimumVersion)) return false;
|
|
441
|
+
const comparison = compareReleaseVersions(currentVersion, minimumVersion);
|
|
442
|
+
return comparison !== null && comparison >= 0;
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const PINNED_RELEASE_CHANNEL = releaseChannelForVersion(PINNED_BACKEND_VERSION);
|
|
446
|
+
|
|
447
|
+
const guidanceMatchesRelease = (guidance, latestVersion) => {
|
|
448
|
+
const versions =
|
|
449
|
+
String(guidance || "").match(
|
|
450
|
+
/\b\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?/g,
|
|
451
|
+
) || [];
|
|
452
|
+
return versions.length === 0 || versions.every((version) => version === latestVersion);
|
|
359
453
|
};
|
|
360
454
|
|
|
361
455
|
const parseReleaseNotification = (result) => {
|
|
@@ -366,18 +460,24 @@ const parseReleaseNotification = (result) => {
|
|
|
366
460
|
const status = JSON.parse(stdout);
|
|
367
461
|
if (!status || typeof status !== "object" || Array.isArray(status)) return null;
|
|
368
462
|
if (status.update_available !== true) return null;
|
|
369
|
-
if (
|
|
463
|
+
if (
|
|
464
|
+
!PINNED_RELEASE_CHANNEL
|
|
465
|
+
|| status.channel !== PINNED_RELEASE_CHANNEL
|
|
466
|
+
|| releaseChannelForVersion(status.latest_version) !== PINNED_RELEASE_CHANNEL
|
|
467
|
+
) {
|
|
370
468
|
return null;
|
|
371
469
|
}
|
|
372
470
|
if (
|
|
373
471
|
typeof status.recommended_action !== "string"
|
|
374
472
|
|| !status.recommended_action.trim()
|
|
375
473
|
|| status.recommended_action.length > MAX_UPDATE_ACTION_CHARS
|
|
474
|
+
|| !guidanceMatchesRelease(status.recommended_action, status.latest_version)
|
|
376
475
|
) {
|
|
377
476
|
return null;
|
|
378
477
|
}
|
|
379
478
|
return {
|
|
380
479
|
latestVersion: status.latest_version,
|
|
480
|
+
channel: PINNED_RELEASE_CHANNEL,
|
|
381
481
|
recommendedAction: status.recommended_action.trim(),
|
|
382
482
|
autoUpdateEligible: status.auto_update_eligible === true,
|
|
383
483
|
};
|
|
@@ -386,6 +486,14 @@ const parseReleaseNotification = (result) => {
|
|
|
386
486
|
}
|
|
387
487
|
};
|
|
388
488
|
|
|
489
|
+
const resolveAutoUpdateEnvironment = ({
|
|
490
|
+
platform = process.platform,
|
|
491
|
+
env = process.env,
|
|
492
|
+
} = {}) => {
|
|
493
|
+
if (platform !== "linux") return env;
|
|
494
|
+
return { ...env, ONNXRUNTIME_NODE_INSTALL: "skip" };
|
|
495
|
+
};
|
|
496
|
+
|
|
389
497
|
const createLogLine = (logPath) => async (line) => {
|
|
390
498
|
if (!logPath) {
|
|
391
499
|
return;
|
|
@@ -632,6 +740,17 @@ const buildViewerIdentityTarget = (env = process.env, cwd = process.cwd()) => {
|
|
|
632
740
|
: trimmed;
|
|
633
741
|
return resolve(cwd, expanded);
|
|
634
742
|
};
|
|
743
|
+
const embeddingModel = env.CODEMEM_EMBEDDING_MODEL || DEFAULT_EMBEDDING_MODEL;
|
|
744
|
+
const configuredEmbeddingRevision = env.CODEMEM_EMBEDDING_REVISION?.trim();
|
|
745
|
+
// Match core's configured request identity. The runtime resolves mutable refs
|
|
746
|
+
// to canonical commits before vectors are persisted.
|
|
747
|
+
let embeddingRevision;
|
|
748
|
+
if (configuredEmbeddingRevision) {
|
|
749
|
+
embeddingRevision = configuredEmbeddingRevision;
|
|
750
|
+
} else {
|
|
751
|
+
embeddingRevision =
|
|
752
|
+
embeddingModel === DEFAULT_EMBEDDING_MODEL ? DEFAULT_EMBEDDING_REVISION : null;
|
|
753
|
+
}
|
|
635
754
|
return {
|
|
636
755
|
device_id: env.CODEMEM_DEVICE_ID?.trim() || null,
|
|
637
756
|
actor_id_present: Object.hasOwn(env, "CODEMEM_ACTOR_ID"),
|
|
@@ -644,7 +763,11 @@ const buildViewerIdentityTarget = (env = process.env, cwd = process.cwd()) => {
|
|
|
644
763
|
embedding_disabled: ["1", "true", "yes"].includes(
|
|
645
764
|
String(env.CODEMEM_EMBEDDING_DISABLED || "").toLowerCase(),
|
|
646
765
|
),
|
|
647
|
-
|
|
766
|
+
embedding_offline: ["1", "true", "yes"].includes(
|
|
767
|
+
String(env.CODEMEM_EMBEDDING_OFFLINE || "").toLowerCase(),
|
|
768
|
+
),
|
|
769
|
+
embedding_model: embeddingModel,
|
|
770
|
+
embedding_revision: embeddingRevision,
|
|
648
771
|
};
|
|
649
772
|
};
|
|
650
773
|
|
|
@@ -736,6 +859,20 @@ const isViewerInvalidRequestPayload = (payload) =>
|
|
|
736
859
|
&& isRecord(payload.error)
|
|
737
860
|
&& payload.error.code === "invalid_request";
|
|
738
861
|
|
|
862
|
+
const classifyRawEventViewerFailure = (payload) => {
|
|
863
|
+
if (isViewerDbMismatchPayload(payload)) return "database";
|
|
864
|
+
if (isViewerIdentityMismatchPayload(payload)) return "identity";
|
|
865
|
+
if (isViewerContractUnsupportedPayload(payload)) return "contract";
|
|
866
|
+
return "connection";
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
const RAW_EVENT_FAILURE_ACTIONS = Object.freeze({
|
|
870
|
+
database: "restart the viewer from the same workspace/config",
|
|
871
|
+
identity: "restart Codemem and OpenCode with the same environment",
|
|
872
|
+
contract: "update Codemem on the installed channel, then restart OpenCode",
|
|
873
|
+
connection: "check or restart the viewer",
|
|
874
|
+
});
|
|
875
|
+
|
|
739
876
|
// Dependency-free port of @codemem/core prompt-transport semantics. Keep the
|
|
740
877
|
// range and classifier parity pinned by the plugin injection tests.
|
|
741
878
|
const PROMPT_TRANSPORT_PROTOCOL_RANGE = Object.freeze({
|
|
@@ -1569,7 +1706,11 @@ const trimEventQueue = ({ events, maxEvents, hardMaxEvents, onUnsentPressure, on
|
|
|
1569
1706
|
hardMaxEvents > 0 &&
|
|
1570
1707
|
events.length > hardMaxEvents
|
|
1571
1708
|
) {
|
|
1572
|
-
const
|
|
1709
|
+
const durableIndex = events.findIndex(
|
|
1710
|
+
(queued) => queued && typeof queued === "object" && queued._raw_spooled
|
|
1711
|
+
);
|
|
1712
|
+
const dropIndex = durableIndex >= 0 ? durableIndex : 0;
|
|
1713
|
+
const [dropped] = events.splice(dropIndex, 1);
|
|
1573
1714
|
if (typeof onForcedDrop === "function") {
|
|
1574
1715
|
onForcedDrop(dropped, events.length, hardMaxEvents);
|
|
1575
1716
|
}
|
|
@@ -1668,22 +1809,10 @@ const detectRunner = ({ cwd, envRunner }) => {
|
|
|
1668
1809
|
// terminal on every OpenCode startup for npx-only installs.
|
|
1669
1810
|
stdio: ["ignore", "pipe", "ignore"],
|
|
1670
1811
|
}).trim();
|
|
1671
|
-
//
|
|
1672
|
-
//
|
|
1673
|
-
//
|
|
1674
|
-
|
|
1675
|
-
// Accept a global codemem only when it is the EXACT pinned version, or a
|
|
1676
|
-
// clean (non-prerelease) release at least as new as the pin. isVersionAtLeast
|
|
1677
|
-
// ignores prerelease suffixes, so without this a prerelease like
|
|
1678
|
-
// "0.36.1-alpha.0" would wrongly compare >= the "0.36.1" release even though
|
|
1679
|
-
// SemVer orders it below. Requiring an exact match for prereleases also
|
|
1680
|
-
// handles a prerelease PIN correctly (the release script can set one): only
|
|
1681
|
-
// the identical prerelease is trusted, never an older/different one.
|
|
1682
|
-
const isPrerelease = versionOutput.includes("-");
|
|
1683
|
-
const matchesOrCleanNewer =
|
|
1684
|
-
versionOutput === PINNED_BACKEND_VERSION ||
|
|
1685
|
-
(!isPrerelease && isVersionAtLeast(versionOutput, PINNED_BACKEND_VERSION));
|
|
1686
|
-
if (matchesOrCleanNewer) {
|
|
1812
|
+
// Use a newer global CLI only when it remains on the plugin's release
|
|
1813
|
+
// channel. This prevents both channel crossing and fallback to an older pin
|
|
1814
|
+
// immediately after a successful prerelease auto-update.
|
|
1815
|
+
if (isSameChannelVersionAtLeast(versionOutput, PINNED_BACKEND_VERSION)) {
|
|
1687
1816
|
return "codemem";
|
|
1688
1817
|
}
|
|
1689
1818
|
} catch {
|
|
@@ -1704,13 +1833,47 @@ const tsCliAvailable = (cliPath) => {
|
|
|
1704
1833
|
}
|
|
1705
1834
|
};
|
|
1706
1835
|
|
|
1836
|
+
// When an explicit npx override targets the codemem package, pair it with the
|
|
1837
|
+
// matching @codemem/embeddings spec so the detached Viewer can resolve the
|
|
1838
|
+
// optional runtime instead of silently falling back to lexical search. Returns
|
|
1839
|
+
// null for any spec that is not a bare `codemem`/`codemem@<version>`, so custom
|
|
1840
|
+
// package overrides pass through unchanged.
|
|
1841
|
+
const pairedEmbeddingsForCodememSpec = (spec) => {
|
|
1842
|
+
if (typeof spec !== "string") return null;
|
|
1843
|
+
const trimmed = spec.trim();
|
|
1844
|
+
if (trimmed === "codemem") return "@codemem/embeddings";
|
|
1845
|
+
const match = /^codemem@([^\s]+)$/.exec(trimmed);
|
|
1846
|
+
if (!match || !/^[A-Za-z0-9~^<>=*][A-Za-z0-9._~^<>=*|-]*$/.test(match[1])) return null;
|
|
1847
|
+
return `@codemem/embeddings@${match[1]}`;
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1707
1850
|
const buildRunnerArgs = ({ runner, runnerFrom, runnerFromExplicit }) => {
|
|
1708
1851
|
if (runner === "codemem") {
|
|
1709
1852
|
return [];
|
|
1710
1853
|
}
|
|
1711
1854
|
if (runner === "npx") {
|
|
1712
|
-
|
|
1713
|
-
|
|
1855
|
+
if (runnerFromExplicit) {
|
|
1856
|
+
const pairedEmbeddings = pairedEmbeddingsForCodememSpec(runnerFrom);
|
|
1857
|
+
if (pairedEmbeddings) {
|
|
1858
|
+
return [
|
|
1859
|
+
"-y",
|
|
1860
|
+
"--package",
|
|
1861
|
+
runnerFrom,
|
|
1862
|
+
"--package",
|
|
1863
|
+
pairedEmbeddings,
|
|
1864
|
+
"codemem",
|
|
1865
|
+
];
|
|
1866
|
+
}
|
|
1867
|
+
return ["-y", runnerFrom];
|
|
1868
|
+
}
|
|
1869
|
+
return [
|
|
1870
|
+
"-y",
|
|
1871
|
+
"--package",
|
|
1872
|
+
`codemem@${PINNED_BACKEND_VERSION}`,
|
|
1873
|
+
"--package",
|
|
1874
|
+
`@codemem/embeddings@${PINNED_BACKEND_VERSION}`,
|
|
1875
|
+
"codemem",
|
|
1876
|
+
];
|
|
1714
1877
|
}
|
|
1715
1878
|
if (runner === "node") {
|
|
1716
1879
|
const cliPath = runnerFromExplicit
|
|
@@ -1763,6 +1926,13 @@ export const CodememPlugin = async ({
|
|
|
1763
1926
|
if (pluginIgnored) {
|
|
1764
1927
|
return {};
|
|
1765
1928
|
}
|
|
1929
|
+
const releasePluginRegistration = claimPluginRegistration(cwd);
|
|
1930
|
+
if (!releasePluginRegistration) {
|
|
1931
|
+
await log("warn", "codemem duplicate plugin registration skipped", {
|
|
1932
|
+
next_action: "remove either the configured npm plugin or the project-local Codemem plugin",
|
|
1933
|
+
});
|
|
1934
|
+
return {};
|
|
1935
|
+
}
|
|
1766
1936
|
|
|
1767
1937
|
const runner = detectRunner({
|
|
1768
1938
|
cwd,
|
|
@@ -1864,9 +2034,22 @@ export const CodememPlugin = async ({
|
|
|
1864
2034
|
process.env.CODEMEM_RAW_EVENTS_HARD_MAX || "2000",
|
|
1865
2035
|
2000
|
|
1866
2036
|
);
|
|
2037
|
+
const rawEventSpoolDrainLimit = parsePositiveInt(
|
|
2038
|
+
process.env.CODEMEM_RAW_EVENT_SPOOL_DRAIN_LIMIT,
|
|
2039
|
+
DEFAULT_DRAIN_LIMIT,
|
|
2040
|
+
);
|
|
2041
|
+
const rawEventSpoolMaxEntries = parsePositiveInt(
|
|
2042
|
+
process.env.CODEMEM_RAW_EVENT_SPOOL_MAX_ENTRIES,
|
|
2043
|
+
DEFAULT_MAX_ENTRIES,
|
|
2044
|
+
);
|
|
2045
|
+
const rawEventSpoolHome = process.env.HOME?.trim() || homedir();
|
|
2046
|
+
const rawEventSpoolDirectory = resolveSpoolDirectory(rawEventSpoolHome);
|
|
2047
|
+
// Memoize the exact serialized envelope by queued-object identity so retries
|
|
2048
|
+
// cannot drift in timestamp, ID, property order, or bytes.
|
|
2049
|
+
const rawEventEnvelopes = new WeakMap();
|
|
1867
2050
|
let streamUnavailableUntil = 0;
|
|
1868
|
-
let
|
|
1869
|
-
let
|
|
2051
|
+
let spoolPersistenceFailureNoted = null;
|
|
2052
|
+
let spoolLoadFailureNoted = false;
|
|
1870
2053
|
let lastStatusCheckAt = 0;
|
|
1871
2054
|
let lastStatusAvailable = true;
|
|
1872
2055
|
let promptPackTransportUnavailableUntil = 0;
|
|
@@ -1878,9 +2061,9 @@ export const CodememPlugin = async ({
|
|
|
1878
2061
|
return `${Date.now()}-${Math.random()}`;
|
|
1879
2062
|
};
|
|
1880
2063
|
|
|
1881
|
-
const queueRawEventViaCli = async (body) => {
|
|
2064
|
+
const queueRawEventViaCli = async (body, serialized = JSON.stringify(body)) => {
|
|
1882
2065
|
const runFallback = () => runCli(["enqueue-raw-event"], {
|
|
1883
|
-
stdinText:
|
|
2066
|
+
stdinText: serialized,
|
|
1884
2067
|
});
|
|
1885
2068
|
let result = await runFallback();
|
|
1886
2069
|
let classification = classifyFallbackCommandResult(result);
|
|
@@ -1895,23 +2078,191 @@ export const CodememPlugin = async ({
|
|
|
1895
2078
|
const error = new Error(
|
|
1896
2079
|
retryExhausted ? `${classification.cause} after retry` : classification.cause
|
|
1897
2080
|
);
|
|
1898
|
-
error.retryable = classification.retryable
|
|
2081
|
+
error.retryable = classification.retryable;
|
|
1899
2082
|
throw error;
|
|
1900
2083
|
}
|
|
1901
2084
|
return true;
|
|
1902
2085
|
};
|
|
1903
2086
|
|
|
1904
2087
|
const lastToastAtBySession = new Map();
|
|
1905
|
-
const shouldToast = (sessionID) => {
|
|
2088
|
+
const shouldToast = (sessionID, category = "general") => {
|
|
1906
2089
|
const now = Date.now();
|
|
1907
|
-
const
|
|
2090
|
+
const key = `${sessionID || "unknown"}:${category}`;
|
|
2091
|
+
const last = lastToastAtBySession.get(key) || 0;
|
|
1908
2092
|
if (now - last < 60000) {
|
|
1909
2093
|
return false;
|
|
1910
2094
|
}
|
|
1911
|
-
lastToastAtBySession.set(
|
|
2095
|
+
lastToastAtBySession.set(key, now);
|
|
1912
2096
|
return true;
|
|
1913
2097
|
};
|
|
1914
2098
|
|
|
2099
|
+
const warnSpoolPersistenceFailure = async (sessionID, reason) => {
|
|
2100
|
+
const message = reason === "spool_full"
|
|
2101
|
+
? "codemem raw-event retry spool is full; repair Codemem, then archive retained entries"
|
|
2102
|
+
: "codemem could not save a raw event for retry; it remains queued in memory";
|
|
2103
|
+
try {
|
|
2104
|
+
await client.app.log({
|
|
2105
|
+
service: "codemem",
|
|
2106
|
+
level: "error",
|
|
2107
|
+
message,
|
|
2108
|
+
extra: { category: "persistence", reason },
|
|
2109
|
+
});
|
|
2110
|
+
} catch {
|
|
2111
|
+
// Best-effort app logging only.
|
|
2112
|
+
}
|
|
2113
|
+
if (spoolPersistenceFailureNoted === reason) return;
|
|
2114
|
+
spoolPersistenceFailureNoted = reason;
|
|
2115
|
+
if (!client.tui?.showToast || !shouldToast(sessionID, `persistence:${reason}`)) return;
|
|
2116
|
+
try {
|
|
2117
|
+
await client.tui.showToast({
|
|
2118
|
+
body: { message: `codemem: ${message}`, variant: "error" },
|
|
2119
|
+
});
|
|
2120
|
+
} catch {
|
|
2121
|
+
// Best-effort toast only.
|
|
2122
|
+
}
|
|
2123
|
+
};
|
|
2124
|
+
|
|
2125
|
+
const persistRawEventForRetry = async ({ body, serialized, payload, sessionID }) => {
|
|
2126
|
+
try {
|
|
2127
|
+
await writeRawEventSpoolEntry({
|
|
2128
|
+
envelope: body,
|
|
2129
|
+
serialized,
|
|
2130
|
+
homeDir: rawEventSpoolHome,
|
|
2131
|
+
maxEntries: rawEventSpoolMaxEntries,
|
|
2132
|
+
});
|
|
2133
|
+
spoolPersistenceFailureNoted = null;
|
|
2134
|
+
if (payload && typeof payload === "object") {
|
|
2135
|
+
payload._raw_spooled = true;
|
|
2136
|
+
}
|
|
2137
|
+
return true;
|
|
2138
|
+
} catch (error) {
|
|
2139
|
+
const reason = error?.code === RAW_EVENT_SPOOL_FULL_CODE ? "spool_full" : "write_failed";
|
|
2140
|
+
await logLine(`raw_events.spool.${reason} category=persistence`);
|
|
2141
|
+
await warnSpoolPersistenceFailure(sessionID, reason);
|
|
2142
|
+
return false;
|
|
2143
|
+
}
|
|
2144
|
+
};
|
|
2145
|
+
|
|
2146
|
+
const removeRawEventFromSpool = async ({ eventId, payload }) => {
|
|
2147
|
+
try {
|
|
2148
|
+
await removeRawEventSpoolEntry({ eventId, homeDir: rawEventSpoolHome });
|
|
2149
|
+
if (payload && typeof payload === "object") {
|
|
2150
|
+
payload._raw_spooled = false;
|
|
2151
|
+
}
|
|
2152
|
+
} catch {
|
|
2153
|
+
await logLine("raw_events.spool.cleanup_failed category=persistence");
|
|
2154
|
+
}
|
|
2155
|
+
};
|
|
2156
|
+
|
|
2157
|
+
const notifyRawEventDelivery = async ({ category, delivered, durable, sessionID }) => {
|
|
2158
|
+
const action = RAW_EVENT_FAILURE_ACTIONS[category] || RAW_EVENT_FAILURE_ACTIONS.connection;
|
|
2159
|
+
let outcome = "was saved for retry";
|
|
2160
|
+
if (delivered) {
|
|
2161
|
+
outcome = "was queued via CLI";
|
|
2162
|
+
} else if (!durable) {
|
|
2163
|
+
outcome = "was not saved for retry; still queued in memory";
|
|
2164
|
+
}
|
|
2165
|
+
const message = `codemem raw event ${outcome}; ${action}`;
|
|
2166
|
+
let delivery = "memory";
|
|
2167
|
+
if (delivered) {
|
|
2168
|
+
delivery = "cli";
|
|
2169
|
+
} else if (durable) {
|
|
2170
|
+
delivery = "spool";
|
|
2171
|
+
}
|
|
2172
|
+
try {
|
|
2173
|
+
await client.app.log({
|
|
2174
|
+
service: "codemem",
|
|
2175
|
+
level: delivered ? "warn" : "error",
|
|
2176
|
+
message,
|
|
2177
|
+
extra: {
|
|
2178
|
+
category,
|
|
2179
|
+
delivery,
|
|
2180
|
+
},
|
|
2181
|
+
});
|
|
2182
|
+
} catch {
|
|
2183
|
+
// Best-effort app logging only.
|
|
2184
|
+
}
|
|
2185
|
+
if (!client.tui?.showToast || !shouldToast(sessionID, `${category}:${delivery}`)) return;
|
|
2186
|
+
try {
|
|
2187
|
+
await client.tui.showToast({
|
|
2188
|
+
body: { message: `codemem: ${message}`, variant: delivered ? "warning" : "error" },
|
|
2189
|
+
});
|
|
2190
|
+
} catch {
|
|
2191
|
+
// Best-effort toast only.
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
2194
|
+
|
|
2195
|
+
const drainRawEventSpool = () => {
|
|
2196
|
+
if (!rawEventsEnabled) {
|
|
2197
|
+
return Promise.resolve();
|
|
2198
|
+
}
|
|
2199
|
+
const existingDrain = rawEventSpoolDrainsInFlight.get(rawEventSpoolDirectory);
|
|
2200
|
+
if (existingDrain) {
|
|
2201
|
+
return existingDrain;
|
|
2202
|
+
}
|
|
2203
|
+
const drainPromise = (async () => {
|
|
2204
|
+
let loaded;
|
|
2205
|
+
try {
|
|
2206
|
+
loaded = await loadRawEventSpoolEntries({
|
|
2207
|
+
homeDir: rawEventSpoolHome,
|
|
2208
|
+
limit: rawEventSpoolDrainLimit,
|
|
2209
|
+
});
|
|
2210
|
+
} catch {
|
|
2211
|
+
await logLine("raw_events.spool.load_failed category=persistence");
|
|
2212
|
+
const message = "codemem could not read saved raw events; spool entries were left untouched";
|
|
2213
|
+
try {
|
|
2214
|
+
await client.app.log({
|
|
2215
|
+
service: "codemem",
|
|
2216
|
+
level: "error",
|
|
2217
|
+
message,
|
|
2218
|
+
extra: { category: "persistence", delivery: "load" },
|
|
2219
|
+
});
|
|
2220
|
+
} catch {
|
|
2221
|
+
// Best-effort app logging only.
|
|
2222
|
+
}
|
|
2223
|
+
if (!spoolLoadFailureNoted) {
|
|
2224
|
+
spoolLoadFailureNoted = true;
|
|
2225
|
+
if (client.tui?.showToast && shouldToast(null, "persistence:load")) {
|
|
2226
|
+
try {
|
|
2227
|
+
await client.tui.showToast({
|
|
2228
|
+
body: { message: `codemem: ${message}`, variant: "error" },
|
|
2229
|
+
});
|
|
2230
|
+
} catch {
|
|
2231
|
+
// Best-effort toast only.
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
spoolLoadFailureNoted = false;
|
|
2238
|
+
if (loaded.corruptCount > 0) {
|
|
2239
|
+
await logLine(`raw_events.spool.corrupt_retained count=${loaded.corruptCount}`);
|
|
2240
|
+
}
|
|
2241
|
+
for (const entry of loaded.entries) {
|
|
2242
|
+
try {
|
|
2243
|
+
await queueRawEventViaCli(entry.envelope, entry.serialized);
|
|
2244
|
+
await removeRawEventFromSpool({ eventId: entry.eventId });
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
await logLine("raw_events.spool.drain_deferred category=fallback");
|
|
2247
|
+
if (error?.retryable === true) {
|
|
2248
|
+
break;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
})();
|
|
2253
|
+
const trackedDrain = drainPromise
|
|
2254
|
+
.catch(async () => {
|
|
2255
|
+
await logLine("raw_events.spool.drain_failed category=persistence");
|
|
2256
|
+
})
|
|
2257
|
+
.finally(() => {
|
|
2258
|
+
if (rawEventSpoolDrainsInFlight.get(rawEventSpoolDirectory) === trackedDrain) {
|
|
2259
|
+
rawEventSpoolDrainsInFlight.delete(rawEventSpoolDirectory);
|
|
2260
|
+
}
|
|
2261
|
+
});
|
|
2262
|
+
rawEventSpoolDrainsInFlight.set(rawEventSpoolDirectory, trackedDrain);
|
|
2263
|
+
return trackedDrain;
|
|
2264
|
+
};
|
|
2265
|
+
|
|
1915
2266
|
const buildViewerCliArgs = (action) => {
|
|
1916
2267
|
const args = ["serve", action, "--host", viewerHost, "--port", viewerPort];
|
|
1917
2268
|
if (String(viewerDbPath || "").trim()) {
|
|
@@ -1931,66 +2282,54 @@ export const CodememPlugin = async ({
|
|
|
1931
2282
|
return false;
|
|
1932
2283
|
}
|
|
1933
2284
|
const now = Date.now();
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2285
|
+
let cachedEnvelope = payload && typeof payload === "object"
|
|
2286
|
+
? rawEventEnvelopes.get(payload)
|
|
2287
|
+
: null;
|
|
2288
|
+
if (!cachedEnvelope) {
|
|
2289
|
+
const builtEnvelope = buildRawEventEnvelope({
|
|
2290
|
+
sessionID,
|
|
2291
|
+
type,
|
|
2292
|
+
payload,
|
|
2293
|
+
cwd,
|
|
2294
|
+
project: resolveProjectName(project, cwd),
|
|
2295
|
+
startedAt: sessionStartedAt,
|
|
2296
|
+
nowMs: now,
|
|
2297
|
+
nowMono:
|
|
2298
|
+
typeof performance !== "undefined" && performance.now
|
|
2299
|
+
? performance.now()
|
|
2300
|
+
: null,
|
|
2301
|
+
nextEventId,
|
|
2302
|
+
});
|
|
2303
|
+
const serialized = JSON.stringify(builtEnvelope);
|
|
2304
|
+
cachedEnvelope = { body: JSON.parse(serialized), serialized };
|
|
2305
|
+
if (payload && typeof payload === "object") {
|
|
2306
|
+
rawEventEnvelopes.set(payload, cachedEnvelope);
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
const { body, serialized } = cachedEnvelope;
|
|
1949
2310
|
if (now < streamUnavailableUntil) {
|
|
2311
|
+
const durable = await persistRawEventForRetry({ body, serialized, payload, sessionID });
|
|
1950
2312
|
try {
|
|
1951
|
-
await queueRawEventViaCli(body);
|
|
1952
|
-
|
|
2313
|
+
await queueRawEventViaCli(body, serialized);
|
|
2314
|
+
await removeRawEventFromSpool({ eventId: body.event_id, payload });
|
|
1953
2315
|
if (payload && typeof payload === "object") {
|
|
1954
2316
|
payload._raw_enqueued = true;
|
|
1955
2317
|
}
|
|
2318
|
+
await notifyRawEventDelivery({
|
|
2319
|
+
category: "connection",
|
|
2320
|
+
delivered: true,
|
|
2321
|
+
durable,
|
|
2322
|
+
sessionID,
|
|
2323
|
+
});
|
|
1956
2324
|
return true;
|
|
1957
|
-
} catch
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
);
|
|
1966
|
-
if (!fallbackFailureNoted) {
|
|
1967
|
-
fallbackFailureNoted = true;
|
|
1968
|
-
try {
|
|
1969
|
-
await client.app.log({
|
|
1970
|
-
service: "codemem",
|
|
1971
|
-
level: "error",
|
|
1972
|
-
message: "codemem fallback enqueue failed during stream backoff",
|
|
1973
|
-
extra: {
|
|
1974
|
-
sessionID,
|
|
1975
|
-
backoffMs: rawEventsBackoffMs,
|
|
1976
|
-
},
|
|
1977
|
-
});
|
|
1978
|
-
} catch (logErr) {
|
|
1979
|
-
// best-effort logging only
|
|
1980
|
-
}
|
|
1981
|
-
if (client.tui?.showToast && shouldToast(sessionID)) {
|
|
1982
|
-
try {
|
|
1983
|
-
await client.tui.showToast({
|
|
1984
|
-
body: {
|
|
1985
|
-
message: "codemem: fallback enqueue failed while stream is down",
|
|
1986
|
-
variant: "error",
|
|
1987
|
-
},
|
|
1988
|
-
});
|
|
1989
|
-
} catch (toastErr) {
|
|
1990
|
-
// best-effort only
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
2325
|
+
} catch {
|
|
2326
|
+
await logLine("raw_events.fallback.error category=connection");
|
|
2327
|
+
await notifyRawEventDelivery({
|
|
2328
|
+
category: "connection",
|
|
2329
|
+
delivered: false,
|
|
2330
|
+
durable,
|
|
2331
|
+
sessionID,
|
|
2332
|
+
});
|
|
1994
2333
|
return false;
|
|
1995
2334
|
}
|
|
1996
2335
|
}
|
|
@@ -2021,130 +2360,46 @@ export const CodememPlugin = async ({
|
|
|
2021
2360
|
});
|
|
2022
2361
|
if (!postResp.ok) {
|
|
2023
2362
|
let responseBody = null;
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
// Generic failure handling below remains the safe fallback.
|
|
2029
|
-
}
|
|
2363
|
+
try {
|
|
2364
|
+
responseBody = await postResp.json();
|
|
2365
|
+
} catch {
|
|
2366
|
+
// Generic connection guidance below remains the safe fallback.
|
|
2030
2367
|
}
|
|
2031
|
-
const targetMismatch = isViewerDbMismatchPayload(responseBody)
|
|
2032
|
-
|| isViewerIdentityMismatchPayload(responseBody)
|
|
2033
|
-
|| isViewerContractUnsupportedPayload(responseBody);
|
|
2034
2368
|
const postError = new Error(`raw-events post failed (${postResp.status})`);
|
|
2035
|
-
postError.
|
|
2369
|
+
postError.rawEventFailureCategory = classifyRawEventViewerFailure(responseBody);
|
|
2036
2370
|
throw postError;
|
|
2037
2371
|
}
|
|
2038
2372
|
streamUnavailableUntil = 0;
|
|
2039
|
-
streamErrorNoted = false;
|
|
2040
|
-
fallbackFailureNoted = false;
|
|
2041
2373
|
lastStatusAvailable = true;
|
|
2374
|
+
await removeRawEventFromSpool({ eventId: body.event_id, payload });
|
|
2042
2375
|
if (payload && typeof payload === "object") {
|
|
2043
2376
|
payload._raw_enqueued = true;
|
|
2044
2377
|
}
|
|
2045
2378
|
return true;
|
|
2046
2379
|
} catch (err) {
|
|
2380
|
+
const category = err?.rawEventFailureCategory || "connection";
|
|
2047
2381
|
streamUnavailableUntil = Date.now() + Math.max(1000, rawEventsBackoffMs);
|
|
2048
|
-
await logLine(`raw_events.error
|
|
2049
|
-
|
|
2050
|
-
await client.app.log({
|
|
2051
|
-
service: "codemem",
|
|
2052
|
-
level: "error",
|
|
2053
|
-
message: "Failed to stream raw events to codemem viewer",
|
|
2054
|
-
extra: {
|
|
2055
|
-
sessionID,
|
|
2056
|
-
type,
|
|
2057
|
-
viewerHost,
|
|
2058
|
-
viewerPort,
|
|
2059
|
-
viewerTargetMismatch: err?.viewerTargetMismatch === true,
|
|
2060
|
-
error: String(err),
|
|
2061
|
-
},
|
|
2062
|
-
});
|
|
2063
|
-
} catch (logErr) {
|
|
2064
|
-
// best-effort logging only
|
|
2065
|
-
}
|
|
2382
|
+
await logLine(`raw_events.error category=${category}`);
|
|
2383
|
+
const durable = await persistRawEventForRetry({ body, serialized, payload, sessionID });
|
|
2066
2384
|
|
|
2067
2385
|
let fallbackOk = false;
|
|
2068
2386
|
try {
|
|
2069
|
-
await queueRawEventViaCli(body);
|
|
2387
|
+
await queueRawEventViaCli(body, serialized);
|
|
2388
|
+
await removeRawEventFromSpool({ eventId: body.event_id, payload });
|
|
2070
2389
|
fallbackOk = true;
|
|
2071
|
-
} catch
|
|
2072
|
-
|
|
2073
|
-
payload._raw_fallback_terminal = fallbackErr?.retryable !== true;
|
|
2074
|
-
}
|
|
2075
|
-
await logLine(
|
|
2076
|
-
`raw_events.fallback.error sessionID=${sessionID} type=${type} err=${String(
|
|
2077
|
-
fallbackErr
|
|
2078
|
-
).slice(0, 200)}`
|
|
2079
|
-
);
|
|
2390
|
+
} catch {
|
|
2391
|
+
await logLine(`raw_events.fallback.error category=${category}`);
|
|
2080
2392
|
}
|
|
2081
2393
|
|
|
2082
2394
|
if (fallbackOk) {
|
|
2083
|
-
fallbackFailureNoted = false;
|
|
2084
2395
|
if (payload && typeof payload === "object") {
|
|
2085
2396
|
payload._raw_enqueued = true;
|
|
2086
2397
|
}
|
|
2087
|
-
|
|
2088
|
-
streamErrorNoted = true;
|
|
2089
|
-
try {
|
|
2090
|
-
await client.app.log({
|
|
2091
|
-
service: "codemem",
|
|
2092
|
-
level: "warn",
|
|
2093
|
-
message: "codemem stream unavailable; queued raw event via CLI fallback",
|
|
2094
|
-
extra: {
|
|
2095
|
-
sessionID,
|
|
2096
|
-
backoffMs: rawEventsBackoffMs,
|
|
2097
|
-
},
|
|
2098
|
-
});
|
|
2099
|
-
} catch (logErr) {
|
|
2100
|
-
// best-effort logging only
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
2103
|
-
if (client.tui?.showToast && shouldToast(sessionID)) {
|
|
2104
|
-
try {
|
|
2105
|
-
await client.tui.showToast({
|
|
2106
|
-
body: {
|
|
2107
|
-
message: "codemem: viewer stream unavailable; queue fallback active",
|
|
2108
|
-
variant: "warning",
|
|
2109
|
-
},
|
|
2110
|
-
});
|
|
2111
|
-
} catch (toastErr) {
|
|
2112
|
-
// best-effort only
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2398
|
+
await notifyRawEventDelivery({ category, delivered: true, durable, sessionID });
|
|
2115
2399
|
return true;
|
|
2116
2400
|
}
|
|
2117
2401
|
|
|
2118
|
-
|
|
2119
|
-
streamErrorNoted = true;
|
|
2120
|
-
fallbackFailureNoted = true;
|
|
2121
|
-
try {
|
|
2122
|
-
await client.app.log({
|
|
2123
|
-
service: "codemem",
|
|
2124
|
-
level: "error",
|
|
2125
|
-
message: "codemem stream unavailable; fallback enqueue failed",
|
|
2126
|
-
extra: {
|
|
2127
|
-
sessionID,
|
|
2128
|
-
backoffMs: rawEventsBackoffMs,
|
|
2129
|
-
},
|
|
2130
|
-
});
|
|
2131
|
-
} catch (logErr) {
|
|
2132
|
-
// best-effort logging only
|
|
2133
|
-
}
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
if (client.tui?.showToast && shouldToast(sessionID)) {
|
|
2137
|
-
try {
|
|
2138
|
-
await client.tui.showToast({
|
|
2139
|
-
body: {
|
|
2140
|
-
message: `codemem: stream unavailable (${viewerHost}:${viewerPort}); fallback failed`,
|
|
2141
|
-
variant: "error",
|
|
2142
|
-
},
|
|
2143
|
-
});
|
|
2144
|
-
} catch (toastErr) {
|
|
2145
|
-
// best-effort only
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2402
|
+
await notifyRawEventDelivery({ category, delivered: false, durable, sessionID });
|
|
2148
2403
|
return false;
|
|
2149
2404
|
}
|
|
2150
2405
|
};
|
|
@@ -2421,12 +2676,16 @@ export const CodememPlugin = async ({
|
|
|
2421
2676
|
};
|
|
2422
2677
|
|
|
2423
2678
|
const runCommand = async (cmd, options = {}) => {
|
|
2424
|
-
const {
|
|
2679
|
+
const {
|
|
2680
|
+
env = process.env,
|
|
2681
|
+
stdinText = null,
|
|
2682
|
+
timeoutMs = commandTimeout,
|
|
2683
|
+
} = options;
|
|
2425
2684
|
const [command, ...args] = cmd;
|
|
2426
2685
|
return new Promise((resolve) => {
|
|
2427
2686
|
const proc = nodeSpawn(command, args, {
|
|
2428
2687
|
cwd,
|
|
2429
|
-
env
|
|
2688
|
+
env,
|
|
2430
2689
|
stdio: ["pipe", "pipe", "pipe"],
|
|
2431
2690
|
});
|
|
2432
2691
|
let stdout = "";
|
|
@@ -2847,11 +3106,36 @@ export const CodememPlugin = async ({
|
|
|
2847
3106
|
|
|
2848
3107
|
if (backendUpdatePolicy === "auto") {
|
|
2849
3108
|
compatibilityAutoUpdateAttempted = true;
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
["update", "install", "--json"],
|
|
2853
|
-
{ timeoutMs: 420_000 }
|
|
3109
|
+
const notification = parseReleaseNotification(
|
|
3110
|
+
await runCli(["update", "check", "--json"])
|
|
2854
3111
|
);
|
|
3112
|
+
if (!notification?.autoUpdateEligible) {
|
|
3113
|
+
await logLine("compat.auto_update_skipped reason=release_not_eligible");
|
|
3114
|
+
await showToast(
|
|
3115
|
+
`${message}. Auto-update skipped (not eligible). Suggested action: ${guidance.action}`,
|
|
3116
|
+
"warning"
|
|
3117
|
+
);
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
const autoPlan = resolveAutoUpdatePlan({
|
|
3121
|
+
runner,
|
|
3122
|
+
runnerFrom,
|
|
3123
|
+
runnerFromExplicit,
|
|
3124
|
+
targetVersion: notification.latestVersion,
|
|
3125
|
+
});
|
|
3126
|
+
if (!autoPlan.allowed) {
|
|
3127
|
+
await logLine(`compat.auto_update_skipped reason=${autoPlan.reason || "not_eligible"}`);
|
|
3128
|
+
await showToast(
|
|
3129
|
+
`${message}. Auto-update skipped (not eligible). Suggested action: ${guidance.action}`,
|
|
3130
|
+
"warning"
|
|
3131
|
+
);
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
await logLine(`compat.auto_update_start cmd=${autoPlan.commandText}`);
|
|
3135
|
+
const updateResult = await runCommand(autoPlan.command, {
|
|
3136
|
+
env: resolveAutoUpdateEnvironment(),
|
|
3137
|
+
timeoutMs: 480_000,
|
|
3138
|
+
});
|
|
2855
3139
|
if (updateResult?.exitCode === 0) {
|
|
2856
3140
|
await logLine(
|
|
2857
3141
|
`compat.auto_update_result exit=${updateResult?.exitCode ?? "unknown"} stderr=${redactLog(
|
|
@@ -2863,6 +3147,7 @@ export const CodememPlugin = async ({
|
|
|
2863
3147
|
const refreshedVersion = (refreshedResult?.stdout || "").trim();
|
|
2864
3148
|
if (
|
|
2865
3149
|
refreshedResult?.exitCode === 0
|
|
3150
|
+
&& refreshedVersion === notification.latestVersion
|
|
2866
3151
|
&& isVersionAtLeast(refreshedVersion, minVersion)
|
|
2867
3152
|
) {
|
|
2868
3153
|
writeCompatCheckCache(cacheKey, refreshedVersion);
|
|
@@ -2891,22 +3176,11 @@ export const CodememPlugin = async ({
|
|
|
2891
3176
|
);
|
|
2892
3177
|
return;
|
|
2893
3178
|
}
|
|
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";
|
|
2905
3179
|
await logLine(
|
|
2906
|
-
`compat.auto_update_skipped reason
|
|
3180
|
+
`compat.auto_update_skipped reason=update_install_failed exit=${updateResult?.exitCode ?? "unknown"} stderr=${redactLog((updateResult?.stderr || "").trim())}`
|
|
2907
3181
|
);
|
|
2908
3182
|
await showToast(
|
|
2909
|
-
`${message}. Auto-update skipped (
|
|
3183
|
+
`${message}. Auto-update skipped (installation failed). Suggested action: ${guidance.action}`,
|
|
2910
3184
|
"warning"
|
|
2911
3185
|
);
|
|
2912
3186
|
return;
|
|
@@ -2916,7 +3190,7 @@ export const CodememPlugin = async ({
|
|
|
2916
3190
|
};
|
|
2917
3191
|
|
|
2918
3192
|
const checkForReleaseUpdate = async () => {
|
|
2919
|
-
if (backendUpdatePolicy === "off") return;
|
|
3193
|
+
if (backendUpdatePolicy === "off" || compatibilityAutoUpdateAttempted) return;
|
|
2920
3194
|
const notification = parseReleaseNotification(
|
|
2921
3195
|
await runCli(["update", "check", "--json"])
|
|
2922
3196
|
);
|
|
@@ -2932,13 +3206,31 @@ export const CodememPlugin = async ({
|
|
|
2932
3206
|
&& notification.autoUpdateEligible
|
|
2933
3207
|
&& !compatibilityAutoUpdateAttempted
|
|
2934
3208
|
) {
|
|
2935
|
-
const autoPlan = resolveAutoUpdatePlan({
|
|
3209
|
+
const autoPlan = resolveAutoUpdatePlan({
|
|
3210
|
+
runner,
|
|
3211
|
+
runnerFrom,
|
|
3212
|
+
runnerFromExplicit,
|
|
3213
|
+
targetVersion: notification.latestVersion,
|
|
3214
|
+
});
|
|
2936
3215
|
if (autoPlan.allowed) {
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
3216
|
+
await logLine(`release.auto_update_start cmd=${autoPlan.commandText}`);
|
|
3217
|
+
const installation = await runCommand(autoPlan.command, {
|
|
3218
|
+
env: resolveAutoUpdateEnvironment(),
|
|
3219
|
+
timeoutMs: 480_000,
|
|
3220
|
+
});
|
|
2941
3221
|
if (installation?.exitCode === 0) {
|
|
3222
|
+
const verification = await runCli(["version"]);
|
|
3223
|
+
const installedVersion = (verification?.stdout || "").trim();
|
|
3224
|
+
if (verification?.exitCode !== 0 || installedVersion !== notification.latestVersion) {
|
|
3225
|
+
await logLine(
|
|
3226
|
+
`release.auto_update_verification_failed current=${redactLog(installedVersion || "unknown")} expected=${notification.latestVersion}`
|
|
3227
|
+
);
|
|
3228
|
+
await showToast(
|
|
3229
|
+
`codemem ${notification.latestVersion} was installed, but the active CLI failed verification.`,
|
|
3230
|
+
"warning"
|
|
3231
|
+
);
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
2942
3234
|
const viewerRestart = await restartViewerAfterAutoUpdate();
|
|
2943
3235
|
await showToast(`Updated codemem to ${notification.latestVersion}.`, "success");
|
|
2944
3236
|
if (viewerRestart.attempted && !viewerRestart.ok) {
|
|
@@ -3479,9 +3771,17 @@ export const CodememPlugin = async ({
|
|
|
3479
3771
|
void logLine(`queue.pressure unsent_preserved queued=${queuedCount} max_events=${cap}`);
|
|
3480
3772
|
},
|
|
3481
3773
|
onForcedDrop: (dropped, queuedCount, hardCap) => {
|
|
3774
|
+
const durableSpoolCoverage = dropped?._raw_spooled === true;
|
|
3482
3775
|
void logLine(
|
|
3483
|
-
`queue.drop hard_cap
|
|
3776
|
+
`queue.drop hard_cap durable_spool_coverage=${durableSpoolCoverage} queued=${queuedCount} hard_max=${hardCap}`
|
|
3484
3777
|
);
|
|
3778
|
+
void log("error", "codemem raw event dropped at in-memory hard cap", {
|
|
3779
|
+
category: "queue_capacity",
|
|
3780
|
+
delivery: durableSpoolCoverage ? "spool" : "memory",
|
|
3781
|
+
durable_spool_coverage: durableSpoolCoverage,
|
|
3782
|
+
queued_count: queuedCount,
|
|
3783
|
+
hard_max_events: hardCap,
|
|
3784
|
+
});
|
|
3485
3785
|
},
|
|
3486
3786
|
});
|
|
3487
3787
|
};
|
|
@@ -3511,7 +3811,6 @@ export const CodememPlugin = async ({
|
|
|
3511
3811
|
...adapterAnnotatedEvent,
|
|
3512
3812
|
_raw_event_id: rawEventId,
|
|
3513
3813
|
_raw_session_id: resolvedSessionID,
|
|
3514
|
-
_raw_retry_count: 0,
|
|
3515
3814
|
};
|
|
3516
3815
|
recordEvent(queuedEvent);
|
|
3517
3816
|
void emitRawEvent({
|
|
@@ -3523,6 +3822,7 @@ export const CodememPlugin = async ({
|
|
|
3523
3822
|
|
|
3524
3823
|
const flushEvents = async () => {
|
|
3525
3824
|
if (!events.length) {
|
|
3825
|
+
await drainRawEventSpool();
|
|
3526
3826
|
await logLine("flush.skip empty");
|
|
3527
3827
|
return;
|
|
3528
3828
|
}
|
|
@@ -3534,18 +3834,10 @@ export const CodememPlugin = async ({
|
|
|
3534
3834
|
}
|
|
3535
3835
|
|
|
3536
3836
|
const failed = [];
|
|
3537
|
-
let droppedCount = 0;
|
|
3538
3837
|
for (const queuedEvent of batch) {
|
|
3539
3838
|
if (queuedEvent && typeof queuedEvent === "object" && queuedEvent._raw_enqueued) {
|
|
3540
3839
|
continue;
|
|
3541
3840
|
}
|
|
3542
|
-
if (queuedEvent && typeof queuedEvent === "object" && queuedEvent._raw_fallback_terminal) {
|
|
3543
|
-
droppedCount += 1;
|
|
3544
|
-
await logLine(
|
|
3545
|
-
`flush.drop terminal_fallback event_id=${queuedEvent?._raw_event_id || "unknown"}`
|
|
3546
|
-
);
|
|
3547
|
-
continue;
|
|
3548
|
-
}
|
|
3549
3841
|
const queuedSessionID =
|
|
3550
3842
|
queuedEvent?._raw_session_id ||
|
|
3551
3843
|
queuedEvent?.properties?.sessionID ||
|
|
@@ -3556,27 +3848,13 @@ export const CodememPlugin = async ({
|
|
|
3556
3848
|
payload: queuedEvent,
|
|
3557
3849
|
});
|
|
3558
3850
|
if (!ok) {
|
|
3559
|
-
|
|
3560
|
-
droppedCount += 1;
|
|
3561
|
-
await logLine(
|
|
3562
|
-
`flush.drop terminal_fallback event_id=${queuedEvent?._raw_event_id || "unknown"}`
|
|
3563
|
-
);
|
|
3564
|
-
continue;
|
|
3565
|
-
}
|
|
3566
|
-
const currentRetry =
|
|
3567
|
-
typeof queuedEvent?._raw_retry_count === "number" && Number.isFinite(queuedEvent._raw_retry_count)
|
|
3568
|
-
? queuedEvent._raw_retry_count
|
|
3569
|
-
: 0;
|
|
3570
|
-
const nextRetry = currentRetry + 1;
|
|
3571
|
-
failed.push({
|
|
3572
|
-
...queuedEvent,
|
|
3573
|
-
_raw_retry_count: nextRetry,
|
|
3574
|
-
});
|
|
3851
|
+
failed.push(queuedEvent);
|
|
3575
3852
|
}
|
|
3576
3853
|
}
|
|
3577
3854
|
if (failed.length) {
|
|
3578
3855
|
events.unshift(...failed);
|
|
3579
3856
|
await logLine(`flush.retry_deferred count=${failed.length}`);
|
|
3857
|
+
await drainRawEventSpool();
|
|
3580
3858
|
return;
|
|
3581
3859
|
}
|
|
3582
3860
|
|
|
@@ -3587,12 +3865,20 @@ export const CodememPlugin = async ({
|
|
|
3587
3865
|
await logLine(
|
|
3588
3866
|
`flush.stream_only finalize count=${batch.length} tools=${sessionContext.toolCount} prompts=${sessionContext.promptCount} duration=${Math.round(durationMs / 1000)}s`
|
|
3589
3867
|
);
|
|
3590
|
-
await logLine(`flush.ok count=${batch.length
|
|
3868
|
+
await logLine(`flush.ok count=${batch.length} dropped=0`);
|
|
3591
3869
|
sessionStartedAt = null;
|
|
3592
3870
|
resetSessionContext();
|
|
3871
|
+
await drainRawEventSpool();
|
|
3593
3872
|
};
|
|
3594
3873
|
|
|
3874
|
+
void drainRawEventSpool();
|
|
3875
|
+
|
|
3595
3876
|
return {
|
|
3877
|
+
dispose: () => {
|
|
3878
|
+
clearTimeout(updateCheckTimer);
|
|
3879
|
+
stopHealthCheck();
|
|
3880
|
+
releasePluginRegistration();
|
|
3881
|
+
},
|
|
3596
3882
|
"experimental.session.compacting": async (input) => {
|
|
3597
3883
|
const sessionID = extractHookSessionID(input) || activeSessionID;
|
|
3598
3884
|
markCompactionInjectionSkip(compactionInjectionSkips, sessionID);
|
|
@@ -3908,6 +4194,11 @@ export const CodememPlugin = async ({
|
|
|
3908
4194
|
if (eventType === "session.deleted") {
|
|
3909
4195
|
activeSessionID = null;
|
|
3910
4196
|
if (sessionID) {
|
|
4197
|
+
for (const key of lastToastAtBySession.keys()) {
|
|
4198
|
+
if (key.startsWith(`${sessionID}:`)) {
|
|
4199
|
+
lastToastAtBySession.delete(key);
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
3911
4202
|
injectionToastShown.delete(sessionID);
|
|
3912
4203
|
messageInjectionCache.delete(sessionID);
|
|
3913
4204
|
compactionInjectionSkips.delete(sessionID);
|
|
@@ -4034,6 +4325,8 @@ export const __testUtils = {
|
|
|
4034
4325
|
redactPackCommand,
|
|
4035
4326
|
rejectsInternalLedgerFlag,
|
|
4036
4327
|
classifyFallbackCommandResult,
|
|
4328
|
+
classifyRawEventViewerFailure,
|
|
4329
|
+
detectRunner,
|
|
4037
4330
|
PROMPT_TRANSPORT_PROTOCOL_RANGE,
|
|
4038
4331
|
normalizePromptTransportProtocolRange,
|
|
4039
4332
|
arePromptTransportProtocolRangesCompatible,
|