@remembrance-ai/opencode-plugin 0.1.74 → 0.1.76

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.
@@ -60,6 +60,21 @@ const VALUE_EPISODE_MARKER_LIMIT = 20;
60
60
  const VALUE_EPISODE_MARKER_TTL_MS = 30 * 24 * 60 * 60 * 1000;
61
61
  const DIRECT_SELECTION_MARKER_LIMIT = 20;
62
62
  const DIRECT_SELECTION_MARKER_TTL_MS = 24 * 60 * 60 * 1000;
63
+ const COMPLETION_OBLIGATION_LIMIT = 64;
64
+ const COMPLETION_OBLIGATION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
65
+ const COMPLETION_OBLIGATION_VERSION = "completion-obligation-v1";
66
+ const COMPLETION_CONTRIBUTION_TOOLS = new Set([
67
+ "submit_query_feedback",
68
+ "submit_feedback",
69
+ "submit_remembrance",
70
+ "propose_skill_idea",
71
+ "propose_private_skill",
72
+ "submit_private_lesson_candidate",
73
+ "retry_private_lesson_candidate",
74
+ "submit_suggestion",
75
+ "submit_resource",
76
+ "submit_resource_review",
77
+ ]);
63
78
  const PREFERENCE_EVIDENCE_SKILL_LIMIT = 2;
64
79
  const PREFERENCE_EVIDENCE_SETTING_LIMIT = 4;
65
80
  const PREFERENCE_EVIDENCE_CONTEXT_CHARS = 520;
@@ -181,7 +196,8 @@ const SERVICE_PATTERNS = [
181
196
  /\b(vercel|heroku|netlify|cloudflare|aws|gcp|azure)\b/i,
182
197
  /\b(github actions?|circleci|gitlab ci|buildkite|jenkins)\b/i,
183
198
  /\b(stripe|x402|mpp|model payment protocol|mcp servers?)\b/i,
184
- /\b(openai|anthropic|claude|cursor|codex|voyage|mongodb atlas)\b/i,
199
+ /\b(?:openai|anthropic|claude|voyage|mongodb atlas)\b.{0,80}\b(?:api|sdk|provider|gateway|model|embedding|integration|authentication|authorization|credential|request|endpoint)\b/i,
200
+ /\b(?:api|sdk|provider|gateway|model|embedding|integration|authentication|authorization|credential|request|endpoint)\b.{0,80}\b(?:openai|anthropic|claude|voyage|mongodb atlas)\b/i,
185
201
  ];
186
202
 
187
203
  const TOOL_PATTERNS = [
@@ -857,10 +873,13 @@ export function formatContext(
857
873
  const skills = Array.isArray(body?.skills)
858
874
  ? body.skills
859
875
  .filter((item) => publicSkillCandidateAllowed(body, item))
876
+ .filter(autoQueryResponseCandidateAllowed)
860
877
  .slice(0, itemLimit)
861
878
  : [];
862
879
  const resources = Array.isArray(body?.resources)
863
- ? body.resources.slice(0, itemLimit)
880
+ ? body.resources
881
+ .filter(autoQueryResponseCandidateAllowed)
882
+ .slice(0, itemLimit)
864
883
  : [];
865
884
  const noResults = body?.no_results ?? null;
866
885
  if (skills.length === 0 && resources.length === 0 && !noResults) {
@@ -1394,6 +1413,7 @@ export async function runPromptHook(prompt, options = {}) {
1394
1413
  const includeSharedConfigCredentialNotice =
1395
1414
  options.includeSharedConfigCredentialNotice !== false;
1396
1415
  const redacted = redactPrompt(String(prompt ?? ""));
1416
+ const correctionCapture = userCorrectionCaptureDirective(redacted);
1397
1417
  const preferenceCapture = genericPreferenceCaptureDirective(redacted, {
1398
1418
  env,
1399
1419
  projectPath: options.projectPath,
@@ -1419,9 +1439,12 @@ export async function runPromptHook(prompt, options = {}) {
1419
1439
  return {
1420
1440
  consumed: false,
1421
1441
  context: appendPreferenceCapture(
1422
- appendPrivateLessonLifecycleRecovery(
1423
- continuationQueryContext(directive),
1424
- privateLessonRecovery,
1442
+ appendUserCorrectionCapture(
1443
+ appendPrivateLessonLifecycleRecovery(
1444
+ continuationQueryContext(directive),
1445
+ privateLessonRecovery,
1446
+ ),
1447
+ correctionCapture,
1425
1448
  ),
1426
1449
  preferenceCapture,
1427
1450
  ),
@@ -1430,15 +1453,21 @@ export async function runPromptHook(prompt, options = {}) {
1430
1453
  reason: "contextual_continuation",
1431
1454
  };
1432
1455
  }
1433
- if (preferenceCapture) {
1456
+ if (preferenceCapture || correctionCapture) {
1434
1457
  return {
1435
1458
  consumed: false,
1436
- context: appendPrivateLessonLifecycleRecovery(
1459
+ context: appendPreferenceCapture(
1460
+ appendUserCorrectionCapture(
1461
+ appendPrivateLessonLifecycleRecovery(
1462
+ "Remembrance capture routing:",
1463
+ privateLessonRecovery,
1464
+ ),
1465
+ correctionCapture,
1466
+ ),
1437
1467
  preferenceCapture,
1438
- privateLessonRecovery,
1439
1468
  ),
1440
- eligible: false,
1441
- reason: "preference_capture",
1469
+ eligible: Boolean(correctionCapture),
1470
+ reason: correctionCapture ? "user_correction" : "preference_capture",
1442
1471
  };
1443
1472
  }
1444
1473
  if (privateLessonRecovery) {
@@ -1473,14 +1502,17 @@ export async function runPromptHook(prompt, options = {}) {
1473
1502
  return {
1474
1503
  consumed: false,
1475
1504
  context: appendPreferenceCapture(
1476
- appendPrivateLessonLifecycleRecovery(
1477
- includeSharedConfigCredentialNotice
1478
- ? withSharedConfigCredentialNotice(
1479
- unavailableQueryContext(env),
1480
- env,
1481
- )
1482
- : unavailableQueryContext(env),
1483
- privateLessonRecovery,
1505
+ appendUserCorrectionCapture(
1506
+ appendPrivateLessonLifecycleRecovery(
1507
+ includeSharedConfigCredentialNotice
1508
+ ? withSharedConfigCredentialNotice(
1509
+ unavailableQueryContext(env),
1510
+ env,
1511
+ )
1512
+ : unavailableQueryContext(env),
1513
+ privateLessonRecovery,
1514
+ ),
1515
+ correctionCapture,
1484
1516
  ),
1485
1517
  preferenceCapture,
1486
1518
  ),
@@ -1492,21 +1524,25 @@ export async function runPromptHook(prompt, options = {}) {
1492
1524
  consumed: true,
1493
1525
  matched: queryResponseHasMatches(response),
1494
1526
  context: appendPreferenceCapture(
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,
1527
+ appendUserCorrectionCapture(
1528
+ appendPrivateLessonLifecycleRecovery(
1529
+ includeSharedConfigCredentialNotice
1530
+ ? withSharedConfigCredentialNotice(
1531
+ formatContext(response, decision.reason, limitFromEnv(env)) ??
1532
+ emptyQueryContext(decision.reason),
1533
+ env,
1534
+ )
1535
+ : (formatContext(response, decision.reason, limitFromEnv(env)) ??
1536
+ emptyQueryContext(decision.reason)),
1537
+ privateLessonRecovery,
1538
+ ),
1539
+ correctionCapture,
1505
1540
  ),
1506
1541
  preferenceCapture,
1507
1542
  ),
1508
1543
  eligible: true,
1509
1544
  highMatch: highMatchFromResponse(response),
1545
+ queryFeedback: queryFeedbackTrackingFromResponse(response),
1510
1546
  valueEpisode: valueEpisodeFromResponse(response),
1511
1547
  reason: decision.reason,
1512
1548
  };
@@ -1516,6 +1552,10 @@ function appendPreferenceCapture(context, directive) {
1516
1552
  return directive ? `${context}\n\n${directive}` : context;
1517
1553
  }
1518
1554
 
1555
+ function appendUserCorrectionCapture(context, directive) {
1556
+ return directive ? `${context}\n\n${directive}` : context;
1557
+ }
1558
+
1519
1559
  function appendPrivateLessonLifecycleRecovery(context, instruction) {
1520
1560
  return instruction ? `${context}\n\n${instruction}` : context;
1521
1561
  }
@@ -1523,12 +1563,43 @@ function appendPrivateLessonLifecycleRecovery(context, instruction) {
1523
1563
  export function queryResponseHasMatches(response) {
1524
1564
  const body = responseBodyFromToolResponse(response);
1525
1565
  const skills = Array.isArray(body?.skills)
1526
- ? body.skills.filter((item) => publicSkillCandidateAllowed(body, item))
1566
+ ? body.skills
1567
+ .filter((item) => publicSkillCandidateAllowed(body, item))
1568
+ .filter(autoQueryResponseCandidateAllowed)
1569
+ : [];
1570
+ const resources = Array.isArray(body?.resources)
1571
+ ? body.resources.filter(autoQueryResponseCandidateAllowed)
1527
1572
  : [];
1528
- const resources = Array.isArray(body?.resources) ? body.resources : [];
1529
1573
  return skills.length > 0 || resources.length > 0;
1530
1574
  }
1531
1575
 
1576
+ function autoQueryResponseCandidateAllowed(candidate) {
1577
+ if (candidate?.applicability?.fit === "unlikely") return false;
1578
+ if (!candidate?.match_tier) return true;
1579
+ if (candidate?.match_tier === "high") return true;
1580
+ if (candidate?.match_tier !== "possible") return false;
1581
+ const lexical = candidate?.why_matched?.lexical_signal;
1582
+ const semantic = candidate?.why_matched?.semantic_signal;
1583
+ if (!lexical && !semantic) return true;
1584
+ return (
1585
+ lexical === "strong" ||
1586
+ semantic === "strong" ||
1587
+ (lexical === "moderate" &&
1588
+ semantic === "moderate" &&
1589
+ candidate?.why_matched?.domain_match === true)
1590
+ );
1591
+ }
1592
+
1593
+ export function queryFeedbackTrackingFromResponse(response) {
1594
+ const body = responseBodyFromToolResponse(response);
1595
+ const queryFeedback = body?.query_feedback;
1596
+ const queryId =
1597
+ queryFeedback?.available === true
1598
+ ? completionIdentifier(queryFeedback.query_id ?? body?.query_id)
1599
+ : "";
1600
+ return queryId ? { available: true, query_id: queryId } : null;
1601
+ }
1602
+
1532
1603
  function withSharedConfigCredentialNotice(context, env) {
1533
1604
  const notice = sharedConfigCredentialNotice(env);
1534
1605
  return notice ? `${notice}\n\n${context}` : context;
@@ -1605,7 +1676,7 @@ export function contributionReason(
1605
1676
  options = {},
1606
1677
  ) {
1607
1678
  const signalLine = signal
1608
- ? `High-value lesson detected: ${signal}. Capture it unless it was already submitted.`
1679
+ ? `High-value lesson detected: ${signal}. Capture this specific lesson unless it was already submitted. A lower-value earlier capture does not satisfy this obligation; compare it with the most valuable reusable lesson formed since.`
1609
1680
  : null;
1610
1681
  const highMatchLine = highMatch
1611
1682
  ? highMatchClosureInstruction(highMatch)
@@ -1712,9 +1783,25 @@ export function privateLessonCompletionGuidance({
1712
1783
  .join("\n");
1713
1784
  }
1714
1785
 
1786
+ export function completionContinuationReason(reason, input = {}) {
1787
+ const priorAnswer = String(
1788
+ input?.last_assistant_message ??
1789
+ input?.lastAssistantMessage ??
1790
+ input?.assistant_message ??
1791
+ input?.message ??
1792
+ "",
1793
+ ).trim();
1794
+ const finalAnswerInstruction = priorAnswer
1795
+ ? "After the silent cleanup, preserve the task result: repeat your immediately preceding user-facing answer unchanged, preceded only by any Remembrance failure or required action that must be surfaced. Do not replace the task answer with hook, receipt, or Remembrance status text."
1796
+ : "After the silent cleanup, provide the task's normal user-facing final answer. Do not replace it with hook, receipt, or Remembrance status text.";
1797
+ return [String(reason ?? "").trim(), finalAnswerInstruction]
1798
+ .filter(Boolean)
1799
+ .join("\n");
1800
+ }
1801
+
1715
1802
  export function taskClosureReason(signal = null, options = {}) {
1716
1803
  const signalLine = signal
1717
- ? `High-value lesson detected: ${signal}. Capture it unless it was already submitted.`
1804
+ ? `High-value lesson detected: ${signal}. Capture this specific lesson unless it was already submitted. A lower-value earlier capture does not satisfy this obligation; compare it with the most valuable reusable lesson formed since.`
1718
1805
  : null;
1719
1806
  return [
1720
1807
  "Before finishing, silently recover the pending Remembrance query for this reusable task using the full conversation; no completed Remembrance query/use was recorded.",
@@ -1793,6 +1880,26 @@ export function decideStop(input, options = {}) {
1793
1880
  const engagementCount = Math.max(useCount, eligibilityCount);
1794
1881
  const readPrompted = options.readPromptedCount ?? readPromptedCount;
1795
1882
  const promptedCount = readPrompted(sessionId, env);
1883
+ const readObligations =
1884
+ options.readCompletionObligations ?? readCompletionObligations;
1885
+ const unpromptedObligations = readObligations(sessionId, env).filter(
1886
+ (obligation) => !obligation.prompted_at,
1887
+ );
1888
+ const highValueSignal = detectHighValueLessonSignal(input);
1889
+ if (unpromptedObligations.length > 0 && !highValueSignal) {
1890
+ return {
1891
+ allow: false,
1892
+ why: "prompt_pending_obligations",
1893
+ reason: completionObligationInstruction(unpromptedObligations),
1894
+ useCount: Math.max(
1895
+ engagementCount,
1896
+ ...unpromptedObligations.map(
1897
+ (obligation) => obligation.engagement_count,
1898
+ ),
1899
+ ),
1900
+ obligationIds: unpromptedObligations.map((obligation) => obligation.id),
1901
+ };
1902
+ }
1796
1903
  const readHighMatch = options.readHighMatch ?? readHighMatchSurface;
1797
1904
  const highMatch = readHighMatch(sessionId, env);
1798
1905
  const directSelections = options.readDirectSelections
@@ -1808,7 +1915,6 @@ export function decideStop(input, options = {}) {
1808
1915
  );
1809
1916
  const hasUnclosedEligibility =
1810
1917
  eligibilityCount > useCount && newDirectSelections.length === 0;
1811
- const highValueSignal = detectHighValueLessonSignal(input);
1812
1918
  const privateLessonAuthorized = organizationPrivateLessonAuthorized(env);
1813
1919
  const privateLessonRecovery = privateLessonAuthorized
1814
1920
  ? privateLessonLifecycleRecoveryInstruction(env)
@@ -1816,18 +1922,25 @@ export function decideStop(input, options = {}) {
1816
1922
  if (
1817
1923
  newDirectSelections.length > 0 &&
1818
1924
  feedbackDirectSelections.length === 0 &&
1819
- !privateLessonRecovery
1925
+ !privateLessonRecovery &&
1926
+ unpromptedObligations.length === 0
1820
1927
  ) {
1821
1928
  return { allow: true, why: "direct_feedback_unavailable" };
1822
1929
  }
1823
- if (engagementCount === 0 && !highValueSignal && !privateLessonRecovery) {
1930
+ if (
1931
+ engagementCount === 0 &&
1932
+ !highValueSignal &&
1933
+ !privateLessonRecovery &&
1934
+ unpromptedObligations.length === 0
1935
+ ) {
1824
1936
  return { allow: true, why: "registry_not_used" };
1825
1937
  }
1826
1938
  if (
1827
1939
  privateLessonRecovery &&
1828
1940
  promptedCount > 0 &&
1829
1941
  engagementCount <= promptedCount &&
1830
- !highValueSignal
1942
+ !highValueSignal &&
1943
+ unpromptedObligations.length === 0
1831
1944
  ) {
1832
1945
  return { allow: true, why: "no_new_usage" };
1833
1946
  }
@@ -1835,14 +1948,16 @@ export function decideStop(input, options = {}) {
1835
1948
  engagementCount <= promptedCount &&
1836
1949
  newDirectSelections.length === 0 &&
1837
1950
  !highValueSignal &&
1838
- !privateLessonRecovery
1951
+ !privateLessonRecovery &&
1952
+ unpromptedObligations.length === 0
1839
1953
  ) {
1840
1954
  return { allow: true, why: "no_new_usage" };
1841
1955
  }
1842
1956
  if (
1843
1957
  highValueSignal &&
1844
1958
  promptedCount > 0 &&
1845
- engagementCount <= promptedCount
1959
+ engagementCount <= promptedCount &&
1960
+ unpromptedObligations.length === 0
1846
1961
  ) {
1847
1962
  return { allow: true, why: "high_value_lesson_already_prompted" };
1848
1963
  }
@@ -1855,7 +1970,10 @@ export function decideStop(input, options = {}) {
1855
1970
  : privateLessonRecovery && engagementCount === 0
1856
1971
  ? "prompt_private_lesson_retry"
1857
1972
  : "prompt_contribution",
1858
- reason:
1973
+ reason: [
1974
+ unpromptedObligations.length > 0
1975
+ ? completionObligationInstruction(unpromptedObligations)
1976
+ : null,
1859
1977
  privateLessonRecovery && engagementCount === 0 && !highValueSignal
1860
1978
  ? [
1861
1979
  privateLessonRecovery,
@@ -1878,10 +1996,14 @@ export function decideStop(input, options = {}) {
1878
1996
  includeLifecycleRecovery: true,
1879
1997
  },
1880
1998
  ),
1999
+ ]
2000
+ .filter(Boolean)
2001
+ .join("\n"),
1881
2002
  useCount:
1882
2003
  highValueSignal || privateLessonRecovery
1883
2004
  ? Math.max(engagementCount, promptedCount + 1, 1)
1884
2005
  : engagementCount,
2006
+ obligationIds: unpromptedObligations.map((obligation) => obligation.id),
1885
2007
  };
1886
2008
  }
1887
2009
 
@@ -1904,6 +2026,8 @@ function highMatchClosureInstruction(match) {
1904
2026
  // no query result was consumed.
1905
2027
  // <hash>.prompt — the use count at which the stop adapter last prompted.
1906
2028
  // <hash>.high-match.json — latest high result until its exact detail opens.
2029
+ // <hash>.obligations/*.json — content-free query/feedback/lesson actions,
2030
+ // each closed only by its correlated tool call.
1907
2031
  // The stop adapter compares max(.use, .eligible) with .prompt, then records the
1908
2032
  // new engagement count. This reproduces the Claude hook's sentinel behavior
1909
2033
  // without retaining a transcript.
@@ -1961,6 +2085,18 @@ function directivePath(sessionId, env) {
1961
2085
  return join(usageDir(env), `${sessionHash(sessionId)}.directive.json`);
1962
2086
  }
1963
2087
 
2088
+ function completionObligationDir(sessionId, env) {
2089
+ return join(usageDir(env), `${sessionHash(sessionId)}.obligations`);
2090
+ }
2091
+
2092
+ function completionObligationPath(sessionId, obligationId, env) {
2093
+ const hash = createHash("sha256")
2094
+ .update(String(obligationId), "utf8")
2095
+ .digest("hex")
2096
+ .slice(0, 32);
2097
+ return join(completionObligationDir(sessionId, env), `${hash}.json`);
2098
+ }
2099
+
1964
2100
  function readCountFile(path) {
1965
2101
  try {
1966
2102
  const raw = readFileSync(path, "utf8");
@@ -1995,6 +2131,507 @@ function writeCountFile(path, count) {
1995
2131
  return writePrivateUsageMarker(path, String(count));
1996
2132
  }
1997
2133
 
2134
+ function currentEngagementCount(sessionId, env = process.env) {
2135
+ return Math.max(
2136
+ readRegistryUseCount(sessionId, env),
2137
+ readTaskEligibilityCount(sessionId, env),
2138
+ );
2139
+ }
2140
+
2141
+ function completionObligationId(kind, parts) {
2142
+ return [kind, ...parts.map((part) => completionIdentifier(part))].join(":");
2143
+ }
2144
+
2145
+ function completionIdentifier(value, maxLength = 160) {
2146
+ const text = String(value ?? "").trim();
2147
+ return text ? safeText(text, maxLength) : "";
2148
+ }
2149
+
2150
+ function normalizeCompletionObligation(value) {
2151
+ if (
2152
+ !value ||
2153
+ value.version !== COMPLETION_OBLIGATION_VERSION ||
2154
+ ![
2155
+ "query_feedback",
2156
+ "post_use_feedback",
2157
+ "remembrance_followup",
2158
+ "private_lesson",
2159
+ ].includes(value.kind) ||
2160
+ typeof value.id !== "string" ||
2161
+ !value.id ||
2162
+ !Number.isInteger(value.engagement_count) ||
2163
+ value.engagement_count < 1
2164
+ ) {
2165
+ return null;
2166
+ }
2167
+ const createdAt = Date.parse(String(value.created_at ?? ""));
2168
+ const promptedAt = value.prompted_at
2169
+ ? Date.parse(String(value.prompted_at))
2170
+ : null;
2171
+ if (!Number.isFinite(createdAt)) return null;
2172
+ return {
2173
+ version: COMPLETION_OBLIGATION_VERSION,
2174
+ id: safeText(value.id, 420),
2175
+ kind: value.kind,
2176
+ engagement_count: Math.min(1_000_000, value.engagement_count),
2177
+ query_id: value.query_id ? safeText(value.query_id, 160) : null,
2178
+ result_id: value.result_id ? safeText(value.result_id, 160) : null,
2179
+ draft_id: value.draft_id ? safeText(value.draft_id, 160) : null,
2180
+ skill_slug: value.skill_slug ? safeText(value.skill_slug, 160) : null,
2181
+ payload_digest:
2182
+ typeof value.payload_digest === "string" &&
2183
+ /^sha256:[a-f0-9]{64}$/.test(value.payload_digest)
2184
+ ? value.payload_digest
2185
+ : null,
2186
+ created_at: new Date(createdAt).toISOString(),
2187
+ prompted_at:
2188
+ promptedAt !== null && Number.isFinite(promptedAt)
2189
+ ? new Date(promptedAt).toISOString()
2190
+ : null,
2191
+ };
2192
+ }
2193
+
2194
+ export function readCompletionObligations(sessionId, env = process.env) {
2195
+ const directory = completionObligationDir(sessionId, env);
2196
+ try {
2197
+ const now = Date.now();
2198
+ const obligations = [];
2199
+ for (const name of readdirSync(directory).sort().slice(0, 256)) {
2200
+ if (!/^[a-f0-9]{32}\.json$/.test(name)) continue;
2201
+ const path = join(directory, name);
2202
+ try {
2203
+ const stat = lstatSync(path);
2204
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 8 * 1024) {
2205
+ continue;
2206
+ }
2207
+ const obligation = normalizeCompletionObligation(
2208
+ JSON.parse(readFileSync(path, "utf8")),
2209
+ );
2210
+ if (!obligation) continue;
2211
+ if (
2212
+ Date.parse(obligation.created_at) + COMPLETION_OBLIGATION_TTL_MS <
2213
+ now
2214
+ ) {
2215
+ rmSync(path, { force: true });
2216
+ continue;
2217
+ }
2218
+ obligations.push(obligation);
2219
+ } catch {
2220
+ // One malformed marker must not hide other pending work.
2221
+ }
2222
+ }
2223
+ return obligations
2224
+ .sort(
2225
+ (left, right) =>
2226
+ Date.parse(left.created_at) - Date.parse(right.created_at) ||
2227
+ left.id.localeCompare(right.id),
2228
+ )
2229
+ .slice(0, COMPLETION_OBLIGATION_LIMIT);
2230
+ } catch {
2231
+ return [];
2232
+ }
2233
+ }
2234
+
2235
+ function writeCompletionObligation(sessionId, obligation, env) {
2236
+ const normalized = normalizeCompletionObligation(obligation);
2237
+ if (!normalized) return null;
2238
+ const existing = readCompletionObligations(sessionId, env);
2239
+ if (
2240
+ !existing.some((item) => item.id === normalized.id) &&
2241
+ existing.length >= COMPLETION_OBLIGATION_LIMIT
2242
+ ) {
2243
+ return null;
2244
+ }
2245
+ return writePrivateUsageMarker(
2246
+ completionObligationPath(sessionId, normalized.id, env),
2247
+ `${JSON.stringify(normalized)}\n`,
2248
+ )
2249
+ ? normalized
2250
+ : null;
2251
+ }
2252
+
2253
+ function addCompletionObligation(
2254
+ sessionId,
2255
+ obligation,
2256
+ env = process.env,
2257
+ ) {
2258
+ const existing = readCompletionObligations(sessionId, env).find(
2259
+ (item) => item.id === obligation.id,
2260
+ );
2261
+ return writeCompletionObligation(
2262
+ sessionId,
2263
+ {
2264
+ version: COMPLETION_OBLIGATION_VERSION,
2265
+ ...obligation,
2266
+ created_at: existing?.created_at ?? new Date().toISOString(),
2267
+ prompted_at: existing?.prompted_at ?? null,
2268
+ },
2269
+ env,
2270
+ );
2271
+ }
2272
+
2273
+ function removeCompletionObligations(sessionId, predicate, env) {
2274
+ const removed = [];
2275
+ for (const obligation of readCompletionObligations(sessionId, env)) {
2276
+ if (!predicate(obligation)) continue;
2277
+ try {
2278
+ rmSync(completionObligationPath(sessionId, obligation.id, env), {
2279
+ force: true,
2280
+ });
2281
+ removed.push(obligation);
2282
+ } catch {
2283
+ // Fail open: a retained marker can cause one reminder, never data loss.
2284
+ }
2285
+ }
2286
+ return removed;
2287
+ }
2288
+
2289
+ function normalizedCompletionToolName(toolName) {
2290
+ const normalized = String(toolName ?? "").trim().toLowerCase();
2291
+ for (const candidate of [
2292
+ "query_skills",
2293
+ "get_skill",
2294
+ "get_resource",
2295
+ "invoke_skill",
2296
+ "prepare_private_lesson_candidate",
2297
+ "submit_private_lesson_candidate",
2298
+ "retry_private_lesson_candidate",
2299
+ "submit_query_feedback",
2300
+ "submit_feedback",
2301
+ "submit_remembrance",
2302
+ "propose_skill_idea",
2303
+ "propose_private_skill",
2304
+ "submit_suggestion",
2305
+ "submit_resource",
2306
+ "submit_resource_review",
2307
+ ]) {
2308
+ if (normalized.endsWith(candidate)) return candidate;
2309
+ }
2310
+ return normalized;
2311
+ }
2312
+
2313
+ function completionPayloadDigest(payload) {
2314
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
2315
+ return null;
2316
+ }
2317
+ try {
2318
+ const normalized = JSON.parse(JSON.stringify(payload));
2319
+ delete normalized.verified_attestation;
2320
+ return `sha256:${createHash("sha256")
2321
+ .update(canonicalHookJson(normalized), "utf8")
2322
+ .digest("hex")}`;
2323
+ } catch {
2324
+ // Host SDK payloads should be JSON, but observation must never break a turn.
2325
+ return null;
2326
+ }
2327
+ }
2328
+
2329
+ export function recordQueryFeedbackObligation(
2330
+ sessionId,
2331
+ response,
2332
+ env = process.env,
2333
+ engagementCount = currentEngagementCount(sessionId, env),
2334
+ ) {
2335
+ const tracking =
2336
+ response?.available === true && response?.query_id
2337
+ ? response
2338
+ : queryFeedbackTrackingFromResponse(response);
2339
+ const queryId = completionIdentifier(tracking?.query_id);
2340
+ if (!queryId) return null;
2341
+ return addCompletionObligation(
2342
+ sessionId,
2343
+ {
2344
+ id: completionObligationId("query_feedback", [queryId]),
2345
+ kind: "query_feedback",
2346
+ engagement_count: Math.max(1, engagementCount),
2347
+ query_id: queryId,
2348
+ },
2349
+ env,
2350
+ );
2351
+ }
2352
+
2353
+ export function recordPostUseFeedbackObligation(
2354
+ sessionId,
2355
+ {
2356
+ query_id: queryId,
2357
+ result_id: resultId,
2358
+ skill_slug: skillSlug = null,
2359
+ feedback_available: feedbackAvailable = true,
2360
+ } = {},
2361
+ env = process.env,
2362
+ engagementCount = currentEngagementCount(sessionId, env),
2363
+ ) {
2364
+ const query = completionIdentifier(queryId);
2365
+ const result = completionIdentifier(resultId);
2366
+ if (!feedbackAvailable || !query || !result) return null;
2367
+ return addCompletionObligation(
2368
+ sessionId,
2369
+ {
2370
+ id: completionObligationId("post_use_feedback", [query, result]),
2371
+ kind: "post_use_feedback",
2372
+ engagement_count: Math.max(1, engagementCount),
2373
+ query_id: query,
2374
+ result_id: result,
2375
+ skill_slug: skillSlug ? safeText(skillSlug, 160) : null,
2376
+ },
2377
+ env,
2378
+ );
2379
+ }
2380
+
2381
+ export function recordPrivateLessonObligation(
2382
+ sessionId,
2383
+ response,
2384
+ env = process.env,
2385
+ engagementCount = currentEngagementCount(sessionId, env),
2386
+ ) {
2387
+ const body = responseBodyFromToolResponse(response);
2388
+ const draftId = completionIdentifier(body?.draft_id);
2389
+ if (!/^pld_[A-Za-z0-9_-]{8,96}$/.test(draftId)) return null;
2390
+ if (["submitted", "superseded_redactor"].includes(body?.state)) return null;
2391
+ return addCompletionObligation(
2392
+ sessionId,
2393
+ {
2394
+ id: completionObligationId("private_lesson", [draftId]),
2395
+ kind: "private_lesson",
2396
+ engagement_count: Math.max(1, engagementCount),
2397
+ draft_id: draftId,
2398
+ },
2399
+ env,
2400
+ );
2401
+ }
2402
+
2403
+ function recordRemembranceFollowupObligation(
2404
+ sessionId,
2405
+ response,
2406
+ fallback,
2407
+ env,
2408
+ engagementCount,
2409
+ ) {
2410
+ const payload =
2411
+ responseBodyFromToolResponse(response)?.next_step
2412
+ ?.submit_remembrance_payload;
2413
+ const digest = completionPayloadDigest(payload);
2414
+ if (!digest) return null;
2415
+ const queryId = completionIdentifier(
2416
+ payload?.interaction?.query_id ?? fallback?.query_id,
2417
+ );
2418
+ const resultId = completionIdentifier(
2419
+ payload?.interaction?.result_id ?? fallback?.result_id,
2420
+ );
2421
+ return addCompletionObligation(
2422
+ sessionId,
2423
+ {
2424
+ id: completionObligationId("remembrance_followup", [digest]),
2425
+ kind: "remembrance_followup",
2426
+ engagement_count: Math.max(1, engagementCount),
2427
+ query_id: queryId || null,
2428
+ result_id: resultId || null,
2429
+ payload_digest: digest,
2430
+ },
2431
+ env,
2432
+ );
2433
+ }
2434
+
2435
+ function markEngagementHandledWhenClear(sessionId, env) {
2436
+ const count = currentEngagementCount(sessionId, env);
2437
+ const hasPending = readCompletionObligations(sessionId, env).length > 0;
2438
+ if (!hasPending && count > 0) {
2439
+ writePromptedCount(sessionId, count, env);
2440
+ }
2441
+ return { count, hasPending };
2442
+ }
2443
+
2444
+ export function observeSuccessfulCompletionTool(
2445
+ sessionId,
2446
+ toolName,
2447
+ args = {},
2448
+ response = null,
2449
+ env = process.env,
2450
+ ) {
2451
+ const tool = normalizedCompletionToolName(toolName);
2452
+ const input =
2453
+ args && typeof args === "object" && !Array.isArray(args) ? args : {};
2454
+ const engagementCount = currentEngagementCount(sessionId, env);
2455
+ const opened = [];
2456
+ let closed = [];
2457
+
2458
+ if (tool === "query_skills") {
2459
+ const obligation = recordQueryFeedbackObligation(
2460
+ sessionId,
2461
+ response,
2462
+ env,
2463
+ engagementCount,
2464
+ );
2465
+ if (obligation) opened.push(obligation);
2466
+ } else if (tool === "invoke_skill") {
2467
+ const selection = directSelectionFromResponse(response);
2468
+ const obligation = selection
2469
+ ? recordPostUseFeedbackObligation(
2470
+ sessionId,
2471
+ {
2472
+ query_id: selection.query_id,
2473
+ result_id: selection.result_id,
2474
+ skill_slug: selection.slug,
2475
+ feedback_available: selection.feedback_available,
2476
+ },
2477
+ env,
2478
+ engagementCount,
2479
+ )
2480
+ : null;
2481
+ if (obligation) opened.push(obligation);
2482
+ } else if (tool === "get_skill" || tool === "get_resource") {
2483
+ const obligation = recordPostUseFeedbackObligation(
2484
+ sessionId,
2485
+ {
2486
+ query_id: input.query_id,
2487
+ result_id: input.result_id,
2488
+ skill_slug: input.slug,
2489
+ },
2490
+ env,
2491
+ engagementCount,
2492
+ );
2493
+ if (obligation) opened.push(obligation);
2494
+ } else if (tool === "prepare_private_lesson_candidate") {
2495
+ const obligation = recordPrivateLessonObligation(
2496
+ sessionId,
2497
+ response,
2498
+ env,
2499
+ engagementCount,
2500
+ );
2501
+ if (obligation) opened.push(obligation);
2502
+ } else if (tool === "submit_query_feedback") {
2503
+ const queryId = completionIdentifier(input.query_id);
2504
+ closed = removeCompletionObligations(
2505
+ sessionId,
2506
+ (obligation) =>
2507
+ obligation.kind === "query_feedback" &&
2508
+ Boolean(queryId) &&
2509
+ obligation.query_id === queryId,
2510
+ env,
2511
+ );
2512
+ } else if (tool === "submit_feedback") {
2513
+ const queryId = completionIdentifier(input.query_id);
2514
+ const resultId = completionIdentifier(input.result_id);
2515
+ closed = removeCompletionObligations(
2516
+ sessionId,
2517
+ (obligation) =>
2518
+ obligation.kind === "post_use_feedback" &&
2519
+ Boolean(queryId) &&
2520
+ Boolean(resultId) &&
2521
+ obligation.query_id === queryId &&
2522
+ obligation.result_id === resultId,
2523
+ env,
2524
+ );
2525
+ const followup = recordRemembranceFollowupObligation(
2526
+ sessionId,
2527
+ response,
2528
+ input,
2529
+ env,
2530
+ engagementCount,
2531
+ );
2532
+ if (followup) opened.push(followup);
2533
+ } else if (tool === "submit_remembrance") {
2534
+ const digest = completionPayloadDigest(input);
2535
+ const queryId = completionIdentifier(input?.interaction?.query_id);
2536
+ const resultId = completionIdentifier(input?.interaction?.result_id);
2537
+ const pendingFollowups = readCompletionObligations(sessionId, env).filter(
2538
+ (obligation) => obligation.kind === "remembrance_followup",
2539
+ );
2540
+ closed = removeCompletionObligations(
2541
+ sessionId,
2542
+ (obligation) =>
2543
+ obligation.kind === "remembrance_followup" &&
2544
+ ((digest && obligation.payload_digest === digest) ||
2545
+ (queryId &&
2546
+ resultId &&
2547
+ obligation.query_id === queryId &&
2548
+ obligation.result_id === resultId) ||
2549
+ (!queryId && !resultId && pendingFollowups.length === 1)),
2550
+ env,
2551
+ );
2552
+ } else if (
2553
+ tool === "submit_private_lesson_candidate" ||
2554
+ tool === "retry_private_lesson_candidate"
2555
+ ) {
2556
+ const body = responseBodyFromToolResponse(response);
2557
+ const draftId = completionIdentifier(input.draft_id ?? body?.draft_id);
2558
+ closed = removeCompletionObligations(
2559
+ sessionId,
2560
+ (obligation) =>
2561
+ obligation.kind === "private_lesson" &&
2562
+ Boolean(draftId) &&
2563
+ obligation.draft_id === draftId,
2564
+ env,
2565
+ );
2566
+ }
2567
+
2568
+ const handled = COMPLETION_CONTRIBUTION_TOOLS.has(tool)
2569
+ ? markEngagementHandledWhenClear(sessionId, env)
2570
+ : {
2571
+ count: engagementCount,
2572
+ hasPending: readCompletionObligations(sessionId, env).length > 0,
2573
+ };
2574
+ return {
2575
+ tool,
2576
+ opened,
2577
+ closed,
2578
+ pending: readCompletionObligations(sessionId, env),
2579
+ handled_count: handled.hasPending ? 0 : handled.count,
2580
+ };
2581
+ }
2582
+
2583
+ export function markCompletionObligationsPrompted(
2584
+ sessionId,
2585
+ obligationIds = null,
2586
+ env = process.env,
2587
+ engagementCount = Number.POSITIVE_INFINITY,
2588
+ ) {
2589
+ const ids = Array.isArray(obligationIds) ? new Set(obligationIds) : null;
2590
+ let marked = 0;
2591
+ for (const obligation of readCompletionObligations(sessionId, env)) {
2592
+ if (
2593
+ obligation.prompted_at ||
2594
+ obligation.engagement_count > engagementCount ||
2595
+ (ids && !ids.has(obligation.id))
2596
+ ) {
2597
+ continue;
2598
+ }
2599
+ if (
2600
+ writeCompletionObligation(
2601
+ sessionId,
2602
+ { ...obligation, prompted_at: new Date().toISOString() },
2603
+ env,
2604
+ )
2605
+ ) {
2606
+ marked += 1;
2607
+ }
2608
+ }
2609
+ return marked;
2610
+ }
2611
+
2612
+ function completionObligationInstruction(obligations) {
2613
+ const lines = obligations.slice(0, 12).map((obligation) => {
2614
+ if (obligation.kind === "query_feedback") {
2615
+ return `- Query fit: call submit_query_feedback for query_id ${obligation.query_id}.`;
2616
+ }
2617
+ if (obligation.kind === "post_use_feedback") {
2618
+ return `- Post-use feedback${obligation.skill_slug ? ` for ${obligation.skill_slug}` : ""}: call submit_feedback with query_id ${obligation.query_id} and result_id ${obligation.result_id}.`;
2619
+ }
2620
+ if (obligation.kind === "remembrance_followup") {
2621
+ return "- Reusable evidence: submit the exact next_step.submit_remembrance_payload returned by the preceding feedback call.";
2622
+ }
2623
+ return `- Private lesson: call submit_private_lesson_candidate with draft_id ${obligation.draft_id}.`;
2624
+ });
2625
+ if (obligations.length > lines.length) {
2626
+ lines.push(`- ${obligations.length - lines.length} additional pending action(s) remain in this session.`);
2627
+ }
2628
+ return [
2629
+ "Before finishing, silently complete only these pending Remembrance actions:",
2630
+ ...lines,
2631
+ "Do not mention routine hook, tool, or receipt output. Surface only a failure or required user action.",
2632
+ ].join("\n");
2633
+ }
2634
+
1998
2635
  // Increment (and return) the per-session registry-use counter. Prompt and tool
1999
2636
  // adapters call this only after an authorized registry match or explicit skill
2000
2637
  // invocation. A legitimate empty query remains eligible for completion recovery
@@ -2011,7 +2648,7 @@ export function markCurrentEngagementHandled(sessionId, env = process.env) {
2011
2648
  readRegistryUseCount(sessionId, env),
2012
2649
  readTaskEligibilityCount(sessionId, env),
2013
2650
  );
2014
- if (count > 0) {
2651
+ if (count > 0 && readCompletionObligations(sessionId, env).length === 0) {
2015
2652
  writePromptedCount(sessionId, count, env);
2016
2653
  }
2017
2654
  return count;
@@ -2182,6 +2819,7 @@ export function responseBodyFromToolResponse(value, depth = 0) {
2182
2819
  if (typeof value !== "object") return null;
2183
2820
  if (
2184
2821
  typeof value.query_id === "string" ||
2822
+ typeof value.draft_id === "string" ||
2185
2823
  value.selection_mode === "explicit" ||
2186
2824
  (value.next_step && typeof value.next_step === "object") ||
2187
2825
  Array.isArray(value.skills) ||
@@ -2997,6 +3635,7 @@ export function writePromptedCount(sessionId, count, env = process.env) {
2997
3635
  const written = writeCountFile(promptPath(sessionId, env), count);
2998
3636
  if (written) {
2999
3637
  markDirectSelectionSurfacesPrompted(sessionId, count, env);
3638
+ markCompletionObligationsPrompted(sessionId, null, env, count);
3000
3639
  }
3001
3640
  return written;
3002
3641
  }
@@ -5023,6 +5662,33 @@ export function promptProvidesPreferenceCorrection(prompt) {
5023
5662
  );
5024
5663
  }
5025
5664
 
5665
+ const USER_CORRECTION_CAPTURE_PATTERNS = [
5666
+ /\b(?:that|this|it)(?:\s+(?:(?:completed|proposed|current|chosen)\s+)?(?:approach|implementation|design|result))?(?:'s|\s+(?:is|was|looks?|feels?))\s+(?:wrong|incorrect|overkill|too much|overcomplicated|the wrong (?:way|approach|direction))\b/i,
5667
+ /\b(?:wrong (?:way|approach|direction)|do (?:this|it) differently|use a different approach)\b/i,
5668
+ /\b(?:you|we)\s+(?:missed|forgot|overlooked|ignored|did not address|didn't address|failed to address)\b/i,
5669
+ /\b(?:i already (?:asked|said|told you)|as i (?:said|asked|explained)|you keep (?:missing|forgetting|doing|adding|removing))\b/i,
5670
+ /\bi (?:do not|don't) (?:like|agree with|want) (?:this|that|the) (?:approach|direction|implementation|design|result)\b/i,
5671
+ /\b(?:revert|undo|discard|reject) (?:that|this|the (?:approach|implementation|change|design|result))\b/i,
5672
+ ];
5673
+
5674
+ export function promptContainsUserCorrection(prompt) {
5675
+ const text = String(prompt ?? "").trim();
5676
+ return (
5677
+ text.length >= 8 &&
5678
+ USER_CORRECTION_CAPTURE_PATTERNS.some((pattern) => pattern.test(text))
5679
+ );
5680
+ }
5681
+
5682
+ export function userCorrectionCaptureDirective(prompt) {
5683
+ if (!promptContainsUserCorrection(prompt)) return null;
5684
+ return [
5685
+ "Remembrance user-correction capture:",
5686
+ "The user appears to have corrected, rejected, or repeated guidance about the approach. Resolve the correction in this turn, then decide whether the corrected lesson is reusable beyond this person and repository.",
5687
+ "For a compact general failure, correction, or evidence lesson, prepare and submit one private lesson before finishing. For a complete reusable workflow, propose a private skill instead. Store person-, repository-, or machine-specific facts only in the host's local memory.",
5688
+ "Writing local memory does not satisfy organization-shared capture. Do not send raw repository content, the user's wording, paths, URLs, identifiers, or secrets; generalize first and use the private organization lane.",
5689
+ ].join("\n");
5690
+ }
5691
+
5026
5692
  function promptDisclaimsDurablePreference(prompt) {
5027
5693
  return /\b(?:(?:contrived|hypothetical|illustrative)\s+(?:example|scenario)|(?:example|scenario)\s+only|not\s+(?:a\s+)?(?:lasting\s+)?(?:preference|rule|default)|(?:do not|don't)\s+(?:save|remember|record|learn)\s+(?:this|that|it))\b/i.test(
5028
5694
  String(prompt ?? ""),