@remembrance-ai/opencode-plugin 0.1.60 → 0.1.63
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 +20 -6
- package/opencode.json +8 -2
- package/package.json +1 -1
- package/scripts/hook-core.mjs +408 -28
- package/servers/remembrance-mcp.mjs +173 -7
- package/skills/remembrancer/references/identity-preferences-topology.md +20 -8
- package/skills/remembrancer/references/remembrance-setup.md +5 -0
- package/src/index.mjs +10 -0
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.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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": [
|
|
6
|
+
"command": [
|
|
7
|
+
"npx",
|
|
8
|
+
"-y",
|
|
9
|
+
"@remembrance-ai/mcp-server@0.1.63"
|
|
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": [
|
|
18
|
+
"plugin": [
|
|
19
|
+
"@remembrance-ai/opencode-plugin@0.1.63"
|
|
20
|
+
]
|
|
15
21
|
}
|
package/package.json
CHANGED
package/scripts/hook-core.mjs
CHANGED
|
@@ -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
|
|
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
|
-
...
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1571
|
-
// os.tmpdir()/remembrance-usage
|
|
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
|
|
1874
|
+
function writePrivateUsageMarker(path, value) {
|
|
1875
|
+
const temporaryPath = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
1635
1876
|
try {
|
|
1636
|
-
|
|
1637
|
-
writeFileSync(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
@@ -3734,6 +4041,7 @@ export function recordPluginLifecycleHealth(
|
|
|
3734
4041
|
hostVersion = null,
|
|
3735
4042
|
credentialSource = null,
|
|
3736
4043
|
sessionId = null,
|
|
4044
|
+
hookTrust = null,
|
|
3737
4045
|
},
|
|
3738
4046
|
env = process.env,
|
|
3739
4047
|
) {
|
|
@@ -3765,6 +4073,7 @@ export function recordPluginLifecycleHealth(
|
|
|
3765
4073
|
hostVersion,
|
|
3766
4074
|
credentialSource,
|
|
3767
4075
|
sessionId: null,
|
|
4076
|
+
hookTrust,
|
|
3768
4077
|
},
|
|
3769
4078
|
env,
|
|
3770
4079
|
);
|
|
@@ -3857,6 +4166,10 @@ export function recordPluginLifecycleHealth(
|
|
|
3857
4166
|
api_destination_fingerprint: apiDestinationFingerprint,
|
|
3858
4167
|
evidence_origin: evidenceOrigin,
|
|
3859
4168
|
release_run_id: releaseRunId || null,
|
|
4169
|
+
hook_trust:
|
|
4170
|
+
sanitizePluginHookTrust(hookTrust) ??
|
|
4171
|
+
sanitizePluginHookTrust(existing.hook_trust) ??
|
|
4172
|
+
null,
|
|
3860
4173
|
components: {
|
|
3861
4174
|
...currentComponents,
|
|
3862
4175
|
[normalizedComponent]: now,
|
|
@@ -3883,6 +4196,73 @@ export function recordPluginLifecycleHealth(
|
|
|
3883
4196
|
}
|
|
3884
4197
|
}
|
|
3885
4198
|
|
|
4199
|
+
function sanitizePluginHookTrust(value) {
|
|
4200
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
4201
|
+
const status = String(value.status ?? "")
|
|
4202
|
+
.trim()
|
|
4203
|
+
.toLowerCase();
|
|
4204
|
+
if (!["trusted", "review_required", "unavailable"].includes(status)) {
|
|
4205
|
+
return null;
|
|
4206
|
+
}
|
|
4207
|
+
const allowedEvents = new Set([
|
|
4208
|
+
"SessionStart",
|
|
4209
|
+
"UserPromptSubmit",
|
|
4210
|
+
"PostToolUse",
|
|
4211
|
+
"Stop",
|
|
4212
|
+
]);
|
|
4213
|
+
const reviewEvents = Array.isArray(value.review_events)
|
|
4214
|
+
? [...new Set(value.review_events)]
|
|
4215
|
+
.filter((event) => allowedEvents.has(event))
|
|
4216
|
+
.slice(0, 4)
|
|
4217
|
+
: [];
|
|
4218
|
+
const hooks = Array.isArray(value.hooks)
|
|
4219
|
+
? value.hooks
|
|
4220
|
+
.filter(
|
|
4221
|
+
(entry) =>
|
|
4222
|
+
entry &&
|
|
4223
|
+
typeof entry === "object" &&
|
|
4224
|
+
!Array.isArray(entry) &&
|
|
4225
|
+
allowedEvents.has(entry.event),
|
|
4226
|
+
)
|
|
4227
|
+
.slice(0, 4)
|
|
4228
|
+
.map((entry) => ({
|
|
4229
|
+
event: entry.event,
|
|
4230
|
+
enabled: entry.enabled === true,
|
|
4231
|
+
trust_status: [
|
|
4232
|
+
"managed",
|
|
4233
|
+
"missing",
|
|
4234
|
+
"modified",
|
|
4235
|
+
"trusted",
|
|
4236
|
+
"unknown",
|
|
4237
|
+
"untrusted",
|
|
4238
|
+
].includes(entry.trust_status)
|
|
4239
|
+
? entry.trust_status
|
|
4240
|
+
: "unknown",
|
|
4241
|
+
}))
|
|
4242
|
+
: [];
|
|
4243
|
+
return {
|
|
4244
|
+
status,
|
|
4245
|
+
checked_at: safeText(value.checked_at ?? "", 64),
|
|
4246
|
+
review_events: reviewEvents,
|
|
4247
|
+
hooks,
|
|
4248
|
+
reason:
|
|
4249
|
+
status === "unavailable" &&
|
|
4250
|
+
[
|
|
4251
|
+
"app_server_closed",
|
|
4252
|
+
"app_server_initialize_failed",
|
|
4253
|
+
"app_server_unavailable",
|
|
4254
|
+
"check_disabled",
|
|
4255
|
+
"codex_executable_not_found",
|
|
4256
|
+
"hooks_list_failed",
|
|
4257
|
+
"hooks_list_timeout",
|
|
4258
|
+
"hooks_list_too_large",
|
|
4259
|
+
"plugin_hooks_not_listed",
|
|
4260
|
+
].includes(value.reason)
|
|
4261
|
+
? value.reason
|
|
4262
|
+
: null,
|
|
4263
|
+
};
|
|
4264
|
+
}
|
|
4265
|
+
|
|
3886
4266
|
function prunePluginHealthSessionMarkers(surface, keepPath, env) {
|
|
3887
4267
|
const directory = pluginHealthDir(env);
|
|
3888
4268
|
try {
|
|
@@ -4706,7 +4706,17 @@ var HOST_POLICY_OPERATION_CLASSES = /* @__PURE__ */ new Set([
|
|
|
4706
4706
|
"other"
|
|
4707
4707
|
]);
|
|
4708
4708
|
var CODEX_HOOK_REVIEW_COMMAND = `CODEX_CLI="\${CODEX_CLI:-$(command -v codex || true)}"; [ -x "$CODEX_CLI" ] || CODEX_CLI="/Applications/ChatGPT.app/Contents/Resources/codex"; [ -x "$CODEX_CLI" ] || CODEX_CLI="/Applications/Codex.app/Contents/Resources/codex"; [ -x "$CODEX_CLI" ] || { printf '%s\\n' "Codex CLI not found. Install the Codex CLI, or install or update the ChatGPT desktop app on macOS, then try again." >&2; exit 1; }; "$CODEX_CLI"`;
|
|
4709
|
-
function codexHookReviewAction(followupTool) {
|
|
4709
|
+
function codexHookReviewAction(followupTool, reviewEvents = []) {
|
|
4710
|
+
const allowedEvents = /* @__PURE__ */ new Set([
|
|
4711
|
+
"SessionStart",
|
|
4712
|
+
"UserPromptSubmit",
|
|
4713
|
+
"PostToolUse",
|
|
4714
|
+
"Stop"
|
|
4715
|
+
]);
|
|
4716
|
+
const events = [...new Set(reviewEvents)].filter((event) => allowedEvents.has(event)).slice(0, 4);
|
|
4717
|
+
if (events.length > 0) {
|
|
4718
|
+
return `Open Terminal and run the command below. Choose Review hooks, open only the Remembrance ${events.join(", ")} hook${events.length === 1 ? "" : "s"}, and press t to trust ${events.length === 1 ? "it" : "them"}. Then fully quit and reopen Codex, submit one prompt, use one Remembrance tool, complete one turn, and call ${followupTool} again.`;
|
|
4719
|
+
}
|
|
4710
4720
|
return `Open Terminal and launch Codex with the command below. If Codex shows a Hooks need review screen, choose Review hooks and trust only the Remembrance SessionStart, UserPromptSubmit, PostToolUse, and Stop hooks. If no review screen appears, continue: Codex may be reusing an existing valid trust decision. Fully restart Codex, submit one prompt, use one Remembrance tool, complete one turn, and call ${followupTool} again. If the lifecycle warning remains, update or reinstall Remembrance and repeat this check.`;
|
|
4711
4721
|
}
|
|
4712
4722
|
function buildConnectionDoctorReport(input) {
|
|
@@ -5000,6 +5010,28 @@ function pluginLifecycleCheck(pluginHealth, issues) {
|
|
|
5000
5010
|
"The native plugin startup and prompt lifecycle is active."
|
|
5001
5011
|
);
|
|
5002
5012
|
}
|
|
5013
|
+
if (stringValue(pluginHealth.surface) === "codex" && issues.includes("hook_trust_required")) {
|
|
5014
|
+
const hookTrust = record(pluginHealth.hook_trust);
|
|
5015
|
+
const allowedEvents = /* @__PURE__ */ new Set([
|
|
5016
|
+
"SessionStart",
|
|
5017
|
+
"UserPromptSubmit",
|
|
5018
|
+
"PostToolUse",
|
|
5019
|
+
"Stop"
|
|
5020
|
+
]);
|
|
5021
|
+
const reviewEvents = Array.isArray(hookTrust.review_events) ? hookTrust.review_events.filter(
|
|
5022
|
+
(event) => typeof event === "string" && allowedEvents.has(event)
|
|
5023
|
+
).slice(0, 4) : [];
|
|
5024
|
+
const eventSummary = reviewEvents.length > 0 ? `the updated Remembrance ${reviewEvents.join(", ")} hook${reviewEvents.length === 1 ? "" : "s"}` : "updated Remembrance hooks";
|
|
5025
|
+
return warning(
|
|
5026
|
+
"plugin_lifecycle",
|
|
5027
|
+
`Codex is waiting for trust approval for ${eventSummary}.`,
|
|
5028
|
+
"review_codex_hook_trust",
|
|
5029
|
+
codexHookReviewAction("run_connection_doctor", reviewEvents),
|
|
5030
|
+
issues,
|
|
5031
|
+
false,
|
|
5032
|
+
CODEX_HOOK_REVIEW_COMMAND
|
|
5033
|
+
);
|
|
5034
|
+
}
|
|
5003
5035
|
if (pluginHealth.status === "partial") {
|
|
5004
5036
|
const codex = stringValue(pluginHealth.surface) === "codex";
|
|
5005
5037
|
return warning(
|
|
@@ -5335,6 +5367,81 @@ var skillPreferenceTraitsSchema = external_exports.array(skillPreferenceTraitSch
|
|
|
5335
5367
|
seen.add(identity);
|
|
5336
5368
|
});
|
|
5337
5369
|
});
|
|
5370
|
+
var preferenceCompatibilityRelationshipSchema = external_exports.enum([
|
|
5371
|
+
"supports",
|
|
5372
|
+
"conflicts",
|
|
5373
|
+
"neutral",
|
|
5374
|
+
"uncertain"
|
|
5375
|
+
]);
|
|
5376
|
+
var preferenceCompatibilitySourceSchema = external_exports.enum([
|
|
5377
|
+
"classifier",
|
|
5378
|
+
"member_feedback",
|
|
5379
|
+
"organization_override",
|
|
5380
|
+
"legacy_trait"
|
|
5381
|
+
]);
|
|
5382
|
+
var preferenceCompatibilityAssessmentSchema = external_exports.object({
|
|
5383
|
+
preference_fingerprint: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
5384
|
+
preference_key: preferenceKeySchema,
|
|
5385
|
+
preference_value: preferenceValueSchema,
|
|
5386
|
+
relationship: preferenceCompatibilityRelationshipSchema,
|
|
5387
|
+
confidence: external_exports.number().min(0).max(1),
|
|
5388
|
+
rationale: external_exports.string().trim().min(1).max(200).nullable().default(null),
|
|
5389
|
+
classification_version: external_exports.string().trim().min(1).max(96),
|
|
5390
|
+
source: preferenceCompatibilitySourceSchema,
|
|
5391
|
+
locked: external_exports.boolean().default(false)
|
|
5392
|
+
}).strict();
|
|
5393
|
+
var preferenceCompatibilityAssessmentsSchema = external_exports.array(preferenceCompatibilityAssessmentSchema).max(MAX_EFFECTIVE_PREFERENCES * 2);
|
|
5394
|
+
var preferenceCompatibilityClassifierResultSchema = external_exports.object({
|
|
5395
|
+
relationship: preferenceCompatibilityRelationshipSchema,
|
|
5396
|
+
confidence: external_exports.number().min(0).max(1),
|
|
5397
|
+
rationale: external_exports.string().trim().min(1).max(200),
|
|
5398
|
+
// `locked` means the skill declares this behavior as a required step. It
|
|
5399
|
+
// can block surgical application after an explicit invocation, but never
|
|
5400
|
+
// lets a preference bypass a skill or organization requirement.
|
|
5401
|
+
locked: external_exports.boolean()
|
|
5402
|
+
}).strict();
|
|
5403
|
+
var preferenceCompatibilityFeedbackReasonCodeSchema = external_exports.enum([
|
|
5404
|
+
"workflow_alignment",
|
|
5405
|
+
"strategy_alignment",
|
|
5406
|
+
"presentation_alignment",
|
|
5407
|
+
"task_mismatch",
|
|
5408
|
+
"skill_requirement",
|
|
5409
|
+
"other_observed"
|
|
5410
|
+
]);
|
|
5411
|
+
var preferenceCompatibilityFeedbackRequestSchema = external_exports.object({
|
|
5412
|
+
query_id: external_exports.string().trim().min(1).max(160),
|
|
5413
|
+
result_id: external_exports.string().trim().min(1).max(160),
|
|
5414
|
+
preference_fingerprint: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
5415
|
+
skill_slug: external_exports.string().trim().toLowerCase().min(1).max(160).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
|
5416
|
+
skill_version_id: external_exports.string().trim().min(1).max(160),
|
|
5417
|
+
relationship: external_exports.enum(["supports", "conflicts"]),
|
|
5418
|
+
evidence_source: external_exports.enum(["explicit_user", "agent_observed"]),
|
|
5419
|
+
reason_code: preferenceCompatibilityFeedbackReasonCodeSchema.nullable().default(null)
|
|
5420
|
+
}).strict();
|
|
5421
|
+
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.";
|
|
5422
|
+
var preferenceCompatibilityAdminActionSchema = external_exports.discriminatedUnion(
|
|
5423
|
+
"action",
|
|
5424
|
+
[
|
|
5425
|
+
external_exports.object({
|
|
5426
|
+
action: external_exports.literal("reclassify"),
|
|
5427
|
+
preference_fingerprint: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
5428
|
+
skill_version_id: external_exports.string().trim().min(1).max(160)
|
|
5429
|
+
}).strict(),
|
|
5430
|
+
external_exports.object({
|
|
5431
|
+
action: external_exports.literal("override"),
|
|
5432
|
+
preference_fingerprint: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
5433
|
+
skill_version_id: external_exports.string().trim().min(1).max(160),
|
|
5434
|
+
relationship: external_exports.enum(["supports", "conflicts"]),
|
|
5435
|
+
rationale: external_exports.string().trim().min(1).max(200),
|
|
5436
|
+
locked: external_exports.boolean().default(false)
|
|
5437
|
+
}).strict(),
|
|
5438
|
+
external_exports.object({
|
|
5439
|
+
action: external_exports.literal("reset"),
|
|
5440
|
+
preference_fingerprint: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
5441
|
+
skill_version_id: external_exports.string().trim().min(1).max(160)
|
|
5442
|
+
}).strict()
|
|
5443
|
+
]
|
|
5444
|
+
);
|
|
5338
5445
|
var INFERRED_MAX_AGE_MS = 180 * 864e5;
|
|
5339
5446
|
function builtInPreferenceDefinition(key) {
|
|
5340
5447
|
return BUILT_IN_PREFERENCE_DEFINITIONS[key] ?? null;
|
|
@@ -5474,6 +5581,7 @@ var REMEMBRANCE_MCP_READ_TOOLS = [
|
|
|
5474
5581
|
var REMEMBRANCE_MCP_FEEDBACK_TOOLS = [
|
|
5475
5582
|
"submit_query_feedback",
|
|
5476
5583
|
"submit_feedback",
|
|
5584
|
+
"submit_preference_compatibility_feedback",
|
|
5477
5585
|
"report_task_outcome"
|
|
5478
5586
|
];
|
|
5479
5587
|
var REMEMBRANCE_MCP_PRIVATE_CONTRIBUTION_TOOLS = [
|
|
@@ -6658,7 +6766,7 @@ var seedSkills = [
|
|
|
6658
6766
|
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
6767
|
status: "active",
|
|
6660
6768
|
visibility: "public",
|
|
6661
|
-
version: "0.1.
|
|
6769
|
+
version: "0.1.19",
|
|
6662
6770
|
domains: ["agent-skills", "mcp", "resource-discovery"],
|
|
6663
6771
|
tags: [
|
|
6664
6772
|
"remembrance",
|
|
@@ -6709,7 +6817,7 @@ var seedSkills = [
|
|
|
6709
6817
|
"gemini",
|
|
6710
6818
|
"rest"
|
|
6711
6819
|
],
|
|
6712
|
-
version: "0.1.
|
|
6820
|
+
version: "0.1.19",
|
|
6713
6821
|
status: "active",
|
|
6714
6822
|
visibility: "public",
|
|
6715
6823
|
providers: ["codex", "claude", "cursor", "openclaw", "generic"],
|
|
@@ -9765,6 +9873,12 @@ var toolDefinitions = [
|
|
|
9765
9873
|
"/api/v1/agent/preferences",
|
|
9766
9874
|
recordPreferenceRequestSchema
|
|
9767
9875
|
),
|
|
9876
|
+
tool(
|
|
9877
|
+
"submit_preference_compatibility_feedback",
|
|
9878
|
+
REMEMBRANCE_PREFERENCE_COMPATIBILITY_FEEDBACK_TOOL_DESCRIPTION,
|
|
9879
|
+
"/api/v1/agent/preferences/compatibility-feedback",
|
|
9880
|
+
preferenceCompatibilityFeedbackRequestSchema
|
|
9881
|
+
),
|
|
9768
9882
|
tool(
|
|
9769
9883
|
"link_current_installation",
|
|
9770
9884
|
"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.",
|
|
@@ -10648,13 +10762,23 @@ function localPluginLifecycleHealth(options) {
|
|
|
10648
10762
|
});
|
|
10649
10763
|
}
|
|
10650
10764
|
const coreLifecycleObserved = components.session_start.observed && components.prompt_hook.observed;
|
|
10651
|
-
|
|
10765
|
+
const hookTrust = normalizedPluginHookTrust(marker.hook_trust);
|
|
10766
|
+
const hookTrustReviewEvents = hookTrust?.status === "review_required" ? new Set(hookTrust.review_events) : /* @__PURE__ */ new Set();
|
|
10767
|
+
if (hookTrustReviewEvents.size > 0) {
|
|
10768
|
+
issues.push({
|
|
10769
|
+
code: "hook_trust_required",
|
|
10770
|
+
action: codexHookReviewAction("get_connection_status", [
|
|
10771
|
+
...hookTrustReviewEvents
|
|
10772
|
+
])
|
|
10773
|
+
});
|
|
10774
|
+
}
|
|
10775
|
+
if (coreLifecycleObserved && !components.tool_observer.observed && !hookTrustReviewEvents.has("PostToolUse")) {
|
|
10652
10776
|
issues.push({
|
|
10653
10777
|
code: "tool_observer_not_observed",
|
|
10654
10778
|
action: surface === "codex" ? `Use a Remembrance MCP tool, then call get_connection_status again. If this remains missing, ${codexHookReviewAction("get_connection_status")}` : "No native tool observer has run in this session yet. Use a Remembrance MCP tool, then call get_connection_status again."
|
|
10655
10779
|
});
|
|
10656
10780
|
}
|
|
10657
|
-
if (coreLifecycleObserved && !components.completion_hook.observed) {
|
|
10781
|
+
if (coreLifecycleObserved && !components.completion_hook.observed && !hookTrustReviewEvents.has("Stop")) {
|
|
10658
10782
|
issues.push({
|
|
10659
10783
|
code: "completion_hook_not_observed",
|
|
10660
10784
|
action: surface === "codex" ? `Complete one turn, then call get_connection_status again. If this remains missing, ${codexHookReviewAction("get_connection_status")}` : "No native completion hook has run in this session yet. Complete one turn, then call get_connection_status again."
|
|
@@ -10713,11 +10837,49 @@ function localPluginLifecycleHealth(options) {
|
|
|
10713
10837
|
matches_mcp: markerDestinationFingerprint && options.apiDestinationFingerprint ? markerDestinationFingerprint === options.apiDestinationFingerprint : null,
|
|
10714
10838
|
mcp_source: options.apiDestinationSource ?? null
|
|
10715
10839
|
},
|
|
10840
|
+
hook_trust: hookTrust,
|
|
10716
10841
|
components,
|
|
10717
10842
|
issues,
|
|
10718
10843
|
explanation: "Each host session is tracked independently. SessionStart and the prompt hook must run first; tool and completion observations appear after those lifecycle events become eligible."
|
|
10719
10844
|
};
|
|
10720
10845
|
}
|
|
10846
|
+
function normalizedPluginHookTrust(value) {
|
|
10847
|
+
if (!isRecord3(value)) return null;
|
|
10848
|
+
const status = value.status;
|
|
10849
|
+
if (status !== "trusted" && status !== "review_required" && status !== "unavailable") {
|
|
10850
|
+
return null;
|
|
10851
|
+
}
|
|
10852
|
+
const allowedEvents = /* @__PURE__ */ new Set([
|
|
10853
|
+
"SessionStart",
|
|
10854
|
+
"UserPromptSubmit",
|
|
10855
|
+
"PostToolUse",
|
|
10856
|
+
"Stop"
|
|
10857
|
+
]);
|
|
10858
|
+
const allowedStatuses = /* @__PURE__ */ new Set([
|
|
10859
|
+
"managed",
|
|
10860
|
+
"missing",
|
|
10861
|
+
"modified",
|
|
10862
|
+
"trusted",
|
|
10863
|
+
"unknown",
|
|
10864
|
+
"untrusted"
|
|
10865
|
+
]);
|
|
10866
|
+
const reviewEvents = Array.isArray(value.review_events) ? [...new Set(value.review_events)].filter(
|
|
10867
|
+
(event) => typeof event === "string" && allowedEvents.has(event)
|
|
10868
|
+
).slice(0, 4) : [];
|
|
10869
|
+
const hooks = Array.isArray(value.hooks) ? value.hooks.filter(isRecord3).filter(
|
|
10870
|
+
(entry) => typeof entry.event === "string" && allowedEvents.has(entry.event)
|
|
10871
|
+
).slice(0, 4).map((entry) => ({
|
|
10872
|
+
event: String(entry.event),
|
|
10873
|
+
enabled: entry.enabled === true,
|
|
10874
|
+
trust_status: typeof entry.trust_status === "string" && allowedStatuses.has(entry.trust_status) ? entry.trust_status : "unknown"
|
|
10875
|
+
})) : [];
|
|
10876
|
+
return {
|
|
10877
|
+
status,
|
|
10878
|
+
checked_at: typeof value.checked_at === "string" && Number.isFinite(Date.parse(value.checked_at)) ? new Date(value.checked_at).toISOString() : null,
|
|
10879
|
+
review_events: reviewEvents,
|
|
10880
|
+
hooks
|
|
10881
|
+
};
|
|
10882
|
+
}
|
|
10721
10883
|
function apiDestinationStatus(baseUrl, source) {
|
|
10722
10884
|
const normalized = baseUrl.replace(/\/+$/, "");
|
|
10723
10885
|
return {
|
|
@@ -10921,7 +11083,7 @@ async function checkClientUpdate(input) {
|
|
|
10921
11083
|
// src/server.ts
|
|
10922
11084
|
var MAX_REMOTE_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
10923
11085
|
var DOCTOR_PROBE_TIMEOUT_MS = 7500;
|
|
10924
|
-
var SERVER_VERSION = true ? "0.1.
|
|
11086
|
+
var SERVER_VERSION = true ? "0.1.63" : "0.0.0-dev";
|
|
10925
11087
|
var tools = toolDefinitions;
|
|
10926
11088
|
var doctorCliRequested = process.argv[2] === "doctor";
|
|
10927
11089
|
var inputBuffer = Buffer.alloc(0);
|
|
@@ -10931,6 +11093,7 @@ var connectedClientInfo = null;
|
|
|
10931
11093
|
var cachedValueProofKeys = null;
|
|
10932
11094
|
var PRINCIPAL_SESSION_REQUIRED_TOOLS = /* @__PURE__ */ new Set([
|
|
10933
11095
|
"record_preference",
|
|
11096
|
+
"submit_preference_compatibility_feedback",
|
|
10934
11097
|
"link_current_installation"
|
|
10935
11098
|
]);
|
|
10936
11099
|
var PRINCIPAL_SESSION_AWAITED_TOOLS = /* @__PURE__ */ new Set([
|
|
@@ -11730,6 +11893,9 @@ async function callRemembrance(definition, rawArguments, options = {}) {
|
|
|
11730
11893
|
if (refreshed && refreshed !== sentPrincipalSession) {
|
|
11731
11894
|
headers["x-remembrance-principal-session"] = refreshed;
|
|
11732
11895
|
({ response, body } = await execute());
|
|
11896
|
+
} else if (!PRINCIPAL_SESSION_REQUIRED_TOOLS.has(definition.name)) {
|
|
11897
|
+
delete headers["x-remembrance-principal-session"];
|
|
11898
|
+
({ response, body } = await execute());
|
|
11733
11899
|
}
|
|
11734
11900
|
}
|
|
11735
11901
|
} catch (error) {
|
|
@@ -11775,7 +11941,7 @@ async function localProjectKey() {
|
|
|
11775
11941
|
function principalSessionAuthenticationFailed(response, body) {
|
|
11776
11942
|
if (response.status !== 401 && response.status !== 403) return false;
|
|
11777
11943
|
if (!isRecord4(body) || typeof body.error !== "string") return false;
|
|
11778
|
-
return /principal session|economics session|installation session/i.test(
|
|
11944
|
+
return /principal session|economics session|installation session|agent principal|installation principal/i.test(
|
|
11779
11945
|
body.error
|
|
11780
11946
|
);
|
|
11781
11947
|
}
|
|
@@ -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
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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.
|
|
125
|
-
|
|
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),
|