@codemem/opencode-plugin 0.41.0 → 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.
@@ -16,7 +16,7 @@ import {
16
16
 
17
17
  const TRUTHY_VALUES = ["1", "true", "yes"];
18
18
  const DISABLED_VALUES = ["0", "false", "off"];
19
- const PINNED_BACKEND_VERSION = "0.41.0";
19
+ const PINNED_BACKEND_VERSION = "0.42.0";
20
20
  const COMPAT_CHECK_DELAY_MS = 1500;
21
21
  const COMPAT_CHECK_CACHE_TTL_MS = 5 * 60 * 1000;
22
22
  const MAX_UPDATE_STATUS_BYTES = 16 * 1024;
@@ -623,6 +623,31 @@ const parsePackMetrics = (stdout) => {
623
623
  }
624
624
  };
625
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
+
626
651
  const parsePackOutput = (result) => {
627
652
  const succeeded = result?.exitCode === 0;
628
653
  let payload = null;
@@ -709,8 +734,67 @@ const isViewerContractUnsupportedPayload = (payload) =>
709
734
  const isViewerInvalidRequestPayload = (payload) =>
710
735
  isRecord(payload)
711
736
  && isRecord(payload.error)
712
- && payload.error.code === "invalid_request"
713
- && typeof payload.error.message === "string";
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
+ });
714
798
 
715
799
  const classifyViewerHttpFailure = ({
716
800
  operation,
@@ -718,31 +802,60 @@ const classifyViewerHttpFailure = ({
718
802
  error = null,
719
803
  malformed = false,
720
804
  body = null,
805
+ compatibleProfile = false,
721
806
  }) => {
722
807
  if (malformed) {
723
- return { cause: `${operation} returned malformed success`, retryable: true };
808
+ return viewerFailureClassification(
809
+ `${operation} returned malformed success`,
810
+ classifyPromptTransportFailure({ kind: "malformed_response" }),
811
+ );
724
812
  }
725
- if (
726
- isViewerDbMismatchPayload(body)
727
- || isViewerIdentityMismatchPayload(body)
728
- || isViewerContractUnsupportedPayload(body)
729
- ) {
730
- return { cause: `${operation} viewer profile mismatch`, retryable: true };
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
+ );
731
833
  }
732
834
  if (operation === "prompt-pack-ledger" && isValidLedgerFailureHttpPayload(body)) {
733
- return { cause: `${operation} request rejected (${status})`, retryable: false };
835
+ return viewerFailureClassification(`${operation} request rejected (${status})`, "terminal");
734
836
  }
735
- if (isViewerInvalidRequestPayload(body) && !operation.endsWith(" profile")) {
736
- return { cause: `${operation} request rejected (${status})`, retryable: false };
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
+ );
737
850
  }
738
851
  if (status === 404 || status === 405) {
739
- return { cause: `${operation} endpoint unavailable (${status})`, retryable: true };
852
+ return viewerFailureClassification(`${operation} endpoint unavailable (${status})`, "fallback");
740
853
  }
741
854
  if (Number.isInteger(status) && status >= 500) {
742
- return { cause: `${operation} server failure (${status})`, retryable: true };
855
+ return viewerFailureClassification(`${operation} server failure (${status})`, "fallback");
743
856
  }
744
857
  if (Number.isInteger(status)) {
745
- return { cause: `${operation} unexpected response (${status})`, retryable: true };
858
+ return viewerFailureClassification(`${operation} unexpected response (${status})`, "fallback");
746
859
  }
747
860
  const diagnostic = [
748
861
  error?.name,
@@ -752,10 +865,10 @@ const classifyViewerHttpFailure = ({
752
865
  error,
753
866
  ].filter(Boolean).join(" ");
754
867
  const timedOut = /AbortError|TimeoutError|timeout|timed out|ETIMEDOUT/i.test(diagnostic);
755
- return {
756
- cause: timedOut ? `${operation} request timeout` : `${operation} connection failed`,
757
- retryable: true,
758
- };
868
+ return viewerFailureClassification(
869
+ timedOut ? `${operation} request timeout` : `${operation} connection failed`,
870
+ "fallback",
871
+ );
759
872
  };
760
873
 
761
874
  const buildPackHttpBody = ({
@@ -1675,28 +1788,7 @@ export const CodememPlugin = async ({
1675
1788
  cwd,
1676
1789
  expandedViewerDbPath || join(homedir(), ".codemem", "mem.sqlite"),
1677
1790
  );
1678
- const normalizeIdentityPath = (value) => {
1679
- const trimmed = String(value || "").trim();
1680
- if (!trimmed) return null;
1681
- const expanded = trimmed.startsWith("~/")
1682
- ? join(process.env.HOME?.trim() || homedir(), trimmed.slice(2))
1683
- : trimmed;
1684
- return resolve(cwd, expanded);
1685
- };
1686
- const promptPackIdentityTarget = {
1687
- device_id: process.env.CODEMEM_DEVICE_ID?.trim() || null,
1688
- actor_id_present: Object.hasOwn(process.env, "CODEMEM_ACTOR_ID"),
1689
- actor_id: process.env.CODEMEM_ACTOR_ID?.trim() || null,
1690
- config_path: normalizeIdentityPath(process.env.CODEMEM_CONFIG),
1691
- runtime_root: normalizeIdentityPath(process.env.CODEMEM_RUNTIME_ROOT),
1692
- workspace_id: process.env.CODEMEM_WORKSPACE_ID?.trim() || null,
1693
- home_dir: normalizeIdentityPath(process.env.HOME || homedir()),
1694
- pack_compression: process.env.CODEMEM_PACK_COMPRESSION?.trim() || null,
1695
- embedding_disabled: ["1", "true", "yes"].includes(
1696
- String(process.env.CODEMEM_EMBEDDING_DISABLED || "").toLowerCase(),
1697
- ),
1698
- embedding_model: process.env.CODEMEM_EMBEDDING_MODEL || "Xenova/bge-small-en-v1.5",
1699
- };
1791
+ const promptPackIdentityTarget = buildViewerIdentityTarget(process.env, cwd);
1700
1792
  const viewerConfigPath = process.env.CODEMEM_CONFIG || "";
1701
1793
  // A malformed value (e.g. "abc", "", "-1") falls back to the 20s default
1702
1794
  // instead of NaN, which would silently disable the timeout. An explicit "0"
@@ -1921,10 +2013,27 @@ export const CodememPlugin = async ({
1921
2013
  const postResp = await fetch(rawEventsUrl, {
1922
2014
  method: "POST",
1923
2015
  headers: { "Content-Type": "application/json" },
1924
- body: JSON.stringify(body),
2016
+ body: JSON.stringify({
2017
+ ...body,
2018
+ db_path: promptPackDbPath,
2019
+ identity_target: promptPackIdentityTarget,
2020
+ }),
1925
2021
  });
1926
2022
  if (!postResp.ok) {
1927
- throw new Error(`raw-events post failed (${postResp.status})`);
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;
1928
2037
  }
1929
2038
  streamUnavailableUntil = 0;
1930
2039
  streamErrorNoted = false;
@@ -1947,6 +2056,7 @@ export const CodememPlugin = async ({
1947
2056
  type,
1948
2057
  viewerHost,
1949
2058
  viewerPort,
2059
+ viewerTargetMismatch: err?.viewerTargetMismatch === true,
1950
2060
  error: String(err),
1951
2061
  },
1952
2062
  });
@@ -2386,19 +2496,19 @@ export const CodememPlugin = async ({
2386
2496
  if (!viewerEnabled) {
2387
2497
  return {
2388
2498
  ok: false,
2389
- classification: {
2390
- cause: `${operation} viewer transport disabled`,
2391
- retryable: true,
2392
- },
2499
+ classification: viewerFailureClassification(
2500
+ `${operation} viewer transport disabled`,
2501
+ "fallback",
2502
+ ),
2393
2503
  };
2394
2504
  }
2395
2505
  if (Date.now() < promptPackTransportUnavailableUntil) {
2396
2506
  return {
2397
2507
  ok: false,
2398
- classification: {
2399
- cause: `${operation} viewer transport in backoff`,
2400
- retryable: true,
2401
- },
2508
+ classification: viewerFailureClassification(
2509
+ `${operation} viewer transport in backoff`,
2510
+ "fallback",
2511
+ ),
2402
2512
  };
2403
2513
  }
2404
2514
 
@@ -2426,20 +2536,49 @@ export const CodememPlugin = async ({
2426
2536
  }
2427
2537
  return { ok: false, classification };
2428
2538
  }
2429
- if (
2430
- !isRecord(profileBody)
2431
- || profileBody.service !== "codemem-viewer"
2432
- || profileBody.protocol_version !== 1
2433
- || profileBody.db_path !== promptPackDbPath
2434
- || canonicalJson(profileBody.identity_target) !== canonicalJson(promptPackIdentityTarget)
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)
2435
2571
  ) {
2572
+ profileFailure = viewerFailureClassification(
2573
+ `${operation} viewer runtime identity mismatch`,
2574
+ classifyPromptTransportFailure({ kind: "runtime_identity_mismatch" }),
2575
+ );
2576
+ }
2577
+ if (profileFailure) {
2436
2578
  promptPackTransportUnavailableUntil = Date.now() + Math.max(1000, rawEventsBackoffMs);
2437
2579
  return {
2438
2580
  ok: false,
2439
- classification: {
2440
- cause: `${operation} viewer profile handshake mismatch`,
2441
- retryable: true,
2442
- },
2581
+ classification: profileFailure,
2443
2582
  };
2444
2583
  }
2445
2584
  response = await fetch(url, {
@@ -2467,6 +2606,7 @@ export const CodememPlugin = async ({
2467
2606
  operation,
2468
2607
  status: response.status,
2469
2608
  body,
2609
+ compatibleProfile: true,
2470
2610
  });
2471
2611
  if (classification.retryable) {
2472
2612
  promptPackTransportUnavailableUntil =
@@ -3894,12 +4034,17 @@ export const __testUtils = {
3894
4034
  redactPackCommand,
3895
4035
  rejectsInternalLedgerFlag,
3896
4036
  classifyFallbackCommandResult,
4037
+ PROMPT_TRANSPORT_PROTOCOL_RANGE,
4038
+ normalizePromptTransportProtocolRange,
4039
+ arePromptTransportProtocolRangesCompatible,
4040
+ classifyPromptTransportFailure,
3897
4041
  classifyViewerHttpFailure,
3898
4042
  isValidPackHttpPayload,
3899
4043
  isValidLedgerHttpPayload,
3900
4044
  buildPackHttpBody,
3901
4045
  parsePackText,
3902
4046
  parsePackMetrics,
4047
+ buildViewerIdentityTarget,
3903
4048
  resolveInjectSurface,
3904
4049
  applyInjectedContextToOutput,
3905
4050
  applyInjectedContextToMessages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemem/opencode-plugin",
3
- "version": "0.41.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.16"
21
+ "@opencode-ai/plugin": "1.18.18"
22
22
  },
23
23
  "repository": {
24
24
  "type": "git",