iranti 0.3.0 → 0.3.2
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 +843 -839
- package/bin/iranti.js +1 -1
- package/dist/scripts/claude-code-memory-hook.js +41 -153
- package/dist/scripts/iranti-cli.js +86 -9
- package/dist/scripts/iranti-mcp.js +141 -69
- package/dist/scripts/seed.js +1 -1
- package/dist/src/api/middleware/validation.d.ts.map +1 -1
- package/dist/src/api/middleware/validation.js +13 -1
- package/dist/src/api/middleware/validation.js.map +1 -1
- package/dist/src/api/routes/knowledge.d.ts.map +1 -1
- package/dist/src/api/routes/knowledge.js +3 -0
- package/dist/src/api/routes/knowledge.js.map +1 -1
- package/dist/src/api/routes/memory.d.ts.map +1 -1
- package/dist/src/api/routes/memory.js +3 -0
- package/dist/src/api/routes/memory.js.map +1 -1
- package/dist/src/api/server.js +1 -1
- package/dist/src/archivist/index.js +9 -9
- package/dist/src/attendant/AttendantInstance.d.ts +42 -1
- package/dist/src/attendant/AttendantInstance.d.ts.map +1 -1
- package/dist/src/attendant/AttendantInstance.js +496 -90
- package/dist/src/attendant/AttendantInstance.js.map +1 -1
- package/dist/src/attendant/index.d.ts +1 -1
- package/dist/src/attendant/index.d.ts.map +1 -1
- package/dist/src/attendant/index.js.map +1 -1
- package/dist/src/chat/index.d.ts +2 -0
- package/dist/src/chat/index.d.ts.map +1 -1
- package/dist/src/chat/index.js +56 -22
- package/dist/src/chat/index.js.map +1 -1
- package/dist/src/lib/assistantCheckpoint.d.ts +21 -0
- package/dist/src/lib/assistantCheckpoint.d.ts.map +1 -0
- package/dist/src/lib/assistantCheckpoint.js +143 -0
- package/dist/src/lib/assistantCheckpoint.js.map +1 -0
- package/dist/src/lib/cliHelpCatalog.js +2 -2
- package/dist/src/lib/cliHelpCatalog.js.map +1 -1
- package/dist/src/lib/hostMemoryFormatting.d.ts +25 -0
- package/dist/src/lib/hostMemoryFormatting.d.ts.map +1 -0
- package/dist/src/lib/hostMemoryFormatting.js +55 -0
- package/dist/src/lib/hostMemoryFormatting.js.map +1 -0
- package/dist/src/lib/projectLearning.d.ts +21 -0
- package/dist/src/lib/projectLearning.d.ts.map +1 -0
- package/dist/src/lib/projectLearning.js +357 -0
- package/dist/src/lib/projectLearning.js.map +1 -0
- package/dist/src/lib/protocolEnforcement.d.ts +3 -1
- package/dist/src/lib/protocolEnforcement.d.ts.map +1 -1
- package/dist/src/lib/protocolEnforcement.js +28 -2
- package/dist/src/lib/protocolEnforcement.js.map +1 -1
- package/dist/src/lib/sessionLedger.d.ts +18 -0
- package/dist/src/lib/sessionLedger.d.ts.map +1 -1
- package/dist/src/lib/sessionLedger.js +78 -0
- package/dist/src/lib/sessionLedger.js.map +1 -1
- package/dist/src/librarian/index.d.ts.map +1 -1
- package/dist/src/librarian/index.js +102 -51
- package/dist/src/librarian/index.js.map +1 -1
- package/dist/src/library/queries.js +56 -56
- package/dist/src/sdk/index.d.ts +2 -0
- package/dist/src/sdk/index.d.ts.map +1 -1
- package/dist/src/sdk/index.js +39 -2
- package/dist/src/sdk/index.js.map +1 -1
- package/package.json +9 -5
|
@@ -5,6 +5,7 @@ exports.normalizeExplicitTask = normalizeExplicitTask;
|
|
|
5
5
|
exports.formatOperatingRulesText = formatOperatingRulesText;
|
|
6
6
|
exports.readPersistedSessionState = readPersistedSessionState;
|
|
7
7
|
exports.summarizeSessionState = summarizeSessionState;
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
8
9
|
const router_1 = require("../lib/router");
|
|
9
10
|
const staffEventRegistry_1 = require("../lib/staffEventRegistry");
|
|
10
11
|
const queries_1 = require("../library/queries");
|
|
@@ -19,6 +20,7 @@ const autoRemember_1 = require("../lib/autoRemember");
|
|
|
19
20
|
const semanticFactTags_1 = require("../lib/semanticFactTags");
|
|
20
21
|
const sessionLedger_1 = require("../lib/sessionLedger");
|
|
21
22
|
const sharedStateInvalidation_1 = require("../lib/sharedStateInvalidation");
|
|
23
|
+
const hostMemoryFormatting_1 = require("../lib/hostMemoryFormatting");
|
|
22
24
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
23
25
|
const ATTENDANT_RULES_QUERY = {
|
|
24
26
|
entityType: 'system',
|
|
@@ -55,6 +57,11 @@ const ENTITY_DETECTION_WINDOW_CHARS = 1500;
|
|
|
55
57
|
const MIN_ENTITY_CONFIDENCE = 0.75;
|
|
56
58
|
const MEMORY_DECISION_CONTEXT_WINDOW_CHARS = 2000;
|
|
57
59
|
const LEDGER_WORKING_MEMORY_PREFIX = 'system/session_ledger/recent_learning_';
|
|
60
|
+
const LEGACY_CONTINUITY_KEY_MAP = {
|
|
61
|
+
checkpoint_current_step: 'current_step',
|
|
62
|
+
checkpoint_next_step: 'next_step',
|
|
63
|
+
checkpoint_open_risks: 'open_risks',
|
|
64
|
+
};
|
|
58
65
|
const ATTEND_EXPECTED_CALL_SEQUENCE = [
|
|
59
66
|
'Call iranti_handshake at session start and again after context compaction.',
|
|
60
67
|
"Call iranti_attend(phase='pre-response') before replying to the user.",
|
|
@@ -67,6 +74,20 @@ const ATTEND_EXPECTED_CALL_SEQUENCE = [
|
|
|
67
74
|
];
|
|
68
75
|
const ATTEND_USAGE_REMINDER = 'Iranti is a hive mind. iranti_attend is mandatory before each reply and around knowledge discovery, and knowledge-changing actions must leave breadcrumbs through iranti_write and/or iranti_checkpoint so later sessions do not have to rediscover context.';
|
|
69
76
|
const OBSERVE_USAGE_NOTE = 'observe() is retrieval-only. It surfaces candidate facts for context and warm-up, but it does not persist memory, replace iranti_attend, or count as a checkpoint/write.';
|
|
77
|
+
function normalizeContinuityKey(key) {
|
|
78
|
+
return LEGACY_CONTINUITY_KEY_MAP[key] ?? key;
|
|
79
|
+
}
|
|
80
|
+
function expandContinuityPriorityKeys(keys) {
|
|
81
|
+
const expanded = new Set();
|
|
82
|
+
for (const rawKey of keys) {
|
|
83
|
+
const key = rawKey.trim();
|
|
84
|
+
if (!key)
|
|
85
|
+
continue;
|
|
86
|
+
expanded.add(key);
|
|
87
|
+
expanded.add(normalizeContinuityKey(key));
|
|
88
|
+
}
|
|
89
|
+
return Array.from(expanded);
|
|
90
|
+
}
|
|
70
91
|
const MEMORY_NEED_POSITIVE_PATTERNS = [
|
|
71
92
|
/\bwhat(?:'s| is| was)?\s+my\b/i,
|
|
72
93
|
/\bdo you remember\b/i,
|
|
@@ -213,6 +234,83 @@ function mergeWorkingMemoryWithLedger(entries, learnings) {
|
|
|
213
234
|
? [...retained, ...toLedgerWorkingMemoryEntries(learnings)]
|
|
214
235
|
: retained;
|
|
215
236
|
}
|
|
237
|
+
function normalizeProjectPolicyRuleLines(value, fallbackSummary) {
|
|
238
|
+
const rules = [];
|
|
239
|
+
if (typeof value === 'string' && value.trim()) {
|
|
240
|
+
rules.push(value.trim());
|
|
241
|
+
}
|
|
242
|
+
else if (value && typeof value === 'object') {
|
|
243
|
+
const record = value;
|
|
244
|
+
if (typeof record.rule === 'string' && record.rule.trim()) {
|
|
245
|
+
rules.push(record.rule.trim());
|
|
246
|
+
}
|
|
247
|
+
if (typeof record.text === 'string' && record.text.trim()) {
|
|
248
|
+
rules.push(record.text.trim());
|
|
249
|
+
}
|
|
250
|
+
if (typeof record.instruction === 'string' && record.instruction.trim()) {
|
|
251
|
+
rules.push(record.instruction.trim());
|
|
252
|
+
}
|
|
253
|
+
if (Array.isArray(record.rules)) {
|
|
254
|
+
for (const rule of record.rules) {
|
|
255
|
+
if (typeof rule === 'string' && rule.trim()) {
|
|
256
|
+
rules.push(rule.trim());
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (Array.isArray(record.preferences)) {
|
|
261
|
+
for (const preference of record.preferences) {
|
|
262
|
+
if (typeof preference === 'string' && preference.trim()) {
|
|
263
|
+
rules.push(preference.trim());
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (rules.length === 0 && fallbackSummary?.trim()) {
|
|
269
|
+
rules.push(fallbackSummary.trim());
|
|
270
|
+
}
|
|
271
|
+
return Array.from(new Set(rules.map((rule) => rule.trim()).filter(Boolean)));
|
|
272
|
+
}
|
|
273
|
+
function isProjectPolicyKey(key) {
|
|
274
|
+
return /(?:^agent_(?:operating_)?(?:rule|rules|preference|preferences)$|(?:_rule|_rules|_preference|_preferences)$)/i.test(key.trim());
|
|
275
|
+
}
|
|
276
|
+
function isProjectPolicyEntry(entry) {
|
|
277
|
+
if (isProjectPolicyKey(entry.key))
|
|
278
|
+
return true;
|
|
279
|
+
const durableClass = typeof entry.properties?.durableClass === 'string'
|
|
280
|
+
? entry.properties.durableClass.trim().toLowerCase()
|
|
281
|
+
: '';
|
|
282
|
+
const semanticIntent = typeof entry.properties?.semanticIntent === 'string'
|
|
283
|
+
? entry.properties.semanticIntent.trim().toLowerCase()
|
|
284
|
+
: '';
|
|
285
|
+
return durableClass === 'preference' || semanticIntent === 'preference_capture';
|
|
286
|
+
}
|
|
287
|
+
function toProjectPolicyWorkingMemoryEntries(entries) {
|
|
288
|
+
return entries.map((entry) => ({
|
|
289
|
+
entityKey: entry.entityKey,
|
|
290
|
+
summary: `Project policy: ${entry.summary}`,
|
|
291
|
+
confidence: 100,
|
|
292
|
+
source: entry.source,
|
|
293
|
+
lastUpdated: entry.lastUpdated,
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
function mergeWorkingMemoryWithProjectPolicies(entries, policies) {
|
|
297
|
+
const retained = entries.filter((entry) => !policies.some((policy) => policy.entityKey === entry.entityKey));
|
|
298
|
+
return policies.length > 0
|
|
299
|
+
? [...toProjectPolicyWorkingMemoryEntries(policies), ...retained]
|
|
300
|
+
: retained;
|
|
301
|
+
}
|
|
302
|
+
function applyProjectPolicyOperatingRules(operatingRules, projectPolicies) {
|
|
303
|
+
let nextRules = operatingRules;
|
|
304
|
+
for (const policy of projectPolicies) {
|
|
305
|
+
for (const rule of policy.rules) {
|
|
306
|
+
const renderedRule = `PROJECT POLICY (${policy.key}): ${rule}`;
|
|
307
|
+
if (!nextRules.includes(renderedRule)) {
|
|
308
|
+
nextRules = `${nextRules}\n- ${renderedRule}`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return nextRules;
|
|
313
|
+
}
|
|
216
314
|
function formatMissingWriteCategories(categories) {
|
|
217
315
|
const labelMap = {
|
|
218
316
|
findings: 'what you found',
|
|
@@ -489,6 +587,18 @@ function buildSessionComplianceState(input) {
|
|
|
489
587
|
requiredAction: 'Persist durable findings with iranti_write or iranti_checkpoint before the next turn if new knowledge, validation, or file changes occurred.',
|
|
490
588
|
});
|
|
491
589
|
}
|
|
590
|
+
if (input.consecutiveUnusedMemoryInjections > 0) {
|
|
591
|
+
const severity = input.consecutiveUnusedMemoryInjections >= 2 ? 'error' : 'warn';
|
|
592
|
+
issues.push({
|
|
593
|
+
code: 'ignored_injected_memory',
|
|
594
|
+
severity,
|
|
595
|
+
count: input.consecutiveUnusedMemoryInjections,
|
|
596
|
+
message: input.consecutiveUnusedMemoryInjections >= 2
|
|
597
|
+
? `Injected memory has been surfaced and then ignored across ${input.consecutiveUnusedMemoryInjections} consecutive turns.`
|
|
598
|
+
: 'Injected memory was surfaced but the response did not use it in the previous turn.',
|
|
599
|
+
requiredAction: 'On the next turn, either answer from the injected facts directly or persist why the injected memory was insufficient before rediscovering the same state manually.',
|
|
600
|
+
});
|
|
601
|
+
}
|
|
492
602
|
let status = 'healthy';
|
|
493
603
|
if (issues.some((issue) => issue.severity === 'error')) {
|
|
494
604
|
status = 'non_compliant';
|
|
@@ -501,10 +611,14 @@ function buildSessionComplianceState(input) {
|
|
|
501
611
|
? 'Lifecycle is currently in progress and waiting for a post-response attend.'
|
|
502
612
|
: 'Lifecycle is currently compliant.'
|
|
503
613
|
: status === 'degraded'
|
|
504
|
-
?
|
|
614
|
+
? input.consecutiveUnusedMemoryInjections > 0
|
|
615
|
+
? 'Lifecycle is degraded: injected memory was surfaced but not used.'
|
|
616
|
+
: 'Lifecycle is degraded: persistence breadcrumbs are lagging.'
|
|
505
617
|
: input.consecutivePreResponseWithoutPost > 0
|
|
506
618
|
? 'Lifecycle is non-compliant: the previous turn is still missing a post-response attend.'
|
|
507
|
-
:
|
|
619
|
+
: input.consecutiveUnusedMemoryInjections > 0
|
|
620
|
+
? 'Lifecycle is non-compliant: injected memory is being ignored instead of used or explicitly challenged.'
|
|
621
|
+
: 'Lifecycle is non-compliant: accountability breadcrumbs are missing.';
|
|
508
622
|
return {
|
|
509
623
|
status,
|
|
510
624
|
summary,
|
|
@@ -513,6 +627,7 @@ function buildSessionComplianceState(input) {
|
|
|
513
627
|
counters: {
|
|
514
628
|
attendsWithoutPersist: input.attendsWithoutPersist,
|
|
515
629
|
consecutivePreResponseWithoutPost: input.consecutivePreResponseWithoutPost,
|
|
630
|
+
consecutiveUnusedMemoryInjections: input.consecutiveUnusedMemoryInjections,
|
|
516
631
|
pendingPostResponse,
|
|
517
632
|
lastAttendPhase: input.lastAttendPhase ?? null,
|
|
518
633
|
},
|
|
@@ -970,13 +1085,13 @@ async function persistSharedCheckpointBreadcrumbs(params) {
|
|
|
970
1085
|
if (checkpoint.currentStep) {
|
|
971
1086
|
await (0, librarian_1.librarianWrite)({
|
|
972
1087
|
...common,
|
|
973
|
-
key: '
|
|
1088
|
+
key: 'current_step',
|
|
974
1089
|
valueRaw: { text: checkpoint.currentStep },
|
|
975
|
-
valueSummary: truncate(`
|
|
1090
|
+
valueSummary: truncate(`current step is ${checkpoint.currentStep}`, 220),
|
|
976
1091
|
properties: {
|
|
977
1092
|
...checkpointBaseProperties,
|
|
978
1093
|
durableClass: 'current_step',
|
|
979
|
-
canonicalKey: '
|
|
1094
|
+
canonicalKey: 'current_step',
|
|
980
1095
|
mergeStrategy: 'replace',
|
|
981
1096
|
...(0, semanticFactTags_1.buildSemanticFactTags)({
|
|
982
1097
|
memoryScope: 'project',
|
|
@@ -989,19 +1104,32 @@ async function persistSharedCheckpointBreadcrumbs(params) {
|
|
|
989
1104
|
expectedKeys.push({
|
|
990
1105
|
entityType: resolved.entityType,
|
|
991
1106
|
entityId: resolved.entityId,
|
|
992
|
-
key: '
|
|
1107
|
+
key: 'current_step',
|
|
993
1108
|
});
|
|
994
1109
|
}
|
|
995
1110
|
if (checkpoint.nextStep) {
|
|
1111
|
+
const existingNextStep = await (0, queries_2.findEntry)({
|
|
1112
|
+
entityType: resolved.entityType,
|
|
1113
|
+
entityId: resolved.entityId,
|
|
1114
|
+
key: 'next_step',
|
|
1115
|
+
});
|
|
1116
|
+
const priorInstruction = existingNextStep?.valueRaw && typeof existingNextStep.valueRaw === 'object'
|
|
1117
|
+
? existingNextStep.valueRaw.instruction
|
|
1118
|
+
: null;
|
|
1119
|
+
const mergedNextStep = typeof priorInstruction === 'string'
|
|
1120
|
+
&& priorInstruction.trim().length > 0
|
|
1121
|
+
&& priorInstruction.trim() !== checkpoint.nextStep.trim()
|
|
1122
|
+
? `${checkpoint.nextStep}. Prior task step: ${priorInstruction.trim()}`
|
|
1123
|
+
: checkpoint.nextStep;
|
|
996
1124
|
await (0, librarian_1.librarianWrite)({
|
|
997
1125
|
...common,
|
|
998
|
-
key: '
|
|
999
|
-
valueRaw: { instruction:
|
|
1000
|
-
valueSummary: truncate(`
|
|
1126
|
+
key: 'next_step',
|
|
1127
|
+
valueRaw: { instruction: mergedNextStep },
|
|
1128
|
+
valueSummary: truncate(`next step is ${mergedNextStep}`, 220),
|
|
1001
1129
|
properties: {
|
|
1002
1130
|
...checkpointBaseProperties,
|
|
1003
1131
|
durableClass: 'next_step',
|
|
1004
|
-
canonicalKey: '
|
|
1132
|
+
canonicalKey: 'next_step',
|
|
1005
1133
|
mergeStrategy: 'replace',
|
|
1006
1134
|
...(0, semanticFactTags_1.buildSemanticFactTags)({
|
|
1007
1135
|
memoryScope: 'project',
|
|
@@ -1014,7 +1142,7 @@ async function persistSharedCheckpointBreadcrumbs(params) {
|
|
|
1014
1142
|
expectedKeys.push({
|
|
1015
1143
|
entityType: resolved.entityType,
|
|
1016
1144
|
entityId: resolved.entityId,
|
|
1017
|
-
key: '
|
|
1145
|
+
key: 'next_step',
|
|
1018
1146
|
});
|
|
1019
1147
|
}
|
|
1020
1148
|
if (Array.isArray(checkpoint.fileChanges) && checkpoint.fileChanges.length > 0) {
|
|
@@ -1082,13 +1210,13 @@ async function persistSharedCheckpointBreadcrumbs(params) {
|
|
|
1082
1210
|
if (checkpoint.openRisks && checkpoint.openRisks.length > 0) {
|
|
1083
1211
|
await (0, librarian_1.librarianWrite)({
|
|
1084
1212
|
...common,
|
|
1085
|
-
key: '
|
|
1213
|
+
key: 'open_risks',
|
|
1086
1214
|
valueRaw: { items: checkpoint.openRisks },
|
|
1087
|
-
valueSummary: truncate(`
|
|
1215
|
+
valueSummary: truncate(`open risks include ${checkpoint.openRisks.join('; ')}`, 220),
|
|
1088
1216
|
properties: {
|
|
1089
1217
|
...checkpointBaseProperties,
|
|
1090
1218
|
durableClass: 'open_risks',
|
|
1091
|
-
canonicalKey: '
|
|
1219
|
+
canonicalKey: 'open_risks',
|
|
1092
1220
|
mergeStrategy: 'replace',
|
|
1093
1221
|
...(0, semanticFactTags_1.buildSemanticFactTags)({
|
|
1094
1222
|
memoryScope: 'project',
|
|
@@ -1101,7 +1229,7 @@ async function persistSharedCheckpointBreadcrumbs(params) {
|
|
|
1101
1229
|
expectedKeys.push({
|
|
1102
1230
|
entityType: resolved.entityType,
|
|
1103
1231
|
entityId: resolved.entityId,
|
|
1104
|
-
key: '
|
|
1232
|
+
key: 'open_risks',
|
|
1105
1233
|
});
|
|
1106
1234
|
}
|
|
1107
1235
|
}
|
|
@@ -1200,6 +1328,7 @@ class AttendantInstance {
|
|
|
1200
1328
|
this.contextCallCount = 0;
|
|
1201
1329
|
this.attendsWithoutPersist = 0;
|
|
1202
1330
|
this.consecutivePreResponseWithoutPost = 0;
|
|
1331
|
+
this.consecutiveUnusedMemoryInjections = 0;
|
|
1203
1332
|
this.lastAttendPhase = undefined;
|
|
1204
1333
|
this.complianceUpdatedAt = new Date().toISOString();
|
|
1205
1334
|
this.sessionStarted = new Date().toISOString();
|
|
@@ -1208,6 +1337,7 @@ class AttendantInstance {
|
|
|
1208
1337
|
this.eventHost = null;
|
|
1209
1338
|
this.sharedStateObservedAt = null;
|
|
1210
1339
|
this.pendingSharedStateInvalidations = new Map();
|
|
1340
|
+
this.pendingMemoryAttributions = [];
|
|
1211
1341
|
this.agentId = agentId;
|
|
1212
1342
|
(0, sharedStateInvalidation_1.registerSharedStateInvalidationObserver)(agentId, this);
|
|
1213
1343
|
}
|
|
@@ -1232,6 +1362,156 @@ class AttendantInstance {
|
|
|
1232
1362
|
...(this.eventHost ? { host: this.eventHost } : {}),
|
|
1233
1363
|
};
|
|
1234
1364
|
}
|
|
1365
|
+
updateBriefPendingMemoryAttributions() {
|
|
1366
|
+
if (!this.brief)
|
|
1367
|
+
return;
|
|
1368
|
+
this.brief = {
|
|
1369
|
+
...this.brief,
|
|
1370
|
+
pendingMemoryAttributions: this.pendingMemoryAttributions.map((entry) => ({ ...entry })),
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
addPendingMemoryAttribution(input) {
|
|
1374
|
+
const attribution = {
|
|
1375
|
+
injectionId: (0, crypto_1.randomUUID)(),
|
|
1376
|
+
surfaced: true,
|
|
1377
|
+
used: false,
|
|
1378
|
+
helpful: false,
|
|
1379
|
+
status: 'pending',
|
|
1380
|
+
phase: input.phase,
|
|
1381
|
+
surfacedAt: new Date().toISOString(),
|
|
1382
|
+
reason: 'awaiting_post_response_evaluation',
|
|
1383
|
+
injectedKeys: [...input.injectedKeys],
|
|
1384
|
+
injectedEntryIds: [...input.injectedEntryIds],
|
|
1385
|
+
evidenceKinds: [],
|
|
1386
|
+
};
|
|
1387
|
+
this.pendingMemoryAttributions.push(attribution);
|
|
1388
|
+
this.updateBriefPendingMemoryAttributions();
|
|
1389
|
+
return attribution;
|
|
1390
|
+
}
|
|
1391
|
+
recordMemoryEvidence(kind) {
|
|
1392
|
+
if (this.pendingMemoryAttributions.length === 0) {
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
for (const attribution of this.pendingMemoryAttributions) {
|
|
1396
|
+
if (attribution.status !== 'pending')
|
|
1397
|
+
continue;
|
|
1398
|
+
if (!attribution.evidenceKinds.includes(kind)) {
|
|
1399
|
+
attribution.evidenceKinds = [...attribution.evidenceKinds, kind];
|
|
1400
|
+
}
|
|
1401
|
+
(0, staffEventRegistry_1.getStaffEventEmitter)().emit({
|
|
1402
|
+
staffComponent: 'Attendant',
|
|
1403
|
+
actionType: 'memory_evidence_observed',
|
|
1404
|
+
agentId: this.agentId,
|
|
1405
|
+
source: this.eventSource,
|
|
1406
|
+
reason: kind,
|
|
1407
|
+
level: 'audit',
|
|
1408
|
+
metadata: this.buildEventMetadata({
|
|
1409
|
+
injectionId: attribution.injectionId,
|
|
1410
|
+
injectedKeys: attribution.injectedKeys,
|
|
1411
|
+
injectedEntryIds: attribution.injectedEntryIds,
|
|
1412
|
+
evidenceKind: kind,
|
|
1413
|
+
}),
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
this.updateBriefPendingMemoryAttributions();
|
|
1417
|
+
}
|
|
1418
|
+
responseMentionsInjectedMemory(response, attribution) {
|
|
1419
|
+
const responseTokens = new Set(tokenize(response));
|
|
1420
|
+
if (responseTokens.size === 0)
|
|
1421
|
+
return false;
|
|
1422
|
+
for (const entityKey of attribution.injectedKeys) {
|
|
1423
|
+
const key = entityKey.split('/').slice(2).join('/');
|
|
1424
|
+
for (const token of tokenize(key.replace(/[_/.-]+/g, ' '))) {
|
|
1425
|
+
if (responseTokens.has(token)) {
|
|
1426
|
+
return true;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
responseShowsRecoveryValue(response, attribution) {
|
|
1433
|
+
const normalized = normalizeText(response);
|
|
1434
|
+
if (!normalized)
|
|
1435
|
+
return false;
|
|
1436
|
+
return attribution.injectedKeys.some((entityKey) => {
|
|
1437
|
+
const key = entityKey.split('/').slice(2).join('/');
|
|
1438
|
+
return (/\b(next step|current step|blocker|blockers|risk|risks|status|progress|file|files|changed|handoff|resume|recovery)\b/.test(normalized)
|
|
1439
|
+
&& /\b(next_step|current_step|open_risks|status|checkpoint_summary|recent_file_changes|recent_actions|implementation_status|blockers?)\b/i.test(key));
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
scorePendingMemoryAttributions(response) {
|
|
1443
|
+
if (this.pendingMemoryAttributions.length === 0) {
|
|
1444
|
+
return [];
|
|
1445
|
+
}
|
|
1446
|
+
const scoredAt = new Date().toISOString();
|
|
1447
|
+
const scored = this.pendingMemoryAttributions.map((entry) => {
|
|
1448
|
+
const evidenceKinds = [...entry.evidenceKinds];
|
|
1449
|
+
const rediscoveredManually = evidenceKinds.includes('rediscovery');
|
|
1450
|
+
if (!rediscoveredManually && this.responseMentionsInjectedMemory(response, entry) && !evidenceKinds.includes('response_reference')) {
|
|
1451
|
+
evidenceKinds.push('response_reference');
|
|
1452
|
+
}
|
|
1453
|
+
if (!rediscoveredManually && this.responseShowsRecoveryValue(response, entry) && !evidenceKinds.includes('response_recovery')) {
|
|
1454
|
+
evidenceKinds.push('response_recovery');
|
|
1455
|
+
}
|
|
1456
|
+
const used = evidenceKinds.includes('write')
|
|
1457
|
+
|| evidenceKinds.includes('checkpoint')
|
|
1458
|
+
|| evidenceKinds.includes('response_reference')
|
|
1459
|
+
|| evidenceKinds.includes('response_recovery');
|
|
1460
|
+
const helpful = evidenceKinds.includes('checkpoint')
|
|
1461
|
+
|| evidenceKinds.includes('write')
|
|
1462
|
+
|| evidenceKinds.includes('response_recovery');
|
|
1463
|
+
const reason = helpful
|
|
1464
|
+
? 'response_or_action_confirmed_memory_helpfulness'
|
|
1465
|
+
: used
|
|
1466
|
+
? 'response_referenced_injected_memory'
|
|
1467
|
+
: 'memory_was_only_surfaced';
|
|
1468
|
+
const scoredEntry = {
|
|
1469
|
+
...entry,
|
|
1470
|
+
used,
|
|
1471
|
+
helpful,
|
|
1472
|
+
status: 'scored',
|
|
1473
|
+
scoredAt,
|
|
1474
|
+
reason,
|
|
1475
|
+
evidenceKinds,
|
|
1476
|
+
};
|
|
1477
|
+
(0, staffEventRegistry_1.getStaffEventEmitter)().emit({
|
|
1478
|
+
staffComponent: 'Attendant',
|
|
1479
|
+
actionType: 'memory_injection_scored',
|
|
1480
|
+
agentId: this.agentId,
|
|
1481
|
+
source: this.eventSource,
|
|
1482
|
+
reason,
|
|
1483
|
+
level: 'audit',
|
|
1484
|
+
metadata: this.buildEventMetadata({
|
|
1485
|
+
injectionId: scoredEntry.injectionId,
|
|
1486
|
+
surfaced: true,
|
|
1487
|
+
used,
|
|
1488
|
+
helpful,
|
|
1489
|
+
phase: scoredEntry.phase,
|
|
1490
|
+
injectedKeys: scoredEntry.injectedKeys,
|
|
1491
|
+
injectedEntryIds: scoredEntry.injectedEntryIds,
|
|
1492
|
+
evidenceKinds,
|
|
1493
|
+
scoredAt,
|
|
1494
|
+
}),
|
|
1495
|
+
});
|
|
1496
|
+
return scoredEntry;
|
|
1497
|
+
});
|
|
1498
|
+
if (scored.some((entry) => entry.used)) {
|
|
1499
|
+
this.consecutiveUnusedMemoryInjections = 0;
|
|
1500
|
+
}
|
|
1501
|
+
else if (scored.some((entry) => entry.surfaced)) {
|
|
1502
|
+
this.consecutiveUnusedMemoryInjections += 1;
|
|
1503
|
+
}
|
|
1504
|
+
this.pendingMemoryAttributions = [];
|
|
1505
|
+
this.updateBriefPendingMemoryAttributions();
|
|
1506
|
+
return scored;
|
|
1507
|
+
}
|
|
1508
|
+
async noteDiscoveryOccurred() {
|
|
1509
|
+
if (this.pendingMemoryAttributions.length === 0) {
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
this.recordMemoryEvidence('rediscovery');
|
|
1513
|
+
await this.persistState();
|
|
1514
|
+
}
|
|
1235
1515
|
async loadSessionLedgerSignals(taskType) {
|
|
1236
1516
|
try {
|
|
1237
1517
|
const source = this.eventSource === 'internal' ? undefined : this.eventSource;
|
|
@@ -1260,6 +1540,33 @@ class AttendantInstance {
|
|
|
1260
1540
|
return { learnings: [], profile: null };
|
|
1261
1541
|
}
|
|
1262
1542
|
}
|
|
1543
|
+
async loadProjectPolicies() {
|
|
1544
|
+
const configured = (0, autoRemember_1.getProjectMemoryEntity)();
|
|
1545
|
+
if (!configured)
|
|
1546
|
+
return [];
|
|
1547
|
+
const parsed = (0, entity_resolution_1.parseEntityString)(configured);
|
|
1548
|
+
const entries = await (0, queries_1.findEntriesByEntity)(parsed.entityType, parsed.entityId);
|
|
1549
|
+
const policies = entries
|
|
1550
|
+
.filter((entry) => isProjectPolicyEntry({
|
|
1551
|
+
key: entry.key,
|
|
1552
|
+
properties: entry.properties ?? null,
|
|
1553
|
+
}))
|
|
1554
|
+
.map((entry) => {
|
|
1555
|
+
const rules = normalizeProjectPolicyRuleLines(entry.valueRaw, entry.valueSummary);
|
|
1556
|
+
if (rules.length === 0)
|
|
1557
|
+
return null;
|
|
1558
|
+
return {
|
|
1559
|
+
entityKey: `${entry.entityType}/${entry.entityId}/${entry.key}`,
|
|
1560
|
+
summary: rules.join(' '),
|
|
1561
|
+
key: entry.key,
|
|
1562
|
+
source: entry.source,
|
|
1563
|
+
lastUpdated: entry.updatedAt.toISOString(),
|
|
1564
|
+
rules,
|
|
1565
|
+
};
|
|
1566
|
+
})
|
|
1567
|
+
.filter((entry) => Boolean(entry));
|
|
1568
|
+
return policies;
|
|
1569
|
+
}
|
|
1263
1570
|
// ── Handshake ────────────────────────────────────────────────────────────
|
|
1264
1571
|
async handshake(context) {
|
|
1265
1572
|
const t0 = (0, metrics_1.timeStart)();
|
|
@@ -1271,13 +1578,15 @@ class AttendantInstance {
|
|
|
1271
1578
|
// Infer task type
|
|
1272
1579
|
const inferredTaskType = await this.inferTask(context);
|
|
1273
1580
|
// Load knowledge — agent entries + related entities
|
|
1274
|
-
const [workingMemory, ledgerSignals] = await Promise.all([
|
|
1581
|
+
const [workingMemory, ledgerSignals, projectPolicies] = await Promise.all([
|
|
1275
1582
|
this.buildWorkingMemory(inferredTaskType),
|
|
1276
1583
|
this.loadSessionLedgerSignals(inferredTaskType),
|
|
1584
|
+
this.loadProjectPolicies(),
|
|
1277
1585
|
]);
|
|
1278
1586
|
const sessionLedgerLearnings = ledgerSignals.learnings;
|
|
1279
1587
|
this.advisoryLearningProfile = ledgerSignals.profile;
|
|
1280
|
-
const
|
|
1588
|
+
const workingMemoryWithPolicies = mergeWorkingMemoryWithProjectPolicies(workingMemory, projectPolicies);
|
|
1589
|
+
const workingMemoryWithLedger = mergeWorkingMemoryWithLedger(workingMemoryWithPolicies, sessionLedgerLearnings);
|
|
1281
1590
|
const recoveryResult = persisted?.sessionCheckpoint
|
|
1282
1591
|
? this.buildRecovery(context, persisted.sessionCheckpoint)
|
|
1283
1592
|
: { interrupted: false, recovery: null };
|
|
@@ -1294,9 +1603,10 @@ class AttendantInstance {
|
|
|
1294
1603
|
}
|
|
1295
1604
|
this.brief = {
|
|
1296
1605
|
agentId: this.agentId,
|
|
1297
|
-
operatingRules: applyAdvisoryOperatingRules(operatingRules, this.advisoryLearningProfile),
|
|
1606
|
+
operatingRules: applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(operatingRules, projectPolicies), this.advisoryLearningProfile),
|
|
1298
1607
|
inferredTaskType,
|
|
1299
1608
|
workingMemory: workingMemoryWithLedger,
|
|
1609
|
+
projectPolicies,
|
|
1300
1610
|
sessionStarted: persisted?.sessionStarted ?? this.sessionStarted,
|
|
1301
1611
|
briefGeneratedAt: new Date().toISOString(),
|
|
1302
1612
|
contextCallCount: this.contextCallCount,
|
|
@@ -1323,6 +1633,7 @@ class AttendantInstance {
|
|
|
1323
1633
|
metadata: this.buildEventMetadata({
|
|
1324
1634
|
briefSize: this.brief?.workingMemory.length ?? 0,
|
|
1325
1635
|
ledgerLearningCount: sessionLedgerLearnings.length,
|
|
1636
|
+
projectPolicyCount: projectPolicies.length,
|
|
1326
1637
|
advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
|
|
1327
1638
|
taskSummary: context.task.slice(0, 120),
|
|
1328
1639
|
}),
|
|
@@ -1340,7 +1651,10 @@ class AttendantInstance {
|
|
|
1340
1651
|
return result;
|
|
1341
1652
|
}
|
|
1342
1653
|
const newTaskType = await this.inferTask(context);
|
|
1343
|
-
const ledgerSignals = await
|
|
1654
|
+
const [ledgerSignals, projectPolicies] = await Promise.all([
|
|
1655
|
+
this.loadSessionLedgerSignals(newTaskType),
|
|
1656
|
+
this.loadProjectPolicies(),
|
|
1657
|
+
]);
|
|
1344
1658
|
this.advisoryLearningProfile = ledgerSignals.profile;
|
|
1345
1659
|
// Task hasn't shifted — update timestamp only
|
|
1346
1660
|
if (newTaskType.toLowerCase() === this.brief.inferredTaskType.toLowerCase()) {
|
|
@@ -1354,8 +1668,9 @@ class AttendantInstance {
|
|
|
1354
1668
|
}
|
|
1355
1669
|
this.brief = {
|
|
1356
1670
|
...this.brief,
|
|
1357
|
-
operatingRules: applyAdvisoryOperatingRules(this.
|
|
1358
|
-
workingMemory: mergeWorkingMemoryWithLedger(this.brief.workingMemory, ledgerSignals.learnings),
|
|
1671
|
+
operatingRules: applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(await this.loadOperatingRules(), projectPolicies), this.advisoryLearningProfile),
|
|
1672
|
+
workingMemory: mergeWorkingMemoryWithLedger(mergeWorkingMemoryWithProjectPolicies(this.brief.workingMemory, projectPolicies), ledgerSignals.learnings),
|
|
1673
|
+
projectPolicies,
|
|
1359
1674
|
briefGeneratedAt: new Date().toISOString(),
|
|
1360
1675
|
contextCallCount: this.contextCallCount,
|
|
1361
1676
|
sessionLedgerLearnings: ledgerSignals.learnings,
|
|
@@ -1375,6 +1690,7 @@ class AttendantInstance {
|
|
|
1375
1690
|
metadata: this.buildEventMetadata({
|
|
1376
1691
|
briefSize: this.brief?.workingMemory.length ?? 0,
|
|
1377
1692
|
contextCallCount: this.contextCallCount,
|
|
1693
|
+
projectPolicyCount: projectPolicies.length,
|
|
1378
1694
|
advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
|
|
1379
1695
|
}),
|
|
1380
1696
|
});
|
|
@@ -1385,9 +1701,10 @@ class AttendantInstance {
|
|
|
1385
1701
|
const workingMemory = await this.buildWorkingMemory(newTaskType);
|
|
1386
1702
|
this.brief = {
|
|
1387
1703
|
...this.brief,
|
|
1388
|
-
operatingRules: applyAdvisoryOperatingRules(this.
|
|
1704
|
+
operatingRules: applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(await this.loadOperatingRules(), projectPolicies), this.advisoryLearningProfile),
|
|
1389
1705
|
inferredTaskType: newTaskType,
|
|
1390
|
-
workingMemory: mergeWorkingMemoryWithLedger(workingMemory, ledgerSignals.learnings),
|
|
1706
|
+
workingMemory: mergeWorkingMemoryWithLedger(mergeWorkingMemoryWithProjectPolicies(workingMemory, projectPolicies), ledgerSignals.learnings),
|
|
1707
|
+
projectPolicies,
|
|
1391
1708
|
briefGeneratedAt: new Date().toISOString(),
|
|
1392
1709
|
contextCallCount: this.contextCallCount,
|
|
1393
1710
|
sessionLedgerLearnings: ledgerSignals.learnings,
|
|
@@ -1407,6 +1724,7 @@ class AttendantInstance {
|
|
|
1407
1724
|
metadata: this.buildEventMetadata({
|
|
1408
1725
|
briefSize: this.brief?.workingMemory.length ?? 0,
|
|
1409
1726
|
contextCallCount: this.contextCallCount,
|
|
1727
|
+
projectPolicyCount: projectPolicies.length,
|
|
1410
1728
|
advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
|
|
1411
1729
|
}),
|
|
1412
1730
|
});
|
|
@@ -1434,8 +1752,10 @@ class AttendantInstance {
|
|
|
1434
1752
|
const operatingRules = rulesResult.found && rulesResult.entry
|
|
1435
1753
|
? formatOperatingRulesText(rulesResult.entry.valueRaw, rulesResult.entry.valueSummary)
|
|
1436
1754
|
: formatOperatingRulesText(null, 'Attendant operating rules:');
|
|
1755
|
+
const projectPolicies = this.brief?.projectPolicies ?? await this.loadProjectPolicies();
|
|
1437
1756
|
if (this.brief) {
|
|
1438
|
-
this.brief.operatingRules = operatingRules;
|
|
1757
|
+
this.brief.operatingRules = applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(operatingRules, projectPolicies), this.advisoryLearningProfile);
|
|
1758
|
+
this.brief.projectPolicies = projectPolicies;
|
|
1439
1759
|
this.brief.contextCallCount = 0;
|
|
1440
1760
|
}
|
|
1441
1761
|
this.contextCallCount = 0;
|
|
@@ -1473,6 +1793,7 @@ class AttendantInstance {
|
|
|
1473
1793
|
return buildSessionComplianceState({
|
|
1474
1794
|
attendsWithoutPersist: this.attendsWithoutPersist,
|
|
1475
1795
|
consecutivePreResponseWithoutPost: this.consecutivePreResponseWithoutPost,
|
|
1796
|
+
consecutiveUnusedMemoryInjections: this.consecutiveUnusedMemoryInjections,
|
|
1476
1797
|
lastAttendPhase: this.lastAttendPhase,
|
|
1477
1798
|
lastUpdated: lastUpdated ?? this.complianceUpdatedAt,
|
|
1478
1799
|
});
|
|
@@ -1482,6 +1803,7 @@ class AttendantInstance {
|
|
|
1482
1803
|
this.lastAttendPhase = undefined;
|
|
1483
1804
|
this.consecutivePreResponseWithoutPost = 0;
|
|
1484
1805
|
this.complianceUpdatedAt = new Date().toISOString();
|
|
1806
|
+
this.recordMemoryEvidence('write');
|
|
1485
1807
|
if (!this.brief) {
|
|
1486
1808
|
return;
|
|
1487
1809
|
}
|
|
@@ -1497,6 +1819,7 @@ class AttendantInstance {
|
|
|
1497
1819
|
this.lastAttendPhase = undefined;
|
|
1498
1820
|
this.consecutivePreResponseWithoutPost = 0;
|
|
1499
1821
|
this.complianceUpdatedAt = new Date().toISOString();
|
|
1822
|
+
this.recordMemoryEvidence('checkpoint');
|
|
1500
1823
|
this.setLedgerContext(input.ledgerContext);
|
|
1501
1824
|
const now = new Date().toISOString();
|
|
1502
1825
|
if (!this.brief) {
|
|
@@ -1730,6 +2053,7 @@ class AttendantInstance {
|
|
|
1730
2053
|
this.attendsWithoutPersist++;
|
|
1731
2054
|
const phase = input.phase;
|
|
1732
2055
|
let complianceWarning;
|
|
2056
|
+
const ignoredMemoryWarning = 'COMPLIANCE: injected memory was surfaced but not used. On the next turn, either answer from injected facts directly or persist why the injected memory was insufficient before rediscovering the same state manually.';
|
|
1733
2057
|
if (phase === 'post-response') {
|
|
1734
2058
|
// Correct post-response call — reset counters
|
|
1735
2059
|
this.attendsWithoutPersist = 0;
|
|
@@ -1753,7 +2077,10 @@ class AttendantInstance {
|
|
|
1753
2077
|
}
|
|
1754
2078
|
}
|
|
1755
2079
|
this.complianceUpdatedAt = new Date().toISOString();
|
|
1756
|
-
|
|
2080
|
+
let compliance = this.buildComplianceState(this.complianceUpdatedAt);
|
|
2081
|
+
if (!complianceWarning && compliance.issues.some((issue) => issue.code === 'ignored_injected_memory')) {
|
|
2082
|
+
complianceWarning = ignoredMemoryWarning;
|
|
2083
|
+
}
|
|
1757
2084
|
if (!this.brief) {
|
|
1758
2085
|
const bootstrapTask = buildAttendBootstrapTask(latestMessage, currentContext);
|
|
1759
2086
|
const bootstrapBrief = await this.handshake({
|
|
@@ -1788,6 +2115,52 @@ class AttendantInstance {
|
|
|
1788
2115
|
}),
|
|
1789
2116
|
});
|
|
1790
2117
|
}
|
|
2118
|
+
if (phase === 'post-response') {
|
|
2119
|
+
const memoryAttributions = this.scorePendingMemoryAttributions(latestMessage || currentContext);
|
|
2120
|
+
compliance = this.buildComplianceState(this.complianceUpdatedAt);
|
|
2121
|
+
if (memoryAttributions.some((entry) => !entry.used)) {
|
|
2122
|
+
complianceWarning = ignoredMemoryWarning;
|
|
2123
|
+
}
|
|
2124
|
+
if (this.brief) {
|
|
2125
|
+
this.brief = {
|
|
2126
|
+
...this.brief,
|
|
2127
|
+
compliance,
|
|
2128
|
+
briefGeneratedAt: this.complianceUpdatedAt,
|
|
2129
|
+
};
|
|
2130
|
+
await this.persistState();
|
|
2131
|
+
}
|
|
2132
|
+
(0, metrics_1.timeEnd)('attendant.attend_ms', t0);
|
|
2133
|
+
return {
|
|
2134
|
+
shouldInject: false,
|
|
2135
|
+
reason: 'memory_not_needed',
|
|
2136
|
+
decision: {
|
|
2137
|
+
needed: false,
|
|
2138
|
+
confidence: 1,
|
|
2139
|
+
method: 'heuristic',
|
|
2140
|
+
explanation: 'post_response_closeout',
|
|
2141
|
+
},
|
|
2142
|
+
bootstrap,
|
|
2143
|
+
complianceWarning,
|
|
2144
|
+
compliance,
|
|
2145
|
+
memoryAttributions,
|
|
2146
|
+
usageGuidance: buildUsageGuidance('attend'),
|
|
2147
|
+
facts: [],
|
|
2148
|
+
entitiesDetected: [],
|
|
2149
|
+
alreadyPresent: 0,
|
|
2150
|
+
totalFound: 0,
|
|
2151
|
+
entitiesResolved: [],
|
|
2152
|
+
debug: {
|
|
2153
|
+
skipped: 'empty_context',
|
|
2154
|
+
contextLength: currentContext.length,
|
|
2155
|
+
detectionWindowChars: Math.min(currentContext.length, ENTITY_DETECTION_WINDOW_CHARS),
|
|
2156
|
+
detectedCandidates: 0,
|
|
2157
|
+
keptCandidates: 0,
|
|
2158
|
+
hintsProvided: effectiveEntityHints.length,
|
|
2159
|
+
hintsResolved: 0,
|
|
2160
|
+
dropped: [{ name: latestMessage || '(none)', reason: 'post_response_closeout' }],
|
|
2161
|
+
},
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
1791
2164
|
let decision = await this.decideMemoryNeed({
|
|
1792
2165
|
currentContext,
|
|
1793
2166
|
latestMessage,
|
|
@@ -1847,6 +2220,7 @@ class AttendantInstance {
|
|
|
1847
2220
|
bootstrap,
|
|
1848
2221
|
complianceWarning,
|
|
1849
2222
|
compliance,
|
|
2223
|
+
memoryAttributions: [],
|
|
1850
2224
|
usageGuidance: buildUsageGuidance('attend'),
|
|
1851
2225
|
facts: [],
|
|
1852
2226
|
entitiesDetected: [],
|
|
@@ -1870,11 +2244,11 @@ class AttendantInstance {
|
|
|
1870
2244
|
currentContext: observationContext,
|
|
1871
2245
|
maxFacts: input.maxFacts,
|
|
1872
2246
|
entityHints: observeEntityHints,
|
|
1873
|
-
priorityKeys: Array.from(new Set([
|
|
2247
|
+
priorityKeys: expandContinuityPriorityKeys(Array.from(new Set([
|
|
1874
2248
|
...(mandatoryRecall.key ? [mandatoryRecall.key] : []),
|
|
1875
2249
|
...(this.advisoryLearningProfile?.priorityKeys ?? []),
|
|
1876
2250
|
...freshState.priorityKeys,
|
|
1877
|
-
])),
|
|
2251
|
+
]))),
|
|
1878
2252
|
skipContextFilter: forceInject,
|
|
1879
2253
|
ledgerContext: input.ledgerContext,
|
|
1880
2254
|
});
|
|
@@ -1891,10 +2265,11 @@ class AttendantInstance {
|
|
|
1891
2265
|
const remainder = slashIdx2 === -1 ? '' : fact.entityKey.slice(slashIdx2);
|
|
1892
2266
|
return { ...fact, entityKey: `${canonicalPersonalType}/${canonicalPersonalId}${remainder}` };
|
|
1893
2267
|
});
|
|
2268
|
+
const structuredFacts = (0, hostMemoryFormatting_1.assignStructuredFactIds)(remappedFacts);
|
|
1894
2269
|
watchedEntitiesChanged = this.updateWatchedEntities(observed.entitiesResolved?.map((entry) => entry.canonicalEntity) ?? []) || watchedEntitiesChanged;
|
|
1895
2270
|
this.markSharedStateObserved(observeEntityHints.length > 0 ? observeEntityHints : freshState.entities);
|
|
1896
2271
|
let reason = 'memory_needed_injected';
|
|
1897
|
-
const shouldInject =
|
|
2272
|
+
const shouldInject = structuredFacts.length > 0;
|
|
1898
2273
|
let searchSuggestion;
|
|
1899
2274
|
if (!shouldInject) {
|
|
1900
2275
|
const allAlreadyInContext = observed.totalFound > 0 && observed.alreadyPresent >= observed.totalFound;
|
|
@@ -1916,16 +2291,28 @@ class AttendantInstance {
|
|
|
1916
2291
|
else if (forceInject) {
|
|
1917
2292
|
reason = 'forced';
|
|
1918
2293
|
}
|
|
2294
|
+
const memoryAttributions = shouldInject
|
|
2295
|
+
? [
|
|
2296
|
+
this.addPendingMemoryAttribution({
|
|
2297
|
+
phase: phase === 'mid-turn' ? 'mid-turn' : 'pre-response',
|
|
2298
|
+
injectedKeys: structuredFacts.map((fact) => fact.entityKey),
|
|
2299
|
+
injectedEntryIds: structuredFacts
|
|
2300
|
+
.map((fact) => fact.knowledgeEntryId)
|
|
2301
|
+
.filter((value) => typeof value === 'number'),
|
|
2302
|
+
}),
|
|
2303
|
+
]
|
|
2304
|
+
: [];
|
|
1919
2305
|
const attendResult = {
|
|
1920
2306
|
...observed,
|
|
1921
|
-
facts:
|
|
1922
|
-
shouldInject,
|
|
2307
|
+
facts: structuredFacts,
|
|
2308
|
+
shouldInject: structuredFacts.length > 0,
|
|
1923
2309
|
reason,
|
|
1924
2310
|
decision,
|
|
1925
2311
|
bootstrap,
|
|
1926
2312
|
searchSuggestion,
|
|
1927
2313
|
complianceWarning,
|
|
1928
2314
|
compliance,
|
|
2315
|
+
memoryAttributions,
|
|
1929
2316
|
usageGuidance: buildUsageGuidance('attend'),
|
|
1930
2317
|
};
|
|
1931
2318
|
if (input.suppressEvents !== true) {
|
|
@@ -1940,6 +2327,7 @@ class AttendantInstance {
|
|
|
1940
2327
|
contextCallCount: this.contextCallCount,
|
|
1941
2328
|
shouldInject,
|
|
1942
2329
|
attendReason: reason,
|
|
2330
|
+
injectionId: memoryAttributions[0]?.injectionId ?? null,
|
|
1943
2331
|
advisoryDecisionMethod: decision.method,
|
|
1944
2332
|
advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
|
|
1945
2333
|
freshStateEntities: freshState.entities,
|
|
@@ -1956,7 +2344,11 @@ class AttendantInstance {
|
|
|
1956
2344
|
metadata: this.buildEventMetadata({
|
|
1957
2345
|
shouldInject,
|
|
1958
2346
|
factCount: observed.facts.length,
|
|
2347
|
+
injectionId: memoryAttributions[0]?.injectionId ?? null,
|
|
1959
2348
|
injectedKeys: observed.facts.map((fact) => fact.entityKey),
|
|
2349
|
+
injectedEntryIds: observed.facts
|
|
2350
|
+
.map((fact) => fact.knowledgeEntryId)
|
|
2351
|
+
.filter((value) => typeof value === 'number'),
|
|
1960
2352
|
entitiesResolved: observed.entitiesResolved?.map((entry) => entry.canonicalEntity) ?? [],
|
|
1961
2353
|
alreadyPresent: observed.alreadyPresent,
|
|
1962
2354
|
totalFound: observed.totalFound,
|
|
@@ -1973,7 +2365,7 @@ class AttendantInstance {
|
|
|
1973
2365
|
briefGeneratedAt: this.complianceUpdatedAt,
|
|
1974
2366
|
};
|
|
1975
2367
|
}
|
|
1976
|
-
if (watchedEntitiesChanged || this.brief?.compliance !== compliance) {
|
|
2368
|
+
if (watchedEntitiesChanged || this.brief?.compliance !== compliance || memoryAttributions.length > 0) {
|
|
1977
2369
|
await this.persistState();
|
|
1978
2370
|
}
|
|
1979
2371
|
(0, metrics_1.timeEnd)('attendant.attend_ms', t0);
|
|
@@ -2034,29 +2426,29 @@ class AttendantInstance {
|
|
|
2034
2426
|
const entityResponse = await (0, router_1.route)('extraction', [
|
|
2035
2427
|
{
|
|
2036
2428
|
role: 'user',
|
|
2037
|
-
content: `Extract explicitly named entities from the text.
|
|
2038
|
-
An entity can be a person, organization, project, technology, or named concept.
|
|
2039
|
-
|
|
2040
|
-
Return ONLY valid JSON as an array of objects in this exact shape:
|
|
2041
|
-
[
|
|
2042
|
-
{
|
|
2043
|
-
"type": "project",
|
|
2044
|
-
"name": "Project Atlas",
|
|
2045
|
-
"id_guess": "project_atlas",
|
|
2046
|
-
"confidence": 0.92,
|
|
2047
|
-
"evidence": "Project Atlas",
|
|
2048
|
-
"start": 123,
|
|
2049
|
-
"end": 136
|
|
2050
|
-
}
|
|
2051
|
-
]
|
|
2052
|
-
|
|
2053
|
-
Rules:
|
|
2054
|
-
- Only include entities explicitly named in the provided text.
|
|
2055
|
-
- Do not infer or carry over entities not present in the text.
|
|
2056
|
-
- If uncertain, omit.
|
|
2057
|
-
- If none are present, return [].
|
|
2058
|
-
|
|
2059
|
-
Text:
|
|
2429
|
+
content: `Extract explicitly named entities from the text.
|
|
2430
|
+
An entity can be a person, organization, project, technology, or named concept.
|
|
2431
|
+
|
|
2432
|
+
Return ONLY valid JSON as an array of objects in this exact shape:
|
|
2433
|
+
[
|
|
2434
|
+
{
|
|
2435
|
+
"type": "project",
|
|
2436
|
+
"name": "Project Atlas",
|
|
2437
|
+
"id_guess": "project_atlas",
|
|
2438
|
+
"confidence": 0.92,
|
|
2439
|
+
"evidence": "Project Atlas",
|
|
2440
|
+
"start": 123,
|
|
2441
|
+
"end": 136
|
|
2442
|
+
}
|
|
2443
|
+
]
|
|
2444
|
+
|
|
2445
|
+
Rules:
|
|
2446
|
+
- Only include entities explicitly named in the provided text.
|
|
2447
|
+
- Do not infer or carry over entities not present in the text.
|
|
2448
|
+
- If uncertain, omit.
|
|
2449
|
+
- If none are present, return [].
|
|
2450
|
+
|
|
2451
|
+
Text:
|
|
2060
2452
|
${detectionWindow}`,
|
|
2061
2453
|
},
|
|
2062
2454
|
], 512);
|
|
@@ -2253,7 +2645,7 @@ ${detectionWindow}`,
|
|
|
2253
2645
|
const allEntries = await (0, queries_1.findEntriesByEntity)(resolvedInfo.entityType, resolvedInfo.entityId);
|
|
2254
2646
|
// Priority keys first
|
|
2255
2647
|
const policyPriorityKeys = policy.observeKeyPriority?.[resolvedInfo.entityType] ?? [];
|
|
2256
|
-
const priorityKeys = new Set([...policyPriorityKeys, ...requestedPriorityKeys]);
|
|
2648
|
+
const priorityKeys = new Set(expandContinuityPriorityKeys([...policyPriorityKeys, ...requestedPriorityKeys]));
|
|
2257
2649
|
const priorityEntries = allEntries.filter((e) => priorityKeys.has(e.key));
|
|
2258
2650
|
const remainingEntries = allEntries
|
|
2259
2651
|
.filter((e) => !priorityKeys.has(e.key))
|
|
@@ -2264,13 +2656,20 @@ ${detectionWindow}`,
|
|
|
2264
2656
|
|| a.key.localeCompare(b.key));
|
|
2265
2657
|
});
|
|
2266
2658
|
const selectedEntries = [...priorityEntries, ...remainingEntries].slice(0, maxKeysPerEntity);
|
|
2659
|
+
const freshestEntry = allEntries
|
|
2660
|
+
.slice()
|
|
2661
|
+
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime() || b.confidence - a.confidence)[0];
|
|
2662
|
+
if (freshestEntry && !selectedEntries.some((entry) => entry.id === freshestEntry.id)) {
|
|
2663
|
+
selectedEntries[selectedEntries.length - 1] = freshestEntry;
|
|
2664
|
+
}
|
|
2267
2665
|
for (const entry of selectedEntries) {
|
|
2268
2666
|
allFacts.push({
|
|
2269
|
-
entityKey: `${resolvedInfo.entityType}/${resolvedInfo.entityId}/${entry.key}`,
|
|
2667
|
+
entityKey: `${resolvedInfo.entityType}/${resolvedInfo.entityId}/${normalizeContinuityKey(entry.key)}`,
|
|
2270
2668
|
summary: entry.valueSummary,
|
|
2271
2669
|
value: entry.valueRaw,
|
|
2272
2670
|
confidence: entry.confidence,
|
|
2273
2671
|
source: entry.source,
|
|
2672
|
+
lastUpdated: entry.updatedAt.toISOString(),
|
|
2274
2673
|
entryId: entry.id,
|
|
2275
2674
|
});
|
|
2276
2675
|
}
|
|
@@ -2312,13 +2711,15 @@ ${detectionWindow}`,
|
|
|
2312
2711
|
});
|
|
2313
2712
|
(0, metrics_1.timeEnd)('attendant.observe_ms', t0);
|
|
2314
2713
|
return {
|
|
2315
|
-
facts: topFacts.map(({ entityKey, summary, value, confidence, source }) => ({
|
|
2714
|
+
facts: (0, hostMemoryFormatting_1.assignStructuredFactIds)(topFacts.map(({ entityKey, summary, value, confidence, source, lastUpdated, entryId }) => ({
|
|
2715
|
+
knowledgeEntryId: entryId,
|
|
2316
2716
|
entityKey,
|
|
2317
2717
|
summary,
|
|
2318
2718
|
value,
|
|
2319
2719
|
confidence,
|
|
2320
2720
|
source,
|
|
2321
|
-
|
|
2721
|
+
lastUpdated,
|
|
2722
|
+
}))),
|
|
2322
2723
|
entitiesDetected: Array.from(entitiesDetected),
|
|
2323
2724
|
alreadyPresent,
|
|
2324
2725
|
totalFound: allFacts.length,
|
|
@@ -2381,21 +2782,21 @@ ${detectionWindow}`,
|
|
|
2381
2782
|
const response = await (0, router_1.route)('classification', [
|
|
2382
2783
|
{
|
|
2383
2784
|
role: 'user',
|
|
2384
|
-
content: `Decide whether this assistant should fetch persistent memory before replying.
|
|
2385
|
-
|
|
2386
|
-
Latest user message:
|
|
2387
|
-
${input.latestMessage || '(none)'}
|
|
2388
|
-
|
|
2389
|
-
Recent context excerpt:
|
|
2390
|
-
${contextWindow || '(empty)'}
|
|
2391
|
-
|
|
2392
|
-
Return ONLY valid JSON with this exact shape:
|
|
2393
|
-
{"needsMemory":true,"confidence":0.81,"reason":"short_reason"}
|
|
2394
|
-
|
|
2395
|
-
Rules:
|
|
2396
|
-
- needsMemory=true when the message involves project context, technical decisions, code state, prior work, open tasks, bugs, architecture, preferences, or anything session- or project-specific.
|
|
2397
|
-
- needsMemory=true when in doubt — false positives are cheap, false negatives lose context.
|
|
2398
|
-
- needsMemory=false ONLY for clear one-word acks, simple greetings, or purely generic factual questions with no project relevance.
|
|
2785
|
+
content: `Decide whether this assistant should fetch persistent memory before replying.
|
|
2786
|
+
|
|
2787
|
+
Latest user message:
|
|
2788
|
+
${input.latestMessage || '(none)'}
|
|
2789
|
+
|
|
2790
|
+
Recent context excerpt:
|
|
2791
|
+
${contextWindow || '(empty)'}
|
|
2792
|
+
|
|
2793
|
+
Return ONLY valid JSON with this exact shape:
|
|
2794
|
+
{"needsMemory":true,"confidence":0.81,"reason":"short_reason"}
|
|
2795
|
+
|
|
2796
|
+
Rules:
|
|
2797
|
+
- needsMemory=true when the message involves project context, technical decisions, code state, prior work, open tasks, bugs, architecture, preferences, or anything session- or project-specific.
|
|
2798
|
+
- needsMemory=true when in doubt — false positives are cheap, false negatives lose context.
|
|
2799
|
+
- needsMemory=false ONLY for clear one-word acks, simple greetings, or purely generic factual questions with no project relevance.
|
|
2399
2800
|
- confidence is a float from 0 to 1.`,
|
|
2400
2801
|
},
|
|
2401
2802
|
], 128);
|
|
@@ -2758,14 +3159,14 @@ Rules:
|
|
|
2758
3159
|
const response = await (0, router_1.route)('task_inference', [
|
|
2759
3160
|
{
|
|
2760
3161
|
role: 'user',
|
|
2761
|
-
content: `You are analyzing what an AI agent is currently working on.
|
|
2762
|
-
|
|
2763
|
-
Agent ID: ${this.agentId}
|
|
2764
|
-
Task description: ${context.task}
|
|
2765
|
-
Recent messages:
|
|
2766
|
-
${context.recentMessages.map((m, i) => `${i + 1}. ${m}`).join('\n')}
|
|
2767
|
-
|
|
2768
|
-
In one short sentence, describe the specific type of task this agent is currently performing.
|
|
3162
|
+
content: `You are analyzing what an AI agent is currently working on.
|
|
3163
|
+
|
|
3164
|
+
Agent ID: ${this.agentId}
|
|
3165
|
+
Task description: ${context.task}
|
|
3166
|
+
Recent messages:
|
|
3167
|
+
${context.recentMessages.map((m, i) => `${i + 1}. ${m}`).join('\n')}
|
|
3168
|
+
|
|
3169
|
+
In one short sentence, describe the specific type of task this agent is currently performing.
|
|
2769
3170
|
Be specific and concrete.`,
|
|
2770
3171
|
},
|
|
2771
3172
|
], 256);
|
|
@@ -2798,15 +3199,15 @@ Be specific and concrete.`,
|
|
|
2798
3199
|
const response = await (0, router_1.route)('relevance_filtering', [
|
|
2799
3200
|
{
|
|
2800
3201
|
role: 'user',
|
|
2801
|
-
content: `You are deciding what knowledge an AI agent needs for its current task.
|
|
2802
|
-
|
|
2803
|
-
Agent task: ${taskType}
|
|
2804
|
-
|
|
2805
|
-
Available knowledge entries:
|
|
2806
|
-
${entryInputs.map((e, i) => `${i + 1}. [${e.key}] ${e.valueSummary} (confidence: ${e.confidence})`).join('\n')}
|
|
2807
|
-
|
|
2808
|
-
Return only the numbers of entries that are directly relevant to the current task.
|
|
2809
|
-
Format: comma-separated numbers only. Example: 1,3,5
|
|
3202
|
+
content: `You are deciding what knowledge an AI agent needs for its current task.
|
|
3203
|
+
|
|
3204
|
+
Agent task: ${taskType}
|
|
3205
|
+
|
|
3206
|
+
Available knowledge entries:
|
|
3207
|
+
${entryInputs.map((e, i) => `${i + 1}. [${e.key}] ${e.valueSummary} (confidence: ${e.confidence})`).join('\n')}
|
|
3208
|
+
|
|
3209
|
+
Return only the numbers of entries that are directly relevant to the current task.
|
|
3210
|
+
Format: comma-separated numbers only. Example: 1,3,5
|
|
2810
3211
|
If nothing is relevant, return: none`,
|
|
2811
3212
|
},
|
|
2812
3213
|
], 128);
|
|
@@ -2832,6 +3233,7 @@ If nothing is relevant, return: none`,
|
|
|
2832
3233
|
this.brief = {
|
|
2833
3234
|
...this.brief,
|
|
2834
3235
|
compliance: this.buildComplianceState(),
|
|
3236
|
+
pendingMemoryAttributions: this.pendingMemoryAttributions.map((entry) => ({ ...entry })),
|
|
2835
3237
|
};
|
|
2836
3238
|
await (0, client_1.getDb)().knowledgeEntry.upsert({
|
|
2837
3239
|
where: {
|
|
@@ -2871,8 +3273,12 @@ If nothing is relevant, return: none`,
|
|
|
2871
3273
|
this.sharedStateObservedAt = state.briefGeneratedAt;
|
|
2872
3274
|
this.attendsWithoutPersist = state.compliance?.counters.attendsWithoutPersist ?? 0;
|
|
2873
3275
|
this.consecutivePreResponseWithoutPost = state.compliance?.counters.consecutivePreResponseWithoutPost ?? 0;
|
|
3276
|
+
this.consecutiveUnusedMemoryInjections = state.compliance?.counters.consecutiveUnusedMemoryInjections ?? 0;
|
|
2874
3277
|
this.lastAttendPhase = state.compliance?.counters.lastAttendPhase ?? undefined;
|
|
2875
3278
|
this.complianceUpdatedAt = state.compliance?.lastUpdated ?? state.briefGeneratedAt;
|
|
3279
|
+
this.pendingMemoryAttributions = Array.isArray(state.pendingMemoryAttributions)
|
|
3280
|
+
? state.pendingMemoryAttributions.map((entry) => ({ ...entry }))
|
|
3281
|
+
: [];
|
|
2876
3282
|
this.brief = state;
|
|
2877
3283
|
return state;
|
|
2878
3284
|
}
|