@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.
- package/README.md +9 -4
- package/opencode.json +2 -2
- package/package.json +1 -1
- package/scripts/hook-core.mjs +706 -40
- package/servers/remembrance-mcp.mjs +91 -20
- package/skills/remembrancer/SKILL.md +26 -8
- package/skills/remembrancer/references/remembrance-setup.md +6 -2
- package/src/index.mjs +50 -11
package/scripts/hook-core.mjs
CHANGED
|
@@ -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|
|
|
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
|
|
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
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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 ?? ""),
|