@remembrance-ai/opencode-plugin 0.1.66 → 0.1.71
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 +32 -2
- package/opencode.json +2 -2
- package/package.json +2 -1
- package/scripts/hook-core.mjs +591 -60
- package/servers/remembrance-mcp.mjs +2917 -66
- package/skills/remembrancer/SKILL.md +71 -2
- package/skills/remembrancer/references/identity-preferences-topology.md +4 -1
- package/skills/remembrancer/references/remembrance-setup.md +72 -0
package/scripts/hook-core.mjs
CHANGED
|
@@ -54,13 +54,15 @@ const MAX_SUMMARY_CHARS = 1200;
|
|
|
54
54
|
const MAX_CONTEXT_CHARS = 4000;
|
|
55
55
|
const MAX_CONTEXT_FIELD_CHARS = 280;
|
|
56
56
|
const MAX_DIRECTIVE_CHARS = 900;
|
|
57
|
+
const MAX_PREFERENCE_CONTEXT_CHARS = 520;
|
|
58
|
+
const MAX_MANDATORY_PREFERENCE_CONTEXT_CHARS = 1200;
|
|
57
59
|
const VALUE_EPISODE_MARKER_LIMIT = 20;
|
|
58
60
|
const VALUE_EPISODE_MARKER_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
59
61
|
const DIRECT_SELECTION_MARKER_LIMIT = 20;
|
|
60
62
|
const DIRECT_SELECTION_MARKER_TTL_MS = 24 * 60 * 60 * 1000;
|
|
61
63
|
const PREFERENCE_EVIDENCE_SKILL_LIMIT = 2;
|
|
62
64
|
const PREFERENCE_EVIDENCE_SETTING_LIMIT = 4;
|
|
63
|
-
const PREFERENCE_EVIDENCE_CONTEXT_CHARS =
|
|
65
|
+
const PREFERENCE_EVIDENCE_CONTEXT_CHARS = 520;
|
|
64
66
|
const EFFECTIVE_PREFERENCE_SOURCES = new Set([
|
|
65
67
|
"mandatory_org",
|
|
66
68
|
"explicit_task",
|
|
@@ -89,6 +91,7 @@ const MAX_LOCAL_CONFIG_BYTES = 64 * 1024;
|
|
|
89
91
|
const MAX_LOCAL_HEALTH_MARKER_BYTES = 16 * 1024;
|
|
90
92
|
const MAX_LOCAL_PLUGIN_ALERT_BYTES = 16 * 1024;
|
|
91
93
|
const MAX_LOCAL_CLIENT_UPDATE_BYTES = 16 * 1024;
|
|
94
|
+
const MAX_PRIVATE_LESSON_ENVELOPE_BYTES = 64 * 1024;
|
|
92
95
|
const PLUGIN_ALERT_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
93
96
|
const PLUGIN_ALERT_LIMIT = 32;
|
|
94
97
|
const CLIENT_UPDATE_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
@@ -98,6 +101,10 @@ const MAX_LOCAL_PRINCIPAL_SESSION_BYTES = 16 * 1024;
|
|
|
98
101
|
const principalSessionWarmups = new Map();
|
|
99
102
|
export const HOST_POLICY_ALERT_TEXT =
|
|
100
103
|
"Remembrance was blocked by host policy before reaching Remembrance. Nothing was sent. Querying remains available.";
|
|
104
|
+
export const PRIVATE_LESSON_HOST_POLICY_ALERT_TEXT =
|
|
105
|
+
"Remembrance was blocked by host policy before reaching Remembrance. No lesson or candidate content was sent. The private draft remains on this device and will be submitted after authorization. Querying remains available.";
|
|
106
|
+
export const PRIVATE_LESSON_AUTHORIZATION_REQUEST_TEXT =
|
|
107
|
+
"Allow Remembrance to submit this retained private lesson to your organization?";
|
|
101
108
|
const PLUGIN_HEALTH_COMPONENTS = new Set([
|
|
102
109
|
"session_start",
|
|
103
110
|
"prompt_hook",
|
|
@@ -869,14 +876,10 @@ export function formatContext(
|
|
|
869
876
|
"Organization policy: use organization skills only. Do not use public skills or bundled public skill references, including as an offline fallback.",
|
|
870
877
|
);
|
|
871
878
|
}
|
|
872
|
-
const
|
|
879
|
+
const effectivePreferenceLines = effectivePreferencesContextLines(
|
|
873
880
|
body?.effective_preferences,
|
|
874
881
|
);
|
|
875
|
-
|
|
876
|
-
lines.push(
|
|
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.`,
|
|
878
|
-
);
|
|
879
|
-
}
|
|
882
|
+
lines.push(...effectivePreferenceLines);
|
|
880
883
|
const queryId = body?.query_feedback?.query_id ?? body?.query_id;
|
|
881
884
|
const highMatch = highMatchFromResponse(body);
|
|
882
885
|
if (queryId) {
|
|
@@ -1381,6 +1384,13 @@ export function emptyQueryContext(reason = "trigger_match") {
|
|
|
1381
1384
|
// the current credential is allowed to use.
|
|
1382
1385
|
export async function runPromptHook(prompt, options = {}) {
|
|
1383
1386
|
const env = options.env ?? process.env;
|
|
1387
|
+
// This is the master switch for prompt-hook automation. Check it before
|
|
1388
|
+
// preference detection, session warming, or any network-capable work.
|
|
1389
|
+
if (disabled(env.REMEMBRANCE_AUTO_QUERY)) {
|
|
1390
|
+
debugLog(env, "disabled", {}, options);
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
const privateLessonRecovery = privateLessonLifecycleRecoveryInstruction(env);
|
|
1384
1394
|
const includeSharedConfigCredentialNotice =
|
|
1385
1395
|
options.includeSharedConfigCredentialNotice !== false;
|
|
1386
1396
|
const redacted = redactPrompt(String(prompt ?? ""));
|
|
@@ -1395,17 +1405,6 @@ export async function runPromptHook(prompt, options = {}) {
|
|
|
1395
1405
|
userAgent: options.userAgent,
|
|
1396
1406
|
projectPath: options.projectPath,
|
|
1397
1407
|
}).catch(() => 0);
|
|
1398
|
-
if (disabled(env.REMEMBRANCE_AUTO_QUERY)) {
|
|
1399
|
-
debugLog(env, "disabled", {}, options);
|
|
1400
|
-
return preferenceCapture
|
|
1401
|
-
? {
|
|
1402
|
-
consumed: false,
|
|
1403
|
-
context: preferenceCapture,
|
|
1404
|
-
eligible: false,
|
|
1405
|
-
reason: "preference_capture",
|
|
1406
|
-
}
|
|
1407
|
-
: null;
|
|
1408
|
-
}
|
|
1409
1408
|
const decision = shouldQueryPrompt(redacted);
|
|
1410
1409
|
if (!decision.likely_match) {
|
|
1411
1410
|
if (isContextualContinuationPrompt(redacted)) {
|
|
@@ -1420,7 +1419,10 @@ export async function runPromptHook(prompt, options = {}) {
|
|
|
1420
1419
|
return {
|
|
1421
1420
|
consumed: false,
|
|
1422
1421
|
context: appendPreferenceCapture(
|
|
1423
|
-
|
|
1422
|
+
appendPrivateLessonLifecycleRecovery(
|
|
1423
|
+
continuationQueryContext(directive),
|
|
1424
|
+
privateLessonRecovery,
|
|
1425
|
+
),
|
|
1424
1426
|
preferenceCapture,
|
|
1425
1427
|
),
|
|
1426
1428
|
directive,
|
|
@@ -1431,11 +1433,22 @@ export async function runPromptHook(prompt, options = {}) {
|
|
|
1431
1433
|
if (preferenceCapture) {
|
|
1432
1434
|
return {
|
|
1433
1435
|
consumed: false,
|
|
1434
|
-
context:
|
|
1436
|
+
context: appendPrivateLessonLifecycleRecovery(
|
|
1437
|
+
preferenceCapture,
|
|
1438
|
+
privateLessonRecovery,
|
|
1439
|
+
),
|
|
1435
1440
|
eligible: false,
|
|
1436
1441
|
reason: "preference_capture",
|
|
1437
1442
|
};
|
|
1438
1443
|
}
|
|
1444
|
+
if (privateLessonRecovery) {
|
|
1445
|
+
return {
|
|
1446
|
+
consumed: false,
|
|
1447
|
+
context: privateLessonRecovery,
|
|
1448
|
+
eligible: false,
|
|
1449
|
+
reason: "private_lesson_lifecycle_recovery",
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1439
1452
|
debugLog(env, "skip", { reason: decision.reason }, options);
|
|
1440
1453
|
return null;
|
|
1441
1454
|
}
|
|
@@ -1460,9 +1473,15 @@ export async function runPromptHook(prompt, options = {}) {
|
|
|
1460
1473
|
return {
|
|
1461
1474
|
consumed: false,
|
|
1462
1475
|
context: appendPreferenceCapture(
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1476
|
+
appendPrivateLessonLifecycleRecovery(
|
|
1477
|
+
includeSharedConfigCredentialNotice
|
|
1478
|
+
? withSharedConfigCredentialNotice(
|
|
1479
|
+
unavailableQueryContext(env),
|
|
1480
|
+
env,
|
|
1481
|
+
)
|
|
1482
|
+
: unavailableQueryContext(env),
|
|
1483
|
+
privateLessonRecovery,
|
|
1484
|
+
),
|
|
1466
1485
|
preferenceCapture,
|
|
1467
1486
|
),
|
|
1468
1487
|
eligible: true,
|
|
@@ -1473,14 +1492,17 @@ export async function runPromptHook(prompt, options = {}) {
|
|
|
1473
1492
|
consumed: true,
|
|
1474
1493
|
matched: queryResponseHasMatches(response),
|
|
1475
1494
|
context: appendPreferenceCapture(
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1495
|
+
appendPrivateLessonLifecycleRecovery(
|
|
1496
|
+
includeSharedConfigCredentialNotice
|
|
1497
|
+
? withSharedConfigCredentialNotice(
|
|
1498
|
+
formatContext(response, decision.reason, limitFromEnv(env)) ??
|
|
1499
|
+
emptyQueryContext(decision.reason),
|
|
1500
|
+
env,
|
|
1501
|
+
)
|
|
1502
|
+
: (formatContext(response, decision.reason, limitFromEnv(env)) ??
|
|
1503
|
+
emptyQueryContext(decision.reason)),
|
|
1504
|
+
privateLessonRecovery,
|
|
1505
|
+
),
|
|
1484
1506
|
preferenceCapture,
|
|
1485
1507
|
),
|
|
1486
1508
|
eligible: true,
|
|
@@ -1494,6 +1516,10 @@ function appendPreferenceCapture(context, directive) {
|
|
|
1494
1516
|
return directive ? `${context}\n\n${directive}` : context;
|
|
1495
1517
|
}
|
|
1496
1518
|
|
|
1519
|
+
function appendPrivateLessonLifecycleRecovery(context, instruction) {
|
|
1520
|
+
return instruction ? `${context}\n\n${instruction}` : context;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1497
1523
|
export function queryResponseHasMatches(response) {
|
|
1498
1524
|
const body = responseBodyFromToolResponse(response);
|
|
1499
1525
|
const skills = Array.isArray(body?.skills)
|
|
@@ -1529,6 +1555,7 @@ const TASK_ELIGIBILITY_MARKERS =
|
|
|
1529
1555
|
|
|
1530
1556
|
const CONTRIBUTION_SUBMITTED_PATTERNS = [
|
|
1531
1557
|
/\brpub_[a-f0-9]{16,}\b/i,
|
|
1558
|
+
/\baccepted_private_candidate\b/i,
|
|
1532
1559
|
/\bsubmitted (?:it|this|the lesson|the failure|the remembrance) (?:to|as) Remembrance\b/i,
|
|
1533
1560
|
/\bsubmit_remembrance\b/i,
|
|
1534
1561
|
/\bREMEMBRANCE_SUBMISSION_PAYLOAD\b/,
|
|
@@ -1575,6 +1602,7 @@ export function contributionReason(
|
|
|
1575
1602
|
signal = null,
|
|
1576
1603
|
highMatch = null,
|
|
1577
1604
|
directSelection = null,
|
|
1605
|
+
options = {},
|
|
1578
1606
|
) {
|
|
1579
1607
|
const signalLine = signal
|
|
1580
1608
|
? `High-value lesson detected: ${signal}. Capture it unless it was already submitted.`
|
|
@@ -1589,6 +1617,12 @@ export function contributionReason(
|
|
|
1589
1617
|
? [directSelection]
|
|
1590
1618
|
: []
|
|
1591
1619
|
).slice(0, DIRECT_SELECTION_MARKER_LIMIT);
|
|
1620
|
+
const privateLessonGuidance = options.privateLessonAuthorized
|
|
1621
|
+
? privateLessonCompletionGuidance({
|
|
1622
|
+
includeLifecycleRecovery: options.includeLifecycleRecovery !== false,
|
|
1623
|
+
env: options.env,
|
|
1624
|
+
})
|
|
1625
|
+
: null;
|
|
1592
1626
|
if (directSelections.length > 0) {
|
|
1593
1627
|
const preferenceEvidenceLine =
|
|
1594
1628
|
directPreferenceCompatibilityInstruction(directSelections);
|
|
@@ -1609,6 +1643,7 @@ export function contributionReason(
|
|
|
1609
1643
|
"Call submit_feedback once for each listed skill with useful true or false and a concise post-use lesson.",
|
|
1610
1644
|
preferenceEvidenceLine,
|
|
1611
1645
|
"If submit_feedback returns next_step.submit_remembrance_payload, submit it when the redacted lesson is reusable.",
|
|
1646
|
+
privateLessonGuidance,
|
|
1612
1647
|
routineContributionVisibilityInstruction(),
|
|
1613
1648
|
]
|
|
1614
1649
|
.filter(Boolean)
|
|
@@ -1618,7 +1653,10 @@ export function contributionReason(
|
|
|
1618
1653
|
"Before finishing, silently close the pending Remembrance loop using this session's query/result IDs.",
|
|
1619
1654
|
signalLine,
|
|
1620
1655
|
highMatchLine,
|
|
1621
|
-
|
|
1656
|
+
options.privateLessonAuthorized
|
|
1657
|
+
? "Call submit_query_feedback for pending query fit and submit_feedback only for used results. Honor an exact next_step.submit_remembrance_payload returned by submit_feedback; route a compact organization evidence lesson through the private lesson lane below. If no skill fit and you built a complete reusable skill or playbook, default to propose_private_skill; use propose_skill_idea only when a public candidate is explicitly appropriate."
|
|
1658
|
+
: "Call submit_query_feedback for pending query fit, submit_feedback only for used results, and submit_remembrance only for a reusable redacted lesson. If no skill fit and you built a complete reusable skill or playbook, default to propose_private_skill; use propose_skill_idea only when a public candidate is explicitly appropriate. A supplied invalid/inactive key fails with 401 and an insufficient key with 403; neither creates a candidate.",
|
|
1659
|
+
privateLessonGuidance,
|
|
1622
1660
|
routineContributionVisibilityInstruction(),
|
|
1623
1661
|
]
|
|
1624
1662
|
.filter(Boolean)
|
|
@@ -1651,10 +1689,30 @@ function directPreferenceCompatibilityInstruction(selections) {
|
|
|
1651
1689
|
}
|
|
1652
1690
|
|
|
1653
1691
|
function routineContributionVisibilityInstruction() {
|
|
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.
|
|
1692
|
+
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. For a host block unrelated to a private-lesson submit/retry action, say exactly: "${HOST_POLICY_ALERT_TEXT}" and do not retry through another transport. Redact secrets and proprietary content.`;
|
|
1655
1693
|
}
|
|
1656
1694
|
|
|
1657
|
-
export function
|
|
1695
|
+
export function privateLessonCompletionGuidance({
|
|
1696
|
+
includeLifecycleRecovery = true,
|
|
1697
|
+
env = process.env,
|
|
1698
|
+
} = {}) {
|
|
1699
|
+
const recovery = includeLifecycleRecovery
|
|
1700
|
+
? privateLessonLifecycleRecoveryInstruction(env)
|
|
1701
|
+
: null;
|
|
1702
|
+
return [
|
|
1703
|
+
"For a new compact failure, correction, or evidence lesson that is not already a complete skill, silently generalize it without raw repository content, then call prepare_private_lesson_candidate. If it returns next_action, immediately call submit_private_lesson_candidate with exactly its draft_id; do not substitute submit_remembrance, REST, or another transport.",
|
|
1704
|
+
"If the work produced a complete reusable procedure, playbook, or actionable instructions, follow the separate skill-proposal guidance instead; do not compress a finished skill into the private lesson contract.",
|
|
1705
|
+
"Let the host's exact submit_private_lesson_candidate approval be the only authorization prompt. If the host cannot present it, ask once: \"" +
|
|
1706
|
+
PRIVATE_LESSON_AUTHORIZATION_REQUEST_TEXT +
|
|
1707
|
+
'\"',
|
|
1708
|
+
`If the host denies the action, say exactly: "${PRIVATE_LESSON_HOST_POLICY_ALERT_TEXT}" Do not retry that draft through another transport. Never auto-delete a draft.`,
|
|
1709
|
+
recovery,
|
|
1710
|
+
]
|
|
1711
|
+
.filter(Boolean)
|
|
1712
|
+
.join("\n");
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
export function taskClosureReason(signal = null, options = {}) {
|
|
1658
1716
|
const signalLine = signal
|
|
1659
1717
|
? `High-value lesson detected: ${signal}. Capture it unless it was already submitted.`
|
|
1660
1718
|
: null;
|
|
@@ -1662,6 +1720,9 @@ export function taskClosureReason(signal = null) {
|
|
|
1662
1720
|
"Before finishing, silently recover the pending Remembrance query for this reusable task using the full conversation; no completed Remembrance query/use was recorded.",
|
|
1663
1721
|
signalLine,
|
|
1664
1722
|
"Call query_skills with a redacted full-context summary, evaluate the returned matches, then close any warranted submit_query_feedback, submit_feedback, or submit_remembrance action before responding.",
|
|
1723
|
+
options.privateLessonAuthorized
|
|
1724
|
+
? privateLessonCompletionGuidance({ env: options.env })
|
|
1725
|
+
: null,
|
|
1665
1726
|
routineContributionVisibilityInstruction(),
|
|
1666
1727
|
]
|
|
1667
1728
|
.filter(Boolean)
|
|
@@ -1702,7 +1763,10 @@ export function hostPolicyAlertWasReported(input) {
|
|
|
1702
1763
|
input?.message ??
|
|
1703
1764
|
"",
|
|
1704
1765
|
);
|
|
1705
|
-
return
|
|
1766
|
+
return (
|
|
1767
|
+
value.includes(HOST_POLICY_ALERT_TEXT) ||
|
|
1768
|
+
value.includes(PRIVATE_LESSON_HOST_POLICY_ALERT_TEXT)
|
|
1769
|
+
);
|
|
1706
1770
|
}
|
|
1707
1771
|
|
|
1708
1772
|
// Pure decision function (unit-tested): compare completed registry use and task
|
|
@@ -1745,19 +1809,36 @@ export function decideStop(input, options = {}) {
|
|
|
1745
1809
|
const hasUnclosedEligibility =
|
|
1746
1810
|
eligibilityCount > useCount && newDirectSelections.length === 0;
|
|
1747
1811
|
const highValueSignal = detectHighValueLessonSignal(input);
|
|
1748
|
-
|
|
1812
|
+
const privateLessonAuthorized = organizationPrivateLessonAuthorized(env);
|
|
1813
|
+
const privateLessonRecovery = privateLessonAuthorized
|
|
1814
|
+
? privateLessonLifecycleRecoveryInstruction(env)
|
|
1815
|
+
: null;
|
|
1816
|
+
if (
|
|
1817
|
+
newDirectSelections.length > 0 &&
|
|
1818
|
+
feedbackDirectSelections.length === 0 &&
|
|
1819
|
+
!privateLessonRecovery
|
|
1820
|
+
) {
|
|
1749
1821
|
return { allow: true, why: "direct_feedback_unavailable" };
|
|
1750
1822
|
}
|
|
1751
|
-
if (engagementCount === 0 && !highValueSignal) {
|
|
1823
|
+
if (engagementCount === 0 && !highValueSignal && !privateLessonRecovery) {
|
|
1752
1824
|
return { allow: true, why: "registry_not_used" };
|
|
1753
1825
|
}
|
|
1754
1826
|
if (
|
|
1827
|
+
privateLessonRecovery &&
|
|
1828
|
+
promptedCount > 0 &&
|
|
1755
1829
|
engagementCount <= promptedCount &&
|
|
1756
|
-
newDirectSelections.length === 0 &&
|
|
1757
1830
|
!highValueSignal
|
|
1758
1831
|
) {
|
|
1759
1832
|
return { allow: true, why: "no_new_usage" };
|
|
1760
1833
|
}
|
|
1834
|
+
if (
|
|
1835
|
+
engagementCount <= promptedCount &&
|
|
1836
|
+
newDirectSelections.length === 0 &&
|
|
1837
|
+
!highValueSignal &&
|
|
1838
|
+
!privateLessonRecovery
|
|
1839
|
+
) {
|
|
1840
|
+
return { allow: true, why: "no_new_usage" };
|
|
1841
|
+
}
|
|
1761
1842
|
if (
|
|
1762
1843
|
highValueSignal &&
|
|
1763
1844
|
promptedCount > 0 &&
|
|
@@ -1771,17 +1852,36 @@ export function decideStop(input, options = {}) {
|
|
|
1771
1852
|
? "prompt_high_value_lesson_contribution"
|
|
1772
1853
|
: hasUnclosedEligibility
|
|
1773
1854
|
? "prompt_task_closure"
|
|
1774
|
-
:
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1855
|
+
: privateLessonRecovery && engagementCount === 0
|
|
1856
|
+
? "prompt_private_lesson_retry"
|
|
1857
|
+
: "prompt_contribution",
|
|
1858
|
+
reason:
|
|
1859
|
+
privateLessonRecovery && engagementCount === 0 && !highValueSignal
|
|
1860
|
+
? [
|
|
1861
|
+
privateLessonRecovery,
|
|
1862
|
+
routineContributionVisibilityInstruction(),
|
|
1863
|
+
].join("\n")
|
|
1864
|
+
: hasUnclosedEligibility
|
|
1865
|
+
? taskClosureReason(highValueSignal, {
|
|
1866
|
+
env,
|
|
1867
|
+
privateLessonAuthorized,
|
|
1868
|
+
})
|
|
1869
|
+
: contributionReason(
|
|
1870
|
+
highValueSignal,
|
|
1871
|
+
highMatch,
|
|
1872
|
+
feedbackDirectSelections.length > 0
|
|
1873
|
+
? feedbackDirectSelections
|
|
1874
|
+
: null,
|
|
1875
|
+
{
|
|
1876
|
+
env,
|
|
1877
|
+
privateLessonAuthorized,
|
|
1878
|
+
includeLifecycleRecovery: true,
|
|
1879
|
+
},
|
|
1880
|
+
),
|
|
1881
|
+
useCount:
|
|
1882
|
+
highValueSignal || privateLessonRecovery
|
|
1883
|
+
? Math.max(engagementCount, promptedCount + 1, 1)
|
|
1884
|
+
: engagementCount,
|
|
1785
1885
|
};
|
|
1786
1886
|
}
|
|
1787
1887
|
|
|
@@ -2134,6 +2234,8 @@ export function toolResponseIndicatesFailure(value, depth = 0) {
|
|
|
2134
2234
|
value.ok === false ||
|
|
2135
2235
|
value.isError === true ||
|
|
2136
2236
|
value.is_error === true ||
|
|
2237
|
+
value.status === "not_submitted" ||
|
|
2238
|
+
value.status === "held_safety" ||
|
|
2137
2239
|
(typeof value.status === "number" && value.status >= 400)
|
|
2138
2240
|
) {
|
|
2139
2241
|
return true;
|
|
@@ -2486,6 +2588,16 @@ export function clearHighMatchSurfaceIfOpened(
|
|
|
2486
2588
|
const normalizedTool = String(toolName ?? "")
|
|
2487
2589
|
.trim()
|
|
2488
2590
|
.toLowerCase();
|
|
2591
|
+
if (
|
|
2592
|
+
normalizedTool.endsWith("submit_private_lesson_candidate") ||
|
|
2593
|
+
normalizedTool.endsWith("retry_private_lesson_candidate")
|
|
2594
|
+
) {
|
|
2595
|
+
// The PostToolUse adapter calls this function only after a successful tool
|
|
2596
|
+
// response. Clear the content-free denial suppression so future retained
|
|
2597
|
+
// drafts can resume normal lifecycle recovery after exact-action approval.
|
|
2598
|
+
clearPrivateLessonHostPolicyDenials(env);
|
|
2599
|
+
return false;
|
|
2600
|
+
}
|
|
2489
2601
|
const targetType = normalizedTool.endsWith("get_resource")
|
|
2490
2602
|
? "resource"
|
|
2491
2603
|
: normalizedTool.endsWith("get_skill")
|
|
@@ -2903,6 +3015,125 @@ export function contributeDisabled(value) {
|
|
|
2903
3015
|
return /^(0|false|no)$/i.test(String(value ?? "").trim());
|
|
2904
3016
|
}
|
|
2905
3017
|
|
|
3018
|
+
function organizationPrivateLessonAuthorized(env = process.env) {
|
|
3019
|
+
if (contributeDisabled(env?.REMEMBRANCE_AUTO_CONTRIBUTE)) return false;
|
|
3020
|
+
const credential = resolveApiCredential(env);
|
|
3021
|
+
return Boolean(
|
|
3022
|
+
credential.apiKey && !isUnusableConfigurationSource(credential.source),
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
function privateLessonOutboxDirectoryForHook(env = process.env) {
|
|
3027
|
+
const stateRoot = String(env?.XDG_STATE_HOME ?? "").trim();
|
|
3028
|
+
return join(
|
|
3029
|
+
stateRoot || join(homedir(), ".local", "state"),
|
|
3030
|
+
"remembrance",
|
|
3031
|
+
"private-lessons",
|
|
3032
|
+
);
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
export function readPrivateLessonLifecycleState(env = process.env) {
|
|
3036
|
+
if (!organizationPrivateLessonAuthorized(env)) return null;
|
|
3037
|
+
const directory = privateLessonOutboxDirectoryForHook(env);
|
|
3038
|
+
try {
|
|
3039
|
+
const directoryMetadata = lstatSync(directory);
|
|
3040
|
+
if (
|
|
3041
|
+
directoryMetadata.isSymbolicLink() ||
|
|
3042
|
+
!directoryMetadata.isDirectory() ||
|
|
3043
|
+
(isPosixHookRuntime() && (directoryMetadata.mode & 0o077) !== 0)
|
|
3044
|
+
) {
|
|
3045
|
+
return null;
|
|
3046
|
+
}
|
|
3047
|
+
assertCurrentHookUser(directoryMetadata.uid);
|
|
3048
|
+
const entries = readdirSync(directory, { withFileTypes: true });
|
|
3049
|
+
const counts = {
|
|
3050
|
+
ready: 0,
|
|
3051
|
+
retry_scheduled: 0,
|
|
3052
|
+
hold_telemetry_pending: 0,
|
|
3053
|
+
hold_telemetry_retry_scheduled: 0,
|
|
3054
|
+
};
|
|
3055
|
+
for (const entry of entries) {
|
|
3056
|
+
if (!entry.name.endsWith(".lesson.json")) continue;
|
|
3057
|
+
if (entry.isSymbolicLink() || !entry.isFile()) return null;
|
|
3058
|
+
const expectedDraftId = entry.name.slice(0, -".lesson.json".length);
|
|
3059
|
+
if (!/^pld_[A-Za-z0-9_-]{8,96}$/.test(expectedDraftId)) return null;
|
|
3060
|
+
const parsed = JSON.parse(
|
|
3061
|
+
readSecureHookFile(
|
|
3062
|
+
join(directory, entry.name),
|
|
3063
|
+
MAX_PRIVATE_LESSON_ENVELOPE_BYTES,
|
|
3064
|
+
),
|
|
3065
|
+
);
|
|
3066
|
+
if (
|
|
3067
|
+
parsed?.version !== "private-lesson-outbox-envelope-v1" ||
|
|
3068
|
+
parsed?.draft_id !== expectedDraftId ||
|
|
3069
|
+
![
|
|
3070
|
+
"ready",
|
|
3071
|
+
"awaiting_authorization",
|
|
3072
|
+
"retry_scheduled",
|
|
3073
|
+
"held_safety",
|
|
3074
|
+
"submitted",
|
|
3075
|
+
"superseded_redactor",
|
|
3076
|
+
].includes(parsed?.state) ||
|
|
3077
|
+
![
|
|
3078
|
+
"not_required",
|
|
3079
|
+
"pending",
|
|
3080
|
+
"awaiting_authorization",
|
|
3081
|
+
"retry_scheduled",
|
|
3082
|
+
"submitted",
|
|
3083
|
+
"disabled",
|
|
3084
|
+
].includes(parsed?.hold_telemetry_status)
|
|
3085
|
+
) {
|
|
3086
|
+
return null;
|
|
3087
|
+
}
|
|
3088
|
+
if (parsed.state === "ready" || parsed.state === "retry_scheduled") {
|
|
3089
|
+
counts[parsed.state] += 1;
|
|
3090
|
+
}
|
|
3091
|
+
if (
|
|
3092
|
+
privateLessonHealthReportingEnabledForHook(env) &&
|
|
3093
|
+
parsed.state === "held_safety" &&
|
|
3094
|
+
parsed.hold_telemetry_status === "pending"
|
|
3095
|
+
) {
|
|
3096
|
+
counts.hold_telemetry_pending += 1;
|
|
3097
|
+
}
|
|
3098
|
+
if (
|
|
3099
|
+
privateLessonHealthReportingEnabledForHook(env) &&
|
|
3100
|
+
parsed.state === "held_safety" &&
|
|
3101
|
+
parsed.hold_telemetry_status === "retry_scheduled"
|
|
3102
|
+
) {
|
|
3103
|
+
counts.hold_telemetry_retry_scheduled += 1;
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
if (Object.values(counts).every((count) => count === 0)) return null;
|
|
3107
|
+
return {
|
|
3108
|
+
...counts,
|
|
3109
|
+
host_denial_active: recentPrivateLessonHostDenial(env),
|
|
3110
|
+
};
|
|
3111
|
+
} catch {
|
|
3112
|
+
return null;
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
export function privateLessonLifecycleRecoveryInstruction(env = process.env) {
|
|
3117
|
+
const state = readPrivateLessonLifecycleState(env);
|
|
3118
|
+
if (!state || state.host_denial_active) return null;
|
|
3119
|
+
const pending =
|
|
3120
|
+
state.ready +
|
|
3121
|
+
state.retry_scheduled +
|
|
3122
|
+
state.hold_telemetry_pending +
|
|
3123
|
+
state.hold_telemetry_retry_scheduled;
|
|
3124
|
+
return [
|
|
3125
|
+
`Private lesson lifecycle recovery: ${pending} retained local draft${pending === 1 ? " needs" : "s need"} attention. Silently call inspect_private_lesson_outbox.`,
|
|
3126
|
+
"Call submit_private_lesson_candidate for each ready candidate or safety-held draft with pending content-free telemetry. Call retry_private_lesson_candidate only when the candidate or telemetry retry is due.",
|
|
3127
|
+
"Do not retry awaiting_authorization, disabled telemetry, submitted telemetry, or superseded-redactor drafts; do not use another transport, and never delete a draft automatically. Hide routine results.",
|
|
3128
|
+
].join("\n");
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
function privateLessonHealthReportingEnabledForHook(env = process.env) {
|
|
3132
|
+
return !/^(0|false|no)$/i.test(
|
|
3133
|
+
String(env?.REMEMBRANCE_HEALTH_REPORTING ?? "").trim(),
|
|
3134
|
+
);
|
|
3135
|
+
}
|
|
3136
|
+
|
|
2906
3137
|
function pluginHealthDir(env = process.env) {
|
|
2907
3138
|
if (env?.REMEMBRANCE_PLUGIN_HEALTH_DIR) {
|
|
2908
3139
|
return String(env.REMEMBRANCE_PLUGIN_HEALTH_DIR);
|
|
@@ -3447,6 +3678,10 @@ function runtimeDisplayName(runtime) {
|
|
|
3447
3678
|
}
|
|
3448
3679
|
|
|
3449
3680
|
function sanitizeEffectivePreferences(value) {
|
|
3681
|
+
return sanitizeEffectivePreferenceEntries(value).map((item) => item.text);
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
function sanitizeEffectivePreferenceEntries(value) {
|
|
3450
3685
|
if (!Array.isArray(value)) return [];
|
|
3451
3686
|
return value
|
|
3452
3687
|
.filter(
|
|
@@ -3471,10 +3706,85 @@ function sanitizeEffectivePreferences(value) {
|
|
|
3471
3706
|
const direction = item.strength === "avoid" ? "avoid" : "prefer";
|
|
3472
3707
|
const authority =
|
|
3473
3708
|
item.source === "mandatory_org" ? "required organization" : item.source;
|
|
3474
|
-
return
|
|
3709
|
+
return {
|
|
3710
|
+
mandatory: item.source === "mandatory_org",
|
|
3711
|
+
key: safeText(item.key, 48),
|
|
3712
|
+
value: safeText(item.value, 48),
|
|
3713
|
+
behavior,
|
|
3714
|
+
text: `${label} [${safeText(item.effect ?? "presentation", 24)}, ${safeText(authority ?? "preference", 32)}]: ${direction} ${behavior}`,
|
|
3715
|
+
};
|
|
3475
3716
|
});
|
|
3476
3717
|
}
|
|
3477
3718
|
|
|
3719
|
+
function effectivePreferencesContextLines(value) {
|
|
3720
|
+
const entries = sanitizeEffectivePreferenceEntries(value).sort(
|
|
3721
|
+
(left, right) => Number(right.mandatory) - Number(left.mandatory),
|
|
3722
|
+
);
|
|
3723
|
+
if (entries.length === 0) return [];
|
|
3724
|
+
const mandatory = entries.filter((entry) => entry.mandatory);
|
|
3725
|
+
const discretionary = entries.filter((entry) => !entry.mandatory);
|
|
3726
|
+
const mandatoryLine = mandatoryPreferenceContextLine(mandatory);
|
|
3727
|
+
const discretionaryLine = discretionaryPreferenceContextLine(discretionary);
|
|
3728
|
+
return [mandatoryLine, discretionaryLine].filter(Boolean);
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
function mandatoryPreferenceContextLine(entries) {
|
|
3732
|
+
if (entries.length === 0) return null;
|
|
3733
|
+
const prefix = "Required organization settings (apply all): ";
|
|
3734
|
+
const suffix =
|
|
3735
|
+
". These remain authoritative and cannot be weakened by task or personal preferences.";
|
|
3736
|
+
const separators = Math.max(0, entries.length - 1) * 2;
|
|
3737
|
+
const perEntryBudget = Math.max(
|
|
3738
|
+
48,
|
|
3739
|
+
Math.floor(
|
|
3740
|
+
(MAX_MANDATORY_PREFERENCE_CONTEXT_CHARS -
|
|
3741
|
+
prefix.length -
|
|
3742
|
+
suffix.length -
|
|
3743
|
+
separators) /
|
|
3744
|
+
entries.length,
|
|
3745
|
+
),
|
|
3746
|
+
);
|
|
3747
|
+
const body = entries
|
|
3748
|
+
.map((entry) =>
|
|
3749
|
+
safeText(
|
|
3750
|
+
`${entry.key}=${entry.value}: ${entry.behavior}`,
|
|
3751
|
+
perEntryBudget,
|
|
3752
|
+
),
|
|
3753
|
+
)
|
|
3754
|
+
.join("; ");
|
|
3755
|
+
return `${prefix}${body}${suffix}`;
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
function discretionaryPreferenceContextLine(entries) {
|
|
3759
|
+
if (entries.length === 0) return null;
|
|
3760
|
+
const prefix = "Apply these persisted discretionary preferences silently: ";
|
|
3761
|
+
const suffix =
|
|
3762
|
+
". Do not ask the user to reconfirm them. Current-task instructions may override these preferences. Never weaken applicability, safety, authorization, privacy, required skill steps, validation, or review.";
|
|
3763
|
+
const budget = Math.max(
|
|
3764
|
+
0,
|
|
3765
|
+
MAX_PREFERENCE_CONTEXT_CHARS - prefix.length - suffix.length,
|
|
3766
|
+
);
|
|
3767
|
+
const selected = [];
|
|
3768
|
+
let used = 0;
|
|
3769
|
+
for (const entry of entries) {
|
|
3770
|
+
const separator = selected.length > 0 ? "; " : "";
|
|
3771
|
+
if (used + separator.length + entry.text.length > budget) break;
|
|
3772
|
+
selected.push(entry);
|
|
3773
|
+
used += separator.length + entry.text.length;
|
|
3774
|
+
}
|
|
3775
|
+
const omitted = entries.slice(selected.length);
|
|
3776
|
+
let body = selected.map((entry) => entry.text).join("; ");
|
|
3777
|
+
if (omitted.length > 0) {
|
|
3778
|
+
const note = `; +${omitted.length} discretionary preference${omitted.length === 1 ? "" : "s"} omitted. Call get_effective_preferences when the omitted choices could affect the task`;
|
|
3779
|
+
while (selected.length > 0 && body.length + note.length > budget) {
|
|
3780
|
+
selected.pop();
|
|
3781
|
+
body = selected.map((entry) => entry.text).join("; ");
|
|
3782
|
+
}
|
|
3783
|
+
body = `${body || "Preferences available"}${note}`;
|
|
3784
|
+
}
|
|
3785
|
+
return safeText(`${prefix}${body}${suffix}`, MAX_PREFERENCE_CONTEXT_CHARS);
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3478
3788
|
function clientUpdatePath(surface, env = process.env) {
|
|
3479
3789
|
const normalized = normalizedPluginHealthSurface(surface);
|
|
3480
3790
|
return normalized ? join(clientUpdateDir(env), `${normalized}.json`) : null;
|
|
@@ -3531,9 +3841,10 @@ export async function checkForClientUpdate(
|
|
|
3531
3841
|
clearTimeout(timer);
|
|
3532
3842
|
}
|
|
3533
3843
|
}
|
|
3844
|
+
const release = clientSurfaceRelease(manifest, normalizedSurface);
|
|
3534
3845
|
if (
|
|
3535
|
-
!
|
|
3536
|
-
compareStableClientVersions(
|
|
3846
|
+
!release ||
|
|
3847
|
+
compareStableClientVersions(release.version, currentVersion) <= 0
|
|
3537
3848
|
) {
|
|
3538
3849
|
return null;
|
|
3539
3850
|
}
|
|
@@ -3544,10 +3855,10 @@ export async function checkForClientUpdate(
|
|
|
3544
3855
|
: "";
|
|
3545
3856
|
return {
|
|
3546
3857
|
current_version: currentVersion,
|
|
3547
|
-
latest_version:
|
|
3858
|
+
latest_version: release.version,
|
|
3548
3859
|
surface: normalizedSurface,
|
|
3549
3860
|
notice: [
|
|
3550
|
-
`Remembrance update available: installed ${currentVersion}, published ${
|
|
3861
|
+
`Remembrance update available: installed ${currentVersion}, published ${release.version}.`,
|
|
3551
3862
|
guidance.update,
|
|
3552
3863
|
command,
|
|
3553
3864
|
guidance.restart,
|
|
@@ -3621,14 +3932,81 @@ function parseClientReleaseManifest(value) {
|
|
|
3621
3932
|
) {
|
|
3622
3933
|
return null;
|
|
3623
3934
|
}
|
|
3935
|
+
const surfaceReleases = parseClientSurfaceReleases(value.surface_releases);
|
|
3936
|
+
if (value.surface_releases !== undefined && !surfaceReleases) return null;
|
|
3937
|
+
if (surfaceReleases) {
|
|
3938
|
+
const entries = Object.entries(surfaceReleases);
|
|
3939
|
+
if (entries.length === 0) return null;
|
|
3940
|
+
const newestVersion = entries.reduce(
|
|
3941
|
+
(latest, [, release]) =>
|
|
3942
|
+
compareStableClientVersions(release.version, latest) > 0
|
|
3943
|
+
? release.version
|
|
3944
|
+
: latest,
|
|
3945
|
+
entries[0][1].version,
|
|
3946
|
+
);
|
|
3947
|
+
const newestSurfaces = entries
|
|
3948
|
+
.filter(([, release]) => release.version === newestVersion)
|
|
3949
|
+
.map(([surface]) => surface);
|
|
3950
|
+
if (
|
|
3951
|
+
newestVersion !== value.latest_version ||
|
|
3952
|
+
newestSurfaces.length !== surfaces.length ||
|
|
3953
|
+
newestSurfaces.some((surface) => !surfaces.includes(surface))
|
|
3954
|
+
) {
|
|
3955
|
+
return null;
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3624
3958
|
return {
|
|
3625
3959
|
schema_version: "1",
|
|
3626
3960
|
latest_version: value.latest_version,
|
|
3627
3961
|
published_at: new Date(value.published_at).toISOString(),
|
|
3628
3962
|
surfaces,
|
|
3963
|
+
...(surfaceReleases ? { surface_releases: surfaceReleases } : {}),
|
|
3629
3964
|
};
|
|
3630
3965
|
}
|
|
3631
3966
|
|
|
3967
|
+
function parseClientSurfaceReleases(value) {
|
|
3968
|
+
if (value === undefined) return undefined;
|
|
3969
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
3970
|
+
const entries = Object.entries(value);
|
|
3971
|
+
if (
|
|
3972
|
+
entries.some(
|
|
3973
|
+
([surface, release]) =>
|
|
3974
|
+
!CLIENT_RELEASE_MANIFEST_SURFACES.has(surface) ||
|
|
3975
|
+
!release ||
|
|
3976
|
+
typeof release !== "object" ||
|
|
3977
|
+
Array.isArray(release) ||
|
|
3978
|
+
Object.keys(release).some(
|
|
3979
|
+
(key) => key !== "version" && key !== "published_at",
|
|
3980
|
+
) ||
|
|
3981
|
+
!parseStableClientVersion(release.version) ||
|
|
3982
|
+
typeof release.published_at !== "string" ||
|
|
3983
|
+
!isCanonicalClientReleaseTimestamp(release.published_at),
|
|
3984
|
+
)
|
|
3985
|
+
) {
|
|
3986
|
+
return null;
|
|
3987
|
+
}
|
|
3988
|
+
return Object.fromEntries(
|
|
3989
|
+
entries.map(([surface, release]) => [
|
|
3990
|
+
surface,
|
|
3991
|
+
{
|
|
3992
|
+
version: release.version,
|
|
3993
|
+
published_at: new Date(release.published_at).toISOString(),
|
|
3994
|
+
},
|
|
3995
|
+
]),
|
|
3996
|
+
);
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
function clientSurfaceRelease(manifest, surface) {
|
|
4000
|
+
const exact = manifest.surface_releases?.[surface];
|
|
4001
|
+
if (exact) return exact;
|
|
4002
|
+
return manifest.surfaces.includes(surface)
|
|
4003
|
+
? {
|
|
4004
|
+
version: manifest.latest_version,
|
|
4005
|
+
published_at: manifest.published_at,
|
|
4006
|
+
}
|
|
4007
|
+
: null;
|
|
4008
|
+
}
|
|
4009
|
+
|
|
3632
4010
|
function isCanonicalClientReleaseTimestamp(value) {
|
|
3633
4011
|
const timestamp = Date.parse(value);
|
|
3634
4012
|
return (
|
|
@@ -3672,6 +4050,12 @@ function pluginAlertSessionPath(surface, sessionId, env = process.env) {
|
|
|
3672
4050
|
|
|
3673
4051
|
function hostPolicyOperationClass(toolName) {
|
|
3674
4052
|
const normalized = String(toolName ?? "").toLowerCase();
|
|
4053
|
+
if (
|
|
4054
|
+
normalized.endsWith("submit_private_lesson_candidate") ||
|
|
4055
|
+
normalized.endsWith("retry_private_lesson_candidate")
|
|
4056
|
+
) {
|
|
4057
|
+
return "private_lesson";
|
|
4058
|
+
}
|
|
3675
4059
|
if (
|
|
3676
4060
|
normalized.endsWith("query_skills") ||
|
|
3677
4061
|
normalized.endsWith("list_skills") ||
|
|
@@ -3679,12 +4063,16 @@ function hostPolicyOperationClass(toolName) {
|
|
|
3679
4063
|
normalized.endsWith("get_resource") ||
|
|
3680
4064
|
normalized.endsWith("invoke_skill") ||
|
|
3681
4065
|
normalized.endsWith("get_effective_preferences") ||
|
|
4066
|
+
normalized.endsWith("get_private_lesson_policy") ||
|
|
4067
|
+
normalized.endsWith("inspect_private_lesson_outbox") ||
|
|
3682
4068
|
normalized.endsWith("run_connection_doctor") ||
|
|
3683
4069
|
normalized.endsWith("get_connection_status")
|
|
3684
4070
|
) {
|
|
3685
4071
|
return "query";
|
|
3686
4072
|
}
|
|
3687
4073
|
if (
|
|
4074
|
+
normalized.endsWith("prepare_private_lesson_candidate") ||
|
|
4075
|
+
normalized.endsWith("delete_private_lesson_candidate") ||
|
|
3688
4076
|
normalized.endsWith("propose_private_skill") ||
|
|
3689
4077
|
normalized.endsWith("queue_private_skill_import")
|
|
3690
4078
|
) {
|
|
@@ -3725,6 +4113,12 @@ function isRemembranceToolName(toolName) {
|
|
|
3725
4113
|
"invoke_skill",
|
|
3726
4114
|
"get_value_proof",
|
|
3727
4115
|
"get_effective_preferences",
|
|
4116
|
+
"get_private_lesson_policy",
|
|
4117
|
+
"prepare_private_lesson_candidate",
|
|
4118
|
+
"inspect_private_lesson_outbox",
|
|
4119
|
+
"submit_private_lesson_candidate",
|
|
4120
|
+
"retry_private_lesson_candidate",
|
|
4121
|
+
"delete_private_lesson_candidate",
|
|
3728
4122
|
"record_preference",
|
|
3729
4123
|
"submit_preference_compatibility_feedback",
|
|
3730
4124
|
"link_current_installation",
|
|
@@ -3884,6 +4278,95 @@ function prunePluginAlertFiles(keepPath, env) {
|
|
|
3884
4278
|
}
|
|
3885
4279
|
}
|
|
3886
4280
|
|
|
4281
|
+
function recentPrivateLessonHostDenial(env = process.env) {
|
|
4282
|
+
const directory = pluginAlertDir(env);
|
|
4283
|
+
if (!existsSync(directory)) return false;
|
|
4284
|
+
try {
|
|
4285
|
+
const entries = readdirSync(directory, { withFileTypes: true });
|
|
4286
|
+
if (entries.length > PLUGIN_ALERT_LIMIT) return true;
|
|
4287
|
+
const cutoff = Date.now() - PLUGIN_ALERT_TTL_MS;
|
|
4288
|
+
for (const entry of entries) {
|
|
4289
|
+
if (
|
|
4290
|
+
!entry.isFile() ||
|
|
4291
|
+
entry.isSymbolicLink() ||
|
|
4292
|
+
!/^[a-z_]+\.[a-f0-9]{24}\.json$/.test(entry.name)
|
|
4293
|
+
) {
|
|
4294
|
+
continue;
|
|
4295
|
+
}
|
|
4296
|
+
const parsed = JSON.parse(
|
|
4297
|
+
readSecureHookFile(
|
|
4298
|
+
join(directory, entry.name),
|
|
4299
|
+
MAX_LOCAL_PLUGIN_ALERT_BYTES,
|
|
4300
|
+
),
|
|
4301
|
+
);
|
|
4302
|
+
if (
|
|
4303
|
+
Array.isArray(parsed?.observations) &&
|
|
4304
|
+
parsed.observations.some(
|
|
4305
|
+
(item) =>
|
|
4306
|
+
item?.operation_class === "private_lesson" &&
|
|
4307
|
+
Number.isFinite(Date.parse(item?.last_seen_at ?? "")) &&
|
|
4308
|
+
Date.parse(item.last_seen_at) >= cutoff,
|
|
4309
|
+
)
|
|
4310
|
+
) {
|
|
4311
|
+
return true;
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
} catch {
|
|
4315
|
+
// An unreadable or malformed alert store cannot prove that retrying is
|
|
4316
|
+
// authorized. Keep retained private lessons local until the state is
|
|
4317
|
+
// repaired or the exact action is invoked explicitly.
|
|
4318
|
+
return true;
|
|
4319
|
+
}
|
|
4320
|
+
return false;
|
|
4321
|
+
}
|
|
4322
|
+
|
|
4323
|
+
export function clearPrivateLessonHostPolicyDenials(env = process.env) {
|
|
4324
|
+
const directory = pluginAlertDir(env);
|
|
4325
|
+
let changed = false;
|
|
4326
|
+
try {
|
|
4327
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
4328
|
+
if (
|
|
4329
|
+
!entry.isFile() ||
|
|
4330
|
+
entry.isSymbolicLink() ||
|
|
4331
|
+
!/^[a-z_]+\.[a-f0-9]{24}\.json$/.test(entry.name)
|
|
4332
|
+
) {
|
|
4333
|
+
continue;
|
|
4334
|
+
}
|
|
4335
|
+
const path = join(directory, entry.name);
|
|
4336
|
+
const parsed = JSON.parse(
|
|
4337
|
+
readSecureHookFile(path, MAX_LOCAL_PLUGIN_ALERT_BYTES),
|
|
4338
|
+
);
|
|
4339
|
+
if (!Array.isArray(parsed?.observations)) continue;
|
|
4340
|
+
const observations = parsed.observations.filter(
|
|
4341
|
+
(item) => item?.operation_class !== "private_lesson",
|
|
4342
|
+
);
|
|
4343
|
+
if (observations.length === parsed.observations.length) continue;
|
|
4344
|
+
changed = true;
|
|
4345
|
+
if (observations.length === 0) {
|
|
4346
|
+
rmSync(path, { force: true });
|
|
4347
|
+
} else {
|
|
4348
|
+
writePrivateUsageMarker(
|
|
4349
|
+
path,
|
|
4350
|
+
`${JSON.stringify({
|
|
4351
|
+
...parsed,
|
|
4352
|
+
observations,
|
|
4353
|
+
updated_at: new Date().toISOString(),
|
|
4354
|
+
})}\n`,
|
|
4355
|
+
);
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
} catch {
|
|
4359
|
+
return false;
|
|
4360
|
+
}
|
|
4361
|
+
return changed;
|
|
4362
|
+
}
|
|
4363
|
+
|
|
4364
|
+
export function hostPolicyAlertTextForOperation(operationClass) {
|
|
4365
|
+
return operationClass === "private_lesson"
|
|
4366
|
+
? PRIVATE_LESSON_HOST_POLICY_ALERT_TEXT
|
|
4367
|
+
: HOST_POLICY_ALERT_TEXT;
|
|
4368
|
+
}
|
|
4369
|
+
|
|
3887
4370
|
export function recordHostPolicyDenial(
|
|
3888
4371
|
{
|
|
3889
4372
|
surface,
|
|
@@ -4402,9 +4885,12 @@ export function resolveApiAccessSnapshot(env = process.env) {
|
|
|
4402
4885
|
}
|
|
4403
4886
|
}
|
|
4404
4887
|
}
|
|
4405
|
-
const
|
|
4406
|
-
env.REMEMBRANCE_MEMBER_LINK_TOKEN ??
|
|
4888
|
+
const environmentMemberLinkToken = String(
|
|
4889
|
+
env.REMEMBRANCE_MEMBER_LINK_TOKEN ?? "",
|
|
4407
4890
|
).trim();
|
|
4891
|
+
const rawMemberLinkToken =
|
|
4892
|
+
environmentMemberLinkToken ||
|
|
4893
|
+
String(shared.config.memberLinkToken ?? "").trim();
|
|
4408
4894
|
return {
|
|
4409
4895
|
apiConfiguration,
|
|
4410
4896
|
credential,
|
|
@@ -4486,9 +4972,49 @@ export function explicitPreferenceSettingsFromPrompt(prompt) {
|
|
|
4486
4972
|
}
|
|
4487
4973
|
|
|
4488
4974
|
export function promptRequestsDurablePreference(prompt) {
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4975
|
+
const text = String(prompt ?? "");
|
|
4976
|
+
if (
|
|
4977
|
+
/\b(?:from now on|going forward|for future tasks?|remember (?:that )?i (?:prefer|want)|my (?:lasting )?preference is|make this my default|(?:i|we)\s+always\s+(?:prefer|want|avoid))\b/i.test(
|
|
4978
|
+
text,
|
|
4979
|
+
)
|
|
4980
|
+
) {
|
|
4981
|
+
return true;
|
|
4982
|
+
}
|
|
4983
|
+
// Terminators must be followed by whitespace or end of input, or a sentence
|
|
4984
|
+
// splits inside an identifier ("console.log", "v1.2") and separates a
|
|
4985
|
+
// durable instruction from the recurring qualifier that licenses it.
|
|
4986
|
+
return text.split(/[.!?]+(?=\s|$)|\n+/).some((sentence) => {
|
|
4987
|
+
const clause = sentence.trim();
|
|
4988
|
+
if (!clause || /^(?:nevermind|never\s+(?:mind|gonna))\b/i.test(clause)) {
|
|
4989
|
+
return false;
|
|
4990
|
+
}
|
|
4991
|
+
// Deliberately broad. Whether "must never return undefined" describes the
|
|
4992
|
+
// agent's working habits or the program's runtime behavior is a semantic
|
|
4993
|
+
// judgment no regex can make, and dropping the ambiguous half would
|
|
4994
|
+
// silently discard real user instructions. This gate only decides whether
|
|
4995
|
+
// to ASK; genericPreferenceCaptureDirective leaves the decision to the model
|
|
4996
|
+
// reading the full conversation, and server-side validation rejects what
|
|
4997
|
+
// slips through.
|
|
4998
|
+
if (/\b(?:should|must)\s+(?:always|never|only)\b/i.test(clause)) {
|
|
4999
|
+
return true;
|
|
5000
|
+
}
|
|
5001
|
+
const recurringContext =
|
|
5002
|
+
/\b(?:by default|as (?:a )?(?:default|rule)|for future tasks?|in future|whenever|every time|each time|(?:for|on) (?:all|each|every)\b|(?:before|after) (?:a|an|the|any|each|every|i|we|you|commits?|releases?|deployments?|reviews?|tasks?)(?:\s|$))/i.test(
|
|
5003
|
+
clause,
|
|
5004
|
+
);
|
|
5005
|
+
if (!recurringContext) return false;
|
|
5006
|
+
// A durable instruction may trail its qualifier ("Whenever you touch a
|
|
5007
|
+
// route, always add a test"), so each comma-separated segment gets its own
|
|
5008
|
+
// start-anchored test. The anchor still blocks descriptive tails such as
|
|
5009
|
+
// "..., and it always returns null".
|
|
5010
|
+
return clause
|
|
5011
|
+
.split(",")
|
|
5012
|
+
.some((segment) =>
|
|
5013
|
+
/^(?:please\s+)?(?:by default,?\s+)?(?:always|never)\s+(?!(?:mind|gonna|going|a|an|the|this|that|there|it)\b)[a-z][a-z-]*\b/i.test(
|
|
5014
|
+
segment.trim(),
|
|
5015
|
+
),
|
|
5016
|
+
);
|
|
5017
|
+
});
|
|
4492
5018
|
}
|
|
4493
5019
|
|
|
4494
5020
|
export function promptProvidesPreferenceCorrection(prompt) {
|
|
@@ -4537,7 +5063,12 @@ export function genericPreferenceCaptureDirective(prompt, options = {}) {
|
|
|
4537
5063
|
: '"scope":"auto"';
|
|
4538
5064
|
return [
|
|
4539
5065
|
"Remembrance durable-preference capture:",
|
|
4540
|
-
|
|
5066
|
+
// The lexical gate that produced this notice cannot tell an instruction
|
|
5067
|
+
// about your working habits from a statement about what the code should
|
|
5068
|
+
// do, so it deliberately over-triggers rather than dropping real user
|
|
5069
|
+
// instructions. Make that judgment here, where the whole conversation is
|
|
5070
|
+
// available, and decline silently when it does not apply.
|
|
5071
|
+
"The user's message may state a lasting or corrective working preference that is not one of the built-in controls. Record it only if it constrains how YOU work: a presentation, workflow, or strategy choice you would otherwise make at your own discretion. Do not record a statement about what the code should do; an expected value, an invariant, an API contract, a test expectation, or a bug report is not a working preference even when it is phrased with always, never, must, or should. If it qualifies, interpret only that preference and call record_preference once, and do not ask the user to classify it. Otherwise take no action and do not mention this notice.",
|
|
4541
5072
|
'Use setting {"key":"<presentation|workflow|strategy_selection>.<stable_concept>","value":"<stable_behavior_id>","label":"<short name>","behavior":"<concise normalized behavior>","effect":"presentation|workflow|strategy_selection","strength":"prefer|avoid","definition_version":1}. This vocabulary is extensible; do not force the preference into a built-in category.',
|
|
4542
5073
|
`Use ${scopeFields}, "source_category":"explicit_user", "evidence_hash":"${evidenceHash}", "task_hash":"${taskHash}", and "confidence":1.`,
|
|
4543
5074
|
"Never send the raw prompt or private task details. Never encode a request to weaken safety, authorization, privacy, required skill steps, validation, or review; those constraints remain authoritative.",
|