@remembrance-ai/opencode-plugin 0.1.60 → 0.1.62

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/README.md CHANGED
@@ -57,12 +57,26 @@ An optional single-use token from **Dashboard > Agents > Instances > Install on
57
57
  this device** links the installation to the signed-in member so bounded working
58
58
  preferences can follow that engineer. The token expires after ten
59
59
  minutes and never belongs in reusable key-distribution instructions. Unlinked
60
- installs remain fully functional with installation-local preferences. Built-in
61
- controls cover common presentation choices; extensible preferences can guide
62
- discretionary workflow and strategy selection. They may reorder only
63
- already-relevant skills inside one match tier or apply a surgical sidecar, and
64
- can never weaken safety, authorization, privacy, applicability, required skill
65
- steps, validation, review, or organization policy.
60
+ installs remain fully functional with installation-local preferences. Private
61
+ working preferences follow each engineer across approved agents and steer
62
+ relevant public and team skills without changing shared instructions or
63
+ weakening organization policy. Profiles, observations, compatibility records,
64
+ and feedback remain private to the organization. Classification runs
65
+ asynchronously against exact skill versions; query and invocation add no
66
+ generative preference call or second embedding request. Material compatibility
67
+ may reorder only already-relevant skills inside one match tier or apply a
68
+ surgical sidecar. Missing or stale coverage is neutral, and no preference can
69
+ weaken safety, authorization, privacy, applicability, required skill steps,
70
+ validation, review, or organization policy. Exact skill-version changes queue
71
+ only that skill for affected organizations; blocked classification resumes
72
+ automatically when the organization becomes eligible.
73
+
74
+ After actually using an exact skill version, the agent may call
75
+ `submit_preference_compatibility_feedback` only with the exact correlation IDs
76
+ and server-issued preference fingerprint in that result's feedback offer. The
77
+ server verifies the fetch and active preference. The operation requires a
78
+ verified principal session, remains private to the organization, and never
79
+ edits the skill.
66
80
 
67
81
  ## Organization key
68
82
 
package/opencode.json CHANGED
@@ -3,7 +3,11 @@
3
3
  "mcp": {
4
4
  "remembrance": {
5
5
  "type": "local",
6
- "command": ["npx", "-y", "@remembrance-ai/mcp-server@0.1.60"],
6
+ "command": [
7
+ "npx",
8
+ "-y",
9
+ "@remembrance-ai/mcp-server@0.1.62"
10
+ ],
7
11
  "enabled": true,
8
12
  "environment": {
9
13
  "REMEMBRANCE_PLUGIN_HOST": "opencode",
@@ -11,5 +15,7 @@
11
15
  }
12
16
  }
13
17
  },
14
- "plugin": ["@remembrance-ai/opencode-plugin@0.1.60"]
18
+ "plugin": [
19
+ "@remembrance-ai/opencode-plugin@0.1.62"
20
+ ]
15
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remembrance-ai/opencode-plugin",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
4
4
  "license": "Apache-2.0",
5
5
  "description": "opencode plugin for Remembrance session hooks, tool observation, and contribution nudges.",
6
6
  "type": "module",
@@ -58,6 +58,33 @@ const VALUE_EPISODE_MARKER_LIMIT = 20;
58
58
  const VALUE_EPISODE_MARKER_TTL_MS = 30 * 24 * 60 * 60 * 1000;
59
59
  const DIRECT_SELECTION_MARKER_LIMIT = 20;
60
60
  const DIRECT_SELECTION_MARKER_TTL_MS = 24 * 60 * 60 * 1000;
61
+ const PREFERENCE_EVIDENCE_SKILL_LIMIT = 2;
62
+ const PREFERENCE_EVIDENCE_SETTING_LIMIT = 4;
63
+ const PREFERENCE_EVIDENCE_CONTEXT_CHARS = 760;
64
+ const EFFECTIVE_PREFERENCE_SOURCES = new Set([
65
+ "mandatory_org",
66
+ "explicit_task",
67
+ "explicit_project",
68
+ "explicit_member_runtime",
69
+ "explicit_member",
70
+ "learned_member_runtime",
71
+ "learned_member",
72
+ "explicit_installation",
73
+ "learned_installation",
74
+ "recommended_org",
75
+ "skill_default",
76
+ ]);
77
+ const PREFERENCE_EVIDENCE_SOURCES = new Set([
78
+ "mandatory_org",
79
+ "explicit_project",
80
+ "explicit_member_runtime",
81
+ "explicit_member",
82
+ "learned_member_runtime",
83
+ "learned_member",
84
+ "explicit_installation",
85
+ "learned_installation",
86
+ "recommended_org",
87
+ ]);
61
88
  const MAX_LOCAL_CONFIG_BYTES = 64 * 1024;
62
89
  const MAX_LOCAL_HEALTH_MARKER_BYTES = 16 * 1024;
63
90
  const MAX_LOCAL_PLUGIN_ALERT_BYTES = 16 * 1024;
@@ -338,14 +365,27 @@ export function buildQueryPayload(
338
365
  redacted.length <= MAX_SUMMARY_CHARS
339
366
  ? redacted
340
367
  : `${redacted.slice(0, MAX_SUMMARY_CHARS - 3).trim()}...`;
341
- const projectKey = projectKeyForHook(env, projectPath);
368
+ const credential = resolveApiCredential(env);
369
+ const organizationCredentialAvailable =
370
+ Boolean(credential.apiKey) &&
371
+ !isUnusableConfigurationSource(credential.source);
372
+ const projectKey = organizationCredentialAvailable
373
+ ? projectKeyForHook(env, projectPath)
374
+ : null;
342
375
  const resolvedClientContext = clientContext
343
- ? {
344
- ...clientContext,
345
- ...(clientContext.project_key || !projectKey
346
- ? {}
347
- : { project_key: projectKey }),
348
- }
376
+ ? (() => {
377
+ const { project_key: requestedProjectKey, ...publicClientContext } =
378
+ clientContext;
379
+ const authorizedProjectKey = organizationCredentialAvailable
380
+ ? requestedProjectKey || projectKey
381
+ : null;
382
+ return {
383
+ ...publicClientContext,
384
+ ...(authorizedProjectKey
385
+ ? { project_key: authorizedProjectKey }
386
+ : {}),
387
+ };
388
+ })()
349
389
  : null;
350
390
  return {
351
391
  agent: {
@@ -834,7 +874,7 @@ export function formatContext(
834
874
  );
835
875
  if (effectivePreferences.length > 0) {
836
876
  lines.push(
837
- `Apply these persisted working preferences silently: ${effectivePreferences.join("; ")}. A clear current-task preference overrides every non-mandatory entry; a Required organization setting remains authoritative. Use preferences only for discretionary presentation, workflow, or strategy choices among already-applicable options. Never weaken applicability, safety, authorization, privacy, required skill steps, validation, or review.`,
877
+ `Apply these persisted working preferences silently: ${effectivePreferences.join("; ")}. Do not ask the user to reconfirm them or mention routine preference application in the final response. A clear current-task preference overrides every non-mandatory entry; a Required organization setting remains authoritative. Use preferences only for discretionary presentation, workflow, or strategy choices among already-applicable options. Never weaken applicability, safety, authorization, privacy, required skill steps, validation, or review.`,
838
878
  );
839
879
  }
840
880
  const queryId = body?.query_feedback?.query_id ?? body?.query_id;
@@ -878,6 +918,12 @@ export function formatContext(
878
918
  `After using Remembrance: ${safeText(contributionDirective, MAX_DIRECTIVE_CHARS)}`,
879
919
  );
880
920
  }
921
+ const preferenceEvidence = preferenceCompatibilityEvidenceFromResponse(body);
922
+ if (preferenceEvidence.length > 0) {
923
+ tailLines.push(
924
+ preferenceCompatibilityEvidenceInstruction(preferenceEvidence),
925
+ );
926
+ }
881
927
  if (skills.length > 0 || resources.length > 0) {
882
928
  tailLines.push(
883
929
  "Delegating this task? Pass the selected slug, query_id, and result_id to the subagent; it should fetch that result or run its own full-context query before custom work.",
@@ -1072,6 +1118,162 @@ function sanitizePreferenceApplication(value, effectivePreferences) {
1072
1118
  : `skill preference defaults ${settings.join("; ")}`;
1073
1119
  }
1074
1120
 
1121
+ export function preferenceCompatibilityEvidenceFromResponse(response) {
1122
+ const body = responseBodyFromToolResponse(response);
1123
+ const candidates = [
1124
+ ...(body?.selection_mode === "explicit" && body?.skill
1125
+ ? [
1126
+ {
1127
+ ...body.skill,
1128
+ result_id: body.result_id,
1129
+ effective_preferences: body.effective_preferences,
1130
+ preference_compatibility_feedback:
1131
+ body.preference_compatibility_feedback,
1132
+ },
1133
+ ]
1134
+ : []),
1135
+ ...(Array.isArray(body?.skills) ? body.skills : []),
1136
+ ];
1137
+ const seen = new Set();
1138
+ return candidates
1139
+ .flatMap((candidate) => {
1140
+ const slug = safePreferenceIdentifier(candidate?.slug, 120);
1141
+ if (!slug || seen.has(slug)) return [];
1142
+ const feedback = candidate?.preference_compatibility_feedback;
1143
+ const queryId = safeCorrelationIdentifier(feedback?.query_id, 160);
1144
+ const resultId = safeCorrelationIdentifier(feedback?.result_id, 160);
1145
+ const skillVersionId = safeCorrelationIdentifier(
1146
+ feedback?.skill_version_id,
1147
+ 160,
1148
+ );
1149
+ const preferences = sanitizePreferenceFeedbackSettings(
1150
+ feedback?.preferences,
1151
+ );
1152
+ if (
1153
+ feedback?.available !== true ||
1154
+ !queryId ||
1155
+ !resultId ||
1156
+ !skillVersionId ||
1157
+ preferences.length === 0
1158
+ ) {
1159
+ return [];
1160
+ }
1161
+ seen.add(slug);
1162
+ return [
1163
+ {
1164
+ query_id: queryId,
1165
+ result_id: resultId,
1166
+ skill_slug: slug,
1167
+ skill_version_id: skillVersionId,
1168
+ preferences,
1169
+ },
1170
+ ];
1171
+ })
1172
+ .slice(0, PREFERENCE_EVIDENCE_SKILL_LIMIT);
1173
+ }
1174
+
1175
+ function preferenceCompatibilityEvidenceInstruction(evidence) {
1176
+ const payload = boundedPreferenceEvidencePayload(evidence);
1177
+ if (!payload) return "";
1178
+ return [
1179
+ "Private preference evidence (silent, after actual use only): if the selected skill clearly supports or conflicts with one server-issued working preference below, call submit_preference_compatibility_feedback using its exact query_id, result_id, preference_fingerprint, skill slug, and version id.",
1180
+ 'Judge observed behavior independently: selection and an existing classifier label are not new evidence. Skip neutral or uncertain cases. Use evidence_source "explicit_user" only for the user\'s explicit assessment; otherwise use "agent_observed". Never send task content, and do not ask the user or mention routine telemetry.',
1181
+ `Candidates: ${payload}`,
1182
+ ].join(" ");
1183
+ }
1184
+
1185
+ function boundedPreferenceEvidencePayload(evidence) {
1186
+ const entries = [];
1187
+ for (const candidate of evidence.slice(0, PREFERENCE_EVIDENCE_SKILL_LIMIT)) {
1188
+ for (const preference of candidate.preferences.slice(
1189
+ 0,
1190
+ PREFERENCE_EVIDENCE_SETTING_LIMIT,
1191
+ )) {
1192
+ const next = {
1193
+ query_id: candidate.query_id,
1194
+ result_id: candidate.result_id,
1195
+ skill_slug: candidate.skill_slug,
1196
+ skill_version_id: candidate.skill_version_id,
1197
+ preference_fingerprint: preference.preference_fingerprint,
1198
+ setting: preference.setting,
1199
+ };
1200
+ const serialized = JSON.stringify([...entries, next]);
1201
+ if (serialized.length > PREFERENCE_EVIDENCE_CONTEXT_CHARS) {
1202
+ return entries.length > 0 ? JSON.stringify(entries) : null;
1203
+ }
1204
+ entries.push(next);
1205
+ }
1206
+ }
1207
+ return entries.length > 0 ? JSON.stringify(entries) : null;
1208
+ }
1209
+
1210
+ function sanitizePreferenceFeedbackSettings(value) {
1211
+ if (!Array.isArray(value)) return [];
1212
+ return value
1213
+ .flatMap((entry) => {
1214
+ const fingerprint =
1215
+ typeof entry?.preference_fingerprint === "string" &&
1216
+ /^sha256:[a-f0-9]{64}$/.test(entry.preference_fingerprint)
1217
+ ? entry.preference_fingerprint
1218
+ : null;
1219
+ const item = entry?.setting;
1220
+ if (
1221
+ !fingerprint ||
1222
+ !item ||
1223
+ typeof item !== "object" ||
1224
+ Array.isArray(item)
1225
+ ) {
1226
+ return [];
1227
+ }
1228
+ const key = safePreferenceIdentifier(item.key, 96);
1229
+ const preferenceValue = safePreferenceIdentifier(item.value, 96);
1230
+ if (!key || !preferenceValue) return [];
1231
+ const setting = { key, value: preferenceValue };
1232
+ if (
1233
+ typeof item.label === "string" &&
1234
+ item.label.trim() &&
1235
+ typeof item.behavior === "string" &&
1236
+ item.behavior.trim() &&
1237
+ ["presentation", "workflow", "strategy_selection"].includes(
1238
+ item.effect,
1239
+ ) &&
1240
+ ["prefer", "avoid"].includes(item.strength) &&
1241
+ Number.isInteger(item.definition_version) &&
1242
+ item.definition_version >= 1 &&
1243
+ item.definition_version <= 1_000_000
1244
+ ) {
1245
+ Object.assign(setting, {
1246
+ label: safeText(item.label, 96),
1247
+ behavior: safeText(item.behavior, 320),
1248
+ effect: item.effect,
1249
+ strength: item.strength,
1250
+ definition_version: item.definition_version,
1251
+ });
1252
+ }
1253
+ return [{ preference_fingerprint: fingerprint, setting }];
1254
+ })
1255
+ .slice(0, PREFERENCE_EVIDENCE_SETTING_LIMIT);
1256
+ }
1257
+
1258
+ function safeCorrelationIdentifier(value, maxLength) {
1259
+ const text = String(value ?? "").trim();
1260
+ return text.length > 0 &&
1261
+ text.length <= maxLength &&
1262
+ /^[A-Za-z0-9_-]+$/.test(text)
1263
+ ? text
1264
+ : null;
1265
+ }
1266
+
1267
+ function safePreferenceIdentifier(value, maxLength) {
1268
+ const text = String(value ?? "")
1269
+ .trim()
1270
+ .toLowerCase();
1271
+ return text.length <= maxLength &&
1272
+ /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/.test(text)
1273
+ ? text
1274
+ : null;
1275
+ }
1276
+
1075
1277
  function sanitizeWhyMatched(value) {
1076
1278
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1077
1279
  return {
@@ -1278,7 +1480,7 @@ export async function runPromptHook(prompt, options = {}) {
1278
1480
  env,
1279
1481
  )
1280
1482
  : (formatContext(response, decision.reason, limitFromEnv(env)) ??
1281
- emptyQueryContext(decision.reason)),
1483
+ emptyQueryContext(decision.reason)),
1282
1484
  preferenceCapture,
1283
1485
  ),
1284
1486
  eligible: true,
@@ -1388,6 +1590,8 @@ export function contributionReason(
1388
1590
  : []
1389
1591
  ).slice(0, DIRECT_SELECTION_MARKER_LIMIT);
1390
1592
  if (directSelections.length > 0) {
1593
+ const preferenceEvidenceLine =
1594
+ directPreferenceCompatibilityInstruction(directSelections);
1391
1595
  const selectionLines = directSelections.map((selection) => {
1392
1596
  const correlation =
1393
1597
  selection.query_id && selection.result_id
@@ -1403,6 +1607,7 @@ export function contributionReason(
1403
1607
  signalLine,
1404
1608
  "Do not submit query-fit feedback for these selections; they were chosen directly rather than ranked for a query.",
1405
1609
  "Call submit_feedback once for each listed skill with useful true or false and a concise post-use lesson.",
1610
+ preferenceEvidenceLine,
1406
1611
  "If submit_feedback returns next_step.submit_remembrance_payload, submit it when the redacted lesson is reusable.",
1407
1612
  routineContributionVisibilityInstruction(),
1408
1613
  ]
@@ -1420,6 +1625,31 @@ export function contributionReason(
1420
1625
  .join("\n");
1421
1626
  }
1422
1627
 
1628
+ function directPreferenceCompatibilityInstruction(selections) {
1629
+ const evidence = selections.flatMap((selection) =>
1630
+ selection?.query_id &&
1631
+ selection?.result_id &&
1632
+ selection?.version_id &&
1633
+ Array.isArray(selection.preference_feedback_settings) &&
1634
+ selection.preference_feedback_settings.length > 0
1635
+ ? [
1636
+ {
1637
+ query_id: safeText(selection.query_id, 160),
1638
+ result_id: safeText(selection.result_id, 160),
1639
+ skill_slug: safeText(selection.slug, 120),
1640
+ skill_version_id: safeText(selection.version_id, 160),
1641
+ preferences: sanitizeStoredPreferenceFeedbackSettings(
1642
+ selection.preference_feedback_settings,
1643
+ ),
1644
+ },
1645
+ ]
1646
+ : [],
1647
+ );
1648
+ return evidence.length > 0
1649
+ ? preferenceCompatibilityEvidenceInstruction(evidence)
1650
+ : null;
1651
+ }
1652
+
1423
1653
  function routineContributionVisibilityInstruction() {
1424
1654
  return `Use Remembrance MCP tools when available; otherwise use this session's REST fallback, or emit REMEMBRANCE_SUBMISSION_PAYLOAD only if the API is unavailable. Never remove or bypass an organization key to force a public candidate. Do not mention routine Remembrance calls, hooks, or receipt IDs in the final answer. Surface only failures, host blocks, or required action. On a host block, say exactly: "${HOST_POLICY_ALERT_TEXT}" and do not retry through another transport. Redact secrets and proprietary content.`;
1425
1655
  }
@@ -1567,8 +1797,8 @@ function highMatchClosureInstruction(match) {
1567
1797
 
1568
1798
  // --- Marker mechanism (Codex has no transcript path) -------------------------
1569
1799
  //
1570
- // Three per-session counters and one bounded high-match marker live under
1571
- // os.tmpdir()/remembrance-usage/<hash>:
1800
+ // Per-session counters and bounded state markers live under a private,
1801
+ // per-user os.tmpdir()/remembrance-usage-<owner>/<hash> directory:
1572
1802
  // <hash>.use — incremented every time the query adapter completes a query.
1573
1803
  // <hash>.eligible — records that a reusable task should be closed out even if
1574
1804
  // no query result was consumed.
@@ -1580,10 +1810,20 @@ function highMatchClosureInstruction(match) {
1580
1810
 
1581
1811
  const USAGE_DIR = "remembrance-usage";
1582
1812
 
1813
+ function localUsageOwnerId() {
1814
+ if (typeof process.getuid === "function") {
1815
+ return `uid-${process.getuid()}`;
1816
+ }
1817
+ return `home-${createHash("sha256")
1818
+ .update(homedir())
1819
+ .digest("hex")
1820
+ .slice(0, 16)}`;
1821
+ }
1822
+
1583
1823
  function usageDir(env = process.env) {
1584
1824
  return env?.REMEMBRANCE_USAGE_DIR
1585
1825
  ? String(env.REMEMBRANCE_USAGE_DIR)
1586
- : join(tmpdir(), USAGE_DIR);
1826
+ : join(tmpdir(), `${USAGE_DIR}-${localUsageOwnerId()}`);
1587
1827
  }
1588
1828
 
1589
1829
  function sessionHash(sessionId) {
@@ -1631,17 +1871,30 @@ function readCountFile(path) {
1631
1871
  }
1632
1872
  }
1633
1873
 
1634
- function writeCountFile(path, count) {
1874
+ function writePrivateUsageMarker(path, value) {
1875
+ const temporaryPath = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
1635
1876
  try {
1636
- mkdirSync(dirname(path), { recursive: true });
1637
- writeFileSync(path, String(count));
1877
+ ensureSecureHookDirectory(dirname(path));
1878
+ writeFileSync(temporaryPath, String(value), {
1879
+ mode: 0o600,
1880
+ flag: "wx",
1881
+ });
1882
+ renameSync(temporaryPath, path);
1638
1883
  return true;
1639
1884
  } catch {
1640
- // Non-fatal: fail-open, the stop_hook_active guard still prevents loops.
1885
+ try {
1886
+ rmSync(temporaryPath, { force: true });
1887
+ } catch {
1888
+ // Best-effort cleanup only; marker persistence must remain fail-open.
1889
+ }
1641
1890
  return false;
1642
1891
  }
1643
1892
  }
1644
1893
 
1894
+ function writeCountFile(path, count) {
1895
+ return writePrivateUsageMarker(path, String(count));
1896
+ }
1897
+
1645
1898
  // Increment (and return) the per-session registry-use counter. Prompt and tool
1646
1899
  // adapters call this only after an authorized registry match or explicit skill
1647
1900
  // invocation. A legitimate empty query remains eligible for completion recovery
@@ -1693,8 +1946,7 @@ export function recordDirectiveSurface(
1693
1946
  rmSync(path, { force: true });
1694
1947
  return true;
1695
1948
  }
1696
- mkdirSync(dirname(path), { recursive: true });
1697
- writeFileSync(
1949
+ return writePrivateUsageMarker(
1698
1950
  path,
1699
1951
  JSON.stringify({
1700
1952
  directive_id: safeText(directive.directive_id, 96),
@@ -1706,7 +1958,6 @@ export function recordDirectiveSurface(
1706
1958
  shown_at: safeText(directive.shown_at ?? new Date().toISOString(), 40),
1707
1959
  }),
1708
1960
  );
1709
- return true;
1710
1961
  } catch {
1711
1962
  return false;
1712
1963
  }
@@ -1916,6 +2167,8 @@ export function directSelectionFromResponse(response) {
1916
2167
  ) {
1917
2168
  return null;
1918
2169
  }
2170
+ const preferenceEvidence =
2171
+ preferenceCompatibilityEvidenceFromResponse(body)[0] ?? null;
1919
2172
  return {
1920
2173
  invocation_id:
1921
2174
  typeof body.invocation_id === "string"
@@ -1938,6 +2191,10 @@ export function directSelectionFromResponse(response) {
1938
2191
  typeof skill.source === "string" ? safeText(skill.source, 40) : null,
1939
2192
  feedback_available: body?.feedback?.available === true,
1940
2193
  task_outcome_available: body?.task_outcome?.available === true,
2194
+ preference_feedback_settings:
2195
+ preferenceEvidence?.skill_version_id === skill.version_id
2196
+ ? preferenceEvidence.preferences
2197
+ : [],
1941
2198
  used_at: new Date().toISOString(),
1942
2199
  };
1943
2200
  }
@@ -2014,6 +2271,9 @@ function normalizeDirectSelectionSurface(selection, sessionId, env) {
2014
2271
  source: selection.source ? safeText(selection.source, 40) : null,
2015
2272
  feedback_available: selection.feedback_available === true,
2016
2273
  task_outcome_available: selection.task_outcome_available === true,
2274
+ preference_feedback_settings: sanitizeStoredPreferenceFeedbackSettings(
2275
+ selection.preference_feedback_settings,
2276
+ ),
2017
2277
  prompted_at: selection.prompted_at
2018
2278
  ? safeText(selection.prompted_at, 40)
2019
2279
  : null,
@@ -2041,6 +2301,9 @@ function normalizeStoredDirectSelectionSurface(selection) {
2041
2301
  source: selection.source ? safeText(selection.source, 40) : null,
2042
2302
  feedback_available: selection.feedback_available === true,
2043
2303
  task_outcome_available: selection.task_outcome_available === true,
2304
+ preference_feedback_settings: sanitizeStoredPreferenceFeedbackSettings(
2305
+ selection.preference_feedback_settings,
2306
+ ),
2044
2307
  prompted_at: selection.prompted_at
2045
2308
  ? safeText(selection.prompted_at, 40)
2046
2309
  : null,
@@ -2051,15 +2314,54 @@ function normalizeStoredDirectSelectionSurface(selection) {
2051
2314
  };
2052
2315
  }
2053
2316
 
2317
+ function sanitizeStoredPreferenceFeedbackSettings(value) {
2318
+ if (!Array.isArray(value)) return [];
2319
+ return value
2320
+ .flatMap((entry) => {
2321
+ const fingerprint =
2322
+ typeof entry?.preference_fingerprint === "string" &&
2323
+ /^sha256:[a-f0-9]{64}$/.test(entry.preference_fingerprint)
2324
+ ? entry.preference_fingerprint
2325
+ : null;
2326
+ const setting = entry?.setting;
2327
+ if (!fingerprint || !setting || typeof setting !== "object") return [];
2328
+ const key = safePreferenceIdentifier(setting?.key, 96);
2329
+ const preferenceValue = safePreferenceIdentifier(setting?.value, 96);
2330
+ if (!key || !preferenceValue) return [];
2331
+ const normalized = { key, value: preferenceValue };
2332
+ if (
2333
+ typeof setting?.label === "string" &&
2334
+ setting.label.trim() &&
2335
+ typeof setting?.behavior === "string" &&
2336
+ setting.behavior.trim() &&
2337
+ ["presentation", "workflow", "strategy_selection"].includes(
2338
+ setting.effect,
2339
+ ) &&
2340
+ ["prefer", "avoid"].includes(setting.strength) &&
2341
+ Number.isInteger(setting.definition_version) &&
2342
+ setting.definition_version >= 1 &&
2343
+ setting.definition_version <= 1_000_000
2344
+ ) {
2345
+ Object.assign(normalized, {
2346
+ label: safeText(setting.label, 96),
2347
+ behavior: safeText(setting.behavior, 320),
2348
+ effect: setting.effect,
2349
+ strength: setting.strength,
2350
+ definition_version: setting.definition_version,
2351
+ });
2352
+ }
2353
+ return [{ preference_fingerprint: fingerprint, setting: normalized }];
2354
+ })
2355
+ .slice(0, PREFERENCE_EVIDENCE_SETTING_LIMIT);
2356
+ }
2357
+
2054
2358
  function writeDirectSelectionSurfaces(sessionId, records, env) {
2055
2359
  try {
2056
2360
  const path = directSelectionPath(sessionId, env);
2057
- mkdirSync(dirname(path), { recursive: true });
2058
- writeFileSync(
2361
+ return writePrivateUsageMarker(
2059
2362
  path,
2060
2363
  JSON.stringify(records.slice(-DIRECT_SELECTION_MARKER_LIMIT)),
2061
2364
  );
2062
- return true;
2063
2365
  } catch {
2064
2366
  return false;
2065
2367
  }
@@ -2109,8 +2411,7 @@ export function recordHighMatchSurface(sessionId, match, env = process.env) {
2109
2411
  rmSync(path, { force: true });
2110
2412
  return true;
2111
2413
  }
2112
- mkdirSync(dirname(path), { recursive: true });
2113
- writeFileSync(
2414
+ return writePrivateUsageMarker(
2114
2415
  path,
2115
2416
  JSON.stringify({
2116
2417
  query_id: match.query_id ? safeText(match.query_id, 80) : null,
@@ -2132,7 +2433,6 @@ export function recordHighMatchSurface(sessionId, match, env = process.env) {
2132
2433
  potential_savings: sanitizePotentialSavings(match.potential_savings),
2133
2434
  }),
2134
2435
  );
2135
- return true;
2136
2436
  } catch {
2137
2437
  return false;
2138
2438
  }
@@ -2488,12 +2788,10 @@ async function postTaskOutcome(payload, options = {}) {
2488
2788
  function writeValueEpisodeSurfaces(sessionId, records, env) {
2489
2789
  try {
2490
2790
  const path = valueEpisodePath(sessionId, env);
2491
- mkdirSync(dirname(path), { recursive: true });
2492
- writeFileSync(
2791
+ return writePrivateUsageMarker(
2493
2792
  path,
2494
2793
  JSON.stringify(records.slice(-VALUE_EPISODE_MARKER_LIMIT)),
2495
2794
  );
2496
- return true;
2497
2795
  } catch {
2498
2796
  return false;
2499
2797
  }
@@ -3144,9 +3442,11 @@ function sanitizeEffectivePreferences(value) {
3144
3442
  item &&
3145
3443
  typeof item === "object" &&
3146
3444
  !Array.isArray(item) &&
3445
+ typeof item.key === "string" &&
3147
3446
  /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/.test(item.key) &&
3148
3447
  typeof item.value === "string" &&
3149
3448
  /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/.test(item.value) &&
3449
+ EFFECTIVE_PREFERENCE_SOURCES.has(item.source) &&
3150
3450
  ["presentation", "workflow", "strategy_selection"].includes(
3151
3451
  item.effect ?? "presentation",
3152
3452
  ) &&
@@ -3366,6 +3666,7 @@ function hostPolicyOperationClass(toolName) {
3366
3666
  normalized.endsWith("get_skill") ||
3367
3667
  normalized.endsWith("get_resource") ||
3368
3668
  normalized.endsWith("invoke_skill") ||
3669
+ normalized.endsWith("get_effective_preferences") ||
3369
3670
  normalized.endsWith("run_connection_doctor") ||
3370
3671
  normalized.endsWith("get_connection_status")
3371
3672
  ) {
@@ -3389,6 +3690,8 @@ function hostPolicyOperationClass(toolName) {
3389
3690
  if (
3390
3691
  normalized.endsWith("submit_query_feedback") ||
3391
3692
  normalized.endsWith("submit_feedback") ||
3693
+ normalized.endsWith("submit_preference_compatibility_feedback") ||
3694
+ normalized.endsWith("record_preference") ||
3392
3695
  normalized.endsWith("report_task_outcome")
3393
3696
  ) {
3394
3697
  return "feedback";
@@ -3409,6 +3712,10 @@ function isRemembranceToolName(toolName) {
3409
3712
  "get_resource",
3410
3713
  "invoke_skill",
3411
3714
  "get_value_proof",
3715
+ "get_effective_preferences",
3716
+ "record_preference",
3717
+ "submit_preference_compatibility_feedback",
3718
+ "link_current_installation",
3412
3719
  "report_task_outcome",
3413
3720
  "submit_query_feedback",
3414
3721
  "submit_feedback",
@@ -5335,6 +5335,81 @@ var skillPreferenceTraitsSchema = external_exports.array(skillPreferenceTraitSch
5335
5335
  seen.add(identity);
5336
5336
  });
5337
5337
  });
5338
+ var preferenceCompatibilityRelationshipSchema = external_exports.enum([
5339
+ "supports",
5340
+ "conflicts",
5341
+ "neutral",
5342
+ "uncertain"
5343
+ ]);
5344
+ var preferenceCompatibilitySourceSchema = external_exports.enum([
5345
+ "classifier",
5346
+ "member_feedback",
5347
+ "organization_override",
5348
+ "legacy_trait"
5349
+ ]);
5350
+ var preferenceCompatibilityAssessmentSchema = external_exports.object({
5351
+ preference_fingerprint: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/),
5352
+ preference_key: preferenceKeySchema,
5353
+ preference_value: preferenceValueSchema,
5354
+ relationship: preferenceCompatibilityRelationshipSchema,
5355
+ confidence: external_exports.number().min(0).max(1),
5356
+ rationale: external_exports.string().trim().min(1).max(200).nullable().default(null),
5357
+ classification_version: external_exports.string().trim().min(1).max(96),
5358
+ source: preferenceCompatibilitySourceSchema,
5359
+ locked: external_exports.boolean().default(false)
5360
+ }).strict();
5361
+ var preferenceCompatibilityAssessmentsSchema = external_exports.array(preferenceCompatibilityAssessmentSchema).max(MAX_EFFECTIVE_PREFERENCES * 2);
5362
+ var preferenceCompatibilityClassifierResultSchema = external_exports.object({
5363
+ relationship: preferenceCompatibilityRelationshipSchema,
5364
+ confidence: external_exports.number().min(0).max(1),
5365
+ rationale: external_exports.string().trim().min(1).max(200),
5366
+ // `locked` means the skill declares this behavior as a required step. It
5367
+ // can block surgical application after an explicit invocation, but never
5368
+ // lets a preference bypass a skill or organization requirement.
5369
+ locked: external_exports.boolean()
5370
+ }).strict();
5371
+ var preferenceCompatibilityFeedbackReasonCodeSchema = external_exports.enum([
5372
+ "workflow_alignment",
5373
+ "strategy_alignment",
5374
+ "presentation_alignment",
5375
+ "task_mismatch",
5376
+ "skill_requirement",
5377
+ "other_observed"
5378
+ ]);
5379
+ var preferenceCompatibilityFeedbackRequestSchema = external_exports.object({
5380
+ query_id: external_exports.string().trim().min(1).max(160),
5381
+ result_id: external_exports.string().trim().min(1).max(160),
5382
+ preference_fingerprint: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/),
5383
+ skill_slug: external_exports.string().trim().toLowerCase().min(1).max(160).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
5384
+ skill_version_id: external_exports.string().trim().min(1).max(160),
5385
+ relationship: external_exports.enum(["supports", "conflicts"]),
5386
+ evidence_source: external_exports.enum(["explicit_user", "agent_observed"]),
5387
+ reason_code: preferenceCompatibilityFeedbackReasonCodeSchema.nullable().default(null)
5388
+ }).strict();
5389
+ var REMEMBRANCE_PREFERENCE_COMPATIBILITY_FEEDBACK_TOOL_DESCRIPTION = "After actually opening or invoking a skill, record whether that exact version supports or conflicts with one server-issued active working-preference fingerprint. This is private to the authenticated organization, requires the same verified installation principal that used the correlated query/result, and never changes the skill or another organization's ranking. Use evidence_source explicit_user only for a user's explicit assessment; otherwise use agent_observed. Send only correlation IDs, the returned fingerprint, exact slug/version id, relationship, source, and an optional allowlisted reason_code; never send prompts, source code, paths, free-text feedback, or a caller-generated idempotency hash.";
5390
+ var preferenceCompatibilityAdminActionSchema = external_exports.discriminatedUnion(
5391
+ "action",
5392
+ [
5393
+ external_exports.object({
5394
+ action: external_exports.literal("reclassify"),
5395
+ preference_fingerprint: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/),
5396
+ skill_version_id: external_exports.string().trim().min(1).max(160)
5397
+ }).strict(),
5398
+ external_exports.object({
5399
+ action: external_exports.literal("override"),
5400
+ preference_fingerprint: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/),
5401
+ skill_version_id: external_exports.string().trim().min(1).max(160),
5402
+ relationship: external_exports.enum(["supports", "conflicts"]),
5403
+ rationale: external_exports.string().trim().min(1).max(200),
5404
+ locked: external_exports.boolean().default(false)
5405
+ }).strict(),
5406
+ external_exports.object({
5407
+ action: external_exports.literal("reset"),
5408
+ preference_fingerprint: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/),
5409
+ skill_version_id: external_exports.string().trim().min(1).max(160)
5410
+ }).strict()
5411
+ ]
5412
+ );
5338
5413
  var INFERRED_MAX_AGE_MS = 180 * 864e5;
5339
5414
  function builtInPreferenceDefinition(key) {
5340
5415
  return BUILT_IN_PREFERENCE_DEFINITIONS[key] ?? null;
@@ -5474,6 +5549,7 @@ var REMEMBRANCE_MCP_READ_TOOLS = [
5474
5549
  var REMEMBRANCE_MCP_FEEDBACK_TOOLS = [
5475
5550
  "submit_query_feedback",
5476
5551
  "submit_feedback",
5552
+ "submit_preference_compatibility_feedback",
5477
5553
  "report_task_outcome"
5478
5554
  ];
5479
5555
  var REMEMBRANCE_MCP_PRIVATE_CONTRIBUTION_TOOLS = [
@@ -6658,7 +6734,7 @@ var seedSkills = [
6658
6734
  summary: "Operational setup and troubleshooting workflow for Remembrance across Claude Code, Codex, OpenClaw, Cursor, Gemini, MCP, REST, skill-only installs, enterprise keys, and local agent identity.",
6659
6735
  status: "active",
6660
6736
  visibility: "public",
6661
- version: "0.1.18",
6737
+ version: "0.1.19",
6662
6738
  domains: ["agent-skills", "mcp", "resource-discovery"],
6663
6739
  tags: [
6664
6740
  "remembrance",
@@ -6709,7 +6785,7 @@ var seedSkills = [
6709
6785
  "gemini",
6710
6786
  "rest"
6711
6787
  ],
6712
- version: "0.1.18",
6788
+ version: "0.1.19",
6713
6789
  status: "active",
6714
6790
  visibility: "public",
6715
6791
  providers: ["codex", "claude", "cursor", "openclaw", "generic"],
@@ -9765,6 +9841,12 @@ var toolDefinitions = [
9765
9841
  "/api/v1/agent/preferences",
9766
9842
  recordPreferenceRequestSchema
9767
9843
  ),
9844
+ tool(
9845
+ "submit_preference_compatibility_feedback",
9846
+ REMEMBRANCE_PREFERENCE_COMPATIBILITY_FEEDBACK_TOOL_DESCRIPTION,
9847
+ "/api/v1/agent/preferences/compatibility-feedback",
9848
+ preferenceCompatibilityFeedbackRequestSchema
9849
+ ),
9768
9850
  tool(
9769
9851
  "link_current_installation",
9770
9852
  "Link this verified installation to the signed-in organization member using a single-use dashboard token. The token expires after ten minutes and is consumed exactly once. A valid organization principal session is required; this never changes the organization API key or creates another billed agent.",
@@ -10921,7 +11003,7 @@ async function checkClientUpdate(input) {
10921
11003
  // src/server.ts
10922
11004
  var MAX_REMOTE_RESPONSE_BYTES = 4 * 1024 * 1024;
10923
11005
  var DOCTOR_PROBE_TIMEOUT_MS = 7500;
10924
- var SERVER_VERSION = true ? "0.1.60" : "0.0.0-dev";
11006
+ var SERVER_VERSION = true ? "0.1.62" : "0.0.0-dev";
10925
11007
  var tools = toolDefinitions;
10926
11008
  var doctorCliRequested = process.argv[2] === "doctor";
10927
11009
  var inputBuffer = Buffer.alloc(0);
@@ -10931,6 +11013,7 @@ var connectedClientInfo = null;
10931
11013
  var cachedValueProofKeys = null;
10932
11014
  var PRINCIPAL_SESSION_REQUIRED_TOOLS = /* @__PURE__ */ new Set([
10933
11015
  "record_preference",
11016
+ "submit_preference_compatibility_feedback",
10934
11017
  "link_current_installation"
10935
11018
  ]);
10936
11019
  var PRINCIPAL_SESSION_AWAITED_TOOLS = /* @__PURE__ */ new Set([
@@ -11730,6 +11813,9 @@ async function callRemembrance(definition, rawArguments, options = {}) {
11730
11813
  if (refreshed && refreshed !== sentPrincipalSession) {
11731
11814
  headers["x-remembrance-principal-session"] = refreshed;
11732
11815
  ({ response, body } = await execute());
11816
+ } else if (!PRINCIPAL_SESSION_REQUIRED_TOOLS.has(definition.name)) {
11817
+ delete headers["x-remembrance-principal-session"];
11818
+ ({ response, body } = await execute());
11733
11819
  }
11734
11820
  }
11735
11821
  } catch (error) {
@@ -11775,7 +11861,7 @@ async function localProjectKey() {
11775
11861
  function principalSessionAuthenticationFailed(response, body) {
11776
11862
  if (response.status !== 401 && response.status !== 403) return false;
11777
11863
  if (!isRecord4(body) || typeof body.error !== "string") return false;
11778
- return /principal session|economics session|installation session/i.test(
11864
+ return /principal session|economics session|installation session|agent principal|installation principal/i.test(
11779
11865
  body.error
11780
11866
  );
11781
11867
  }
@@ -107,12 +107,11 @@ Resolution order is:
107
107
  11. recommended organization guidance;
108
108
  12. declared skill default.
109
109
 
110
- Organization admins choose Required or Recommended under \*\*Dashboard > Agents
111
-
112
- > Preferences\*\*. Required applies to every organization agent. Recommended is a
113
- > default that a clear task or personal preference may replace. The API field
114
- > `project_key` is an opaque local project-context hash, not an administrator
115
- > policy scope.
110
+ Organization admins choose Required or Recommended under **Dashboard > Agents >
111
+ Preferences**. Required applies to every organization agent. Recommended is a
112
+ default that a clear task or personal preference may replace. The API field
113
+ `project_key` is an opaque local project-context hash, not an administrator
114
+ policy scope.
116
115
 
117
116
  Use `record_preference` or `POST /api/v1/agent/preferences` only for an
118
117
  built-in or complete bounded extensible setting. Submit an `evidence_hash`,
@@ -121,14 +120,27 @@ source path, or private task content.
121
120
 
122
121
  Relevance and preference influence remain separate. The normal ranker first
123
122
  establishes applicability and the stable `high`, `possible`, or `exploratory`
124
- tier. Declared skill preference traits can then reorder candidates only inside
125
- the same tier. The response reports qualitative matched/conflicted traits plus
123
+ tier. Organization-private compatibility records for the exact skill and
124
+ preference versions can then reorder candidates only inside that same tier.
125
+ The response reports qualitative matched/conflicted relationships plus
126
126
  `relevance_rank` and `personalized_rank`; it never exposes the internal bounded
127
127
  adjustment. A single selected skill still receives a surgical preference
128
128
  sidecar for discretionary behavior. A locked skill requirement blocks an
129
129
  incompatible personal preference. A locked conflict with Required organization
130
130
  guidance makes the skill ineligible. No preference weakens a hard constraint.
131
131
 
132
+ Compatibility is classified asynchronously. Query and invocation do not add a
133
+ generative preference call or a second embedding request. Missing, stale, or
134
+ unavailable coverage is neutral. Preference and policy changes use leased
135
+ catalog sweeps, while an exact skill-version change queues only that skill for
136
+ affected organizations. An ineligible organization is visibly blocked and
137
+ resumes automatically instead of retrying classification work continuously.
138
+ After actual use, submit compatibility
139
+ feedback only with the exact `query_id`, `result_id`, skill/version, evidence
140
+ source, and server-issued preference fingerprint from that result's feedback
141
+ offer. The server derives the evidence identity and rejects expired,
142
+ unfetched, cross-principal, stale-version, or unoffered claims.
143
+
132
144
  - An explicit durable user correction applies immediately.
133
145
  - Inferred member or installation values require at least three consistent
134
146
  observations across two tasks and two days with confidence at least `0.85`.
@@ -335,6 +335,7 @@ enabled_tools = [
335
335
  "get_value_proof",
336
336
  "submit_query_feedback",
337
337
  "submit_feedback",
338
+ "submit_preference_compatibility_feedback",
338
339
  "report_task_outcome",
339
340
  "submit_remembrance",
340
341
  "propose_private_skill",
@@ -439,6 +440,7 @@ get_resource
439
440
  get_value_proof
440
441
  submit_query_feedback
441
442
  submit_feedback
443
+ submit_preference_compatibility_feedback
442
444
  report_task_outcome
443
445
  submit_remembrance
444
446
  propose_private_skill
@@ -508,6 +510,7 @@ Merge this into managed settings:
508
510
  "mcp__remembrance__get_value_proof",
509
511
  "mcp__remembrance__submit_query_feedback",
510
512
  "mcp__remembrance__submit_feedback",
513
+ "mcp__remembrance__submit_preference_compatibility_feedback",
511
514
  "mcp__remembrance__report_task_outcome",
512
515
  "mcp__remembrance__submit_remembrance",
513
516
  "mcp__remembrance__propose_private_skill",
@@ -566,6 +569,7 @@ narrow server/tool set:
566
569
  "get_value_proof",
567
570
  "submit_query_feedback",
568
571
  "submit_feedback",
572
+ "submit_preference_compatibility_feedback",
569
573
  "report_task_outcome",
570
574
  "submit_remembrance",
571
575
  "propose_private_skill",
@@ -622,6 +626,7 @@ tool profile hides MCP tools, and `tools.deny: ["bundle-mcp"]` disables them:
622
626
  "get_value_proof",
623
627
  "submit_query_feedback",
624
628
  "submit_feedback",
629
+ "submit_preference_compatibility_feedback",
625
630
  "report_task_outcome",
626
631
  "submit_remembrance",
627
632
  "propose_private_skill",
package/src/index.mjs CHANGED
@@ -80,6 +80,9 @@ const CONTRIBUTION_TOOLS = [
80
80
  "submit_resource",
81
81
  "submit_resource_review",
82
82
  ];
83
+ const PREFERENCE_EVIDENCE_TOOLS = [
84
+ "submit_preference_compatibility_feedback",
85
+ ];
83
86
 
84
87
  export function pluginVersion(readFile = readFileSync) {
85
88
  try {
@@ -598,6 +601,13 @@ export const Remembrance = async (context = {}) => {
598
601
  );
599
602
  return;
600
603
  }
604
+ if (
605
+ PREFERENCE_EVIDENCE_TOOLS.some((candidate) =>
606
+ normalizedTool.endsWith(candidate),
607
+ )
608
+ ) {
609
+ return;
610
+ }
601
611
  if (
602
612
  CONTRIBUTION_TOOLS.some((candidate) =>
603
613
  normalizedTool.endsWith(candidate),