iranti 0.2.51 → 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 +30 -17
- package/dist/scripts/api-key-create.js +1 -1
- package/dist/scripts/api-key-list.js +1 -1
- package/dist/scripts/api-key-revoke.js +1 -1
- package/dist/scripts/claude-code-memory-hook.js +116 -30
- package/dist/scripts/codex-setup.js +86 -4
- package/dist/scripts/iranti-cli.js +1359 -57
- package/dist/scripts/iranti-mcp.js +578 -75
- package/dist/scripts/seed.js +11 -6
- package/dist/scripts/setup.js +1 -1
- package/dist/src/api/healthChecks.d.ts +29 -0
- package/dist/src/api/healthChecks.d.ts.map +1 -0
- package/dist/src/api/healthChecks.js +72 -0
- package/dist/src/api/healthChecks.js.map +1 -0
- package/dist/src/api/middleware/validation.d.ts +22 -0
- package/dist/src/api/middleware/validation.d.ts.map +1 -1
- package/dist/src/api/middleware/validation.js +93 -3
- 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 +53 -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 +73 -9
- package/dist/src/api/routes/memory.js.map +1 -1
- package/dist/src/api/server.js +38 -43
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/attendant/AttendantInstance.d.ts +135 -2
- package/dist/src/attendant/AttendantInstance.d.ts.map +1 -1
- package/dist/src/attendant/AttendantInstance.js +1836 -93
- 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 +1 -1
- package/dist/src/attendant/index.js.map +1 -1
- package/dist/src/attendant/registry.d.ts.map +1 -1
- package/dist/src/attendant/registry.js +2 -0
- package/dist/src/attendant/registry.js.map +1 -1
- package/dist/src/chat/index.d.ts +23 -0
- package/dist/src/chat/index.d.ts.map +1 -1
- package/dist/src/chat/index.js +111 -22
- package/dist/src/chat/index.js.map +1 -1
- package/dist/src/generated/prisma/browser.d.ts +5 -0
- package/dist/src/generated/prisma/browser.d.ts.map +1 -1
- package/dist/src/generated/prisma/client.d.ts +5 -0
- package/dist/src/generated/prisma/client.d.ts.map +1 -1
- package/dist/src/generated/prisma/commonInputTypes.d.ts +48 -0
- package/dist/src/generated/prisma/commonInputTypes.d.ts.map +1 -1
- package/dist/src/generated/prisma/internal/class.d.ts +11 -0
- package/dist/src/generated/prisma/internal/class.d.ts.map +1 -1
- package/dist/src/generated/prisma/internal/class.js +4 -4
- package/dist/src/generated/prisma/internal/class.js.map +1 -1
- package/dist/src/generated/prisma/internal/prismaNamespace.d.ts +92 -1
- package/dist/src/generated/prisma/internal/prismaNamespace.d.ts.map +1 -1
- package/dist/src/generated/prisma/internal/prismaNamespace.js +17 -2
- package/dist/src/generated/prisma/internal/prismaNamespace.js.map +1 -1
- package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.d.ts +16 -0
- package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.d.ts.map +1 -1
- package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.js +17 -2
- package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.js.map +1 -1
- package/dist/src/generated/prisma/models/StaffEvent.d.ts +1184 -0
- package/dist/src/generated/prisma/models/StaffEvent.d.ts.map +1 -0
- package/dist/src/generated/prisma/models/StaffEvent.js +3 -0
- package/dist/src/generated/prisma/models/StaffEvent.js.map +1 -0
- package/dist/src/generated/prisma/models.d.ts +1 -0
- package/dist/src/generated/prisma/models.d.ts.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/autoRemember.d.ts +15 -0
- package/dist/src/lib/autoRemember.d.ts.map +1 -1
- package/dist/src/lib/autoRemember.js +433 -71
- package/dist/src/lib/autoRemember.js.map +1 -1
- package/dist/src/lib/cliHelpCatalog.d.ts.map +1 -1
- package/dist/src/lib/cliHelpCatalog.js +23 -11
- package/dist/src/lib/cliHelpCatalog.js.map +1 -1
- package/dist/src/lib/cliHelpRenderer.d.ts +1 -0
- package/dist/src/lib/cliHelpRenderer.d.ts.map +1 -1
- package/dist/src/lib/cliHelpRenderer.js +4 -0
- package/dist/src/lib/cliHelpRenderer.js.map +1 -1
- package/dist/src/lib/commandErrors.d.ts +5 -1
- package/dist/src/lib/commandErrors.d.ts.map +1 -1
- package/dist/src/lib/commandErrors.js +250 -17
- package/dist/src/lib/commandErrors.js.map +1 -1
- package/dist/src/lib/createFirstPartyIranti.d.ts.map +1 -1
- package/dist/src/lib/createFirstPartyIranti.js +1 -0
- package/dist/src/lib/createFirstPartyIranti.js.map +1 -1
- package/dist/src/lib/dbStaffEventEmitter.d.ts +2 -0
- package/dist/src/lib/dbStaffEventEmitter.d.ts.map +1 -1
- package/dist/src/lib/dbStaffEventEmitter.js +15 -0
- package/dist/src/lib/dbStaffEventEmitter.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/issueFacts.d.ts +37 -0
- package/dist/src/lib/issueFacts.d.ts.map +1 -0
- package/dist/src/lib/issueFacts.js +72 -0
- package/dist/src/lib/issueFacts.js.map +1 -0
- package/dist/src/lib/llm.d.ts +8 -0
- package/dist/src/lib/llm.d.ts.map +1 -1
- package/dist/src/lib/llm.js +33 -0
- package/dist/src/lib/llm.js.map +1 -1
- package/dist/src/lib/packageRoot.d.ts +2 -0
- package/dist/src/lib/packageRoot.d.ts.map +1 -0
- package/dist/src/lib/packageRoot.js +22 -0
- package/dist/src/lib/packageRoot.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 +29 -0
- package/dist/src/lib/protocolEnforcement.d.ts.map +1 -0
- package/dist/src/lib/protocolEnforcement.js +124 -0
- package/dist/src/lib/protocolEnforcement.js.map +1 -0
- package/dist/src/lib/providers/claude.js +1 -1
- package/dist/src/lib/providers/claude.js.map +1 -1
- package/dist/src/lib/router.js +1 -1
- package/dist/src/lib/router.js.map +1 -1
- package/dist/src/lib/runtimeEnv.d.ts.map +1 -1
- package/dist/src/lib/runtimeEnv.js +8 -3
- package/dist/src/lib/runtimeEnv.js.map +1 -1
- package/dist/src/lib/scaffoldCloseout.d.ts +27 -0
- package/dist/src/lib/scaffoldCloseout.d.ts.map +1 -0
- package/dist/src/lib/scaffoldCloseout.js +139 -0
- package/dist/src/lib/scaffoldCloseout.js.map +1 -0
- package/dist/src/lib/semanticFactTags.d.ts +10 -0
- package/dist/src/lib/semanticFactTags.d.ts.map +1 -0
- package/dist/src/lib/semanticFactTags.js +166 -0
- package/dist/src/lib/semanticFactTags.js.map +1 -0
- package/dist/src/lib/sessionLedger.d.ts +94 -0
- package/dist/src/lib/sessionLedger.d.ts.map +1 -0
- package/dist/src/lib/sessionLedger.js +997 -0
- package/dist/src/lib/sessionLedger.js.map +1 -0
- package/dist/src/lib/sharedStateInvalidation.d.ts +10 -0
- package/dist/src/lib/sharedStateInvalidation.d.ts.map +1 -0
- package/dist/src/lib/sharedStateInvalidation.js +184 -0
- package/dist/src/lib/sharedStateInvalidation.js.map +1 -0
- package/dist/src/lib/staffEventsTable.d.ts +3 -0
- package/dist/src/lib/staffEventsTable.d.ts.map +1 -0
- package/dist/src/lib/staffEventsTable.js +58 -0
- package/dist/src/lib/staffEventsTable.js.map +1 -0
- package/dist/src/librarian/index.d.ts.map +1 -1
- package/dist/src/librarian/index.js +113 -2
- package/dist/src/librarian/index.js.map +1 -1
- package/dist/src/library/client.d.ts +6 -1
- package/dist/src/library/client.d.ts.map +1 -1
- package/dist/src/library/client.js +21 -7
- package/dist/src/library/client.js.map +1 -1
- package/dist/src/library/embeddings.d.ts +9 -1
- package/dist/src/library/embeddings.d.ts.map +1 -1
- package/dist/src/library/embeddings.js +28 -3
- package/dist/src/library/embeddings.js.map +1 -1
- package/dist/src/library/queries.d.ts.map +1 -1
- package/dist/src/library/queries.js +263 -46
- package/dist/src/library/queries.js.map +1 -1
- package/dist/src/sdk/index.d.ts +52 -1
- package/dist/src/sdk/index.d.ts.map +1 -1
- package/dist/src/sdk/index.js +546 -98
- package/dist/src/sdk/index.js.map +1 -1
- package/package.json +24 -3
- package/prisma/migrations/20260331101500_add_staff_events_ledger/migration.sql +24 -0
- package/prisma/schema.prisma +22 -0
|
@@ -5,15 +5,22 @@ 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");
|
|
12
|
+
const queries_2 = require("../library/queries");
|
|
11
13
|
const relationships_1 = require("../library/relationships");
|
|
12
14
|
const entity_resolution_1 = require("../library/entity-resolution");
|
|
15
|
+
const librarian_1 = require("../librarian");
|
|
13
16
|
const client_1 = require("../library/client");
|
|
14
17
|
const metrics_1 = require("../lib/metrics");
|
|
15
18
|
const getPolicy_1 = require("../librarian/getPolicy");
|
|
16
19
|
const autoRemember_1 = require("../lib/autoRemember");
|
|
20
|
+
const semanticFactTags_1 = require("../lib/semanticFactTags");
|
|
21
|
+
const sessionLedger_1 = require("../lib/sessionLedger");
|
|
22
|
+
const sharedStateInvalidation_1 = require("../lib/sharedStateInvalidation");
|
|
23
|
+
const hostMemoryFormatting_1 = require("../lib/hostMemoryFormatting");
|
|
17
24
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
18
25
|
const ATTENDANT_RULES_QUERY = {
|
|
19
26
|
entityType: 'system',
|
|
@@ -22,40 +29,117 @@ const ATTENDANT_RULES_QUERY = {
|
|
|
22
29
|
};
|
|
23
30
|
exports.DEFAULT_ATTENDANT_OPERATING_RULES = [
|
|
24
31
|
'Serve one external agent only; optimize for that agent keeping task context coherent across turns and sessions.',
|
|
25
|
-
'
|
|
32
|
+
'ACKNOWLEDGE: This is strictly mandatory — you MUST state the following before proceeding with any user request, and no work should begin until it is stated: "I acknowledge that Iranti is a hive mind. I have read the Iranti operating rules and the protocols for iranti_handshake, iranti_attend, iranti_write, iranti_checkpoint, and iranti_remember_response, and I pledge to use them to persist what I find, what works, what fails, what changes, and what happens next so later sessions do not have to rediscover it." Skipping this acknowledgment is non-compliant regardless of context.',
|
|
33
|
+
'SESSION START: Call iranti_handshake before responding to the first user message. After context compaction, call iranti_handshake again before responding to the next message.',
|
|
34
|
+
"ATTEND PROTOCOL — follow this sequence every turn: (1) Call iranti_attend before responding to the user message — pass phase='pre-response' to enable precise compliance tracking. (2) Call iranti_attend before any lookup where Iranti might already hold the answer — this includes Read, Grep, Glob, WebSearch, WebFetch, and Bash commands used as factual basis for a decision. (3) Call iranti_attend after any such lookup to check new findings against stored memory and decide what to inject, write, or checkpoint. (4) Call iranti_write after every Edit or Write tool call — file changes are always durable, record what changed, why, and what the file now does. (5) Call iranti_write after any Bash command that reveals system state — build results, test outcomes, container state, port availability, environment facts, errors. (6) Call iranti_write after any WebSearch or WebFetch that surfaces confirmed external facts. (7) Call iranti_write after any subagent (Agent tool) completes — subagent findings are invisible to the hive mind otherwise. (8) Call iranti_attend after every response without exception — pass phase='post-response'. Even short or conversational replies may contain durable decisions, confirmed facts, or next steps. Assess for write or checkpoint before the next turn. (9) Call iranti_attend again when new knowledge should change what is loaded for the next step.",
|
|
26
35
|
'Treat Iranti as the default shared working-memory layer. Keep using your own private notes if you want, but prefer Iranti for anything another session, another agent, or a later handoff may need.',
|
|
27
36
|
'Before answering recall-style questions about remembered preferences, decisions, blockers, next steps, prior project state, or earlier findings, consult Iranti instead of guessing.',
|
|
37
|
+
'If a recall-style lookup returns no facts, do not treat empty as confirmation of absence — try at least one alternative retrieval angle before concluding the fact is not stored: switch between exact query and search, try a different entity path or key fragment, or rephrase the search term. Absence is confirmed only after two distinct retrieval attempts with different angles both return empty.',
|
|
28
38
|
'Before making or repeating architectural, product, workflow, or debugging decisions, check Iranti for earlier decisions, constraints, blockers, and validated environment details.',
|
|
29
39
|
'Use exact query when the entity and key are known. Use search or attend when the fact must be discovered from shared memory.',
|
|
30
|
-
'Persist durable knowledge when it is learned or confirmed: decisions, blockers, next steps, owners, stable preferences, project constraints, important file purposes,
|
|
40
|
+
'Persist durable knowledge when it is learned or confirmed: decisions, blockers, next steps, owners, stable preferences, project constraints, important file purposes, validated environment details, what worked, what failed, and what remains risky.',
|
|
41
|
+
'Write facts with the depth of someone who built the system — include what the thing does, why it exists, how it connects to other parts, and what would break or change if it were removed. A fact that reads "file X was edited" is insufficient; "file X controls Y because Z, edited to fix W" is the target. Iranti should accumulate enough detail that any agent reading its memory feels like it built the repo.',
|
|
31
42
|
'When a file is created, renamed, moved, deleted, or substantially repurposed, capture that change and what the file is for whenever the state will matter to another agent or a later session.',
|
|
32
43
|
'When a task reaches a useful checkpoint, store the current step, next step, open risks, and any important artifacts or paths so another agent can resume without reconstructing context from scratch.',
|
|
33
44
|
'When an approach fails and the failure or workaround is likely to matter later, store the failed path and the chosen alternative route as durable memory.',
|
|
34
|
-
'Use iranti_write for durable facts, iranti_ingest for stable source material worth chunking, and iranti_remember_response for strict assistant summaries such as next steps or blockers.',
|
|
35
|
-
'
|
|
45
|
+
'Use iranti_checkpoint for active shared progress, iranti_write for durable facts, iranti_ingest for stable source material worth chunking, and iranti_remember_response for strict assistant summaries such as next steps or blockers.',
|
|
46
|
+
'CHECKPOINT PROTOCOL: Call iranti_checkpoint (1) when completing a task, (2) when shifting to a new task mid-session, and (3) at any natural pause point where another session should resume — not only when saving facts with iranti_write. A checkpoint not written means the next handshake recovers from stale data, and a long run without structured writes/checkpoints is non-compliant for Iranti. Write checkpoints like the best possible commit message but with more detail — lead with the why (what problem this solved, what decision was made, what changed and why it matters), then add structured recovery context: current step, next step, what worked, what failed, open risks, and file changes. A checkpoint that reads "did some edits" is non-compliant; one that reads "fixed missing docker dependency in cofactor instance.json — container name was never recorded at setup so iranti run silently skipped docker start; added iranti_cofactor_db dependency, verified against docker ps, control panel start will now auto-boot the container" is the target.',
|
|
47
|
+
'Do not save every turn. Skip idle chatter and transient execution noise. Do save design options, architectural proposals, and considered-but-not-yet-decided directions with their reasoning — a future session should not have to re-explore ground already covered, even when no final decision was made. Do not skip discoveries, failed paths, validations, file changes, risks, or next steps that another session would otherwise have to rediscover.',
|
|
36
48
|
'Deliver a compressed working-memory brief, not the full knowledge base. Load only what is relevant to the current task.',
|
|
37
49
|
'Reconvene or attend again when context shifts, when the visible window is missing needed facts, or when a different entity becomes relevant.',
|
|
38
50
|
'If context gets stale or the task has gone long enough that reasoning may drift, re-read the operating rules from the Staff Namespace before proceeding.',
|
|
39
51
|
];
|
|
40
52
|
const CONTEXT_RECOVERY_THRESHOLD = 20; // LLM calls before context recovery
|
|
41
53
|
const SESSION_INTERRUPTION_TTL_MS = 5 * 60 * 1000;
|
|
54
|
+
const PERSISTENCE_WARNING_THRESHOLD = 3;
|
|
55
|
+
const PERSISTENCE_NON_COMPLIANT_THRESHOLD = 5;
|
|
42
56
|
const ENTITY_DETECTION_WINDOW_CHARS = 1500;
|
|
43
57
|
const MIN_ENTITY_CONFIDENCE = 0.75;
|
|
44
58
|
const MEMORY_DECISION_CONTEXT_WINDOW_CHARS = 2000;
|
|
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
|
+
};
|
|
65
|
+
const ATTEND_EXPECTED_CALL_SEQUENCE = [
|
|
66
|
+
'Call iranti_handshake at session start and again after context compaction.',
|
|
67
|
+
"Call iranti_attend(phase='pre-response') before replying to the user.",
|
|
68
|
+
'Call iranti_attend before any lookup where Iranti might already hold the answer — Read, Grep, Glob, WebSearch, WebFetch, Bash-as-factual-basis.',
|
|
69
|
+
'Call iranti_attend again after any such lookup when new findings may affect what to inject, write, or checkpoint.',
|
|
70
|
+
'Call iranti_write after every Edit or Write tool call — file changes are always durable.',
|
|
71
|
+
'Call iranti_write after Bash commands that reveal system state, after WebSearch/WebFetch with confirmed facts, and after any subagent completes.',
|
|
72
|
+
"Call iranti_attend(phase='post-response') after every response without exception — even short replies may contain durable findings. Omitting this call is a compliance violation.",
|
|
73
|
+
'Call iranti_attend again when the new knowledge should change what is loaded next.',
|
|
74
|
+
];
|
|
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.';
|
|
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
|
+
}
|
|
45
91
|
const MEMORY_NEED_POSITIVE_PATTERNS = [
|
|
46
92
|
/\bwhat(?:'s| is| was)?\s+my\b/i,
|
|
47
93
|
/\bdo you remember\b/i,
|
|
48
94
|
/\bremind me\b/i,
|
|
95
|
+
/\bbring me up to speed\b/i,
|
|
96
|
+
/\bcatch me up\b/i,
|
|
97
|
+
/\brecap\b/i,
|
|
98
|
+
/\bwhere did we leave off\b/i,
|
|
99
|
+
/\bwhere are we\b/i,
|
|
100
|
+
/\bwhat did we learn\b/i,
|
|
101
|
+
/\bwhat did we decide\b/i,
|
|
102
|
+
/\bwhat do we know\b/i,
|
|
103
|
+
/\bnext step\b/i,
|
|
104
|
+
/\bwhat(?:'s| is)?\s+next\b/i,
|
|
105
|
+
/\b(?:what|which)\s+(?:bugs?|issues?|defects?|tasks?|blockers?|risks?)\s+(?:are\s+)?(?:left|open|remaining)\b/i,
|
|
106
|
+
/\bwhat\s+(?:changed|worked|failed)\b/i,
|
|
107
|
+
/\b(?:current\s+)?status\b/i,
|
|
108
|
+
/\b(?:current\s+)?progress\b/i,
|
|
109
|
+
/\b(?:summary|summarize|overview)\b/i,
|
|
49
110
|
/\bmy\s+(?:favorite|favourite|name|email|phone|address|city|country|movie|snack|color|colour)\b/i,
|
|
50
111
|
/\bwe decided\b/i,
|
|
51
112
|
/\bearlier\b/i,
|
|
52
113
|
/\bprevious(?:ly)?\b/i,
|
|
53
114
|
/\bagain\b/i,
|
|
115
|
+
// Common imperative work-task prefixes — short messages like "fix it", "add tests",
|
|
116
|
+
// "help me debug", "explain this" are almost always project-contextual and benefit from
|
|
117
|
+
// memory injection. Catching these here prevents fall-through to the LLM classifier and
|
|
118
|
+
// the classification_parse_failed_default_false silent miss.
|
|
119
|
+
/^\s*(?:fix|debug|refactor|implement|add|update|change|remove|delete|create|write|check|review|test|run|deploy|build|enable|disable|configure|set up|setup)\b/i,
|
|
120
|
+
/\bhelp\s+me\b/i,
|
|
121
|
+
/\bexplain\s+(?:this|that|how|why|what)\b/i,
|
|
122
|
+
/\bwhat\s+(?:is|are|was|were)\s+(?:the|a|an|this|that|my|our|its)\b/i,
|
|
123
|
+
/\bhow\s+(?:do|does|did|should|can|could|would|to)\b/i,
|
|
124
|
+
/\bwhy\s+(?:is|are|was|were|did|does|do|not)\b/i,
|
|
125
|
+
// Technical vocabulary — file paths, function-call syntax, camelCase/snake_case identifiers,
|
|
126
|
+
// and dot-notation references are strong signals that the message is project-bound.
|
|
127
|
+
/(?:\/[\w.\-]+){1,}|[\w]+\.[a-z]{1,5}\b/i,
|
|
128
|
+
/\b[a-z][a-zA-Z0-9]*(?:[A-Z][a-zA-Z0-9]*)+\b/, // camelCase
|
|
129
|
+
/\b[a-z][a-z0-9]*(?:_[a-z][a-z0-9]*)+\b/, // snake_case
|
|
130
|
+
/\b\w+\s*\(/, // function call syntax
|
|
54
131
|
];
|
|
55
132
|
const MEMORY_NEED_NEGATIVE_PATTERNS = [
|
|
56
133
|
/^\s*(hi|hello|hey|yo|sup|good (?:morning|afternoon|evening))\b[!.?\s]*$/i,
|
|
57
134
|
/^\s*(thanks|thank you|cool|great|nice)\b[!.?\s]*$/i,
|
|
58
135
|
];
|
|
136
|
+
const MEMORY_PARSE_FAILURE_PROJECT_CUE_PATTERNS = [
|
|
137
|
+
/\b(?:status|progress|summary|summar(?:y|ize)|recap|overview|state)\b/i,
|
|
138
|
+
/\b(?:decision|decisions|findings?|artifacts?|changes?|work|implementation|architecture|code(?:base)?|repo|repository)\b/i,
|
|
139
|
+
/\b(?:deployment|setup|bug|bugs|issue|issues|defect|defects|task|tasks|blocker|blockers|risk|risks)\b/i,
|
|
140
|
+
/\bwhat\s+(?:did|do|have)\s+we\b/i,
|
|
141
|
+
/\bwhere\s+do\s+we\s+stand\b/i,
|
|
142
|
+
];
|
|
59
143
|
const EXPLICIT_TASK_PREFIX_PATTERNS = [
|
|
60
144
|
/^\s*general session\s*[:\-]\s*/i,
|
|
61
145
|
/^\s*general session assistance\s*(?:for)?\s*/i,
|
|
@@ -69,6 +153,20 @@ const WEAK_EXPLICIT_TASK_PATTERNS = [
|
|
|
69
153
|
/^assistance$/i,
|
|
70
154
|
/^help$/i,
|
|
71
155
|
];
|
|
156
|
+
const WATCHED_ENTITY_PROMPT_PATTERNS = [
|
|
157
|
+
/\bcontinue\b/i,
|
|
158
|
+
/\bresume\b/i,
|
|
159
|
+
/\bpick up\b/i,
|
|
160
|
+
/\bwhat(?:'s| is)?\s+next\b/i,
|
|
161
|
+
/\bwhat(?:'s| is)?\s+the\s+next\s+step\b/i,
|
|
162
|
+
/\bwhat(?:'s| is)?\s+the\s+status\b/i,
|
|
163
|
+
/\bstatus\b/i,
|
|
164
|
+
/\bprogress\b/i,
|
|
165
|
+
/\brecap\b/i,
|
|
166
|
+
/\bwhat\s+changed\b/i,
|
|
167
|
+
/\bwhat\s+did\s+you\s+change\b/i,
|
|
168
|
+
/\bwhere\s+were\s+we\b/i,
|
|
169
|
+
];
|
|
72
170
|
function normalizeExplicitTask(task) {
|
|
73
171
|
if (typeof task !== 'string')
|
|
74
172
|
return null;
|
|
@@ -118,6 +216,162 @@ function formatOperatingRulesText(rawValue, summary, fallbackRules = exports.DEF
|
|
|
118
216
|
...mergedRules.map((rule) => `- ${rule}`),
|
|
119
217
|
].join('\n');
|
|
120
218
|
}
|
|
219
|
+
function summarizeLedgerLearning(entry) {
|
|
220
|
+
return `Recent ledger learning: ${entry.summary}`;
|
|
221
|
+
}
|
|
222
|
+
function toLedgerWorkingMemoryEntries(entries) {
|
|
223
|
+
return entries.map((entry, index) => ({
|
|
224
|
+
entityKey: `system/session_ledger/recent_learning_${index + 1}`,
|
|
225
|
+
summary: summarizeLedgerLearning(entry),
|
|
226
|
+
confidence: 100,
|
|
227
|
+
source: 'session_ledger',
|
|
228
|
+
lastUpdated: entry.timestamp,
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
function mergeWorkingMemoryWithLedger(entries, learnings) {
|
|
232
|
+
const retained = entries.filter((entry) => !entry.entityKey.startsWith(LEDGER_WORKING_MEMORY_PREFIX));
|
|
233
|
+
return learnings.length > 0
|
|
234
|
+
? [...retained, ...toLedgerWorkingMemoryEntries(learnings)]
|
|
235
|
+
: retained;
|
|
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
|
+
}
|
|
314
|
+
function formatMissingWriteCategories(categories) {
|
|
315
|
+
const labelMap = {
|
|
316
|
+
findings: 'what you found',
|
|
317
|
+
validated_results: 'what worked',
|
|
318
|
+
failed_paths: 'what failed',
|
|
319
|
+
file_changes: 'what changed',
|
|
320
|
+
risks_and_next_steps: 'what remains risky and what happens next',
|
|
321
|
+
};
|
|
322
|
+
const labels = Array.from(new Set(categories.map((category) => labelMap[category] ?? category)));
|
|
323
|
+
if (labels.length === 0)
|
|
324
|
+
return 'what you found and what happens next';
|
|
325
|
+
if (labels.length === 1)
|
|
326
|
+
return labels[0];
|
|
327
|
+
if (labels.length === 2)
|
|
328
|
+
return `${labels[0]} and ${labels[1]}`;
|
|
329
|
+
return `${labels.slice(0, -1).join(', ')}, and ${labels[labels.length - 1]}`;
|
|
330
|
+
}
|
|
331
|
+
function applyAdvisoryOperatingRules(operatingRules, profile) {
|
|
332
|
+
const reminder = profile?.checkpointReminder?.trim();
|
|
333
|
+
const categories = profile?.missingWriteCategories ?? [];
|
|
334
|
+
let nextRules = operatingRules;
|
|
335
|
+
if (reminder && !nextRules.includes(reminder)) {
|
|
336
|
+
nextRules = `${nextRules}\n- ${reminder}`;
|
|
337
|
+
}
|
|
338
|
+
if (categories.length > 0) {
|
|
339
|
+
const categoryRule = `Compliance follow-up: before the next pause, persist ${formatMissingWriteCategories(categories)} as structured durable memory when applicable, not just a broad summary.`;
|
|
340
|
+
if (!nextRules.includes(categoryRule)) {
|
|
341
|
+
nextRules = `${nextRules}\n- ${categoryRule}`;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return nextRules;
|
|
345
|
+
}
|
|
346
|
+
function advisoryTaskTokens(taskType) {
|
|
347
|
+
if (!taskType)
|
|
348
|
+
return [];
|
|
349
|
+
return Array.from(new Set(taskType
|
|
350
|
+
.toLowerCase()
|
|
351
|
+
.split(/[^a-z0-9_]+/)
|
|
352
|
+
.map((token) => token.trim())
|
|
353
|
+
.filter((token) => token.length >= 4)));
|
|
354
|
+
}
|
|
355
|
+
function buildUsageGuidance(tool) {
|
|
356
|
+
return {
|
|
357
|
+
tool,
|
|
358
|
+
reminder: ATTEND_USAGE_REMINDER,
|
|
359
|
+
expectedCallSequence: ATTEND_EXPECTED_CALL_SEQUENCE,
|
|
360
|
+
note: tool === 'observe'
|
|
361
|
+
? OBSERVE_USAGE_NOTE
|
|
362
|
+
: 'After using attend() and any retrieved facts, persist durable learnings with iranti_write and shared progress with iranti_checkpoint when applicable.',
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function messageHasAdvisoryCue(message, taskType) {
|
|
366
|
+
const normalized = normalizeMessage(message);
|
|
367
|
+
if (!normalized)
|
|
368
|
+
return false;
|
|
369
|
+
if (/\b(next|status|blocker|owner|risk|issue|bug|weakness|problem|continue|resume|pickup|tackle|ship|release)\b/i.test(normalized)) {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
const messageTokens = new Set(advisoryTaskTokens(normalized));
|
|
373
|
+
return advisoryTaskTokens(taskType).some((token) => messageTokens.has(token));
|
|
374
|
+
}
|
|
121
375
|
function collectBackfillCandidates(messages) {
|
|
122
376
|
const deduped = new Map();
|
|
123
377
|
for (const message of messages) {
|
|
@@ -140,7 +394,7 @@ function collectBackfillCandidates(messages) {
|
|
|
140
394
|
return Array.from(deduped.values());
|
|
141
395
|
}
|
|
142
396
|
function buildBackfillSuggestion(context, workingMemory) {
|
|
143
|
-
const recentMessages = context.recentMessages
|
|
397
|
+
const recentMessages = (context.recentMessages ?? [])
|
|
144
398
|
.map((message) => message.trim())
|
|
145
399
|
.filter(Boolean);
|
|
146
400
|
if (recentMessages.length === 0)
|
|
@@ -195,6 +449,85 @@ function buildAttendBootstrapMessages(latestMessage, currentContext) {
|
|
|
195
449
|
}
|
|
196
450
|
return out.slice(-6);
|
|
197
451
|
}
|
|
452
|
+
function tokenizePresenceText(value) {
|
|
453
|
+
return value
|
|
454
|
+
.toLowerCase()
|
|
455
|
+
.split(/[^a-z0-9_]+/)
|
|
456
|
+
.map((token) => token.trim())
|
|
457
|
+
.filter((token) => token.length > 4);
|
|
458
|
+
}
|
|
459
|
+
function countPresenceMatches(contextLower, tokens) {
|
|
460
|
+
return tokens.filter((token) => contextLower.includes(token)).length;
|
|
461
|
+
}
|
|
462
|
+
function factAlreadyPresentInContext(contextLower, fact) {
|
|
463
|
+
const summaryLower = fact.summary.toLowerCase().trim();
|
|
464
|
+
if (summaryLower.length >= 24 && contextLower.includes(summaryLower)) {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
const [entityType = '', entityId = '', key = ''] = fact.entityKey.split('/');
|
|
468
|
+
const entityTokens = new Set([
|
|
469
|
+
...tokenizePresenceText(entityType),
|
|
470
|
+
...tokenizePresenceText(entityId),
|
|
471
|
+
...tokenizePresenceText(key),
|
|
472
|
+
]);
|
|
473
|
+
const summaryTokens = tokenizePresenceText(fact.summary);
|
|
474
|
+
const meaningfulTokens = summaryTokens.filter((token) => !entityTokens.has(token));
|
|
475
|
+
const tokensToCheck = meaningfulTokens.length > 0 ? meaningfulTokens : summaryTokens;
|
|
476
|
+
if (tokensToCheck.length === 0) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
const matches = countPresenceMatches(contextLower, tokensToCheck);
|
|
480
|
+
return matches >= Math.ceil(tokensToCheck.length * 0.6);
|
|
481
|
+
}
|
|
482
|
+
function normalizeWatchedEntities(values) {
|
|
483
|
+
const seen = new Set();
|
|
484
|
+
const normalized = [];
|
|
485
|
+
for (const value of values ?? []) {
|
|
486
|
+
if (typeof value !== 'string')
|
|
487
|
+
continue;
|
|
488
|
+
const trimmed = value.trim();
|
|
489
|
+
if (!trimmed || !trimmed.includes('/') || seen.has(trimmed))
|
|
490
|
+
continue;
|
|
491
|
+
seen.add(trimmed);
|
|
492
|
+
normalized.push(trimmed);
|
|
493
|
+
if (normalized.length >= 8)
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
return normalized;
|
|
497
|
+
}
|
|
498
|
+
function shouldUseWatchedEntitiesForPrompt(latestMessage) {
|
|
499
|
+
const normalized = normalizeMessage(latestMessage);
|
|
500
|
+
if (!normalized)
|
|
501
|
+
return false;
|
|
502
|
+
if (WATCHED_ENTITY_PROMPT_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
return (0, autoRemember_1.classifyMemoryScope)(normalized) === 'project';
|
|
506
|
+
}
|
|
507
|
+
function inferContextWatchedEntities(task, recentMessages) {
|
|
508
|
+
const exact = normalizeWatchedEntities([
|
|
509
|
+
...extractExactEntityReferences(task),
|
|
510
|
+
...recentMessages.flatMap((message) => extractExactEntityReferences(message)),
|
|
511
|
+
]);
|
|
512
|
+
if (exact.length > 0) {
|
|
513
|
+
return exact;
|
|
514
|
+
}
|
|
515
|
+
const combined = [task, ...recentMessages]
|
|
516
|
+
.map((message) => normalizeMessage(message))
|
|
517
|
+
.filter(Boolean)
|
|
518
|
+
.join('\n');
|
|
519
|
+
const scope = (0, autoRemember_1.classifyMemoryScope)(combined);
|
|
520
|
+
if (scope === 'personal') {
|
|
521
|
+
return (0, autoRemember_1.getPersonalRecallEntities)();
|
|
522
|
+
}
|
|
523
|
+
if (scope === 'project') {
|
|
524
|
+
const configured = (0, autoRemember_1.getProjectMemoryEntity)();
|
|
525
|
+
if (configured) {
|
|
526
|
+
return [configured];
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return [];
|
|
530
|
+
}
|
|
198
531
|
async function readPersistedBriefForAgent(agentId) {
|
|
199
532
|
const entry = await (0, client_1.getDb)().knowledgeEntry.findUnique({
|
|
200
533
|
where: {
|
|
@@ -229,9 +562,78 @@ function buildCheckpointSummary(checkpoint) {
|
|
|
229
562
|
nextStep: checkpoint.checkpoint.nextStep ?? null,
|
|
230
563
|
openRiskCount: Array.isArray(checkpoint.checkpoint.openRisks) ? checkpoint.checkpoint.openRisks.length : 0,
|
|
231
564
|
entityTargetCount: Array.isArray(checkpoint.checkpoint.entityTargets) ? checkpoint.checkpoint.entityTargets.length : 0,
|
|
565
|
+
actionCount: Array.isArray(checkpoint.checkpoint.actions) ? checkpoint.checkpoint.actions.length : 0,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function buildSessionComplianceState(input) {
|
|
569
|
+
const pendingPostResponse = input.lastAttendPhase === 'pre-response';
|
|
570
|
+
const issues = [];
|
|
571
|
+
if (input.consecutivePreResponseWithoutPost > 0) {
|
|
572
|
+
issues.push({
|
|
573
|
+
code: 'missing_post_response_attend',
|
|
574
|
+
severity: 'error',
|
|
575
|
+
count: input.consecutivePreResponseWithoutPost,
|
|
576
|
+
message: 'The previous turn has not been closed with iranti_attend(phase=\'post-response\').',
|
|
577
|
+
requiredAction: 'Call iranti_attend(phase=\'post-response\') to close the prior turn, then persist any durable findings before the next pre-response attend.',
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
if (input.attendsWithoutPersist >= PERSISTENCE_WARNING_THRESHOLD) {
|
|
581
|
+
const severity = input.attendsWithoutPersist >= PERSISTENCE_NON_COMPLIANT_THRESHOLD ? 'error' : 'warn';
|
|
582
|
+
issues.push({
|
|
583
|
+
code: 'missing_durable_persistence',
|
|
584
|
+
severity,
|
|
585
|
+
count: input.attendsWithoutPersist,
|
|
586
|
+
message: `There have been ${input.attendsWithoutPersist} attend calls since the last iranti_write or iranti_checkpoint.`,
|
|
587
|
+
requiredAction: 'Persist durable findings with iranti_write or iranti_checkpoint before the next turn if new knowledge, validation, or file changes occurred.',
|
|
588
|
+
});
|
|
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
|
+
}
|
|
602
|
+
let status = 'healthy';
|
|
603
|
+
if (issues.some((issue) => issue.severity === 'error')) {
|
|
604
|
+
status = 'non_compliant';
|
|
605
|
+
}
|
|
606
|
+
else if (issues.length > 0) {
|
|
607
|
+
status = 'degraded';
|
|
608
|
+
}
|
|
609
|
+
const summary = status === 'healthy'
|
|
610
|
+
? pendingPostResponse
|
|
611
|
+
? 'Lifecycle is currently in progress and waiting for a post-response attend.'
|
|
612
|
+
: 'Lifecycle is currently compliant.'
|
|
613
|
+
: status === 'degraded'
|
|
614
|
+
? input.consecutiveUnusedMemoryInjections > 0
|
|
615
|
+
? 'Lifecycle is degraded: injected memory was surfaced but not used.'
|
|
616
|
+
: 'Lifecycle is degraded: persistence breadcrumbs are lagging.'
|
|
617
|
+
: input.consecutivePreResponseWithoutPost > 0
|
|
618
|
+
? 'Lifecycle is non-compliant: the previous turn is still missing a post-response attend.'
|
|
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.';
|
|
622
|
+
return {
|
|
623
|
+
status,
|
|
624
|
+
summary,
|
|
625
|
+
issues,
|
|
626
|
+
lastUpdated: input.lastUpdated ?? new Date().toISOString(),
|
|
627
|
+
counters: {
|
|
628
|
+
attendsWithoutPersist: input.attendsWithoutPersist,
|
|
629
|
+
consecutivePreResponseWithoutPost: input.consecutivePreResponseWithoutPost,
|
|
630
|
+
consecutiveUnusedMemoryInjections: input.consecutiveUnusedMemoryInjections,
|
|
631
|
+
pendingPostResponse,
|
|
632
|
+
lastAttendPhase: input.lastAttendPhase ?? null,
|
|
633
|
+
},
|
|
232
634
|
};
|
|
233
635
|
}
|
|
234
|
-
function summarizeSessionState(agentId, checkpoint, persistedBriefGeneratedAt) {
|
|
636
|
+
function summarizeSessionState(agentId, checkpoint, persistedBriefGeneratedAt, compliance = null) {
|
|
235
637
|
const hasCheckpoint = Boolean(checkpoint);
|
|
236
638
|
const lastHeartbeatAt = checkpoint?.lastHeartbeatAt ?? null;
|
|
237
639
|
const isStale = Boolean(checkpoint
|
|
@@ -259,6 +661,7 @@ function summarizeSessionState(agentId, checkpoint, persistedBriefGeneratedAt) {
|
|
|
259
661
|
isStale,
|
|
260
662
|
persistedBriefGeneratedAt,
|
|
261
663
|
checkpointSummary: buildCheckpointSummary(checkpoint),
|
|
664
|
+
compliance,
|
|
262
665
|
};
|
|
263
666
|
}
|
|
264
667
|
function heuristicEntityId(name) {
|
|
@@ -361,6 +764,26 @@ function extractFallbackCandidates(text) {
|
|
|
361
764
|
}
|
|
362
765
|
return candidates;
|
|
363
766
|
}
|
|
767
|
+
function extractExactEntityReferences(text) {
|
|
768
|
+
const matches = text.match(/\b[a-z][a-z0-9_-]*\/[a-z0-9_][a-z0-9_/-]*\b/gi) ?? [];
|
|
769
|
+
const deduped = [];
|
|
770
|
+
const seen = new Set();
|
|
771
|
+
for (const rawMatch of matches) {
|
|
772
|
+
const candidate = rawMatch.trim().replace(/[.,!?;:]+$/g, '');
|
|
773
|
+
try {
|
|
774
|
+
const parsed = (0, entity_resolution_1.parseEntityString)(candidate);
|
|
775
|
+
const normalized = `${parsed.entityType}/${parsed.entityId}`;
|
|
776
|
+
if (seen.has(normalized))
|
|
777
|
+
continue;
|
|
778
|
+
seen.add(normalized);
|
|
779
|
+
deduped.push(normalized);
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return deduped;
|
|
786
|
+
}
|
|
364
787
|
function normalizeMessage(message) {
|
|
365
788
|
return (message ?? '').trim();
|
|
366
789
|
}
|
|
@@ -427,6 +850,134 @@ function normalizeStringArray(value, maxItems = 5, maxLength = 160) {
|
|
|
427
850
|
}
|
|
428
851
|
return out;
|
|
429
852
|
}
|
|
853
|
+
function normalizeCheckpointFileChanges(value, maxItems = 25) {
|
|
854
|
+
if (!Array.isArray(value))
|
|
855
|
+
return [];
|
|
856
|
+
const out = [];
|
|
857
|
+
for (const item of value) {
|
|
858
|
+
if (!item || typeof item !== 'object')
|
|
859
|
+
continue;
|
|
860
|
+
const record = item;
|
|
861
|
+
const path = typeof record.path === 'string' ? truncate(record.path.trim(), 240) : '';
|
|
862
|
+
if (!path)
|
|
863
|
+
continue;
|
|
864
|
+
const action = typeof record.action === 'string' && record.action.trim()
|
|
865
|
+
? truncate(record.action.trim().toLowerCase(), 40)
|
|
866
|
+
: 'updated';
|
|
867
|
+
const next = { action, path };
|
|
868
|
+
if (typeof record.toPath === 'string' && record.toPath.trim()) {
|
|
869
|
+
next.toPath = truncate(record.toPath.trim(), 240);
|
|
870
|
+
}
|
|
871
|
+
if (typeof record.purpose === 'string' && record.purpose.trim()) {
|
|
872
|
+
next.purpose = truncate(record.purpose.trim(), 180);
|
|
873
|
+
}
|
|
874
|
+
out.push(next);
|
|
875
|
+
if (out.length >= maxItems)
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
return out;
|
|
879
|
+
}
|
|
880
|
+
function normalizeCheckpointActions(value, maxItems = 25) {
|
|
881
|
+
if (!Array.isArray(value))
|
|
882
|
+
return [];
|
|
883
|
+
const out = [];
|
|
884
|
+
for (const item of value) {
|
|
885
|
+
if (!item || typeof item !== 'object')
|
|
886
|
+
continue;
|
|
887
|
+
const record = item;
|
|
888
|
+
const summary = typeof record.summary === 'string' ? truncate(record.summary.trim(), 220) : '';
|
|
889
|
+
if (!summary)
|
|
890
|
+
continue;
|
|
891
|
+
const kind = typeof record.kind === 'string' && record.kind.trim()
|
|
892
|
+
? truncate(record.kind.trim().toLowerCase(), 40)
|
|
893
|
+
: 'action';
|
|
894
|
+
const next = { kind, summary };
|
|
895
|
+
if (typeof record.status === 'string' && record.status.trim()) {
|
|
896
|
+
next.status = truncate(record.status.trim().toLowerCase(), 40);
|
|
897
|
+
}
|
|
898
|
+
if (typeof record.target === 'string' && record.target.trim()) {
|
|
899
|
+
next.target = truncate(record.target.trim(), 240);
|
|
900
|
+
}
|
|
901
|
+
if (typeof record.detail === 'string' && record.detail.trim()) {
|
|
902
|
+
next.detail = truncate(record.detail.trim(), 220);
|
|
903
|
+
}
|
|
904
|
+
out.push(next);
|
|
905
|
+
if (out.length >= maxItems)
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
return out;
|
|
909
|
+
}
|
|
910
|
+
function readCheckpointFileChanges(value) {
|
|
911
|
+
if (!value || typeof value !== 'object')
|
|
912
|
+
return [];
|
|
913
|
+
const record = value;
|
|
914
|
+
const items = Array.isArray(record.items) ? record.items : [];
|
|
915
|
+
return items.filter((item) => typeof item === 'object' && item !== null);
|
|
916
|
+
}
|
|
917
|
+
function mergeCheckpointFileChanges(existing, incoming) {
|
|
918
|
+
const seen = new Set();
|
|
919
|
+
const merged = [];
|
|
920
|
+
for (const item of [...readCheckpointFileChanges(existing), ...incoming]) {
|
|
921
|
+
const identity = JSON.stringify(item);
|
|
922
|
+
if (seen.has(identity))
|
|
923
|
+
continue;
|
|
924
|
+
seen.add(identity);
|
|
925
|
+
merged.push(item);
|
|
926
|
+
}
|
|
927
|
+
return { items: merged };
|
|
928
|
+
}
|
|
929
|
+
function readCheckpointActions(value) {
|
|
930
|
+
if (!value || typeof value !== 'object')
|
|
931
|
+
return [];
|
|
932
|
+
const record = value;
|
|
933
|
+
const items = Array.isArray(record.items) ? record.items : [];
|
|
934
|
+
return items.filter((item) => typeof item === 'object' && item !== null);
|
|
935
|
+
}
|
|
936
|
+
function mergeCheckpointActions(existing, incoming) {
|
|
937
|
+
const seen = new Set();
|
|
938
|
+
const merged = [];
|
|
939
|
+
for (const item of [...readCheckpointActions(existing), ...incoming]) {
|
|
940
|
+
const identity = JSON.stringify(item);
|
|
941
|
+
if (seen.has(identity))
|
|
942
|
+
continue;
|
|
943
|
+
seen.add(identity);
|
|
944
|
+
merged.push(item);
|
|
945
|
+
}
|
|
946
|
+
return { items: merged };
|
|
947
|
+
}
|
|
948
|
+
function summarizeCheckpointFileChanges(value) {
|
|
949
|
+
const parts = value.items.map((item) => {
|
|
950
|
+
const action = String(item.action ?? 'updated').trim();
|
|
951
|
+
const path = String(item.path ?? '').trim();
|
|
952
|
+
const toPath = String(item.toPath ?? '').trim();
|
|
953
|
+
const purpose = String(item.purpose ?? '').trim();
|
|
954
|
+
if (!path)
|
|
955
|
+
return '';
|
|
956
|
+
if (toPath) {
|
|
957
|
+
return `${action} ${path} to ${toPath}${purpose ? ` (${purpose})` : ''}`;
|
|
958
|
+
}
|
|
959
|
+
return `${action} ${path}${purpose ? ` (${purpose})` : ''}`;
|
|
960
|
+
}).filter(Boolean);
|
|
961
|
+
return truncate(parts.length > 0
|
|
962
|
+
? `recent file changes include ${parts.join('; ')}`
|
|
963
|
+
: 'recent file changes were logged.', 220);
|
|
964
|
+
}
|
|
965
|
+
function summarizeCheckpointActions(value) {
|
|
966
|
+
const parts = value.items.map((item) => {
|
|
967
|
+
const kind = String(item.kind ?? 'action').trim();
|
|
968
|
+
const summary = String(item.summary ?? '').trim();
|
|
969
|
+
const status = String(item.status ?? '').trim();
|
|
970
|
+
const target = String(item.target ?? '').trim();
|
|
971
|
+
if (!summary)
|
|
972
|
+
return '';
|
|
973
|
+
const prefix = status ? `[${status}] ` : '';
|
|
974
|
+
const suffix = target ? ` (${target})` : '';
|
|
975
|
+
return `${prefix}${kind}: ${summary}${suffix}`;
|
|
976
|
+
}).filter(Boolean);
|
|
977
|
+
return truncate(parts.length > 0
|
|
978
|
+
? `recent actions include ${parts.join('; ')}`
|
|
979
|
+
: 'recent actions were logged.', 220);
|
|
980
|
+
}
|
|
430
981
|
function normalizeCheckpointPayload(payload) {
|
|
431
982
|
if (typeof payload === 'string') {
|
|
432
983
|
return {
|
|
@@ -452,6 +1003,14 @@ function normalizeCheckpointPayload(payload) {
|
|
|
452
1003
|
if (recentOutputs.length > 0) {
|
|
453
1004
|
normalized.recentOutputs = recentOutputs;
|
|
454
1005
|
}
|
|
1006
|
+
const actions = normalizeCheckpointActions(raw.actions);
|
|
1007
|
+
if (actions.length > 0) {
|
|
1008
|
+
normalized.actions = actions;
|
|
1009
|
+
}
|
|
1010
|
+
const fileChanges = normalizeCheckpointFileChanges(raw.fileChanges);
|
|
1011
|
+
if (fileChanges.length > 0) {
|
|
1012
|
+
normalized.fileChanges = fileChanges;
|
|
1013
|
+
}
|
|
455
1014
|
const entityTargets = normalizeStringArray(raw.entityTargets, 5, 180);
|
|
456
1015
|
if (entityTargets.length > 0) {
|
|
457
1016
|
normalized.entityTargets = entityTargets;
|
|
@@ -461,6 +1020,221 @@ function normalizeCheckpointPayload(payload) {
|
|
|
461
1020
|
}
|
|
462
1021
|
return normalized;
|
|
463
1022
|
}
|
|
1023
|
+
async function persistSharedCheckpointBreadcrumbs(params) {
|
|
1024
|
+
const { agentId, sessionId, checkpoint } = params;
|
|
1025
|
+
const targets = Array.isArray(checkpoint.entityTargets) ? checkpoint.entityTargets : [];
|
|
1026
|
+
if (targets.length === 0)
|
|
1027
|
+
return [];
|
|
1028
|
+
const expectedKeys = [];
|
|
1029
|
+
const checkpointSummary = {
|
|
1030
|
+
currentStep: checkpoint.currentStep ?? null,
|
|
1031
|
+
nextStep: checkpoint.nextStep ?? null,
|
|
1032
|
+
openRisks: checkpoint.openRisks ?? [],
|
|
1033
|
+
recentOutputs: checkpoint.recentOutputs ?? [],
|
|
1034
|
+
actions: checkpoint.actions ?? [],
|
|
1035
|
+
fileChanges: checkpoint.fileChanges ?? [],
|
|
1036
|
+
notes: checkpoint.notes ?? null,
|
|
1037
|
+
sessionId,
|
|
1038
|
+
};
|
|
1039
|
+
for (const target of targets) {
|
|
1040
|
+
const parsed = (0, entity_resolution_1.parseEntityString)(target);
|
|
1041
|
+
const resolved = await (0, entity_resolution_1.resolveEntity)({
|
|
1042
|
+
entityType: parsed.entityType,
|
|
1043
|
+
entityId: parsed.entityId,
|
|
1044
|
+
rawName: target,
|
|
1045
|
+
aliases: [target],
|
|
1046
|
+
source: 'AttendantCheckpoint',
|
|
1047
|
+
confidence: 95,
|
|
1048
|
+
createIfMissing: true,
|
|
1049
|
+
});
|
|
1050
|
+
const common = {
|
|
1051
|
+
entityType: resolved.entityType,
|
|
1052
|
+
entityId: resolved.entityId,
|
|
1053
|
+
confidence: 95,
|
|
1054
|
+
source: 'AttendantCheckpoint',
|
|
1055
|
+
createdBy: agentId,
|
|
1056
|
+
};
|
|
1057
|
+
const checkpointBaseProperties = {
|
|
1058
|
+
memoryScope: 'project',
|
|
1059
|
+
capturePhase: 'checkpoint',
|
|
1060
|
+
sessionId,
|
|
1061
|
+
};
|
|
1062
|
+
await (0, librarian_1.librarianWrite)({
|
|
1063
|
+
...common,
|
|
1064
|
+
key: 'checkpoint_summary',
|
|
1065
|
+
valueRaw: checkpointSummary,
|
|
1066
|
+
valueSummary: truncate(`checkpoint summary: current step ${checkpoint.currentStep ?? 'n/a'}; next step ${checkpoint.nextStep ?? 'n/a'}`, 220),
|
|
1067
|
+
properties: {
|
|
1068
|
+
...checkpointBaseProperties,
|
|
1069
|
+
durableClass: 'checkpoint_summary',
|
|
1070
|
+
canonicalKey: 'checkpoint_summary',
|
|
1071
|
+
mergeStrategy: 'replace',
|
|
1072
|
+
...(0, semanticFactTags_1.buildSemanticFactTags)({
|
|
1073
|
+
memoryScope: 'project',
|
|
1074
|
+
durableClass: 'checkpoint_summary',
|
|
1075
|
+
mergeStrategy: 'replace',
|
|
1076
|
+
extraTags: ['checkpoint', 'breadcrumb'],
|
|
1077
|
+
}),
|
|
1078
|
+
},
|
|
1079
|
+
});
|
|
1080
|
+
expectedKeys.push({
|
|
1081
|
+
entityType: resolved.entityType,
|
|
1082
|
+
entityId: resolved.entityId,
|
|
1083
|
+
key: 'checkpoint_summary',
|
|
1084
|
+
});
|
|
1085
|
+
if (checkpoint.currentStep) {
|
|
1086
|
+
await (0, librarian_1.librarianWrite)({
|
|
1087
|
+
...common,
|
|
1088
|
+
key: 'current_step',
|
|
1089
|
+
valueRaw: { text: checkpoint.currentStep },
|
|
1090
|
+
valueSummary: truncate(`current step is ${checkpoint.currentStep}`, 220),
|
|
1091
|
+
properties: {
|
|
1092
|
+
...checkpointBaseProperties,
|
|
1093
|
+
durableClass: 'current_step',
|
|
1094
|
+
canonicalKey: 'current_step',
|
|
1095
|
+
mergeStrategy: 'replace',
|
|
1096
|
+
...(0, semanticFactTags_1.buildSemanticFactTags)({
|
|
1097
|
+
memoryScope: 'project',
|
|
1098
|
+
durableClass: 'current_step',
|
|
1099
|
+
mergeStrategy: 'replace',
|
|
1100
|
+
extraTags: ['checkpoint', 'breadcrumb'],
|
|
1101
|
+
}),
|
|
1102
|
+
},
|
|
1103
|
+
});
|
|
1104
|
+
expectedKeys.push({
|
|
1105
|
+
entityType: resolved.entityType,
|
|
1106
|
+
entityId: resolved.entityId,
|
|
1107
|
+
key: 'current_step',
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
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;
|
|
1124
|
+
await (0, librarian_1.librarianWrite)({
|
|
1125
|
+
...common,
|
|
1126
|
+
key: 'next_step',
|
|
1127
|
+
valueRaw: { instruction: mergedNextStep },
|
|
1128
|
+
valueSummary: truncate(`next step is ${mergedNextStep}`, 220),
|
|
1129
|
+
properties: {
|
|
1130
|
+
...checkpointBaseProperties,
|
|
1131
|
+
durableClass: 'next_step',
|
|
1132
|
+
canonicalKey: 'next_step',
|
|
1133
|
+
mergeStrategy: 'replace',
|
|
1134
|
+
...(0, semanticFactTags_1.buildSemanticFactTags)({
|
|
1135
|
+
memoryScope: 'project',
|
|
1136
|
+
durableClass: 'next_step',
|
|
1137
|
+
mergeStrategy: 'replace',
|
|
1138
|
+
extraTags: ['checkpoint', 'breadcrumb'],
|
|
1139
|
+
}),
|
|
1140
|
+
},
|
|
1141
|
+
});
|
|
1142
|
+
expectedKeys.push({
|
|
1143
|
+
entityType: resolved.entityType,
|
|
1144
|
+
entityId: resolved.entityId,
|
|
1145
|
+
key: 'next_step',
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
if (Array.isArray(checkpoint.fileChanges) && checkpoint.fileChanges.length > 0) {
|
|
1149
|
+
const existingFileChanges = await (0, queries_2.findEntry)({
|
|
1150
|
+
entityType: resolved.entityType,
|
|
1151
|
+
entityId: resolved.entityId,
|
|
1152
|
+
key: 'recent_file_changes',
|
|
1153
|
+
});
|
|
1154
|
+
const mergedFileChanges = mergeCheckpointFileChanges(existingFileChanges?.valueRaw, checkpoint.fileChanges.map((change) => ({ ...change })));
|
|
1155
|
+
await (0, librarian_1.librarianWrite)({
|
|
1156
|
+
...common,
|
|
1157
|
+
key: 'recent_file_changes',
|
|
1158
|
+
valueRaw: mergedFileChanges,
|
|
1159
|
+
valueSummary: summarizeCheckpointFileChanges(mergedFileChanges),
|
|
1160
|
+
properties: {
|
|
1161
|
+
...checkpointBaseProperties,
|
|
1162
|
+
durableClass: 'file_change',
|
|
1163
|
+
canonicalKey: 'recent_file_changes',
|
|
1164
|
+
mergeStrategy: 'append_dedupe',
|
|
1165
|
+
...(0, semanticFactTags_1.buildSemanticFactTags)({
|
|
1166
|
+
memoryScope: 'project',
|
|
1167
|
+
durableClass: 'file_change',
|
|
1168
|
+
mergeStrategy: 'append_dedupe',
|
|
1169
|
+
extraTags: ['checkpoint'],
|
|
1170
|
+
}),
|
|
1171
|
+
},
|
|
1172
|
+
});
|
|
1173
|
+
expectedKeys.push({
|
|
1174
|
+
entityType: resolved.entityType,
|
|
1175
|
+
entityId: resolved.entityId,
|
|
1176
|
+
key: 'recent_file_changes',
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
if (Array.isArray(checkpoint.actions) && checkpoint.actions.length > 0) {
|
|
1180
|
+
const existingActions = await (0, queries_2.findEntry)({
|
|
1181
|
+
entityType: resolved.entityType,
|
|
1182
|
+
entityId: resolved.entityId,
|
|
1183
|
+
key: 'recent_actions',
|
|
1184
|
+
});
|
|
1185
|
+
const mergedActions = mergeCheckpointActions(existingActions?.valueRaw, checkpoint.actions.map((action) => ({ ...action })));
|
|
1186
|
+
await (0, librarian_1.librarianWrite)({
|
|
1187
|
+
...common,
|
|
1188
|
+
key: 'recent_actions',
|
|
1189
|
+
valueRaw: mergedActions,
|
|
1190
|
+
valueSummary: summarizeCheckpointActions(mergedActions),
|
|
1191
|
+
properties: {
|
|
1192
|
+
...checkpointBaseProperties,
|
|
1193
|
+
durableClass: 'action_log',
|
|
1194
|
+
canonicalKey: 'recent_actions',
|
|
1195
|
+
mergeStrategy: 'append_dedupe',
|
|
1196
|
+
...(0, semanticFactTags_1.buildSemanticFactTags)({
|
|
1197
|
+
memoryScope: 'project',
|
|
1198
|
+
durableClass: 'action_log',
|
|
1199
|
+
mergeStrategy: 'append_dedupe',
|
|
1200
|
+
extraTags: ['checkpoint'],
|
|
1201
|
+
}),
|
|
1202
|
+
},
|
|
1203
|
+
});
|
|
1204
|
+
expectedKeys.push({
|
|
1205
|
+
entityType: resolved.entityType,
|
|
1206
|
+
entityId: resolved.entityId,
|
|
1207
|
+
key: 'recent_actions',
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
if (checkpoint.openRisks && checkpoint.openRisks.length > 0) {
|
|
1211
|
+
await (0, librarian_1.librarianWrite)({
|
|
1212
|
+
...common,
|
|
1213
|
+
key: 'open_risks',
|
|
1214
|
+
valueRaw: { items: checkpoint.openRisks },
|
|
1215
|
+
valueSummary: truncate(`open risks include ${checkpoint.openRisks.join('; ')}`, 220),
|
|
1216
|
+
properties: {
|
|
1217
|
+
...checkpointBaseProperties,
|
|
1218
|
+
durableClass: 'open_risks',
|
|
1219
|
+
canonicalKey: 'open_risks',
|
|
1220
|
+
mergeStrategy: 'replace',
|
|
1221
|
+
...(0, semanticFactTags_1.buildSemanticFactTags)({
|
|
1222
|
+
memoryScope: 'project',
|
|
1223
|
+
durableClass: 'open_risks',
|
|
1224
|
+
mergeStrategy: 'replace',
|
|
1225
|
+
extraTags: ['checkpoint', 'breadcrumb'],
|
|
1226
|
+
}),
|
|
1227
|
+
},
|
|
1228
|
+
});
|
|
1229
|
+
expectedKeys.push({
|
|
1230
|
+
entityType: resolved.entityType,
|
|
1231
|
+
entityId: resolved.entityId,
|
|
1232
|
+
key: 'open_risks',
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return expectedKeys;
|
|
1237
|
+
}
|
|
464
1238
|
function createSessionId(agentId, taskFingerprint) {
|
|
465
1239
|
const seed = `${agentId}:${taskFingerprint}:${Date.now()}`;
|
|
466
1240
|
let hash = 0;
|
|
@@ -531,6 +1305,15 @@ function heuristicMemoryNeed(message) {
|
|
|
531
1305
|
explanation: 'memory_reference_detected',
|
|
532
1306
|
};
|
|
533
1307
|
}
|
|
1308
|
+
// Any substantive message in a project-bound context likely benefits from memory.
|
|
1309
|
+
// Greetings and acks are already caught by MEMORY_NEED_NEGATIVE_PATTERNS above.
|
|
1310
|
+
if (normalized.length > 20) {
|
|
1311
|
+
return {
|
|
1312
|
+
needed: true,
|
|
1313
|
+
confidence: 0.75,
|
|
1314
|
+
explanation: 'substantive_project_prompt',
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
534
1317
|
return {
|
|
535
1318
|
needed: null,
|
|
536
1319
|
confidence: 0.55,
|
|
@@ -541,14 +1324,253 @@ function heuristicMemoryNeed(message) {
|
|
|
541
1324
|
class AttendantInstance {
|
|
542
1325
|
constructor(agentId) {
|
|
543
1326
|
this.brief = null;
|
|
1327
|
+
this.advisoryLearningProfile = null;
|
|
544
1328
|
this.contextCallCount = 0;
|
|
1329
|
+
this.attendsWithoutPersist = 0;
|
|
1330
|
+
this.consecutivePreResponseWithoutPost = 0;
|
|
1331
|
+
this.consecutiveUnusedMemoryInjections = 0;
|
|
1332
|
+
this.lastAttendPhase = undefined;
|
|
1333
|
+
this.complianceUpdatedAt = new Date().toISOString();
|
|
545
1334
|
this.sessionStarted = new Date().toISOString();
|
|
546
1335
|
this.sessionCheckpoint = null;
|
|
1336
|
+
this.eventSource = 'internal';
|
|
1337
|
+
this.eventHost = null;
|
|
1338
|
+
this.sharedStateObservedAt = null;
|
|
1339
|
+
this.pendingSharedStateInvalidations = new Map();
|
|
1340
|
+
this.pendingMemoryAttributions = [];
|
|
547
1341
|
this.agentId = agentId;
|
|
1342
|
+
(0, sharedStateInvalidation_1.registerSharedStateInvalidationObserver)(agentId, this);
|
|
1343
|
+
}
|
|
1344
|
+
setLedgerContext(context) {
|
|
1345
|
+
if (context?.source?.trim()) {
|
|
1346
|
+
this.eventSource = context.source.trim();
|
|
1347
|
+
}
|
|
1348
|
+
if (typeof context?.host === 'string') {
|
|
1349
|
+
const trimmed = context.host.trim();
|
|
1350
|
+
this.eventHost = trimmed || null;
|
|
1351
|
+
}
|
|
1352
|
+
else if (context?.host === null) {
|
|
1353
|
+
this.eventHost = null;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
buildEventMetadata(metadata = {}) {
|
|
1357
|
+
const withSession = Object.prototype.hasOwnProperty.call(metadata, 'sessionId')
|
|
1358
|
+
? metadata
|
|
1359
|
+
: { ...metadata, sessionId: this.sessionStarted };
|
|
1360
|
+
return {
|
|
1361
|
+
...withSession,
|
|
1362
|
+
...(this.eventHost ? { host: this.eventHost } : {}),
|
|
1363
|
+
};
|
|
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
|
+
}
|
|
1515
|
+
async loadSessionLedgerSignals(taskType) {
|
|
1516
|
+
try {
|
|
1517
|
+
const source = this.eventSource === 'internal' ? undefined : this.eventSource;
|
|
1518
|
+
const [learnings, profile] = await Promise.all([
|
|
1519
|
+
(0, sessionLedger_1.summarizeSessionLedgerLearnings)({
|
|
1520
|
+
agentId: this.agentId,
|
|
1521
|
+
source,
|
|
1522
|
+
host: this.eventHost ?? undefined,
|
|
1523
|
+
limit: 40,
|
|
1524
|
+
maxLearnings: 4,
|
|
1525
|
+
}),
|
|
1526
|
+
(0, sessionLedger_1.buildSessionLedgerLearningProfile)({
|
|
1527
|
+
agentId: this.agentId,
|
|
1528
|
+
source,
|
|
1529
|
+
host: this.eventHost ?? undefined,
|
|
1530
|
+
taskType,
|
|
1531
|
+
limit: 60,
|
|
1532
|
+
}),
|
|
1533
|
+
]);
|
|
1534
|
+
return { learnings, profile };
|
|
1535
|
+
}
|
|
1536
|
+
catch (error) {
|
|
1537
|
+
if (error instanceof sessionLedger_1.SessionLedgerUnavailableError) {
|
|
1538
|
+
return { learnings: [], profile: null };
|
|
1539
|
+
}
|
|
1540
|
+
return { learnings: [], profile: null };
|
|
1541
|
+
}
|
|
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;
|
|
548
1569
|
}
|
|
549
1570
|
// ── Handshake ────────────────────────────────────────────────────────────
|
|
550
1571
|
async handshake(context) {
|
|
551
1572
|
const t0 = (0, metrics_1.timeStart)();
|
|
1573
|
+
this.setLedgerContext(context.ledgerContext);
|
|
552
1574
|
// Try to resume from persisted state first
|
|
553
1575
|
const persisted = await this.loadPersistedState();
|
|
554
1576
|
// Load operating rules from Staff Namespace
|
|
@@ -556,7 +1578,15 @@ class AttendantInstance {
|
|
|
556
1578
|
// Infer task type
|
|
557
1579
|
const inferredTaskType = await this.inferTask(context);
|
|
558
1580
|
// Load knowledge — agent entries + related entities
|
|
559
|
-
const workingMemory = await
|
|
1581
|
+
const [workingMemory, ledgerSignals, projectPolicies] = await Promise.all([
|
|
1582
|
+
this.buildWorkingMemory(inferredTaskType),
|
|
1583
|
+
this.loadSessionLedgerSignals(inferredTaskType),
|
|
1584
|
+
this.loadProjectPolicies(),
|
|
1585
|
+
]);
|
|
1586
|
+
const sessionLedgerLearnings = ledgerSignals.learnings;
|
|
1587
|
+
this.advisoryLearningProfile = ledgerSignals.profile;
|
|
1588
|
+
const workingMemoryWithPolicies = mergeWorkingMemoryWithProjectPolicies(workingMemory, projectPolicies);
|
|
1589
|
+
const workingMemoryWithLedger = mergeWorkingMemoryWithLedger(workingMemoryWithPolicies, sessionLedgerLearnings);
|
|
560
1590
|
const recoveryResult = persisted?.sessionCheckpoint
|
|
561
1591
|
? this.buildRecovery(context, persisted.sessionCheckpoint)
|
|
562
1592
|
: { interrupted: false, recovery: null };
|
|
@@ -573,29 +1603,40 @@ class AttendantInstance {
|
|
|
573
1603
|
}
|
|
574
1604
|
this.brief = {
|
|
575
1605
|
agentId: this.agentId,
|
|
576
|
-
operatingRules,
|
|
1606
|
+
operatingRules: applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(operatingRules, projectPolicies), this.advisoryLearningProfile),
|
|
577
1607
|
inferredTaskType,
|
|
578
|
-
workingMemory,
|
|
1608
|
+
workingMemory: workingMemoryWithLedger,
|
|
1609
|
+
projectPolicies,
|
|
579
1610
|
sessionStarted: persisted?.sessionStarted ?? this.sessionStarted,
|
|
580
1611
|
briefGeneratedAt: new Date().toISOString(),
|
|
581
1612
|
contextCallCount: this.contextCallCount,
|
|
582
|
-
backfillSuggestion: buildBackfillSuggestion(context,
|
|
1613
|
+
backfillSuggestion: buildBackfillSuggestion(context, workingMemoryWithLedger),
|
|
1614
|
+
sessionLedgerLearnings,
|
|
583
1615
|
sessionCheckpoint: this.sessionCheckpoint,
|
|
584
1616
|
sessionRecovery: recoveryResult.recovery,
|
|
1617
|
+
compliance: persisted?.compliance ?? this.buildComplianceState(),
|
|
1618
|
+
watchedEntities: normalizeWatchedEntities([
|
|
1619
|
+
...(persisted?.watchedEntities ?? []),
|
|
1620
|
+
...(this.sessionCheckpoint?.checkpoint.entityTargets ?? []),
|
|
1621
|
+
...inferContextWatchedEntities(context.task, context.recentMessages),
|
|
1622
|
+
]),
|
|
585
1623
|
};
|
|
1624
|
+
this.sharedStateObservedAt = this.brief.briefGeneratedAt;
|
|
586
1625
|
await this.persistState();
|
|
587
1626
|
(0, staffEventRegistry_1.getStaffEventEmitter)().emit({
|
|
588
1627
|
staffComponent: 'Attendant',
|
|
589
1628
|
actionType: 'handshake_completed',
|
|
590
1629
|
agentId: this.agentId,
|
|
591
|
-
source:
|
|
592
|
-
reason:
|
|
593
|
-
level: '
|
|
594
|
-
metadata: {
|
|
1630
|
+
source: this.eventSource,
|
|
1631
|
+
reason: 'session_started',
|
|
1632
|
+
level: 'audit',
|
|
1633
|
+
metadata: this.buildEventMetadata({
|
|
595
1634
|
briefSize: this.brief?.workingMemory.length ?? 0,
|
|
1635
|
+
ledgerLearningCount: sessionLedgerLearnings.length,
|
|
1636
|
+
projectPolicyCount: projectPolicies.length,
|
|
1637
|
+
advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
|
|
596
1638
|
taskSummary: context.task.slice(0, 120),
|
|
597
|
-
|
|
598
|
-
},
|
|
1639
|
+
}),
|
|
599
1640
|
});
|
|
600
1641
|
(0, metrics_1.timeEnd)('attendant.handshake_ms', t0);
|
|
601
1642
|
return this.brief;
|
|
@@ -603,12 +1644,18 @@ class AttendantInstance {
|
|
|
603
1644
|
// ── Reconvene ────────────────────────────────────────────────────────────
|
|
604
1645
|
async reconvene(context) {
|
|
605
1646
|
const t0 = (0, metrics_1.timeStart)();
|
|
1647
|
+
this.setLedgerContext(context.ledgerContext);
|
|
606
1648
|
if (!this.brief) {
|
|
607
1649
|
const result = await this.handshake(context);
|
|
608
1650
|
(0, metrics_1.timeEnd)('attendant.reconvene_ms', t0);
|
|
609
1651
|
return result;
|
|
610
1652
|
}
|
|
611
1653
|
const newTaskType = await this.inferTask(context);
|
|
1654
|
+
const [ledgerSignals, projectPolicies] = await Promise.all([
|
|
1655
|
+
this.loadSessionLedgerSignals(newTaskType),
|
|
1656
|
+
this.loadProjectPolicies(),
|
|
1657
|
+
]);
|
|
1658
|
+
this.advisoryLearningProfile = ledgerSignals.profile;
|
|
612
1659
|
// Task hasn't shifted — update timestamp only
|
|
613
1660
|
if (newTaskType.toLowerCase() === this.brief.inferredTaskType.toLowerCase()) {
|
|
614
1661
|
if (this.sessionCheckpoint && this.sessionCheckpoint.status === 'active') {
|
|
@@ -621,24 +1668,31 @@ class AttendantInstance {
|
|
|
621
1668
|
}
|
|
622
1669
|
this.brief = {
|
|
623
1670
|
...this.brief,
|
|
1671
|
+
operatingRules: applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(await this.loadOperatingRules(), projectPolicies), this.advisoryLearningProfile),
|
|
1672
|
+
workingMemory: mergeWorkingMemoryWithLedger(mergeWorkingMemoryWithProjectPolicies(this.brief.workingMemory, projectPolicies), ledgerSignals.learnings),
|
|
1673
|
+
projectPolicies,
|
|
624
1674
|
briefGeneratedAt: new Date().toISOString(),
|
|
625
1675
|
contextCallCount: this.contextCallCount,
|
|
1676
|
+
sessionLedgerLearnings: ledgerSignals.learnings,
|
|
626
1677
|
sessionCheckpoint: this.sessionCheckpoint,
|
|
627
1678
|
sessionRecovery: null,
|
|
1679
|
+
compliance: this.buildComplianceState(),
|
|
628
1680
|
};
|
|
1681
|
+
this.sharedStateObservedAt = this.brief.briefGeneratedAt;
|
|
629
1682
|
await this.persistState();
|
|
630
1683
|
(0, staffEventRegistry_1.getStaffEventEmitter)().emit({
|
|
631
1684
|
staffComponent: 'Attendant',
|
|
632
1685
|
actionType: 'reconvene_completed',
|
|
633
1686
|
agentId: this.agentId,
|
|
634
|
-
source:
|
|
635
|
-
reason: 'Task unchanged
|
|
1687
|
+
source: this.eventSource,
|
|
1688
|
+
reason: 'Task unchanged - brief timestamp refreshed.',
|
|
636
1689
|
level: 'audit',
|
|
637
|
-
metadata: {
|
|
1690
|
+
metadata: this.buildEventMetadata({
|
|
638
1691
|
briefSize: this.brief?.workingMemory.length ?? 0,
|
|
639
|
-
sessionId: this.sessionStarted,
|
|
640
1692
|
contextCallCount: this.contextCallCount,
|
|
641
|
-
|
|
1693
|
+
projectPolicyCount: projectPolicies.length,
|
|
1694
|
+
advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
|
|
1695
|
+
}),
|
|
642
1696
|
});
|
|
643
1697
|
(0, metrics_1.timeEnd)('attendant.reconvene_ms', t0);
|
|
644
1698
|
return this.brief;
|
|
@@ -647,26 +1701,32 @@ class AttendantInstance {
|
|
|
647
1701
|
const workingMemory = await this.buildWorkingMemory(newTaskType);
|
|
648
1702
|
this.brief = {
|
|
649
1703
|
...this.brief,
|
|
1704
|
+
operatingRules: applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(await this.loadOperatingRules(), projectPolicies), this.advisoryLearningProfile),
|
|
650
1705
|
inferredTaskType: newTaskType,
|
|
651
|
-
workingMemory,
|
|
1706
|
+
workingMemory: mergeWorkingMemoryWithLedger(mergeWorkingMemoryWithProjectPolicies(workingMemory, projectPolicies), ledgerSignals.learnings),
|
|
1707
|
+
projectPolicies,
|
|
652
1708
|
briefGeneratedAt: new Date().toISOString(),
|
|
653
1709
|
contextCallCount: this.contextCallCount,
|
|
1710
|
+
sessionLedgerLearnings: ledgerSignals.learnings,
|
|
654
1711
|
sessionCheckpoint: this.sessionCheckpoint,
|
|
655
1712
|
sessionRecovery: null,
|
|
1713
|
+
compliance: this.buildComplianceState(),
|
|
656
1714
|
};
|
|
1715
|
+
this.sharedStateObservedAt = this.brief.briefGeneratedAt;
|
|
657
1716
|
await this.persistState();
|
|
658
1717
|
(0, staffEventRegistry_1.getStaffEventEmitter)().emit({
|
|
659
1718
|
staffComponent: 'Attendant',
|
|
660
1719
|
actionType: 'reconvene_completed',
|
|
661
1720
|
agentId: this.agentId,
|
|
662
|
-
source:
|
|
663
|
-
reason: 'Task shifted
|
|
1721
|
+
source: this.eventSource,
|
|
1722
|
+
reason: 'Task shifted - working memory rebuilt.',
|
|
664
1723
|
level: 'audit',
|
|
665
|
-
metadata: {
|
|
1724
|
+
metadata: this.buildEventMetadata({
|
|
666
1725
|
briefSize: this.brief?.workingMemory.length ?? 0,
|
|
667
|
-
sessionId: this.sessionStarted,
|
|
668
1726
|
contextCallCount: this.contextCallCount,
|
|
669
|
-
|
|
1727
|
+
projectPolicyCount: projectPolicies.length,
|
|
1728
|
+
advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
|
|
1729
|
+
}),
|
|
670
1730
|
});
|
|
671
1731
|
(0, metrics_1.timeEnd)('attendant.reconvene_ms', t0);
|
|
672
1732
|
return this.brief;
|
|
@@ -692,8 +1752,10 @@ class AttendantInstance {
|
|
|
692
1752
|
const operatingRules = rulesResult.found && rulesResult.entry
|
|
693
1753
|
? formatOperatingRulesText(rulesResult.entry.valueRaw, rulesResult.entry.valueSummary)
|
|
694
1754
|
: formatOperatingRulesText(null, 'Attendant operating rules:');
|
|
1755
|
+
const projectPolicies = this.brief?.projectPolicies ?? await this.loadProjectPolicies();
|
|
695
1756
|
if (this.brief) {
|
|
696
|
-
this.brief.operatingRules = operatingRules;
|
|
1757
|
+
this.brief.operatingRules = applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(operatingRules, projectPolicies), this.advisoryLearningProfile);
|
|
1758
|
+
this.brief.projectPolicies = projectPolicies;
|
|
697
1759
|
this.brief.contextCallCount = 0;
|
|
698
1760
|
}
|
|
699
1761
|
this.contextCallCount = 0;
|
|
@@ -702,26 +1764,69 @@ class AttendantInstance {
|
|
|
702
1764
|
staffComponent: 'Attendant',
|
|
703
1765
|
actionType: 'session_expired',
|
|
704
1766
|
agentId: this.agentId,
|
|
705
|
-
source:
|
|
1767
|
+
source: this.eventSource,
|
|
706
1768
|
reason: 'Context window threshold reached. Session archived.',
|
|
707
1769
|
level: 'audit',
|
|
708
|
-
metadata: {
|
|
709
|
-
sessionId: this.sessionStarted,
|
|
1770
|
+
metadata: this.buildEventMetadata({
|
|
710
1771
|
contextCallCount: 0,
|
|
711
1772
|
expiryReason: 'context_low',
|
|
712
|
-
},
|
|
1773
|
+
}),
|
|
713
1774
|
});
|
|
714
1775
|
}
|
|
715
1776
|
// ── Getters ──────────────────────────────────────────────────────────────
|
|
716
1777
|
getBrief() {
|
|
717
1778
|
return this.brief;
|
|
718
1779
|
}
|
|
1780
|
+
async verifyCheckpointAvailability(sessionId, expectedKeys) {
|
|
1781
|
+
const persisted = await readPersistedBriefForAgent(this.agentId);
|
|
1782
|
+
if (!persisted?.sessionCheckpoint || persisted.sessionCheckpoint.sessionId !== sessionId) {
|
|
1783
|
+
throw new Error(`CHECKPOINT_AVAILABILITY_FAILED: agent/${this.agentId}/attendant_state was not immediately queryable with session ${sessionId}.`);
|
|
1784
|
+
}
|
|
1785
|
+
for (const expected of expectedKeys) {
|
|
1786
|
+
const observed = await (0, queries_2.findEntry)(expected);
|
|
1787
|
+
if (!observed) {
|
|
1788
|
+
throw new Error(`CHECKPOINT_AVAILABILITY_FAILED: ${expected.entityType}/${expected.entityId}/${expected.key} was not immediately queryable after checkpoint success.`);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
buildComplianceState(lastUpdated) {
|
|
1793
|
+
return buildSessionComplianceState({
|
|
1794
|
+
attendsWithoutPersist: this.attendsWithoutPersist,
|
|
1795
|
+
consecutivePreResponseWithoutPost: this.consecutivePreResponseWithoutPost,
|
|
1796
|
+
consecutiveUnusedMemoryInjections: this.consecutiveUnusedMemoryInjections,
|
|
1797
|
+
lastAttendPhase: this.lastAttendPhase,
|
|
1798
|
+
lastUpdated: lastUpdated ?? this.complianceUpdatedAt,
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
async notifyWriteOccurred() {
|
|
1802
|
+
this.attendsWithoutPersist = 0;
|
|
1803
|
+
this.lastAttendPhase = undefined;
|
|
1804
|
+
this.consecutivePreResponseWithoutPost = 0;
|
|
1805
|
+
this.complianceUpdatedAt = new Date().toISOString();
|
|
1806
|
+
this.recordMemoryEvidence('write');
|
|
1807
|
+
if (!this.brief) {
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
this.brief = {
|
|
1811
|
+
...this.brief,
|
|
1812
|
+
compliance: this.buildComplianceState(this.complianceUpdatedAt),
|
|
1813
|
+
briefGeneratedAt: this.complianceUpdatedAt,
|
|
1814
|
+
};
|
|
1815
|
+
await this.persistState();
|
|
1816
|
+
}
|
|
719
1817
|
async checkpoint(input) {
|
|
1818
|
+
this.attendsWithoutPersist = 0;
|
|
1819
|
+
this.lastAttendPhase = undefined;
|
|
1820
|
+
this.consecutivePreResponseWithoutPost = 0;
|
|
1821
|
+
this.complianceUpdatedAt = new Date().toISOString();
|
|
1822
|
+
this.recordMemoryEvidence('checkpoint');
|
|
1823
|
+
this.setLedgerContext(input.ledgerContext);
|
|
720
1824
|
const now = new Date().toISOString();
|
|
721
1825
|
if (!this.brief) {
|
|
722
1826
|
await this.handshake({
|
|
723
1827
|
task: input.task,
|
|
724
1828
|
recentMessages: input.recentMessages,
|
|
1829
|
+
ledgerContext: input.ledgerContext,
|
|
725
1830
|
});
|
|
726
1831
|
}
|
|
727
1832
|
const normalizedCheckpoint = normalizeCheckpointPayload(input.checkpoint);
|
|
@@ -746,15 +1851,88 @@ class AttendantInstance {
|
|
|
746
1851
|
...this.brief,
|
|
747
1852
|
sessionCheckpoint: this.sessionCheckpoint,
|
|
748
1853
|
sessionRecovery: null,
|
|
1854
|
+
compliance: this.buildComplianceState(now),
|
|
749
1855
|
briefGeneratedAt: now,
|
|
1856
|
+
watchedEntities: normalizeWatchedEntities([
|
|
1857
|
+
...(this.brief.watchedEntities ?? []),
|
|
1858
|
+
...(normalizedCheckpoint.entityTargets ?? []),
|
|
1859
|
+
]),
|
|
750
1860
|
};
|
|
1861
|
+
this.sharedStateObservedAt = now;
|
|
1862
|
+
let expectedSharedKeys = [];
|
|
1863
|
+
try {
|
|
1864
|
+
expectedSharedKeys = await persistSharedCheckpointBreadcrumbs({
|
|
1865
|
+
agentId: this.agentId,
|
|
1866
|
+
sessionId,
|
|
1867
|
+
checkpoint: normalizedCheckpoint,
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
catch (error) {
|
|
1871
|
+
(0, staffEventRegistry_1.getStaffEventEmitter)().emit({
|
|
1872
|
+
staffComponent: 'Attendant',
|
|
1873
|
+
actionType: 'checkpoint_shared_breadcrumb_failed',
|
|
1874
|
+
agentId: this.agentId,
|
|
1875
|
+
source: this.eventSource,
|
|
1876
|
+
reason: 'shared_checkpoint_breadcrumb_failed',
|
|
1877
|
+
level: 'audit',
|
|
1878
|
+
metadata: this.buildEventMetadata({
|
|
1879
|
+
sessionId,
|
|
1880
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1881
|
+
}),
|
|
1882
|
+
});
|
|
1883
|
+
throw error;
|
|
1884
|
+
}
|
|
751
1885
|
await this.persistState();
|
|
1886
|
+
try {
|
|
1887
|
+
await this.verifyCheckpointAvailability(sessionId, expectedSharedKeys);
|
|
1888
|
+
}
|
|
1889
|
+
catch (error) {
|
|
1890
|
+
(0, staffEventRegistry_1.getStaffEventEmitter)().emit({
|
|
1891
|
+
staffComponent: 'Attendant',
|
|
1892
|
+
actionType: 'checkpoint_availability_failed',
|
|
1893
|
+
agentId: this.agentId,
|
|
1894
|
+
source: this.eventSource,
|
|
1895
|
+
reason: 'checkpoint_not_immediately_queryable',
|
|
1896
|
+
level: 'audit',
|
|
1897
|
+
metadata: this.buildEventMetadata({
|
|
1898
|
+
sessionId,
|
|
1899
|
+
sharedKeyCount: expectedSharedKeys.length,
|
|
1900
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1901
|
+
}),
|
|
1902
|
+
});
|
|
1903
|
+
throw error;
|
|
1904
|
+
}
|
|
1905
|
+
(0, staffEventRegistry_1.getStaffEventEmitter)().emit({
|
|
1906
|
+
staffComponent: 'Attendant',
|
|
1907
|
+
actionType: 'checkpoint_written',
|
|
1908
|
+
agentId: this.agentId,
|
|
1909
|
+
source: this.eventSource,
|
|
1910
|
+
reason: 'shared_checkpoint_written',
|
|
1911
|
+
level: 'audit',
|
|
1912
|
+
metadata: this.buildEventMetadata({
|
|
1913
|
+
sessionId,
|
|
1914
|
+
currentStep: normalizedCheckpoint.currentStep ?? null,
|
|
1915
|
+
nextStep: normalizedCheckpoint.nextStep ?? null,
|
|
1916
|
+
openRiskCount: normalizedCheckpoint.openRisks?.length ?? 0,
|
|
1917
|
+
recentOutputCount: normalizedCheckpoint.recentOutputs?.length ?? 0,
|
|
1918
|
+
actionCount: normalizedCheckpoint.actions?.length ?? 0,
|
|
1919
|
+
fileChangeCount: normalizedCheckpoint.fileChanges?.length ?? 0,
|
|
1920
|
+
entityTargetCount: normalizedCheckpoint.entityTargets?.length ?? 0,
|
|
1921
|
+
sharedKeyCount: expectedSharedKeys.length,
|
|
1922
|
+
availabilityVerified: true,
|
|
1923
|
+
}),
|
|
1924
|
+
});
|
|
752
1925
|
return this.brief;
|
|
753
1926
|
}
|
|
754
1927
|
async resumeSession(input = {}) {
|
|
1928
|
+
this.setLedgerContext(input.ledgerContext);
|
|
755
1929
|
await this.ensureSessionLoaded();
|
|
756
1930
|
if (!this.brief || !this.sessionCheckpoint) {
|
|
757
|
-
return this.brief ?? (await this.handshake({
|
|
1931
|
+
return this.brief ?? (await this.handshake({
|
|
1932
|
+
task: 'resume session',
|
|
1933
|
+
recentMessages: [],
|
|
1934
|
+
ledgerContext: input.ledgerContext,
|
|
1935
|
+
}));
|
|
758
1936
|
}
|
|
759
1937
|
if (input.sessionId && input.sessionId.trim() !== this.sessionCheckpoint.sessionId) {
|
|
760
1938
|
throw new Error(`Session "${input.sessionId}" does not match the active checkpoint.`);
|
|
@@ -771,15 +1949,22 @@ class AttendantInstance {
|
|
|
771
1949
|
...this.brief,
|
|
772
1950
|
sessionCheckpoint: this.sessionCheckpoint,
|
|
773
1951
|
sessionRecovery: null,
|
|
1952
|
+
compliance: this.buildComplianceState(now),
|
|
774
1953
|
briefGeneratedAt: now,
|
|
775
1954
|
};
|
|
1955
|
+
this.sharedStateObservedAt = now;
|
|
776
1956
|
await this.persistState();
|
|
777
1957
|
return this.brief;
|
|
778
1958
|
}
|
|
779
1959
|
async completeSession(input = {}) {
|
|
1960
|
+
this.setLedgerContext(input.ledgerContext);
|
|
780
1961
|
await this.ensureSessionLoaded();
|
|
781
1962
|
if (!this.brief || !this.sessionCheckpoint) {
|
|
782
|
-
return this.brief ?? (await this.handshake({
|
|
1963
|
+
return this.brief ?? (await this.handshake({
|
|
1964
|
+
task: 'complete session',
|
|
1965
|
+
recentMessages: [],
|
|
1966
|
+
ledgerContext: input.ledgerContext,
|
|
1967
|
+
}));
|
|
783
1968
|
}
|
|
784
1969
|
if (input.sessionId && input.sessionId.trim() !== this.sessionCheckpoint.sessionId) {
|
|
785
1970
|
throw new Error(`Session "${input.sessionId}" does not match the active checkpoint.`);
|
|
@@ -796,15 +1981,22 @@ class AttendantInstance {
|
|
|
796
1981
|
...this.brief,
|
|
797
1982
|
sessionCheckpoint: this.sessionCheckpoint,
|
|
798
1983
|
sessionRecovery: null,
|
|
1984
|
+
compliance: this.buildComplianceState(now),
|
|
799
1985
|
briefGeneratedAt: now,
|
|
800
1986
|
};
|
|
1987
|
+
this.sharedStateObservedAt = now;
|
|
801
1988
|
await this.persistState();
|
|
802
1989
|
return this.brief;
|
|
803
1990
|
}
|
|
804
1991
|
async abandonSession(input = {}) {
|
|
1992
|
+
this.setLedgerContext(input.ledgerContext);
|
|
805
1993
|
await this.ensureSessionLoaded();
|
|
806
1994
|
if (!this.brief || !this.sessionCheckpoint) {
|
|
807
|
-
return this.brief ?? (await this.handshake({
|
|
1995
|
+
return this.brief ?? (await this.handshake({
|
|
1996
|
+
task: 'abandon session',
|
|
1997
|
+
recentMessages: [],
|
|
1998
|
+
ledgerContext: input.ledgerContext,
|
|
1999
|
+
}));
|
|
808
2000
|
}
|
|
809
2001
|
if (input.sessionId && input.sessionId.trim() !== this.sessionCheckpoint.sessionId) {
|
|
810
2002
|
throw new Error(`Session "${input.sessionId}" does not match the active checkpoint.`);
|
|
@@ -821,14 +2013,18 @@ class AttendantInstance {
|
|
|
821
2013
|
...this.brief,
|
|
822
2014
|
sessionCheckpoint: this.sessionCheckpoint,
|
|
823
2015
|
sessionRecovery: null,
|
|
2016
|
+
compliance: this.buildComplianceState(now),
|
|
824
2017
|
briefGeneratedAt: now,
|
|
825
2018
|
};
|
|
2019
|
+
this.sharedStateObservedAt = now;
|
|
826
2020
|
await this.persistState();
|
|
827
2021
|
return this.brief;
|
|
828
2022
|
}
|
|
829
2023
|
async inspectSession(context) {
|
|
2024
|
+
this.setLedgerContext(context?.ledgerContext);
|
|
830
2025
|
const persisted = await this.loadPersistedState();
|
|
831
2026
|
const checkpoint = persisted?.sessionCheckpoint ?? this.sessionCheckpoint ?? null;
|
|
2027
|
+
const compliance = persisted?.compliance ?? this.buildComplianceState();
|
|
832
2028
|
const normalizedTask = typeof context?.task === 'string' ? context.task.trim() : '';
|
|
833
2029
|
const recentMessages = Array.isArray(context?.recentMessages) ? context.recentMessages : [];
|
|
834
2030
|
const recovery = checkpoint && normalizedTask
|
|
@@ -840,7 +2036,8 @@ class AttendantInstance {
|
|
|
840
2036
|
sessionCheckpoint: checkpoint,
|
|
841
2037
|
sessionRecovery: recovery,
|
|
842
2038
|
persistedBriefGeneratedAt: persisted?.briefGeneratedAt,
|
|
843
|
-
summary: summarizeSessionState(this.agentId, checkpoint, persisted?.briefGeneratedAt),
|
|
2039
|
+
summary: summarizeSessionState(this.agentId, checkpoint, persisted?.briefGeneratedAt, compliance),
|
|
2040
|
+
compliance,
|
|
844
2041
|
};
|
|
845
2042
|
}
|
|
846
2043
|
getAgentId() {
|
|
@@ -848,53 +2045,183 @@ class AttendantInstance {
|
|
|
848
2045
|
}
|
|
849
2046
|
async attend(input) {
|
|
850
2047
|
const t0 = (0, metrics_1.timeStart)();
|
|
2048
|
+
this.setLedgerContext(input.ledgerContext);
|
|
851
2049
|
const currentContext = input.currentContext ?? '';
|
|
852
2050
|
const latestMessage = normalizeMessage(input.latestMessage);
|
|
853
2051
|
const forceInject = input.forceInject === true;
|
|
854
2052
|
let bootstrap = null;
|
|
2053
|
+
this.attendsWithoutPersist++;
|
|
2054
|
+
const phase = input.phase;
|
|
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.';
|
|
2057
|
+
if (phase === 'post-response') {
|
|
2058
|
+
// Correct post-response call — reset counters
|
|
2059
|
+
this.attendsWithoutPersist = 0;
|
|
2060
|
+
this.lastAttendPhase = 'post-response';
|
|
2061
|
+
this.consecutivePreResponseWithoutPost = 0;
|
|
2062
|
+
}
|
|
2063
|
+
else if (phase === 'pre-response') {
|
|
2064
|
+
if (this.lastAttendPhase === 'pre-response') {
|
|
2065
|
+
this.consecutivePreResponseWithoutPost++;
|
|
2066
|
+
complianceWarning = `COMPLIANCE: iranti_attend(phase='pre-response') was called without a preceding phase='post-response'. The previous response was delivered without the required post-response attend. After every response, call iranti_attend(phase='post-response') then persist durable findings with iranti_write or iranti_checkpoint.`;
|
|
2067
|
+
}
|
|
2068
|
+
else {
|
|
2069
|
+
this.consecutivePreResponseWithoutPost = 0;
|
|
2070
|
+
}
|
|
2071
|
+
this.lastAttendPhase = 'pre-response';
|
|
2072
|
+
}
|
|
2073
|
+
else {
|
|
2074
|
+
// No phase provided — fall back to heuristic counter
|
|
2075
|
+
if (this.attendsWithoutPersist >= 3) {
|
|
2076
|
+
complianceWarning = `COMPLIANCE: iranti_attend has been called ${this.attendsWithoutPersist} times since the last iranti_write or iranti_checkpoint. You are likely missing post-response attend calls and durable writes. Call iranti_attend(phase='post-response') after every response, then persist durable findings with iranti_write or iranti_checkpoint before the next turn.`;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
this.complianceUpdatedAt = new Date().toISOString();
|
|
2080
|
+
let compliance = this.buildComplianceState(this.complianceUpdatedAt);
|
|
2081
|
+
if (!complianceWarning && compliance.issues.some((issue) => issue.code === 'ignored_injected_memory')) {
|
|
2082
|
+
complianceWarning = ignoredMemoryWarning;
|
|
2083
|
+
}
|
|
855
2084
|
if (!this.brief) {
|
|
856
2085
|
const bootstrapTask = buildAttendBootstrapTask(latestMessage, currentContext);
|
|
857
|
-
await this.handshake({
|
|
2086
|
+
const bootstrapBrief = await this.handshake({
|
|
858
2087
|
task: bootstrapTask,
|
|
859
2088
|
recentMessages: buildAttendBootstrapMessages(latestMessage, currentContext),
|
|
2089
|
+
ledgerContext: input.ledgerContext,
|
|
860
2090
|
});
|
|
861
2091
|
bootstrap = {
|
|
862
2092
|
handshakePerformed: true,
|
|
863
2093
|
reason: 'no_existing_brief',
|
|
864
2094
|
task: bootstrapTask,
|
|
2095
|
+
operatingRules: bootstrapBrief.operatingRules,
|
|
2096
|
+
note: 'A handshake was auto-performed because no session brief existed. Read operatingRules now and follow any instructions there — including ACKNOWLEDGE — before replying to the user.',
|
|
865
2097
|
};
|
|
866
2098
|
}
|
|
867
2099
|
const effectiveEntityHints = this.resolveAttendEntityHints(input.entityHints, latestMessage);
|
|
2100
|
+
let watchedEntitiesChanged = this.updateWatchedEntities(effectiveEntityHints);
|
|
2101
|
+
const freshState = await this.detectRelevantFreshState(effectiveEntityHints, latestMessage);
|
|
868
2102
|
const observationContext = currentContext.trim().length > 0 ? currentContext : latestMessage;
|
|
869
2103
|
const mandatoryRecall = (0, autoRemember_1.detectMandatoryRecall)(latestMessage);
|
|
870
|
-
|
|
2104
|
+
if (mandatoryRecall.required && input.suppressEvents !== true) {
|
|
2105
|
+
(0, staffEventRegistry_1.getStaffEventEmitter)().emit({
|
|
2106
|
+
staffComponent: 'Attendant',
|
|
2107
|
+
actionType: 'mandatory_recall_forced',
|
|
2108
|
+
agentId: this.agentId,
|
|
2109
|
+
source: this.eventSource,
|
|
2110
|
+
reason: mandatoryRecall.reason ?? 'mandatory_recall_prompt',
|
|
2111
|
+
level: 'audit',
|
|
2112
|
+
metadata: this.buildEventMetadata({
|
|
2113
|
+
key: mandatoryRecall.key ?? null,
|
|
2114
|
+
latestMessage: latestMessage.slice(0, 160),
|
|
2115
|
+
}),
|
|
2116
|
+
});
|
|
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
|
+
}
|
|
2164
|
+
let decision = await this.decideMemoryNeed({
|
|
871
2165
|
currentContext,
|
|
872
2166
|
latestMessage,
|
|
873
2167
|
forceInject,
|
|
2168
|
+
entityHintCount: effectiveEntityHints.length,
|
|
874
2169
|
});
|
|
2170
|
+
if (freshState.hasFreshState && !decision.needed) {
|
|
2171
|
+
decision = {
|
|
2172
|
+
needed: true,
|
|
2173
|
+
confidence: 0.92,
|
|
2174
|
+
method: 'heuristic',
|
|
2175
|
+
explanation: 'relevant_shared_state_changed',
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
875
2178
|
if (!decision.needed) {
|
|
876
2179
|
if (input.suppressEvents !== true) {
|
|
877
2180
|
(0, staffEventRegistry_1.getStaffEventEmitter)().emit({
|
|
878
2181
|
staffComponent: 'Attendant',
|
|
879
2182
|
actionType: 'attend_completed',
|
|
880
2183
|
agentId: this.agentId,
|
|
881
|
-
source:
|
|
882
|
-
reason:
|
|
883
|
-
level: '
|
|
884
|
-
metadata: {
|
|
2184
|
+
source: this.eventSource,
|
|
2185
|
+
reason: 'memory_not_injected',
|
|
2186
|
+
level: 'audit',
|
|
2187
|
+
metadata: this.buildEventMetadata({
|
|
885
2188
|
contextCallCount: this.contextCallCount,
|
|
886
|
-
sessionId: this.sessionStarted,
|
|
887
2189
|
shouldInject: false,
|
|
888
2190
|
attendReason: 'memory_not_needed',
|
|
889
|
-
},
|
|
2191
|
+
}),
|
|
2192
|
+
});
|
|
2193
|
+
(0, staffEventRegistry_1.getStaffEventEmitter)().emit({
|
|
2194
|
+
staffComponent: 'Attendant',
|
|
2195
|
+
actionType: 'memory_not_injected',
|
|
2196
|
+
agentId: this.agentId,
|
|
2197
|
+
source: this.eventSource,
|
|
2198
|
+
reason: 'memory_not_needed',
|
|
2199
|
+
level: 'audit',
|
|
2200
|
+
metadata: this.buildEventMetadata({
|
|
2201
|
+
shouldInject: false,
|
|
2202
|
+
factCount: 0,
|
|
2203
|
+
injectedKeys: [],
|
|
2204
|
+
}),
|
|
890
2205
|
});
|
|
891
2206
|
}
|
|
2207
|
+
if (this.brief) {
|
|
2208
|
+
this.brief = {
|
|
2209
|
+
...this.brief,
|
|
2210
|
+
compliance,
|
|
2211
|
+
briefGeneratedAt: this.complianceUpdatedAt,
|
|
2212
|
+
};
|
|
2213
|
+
await this.persistState();
|
|
2214
|
+
}
|
|
892
2215
|
(0, metrics_1.timeEnd)('attendant.attend_ms', t0);
|
|
893
2216
|
return {
|
|
894
2217
|
shouldInject: false,
|
|
895
2218
|
reason: 'memory_not_needed',
|
|
896
2219
|
decision,
|
|
897
2220
|
bootstrap,
|
|
2221
|
+
complianceWarning,
|
|
2222
|
+
compliance,
|
|
2223
|
+
memoryAttributions: [],
|
|
2224
|
+
usageGuidance: buildUsageGuidance('attend'),
|
|
898
2225
|
facts: [],
|
|
899
2226
|
entitiesDetected: [],
|
|
900
2227
|
alreadyPresent: 0,
|
|
@@ -912,55 +2239,145 @@ class AttendantInstance {
|
|
|
912
2239
|
},
|
|
913
2240
|
};
|
|
914
2241
|
}
|
|
2242
|
+
const observeEntityHints = effectiveEntityHints.length > 0 ? effectiveEntityHints : freshState.entities;
|
|
915
2243
|
const observed = await this.observe({
|
|
916
2244
|
currentContext: observationContext,
|
|
917
2245
|
maxFacts: input.maxFacts,
|
|
918
|
-
entityHints:
|
|
919
|
-
priorityKeys:
|
|
2246
|
+
entityHints: observeEntityHints,
|
|
2247
|
+
priorityKeys: expandContinuityPriorityKeys(Array.from(new Set([
|
|
2248
|
+
...(mandatoryRecall.key ? [mandatoryRecall.key] : []),
|
|
2249
|
+
...(this.advisoryLearningProfile?.priorityKeys ?? []),
|
|
2250
|
+
...freshState.priorityKeys,
|
|
2251
|
+
]))),
|
|
2252
|
+
skipContextFilter: forceInject,
|
|
2253
|
+
ledgerContext: input.ledgerContext,
|
|
920
2254
|
});
|
|
2255
|
+
// Remap facts from personal recall fallback entities to the canonical personal entity.
|
|
2256
|
+
// E.g. if person/user is a legacy alias for user/main, surface facts as user/main/<key>.
|
|
2257
|
+
const canonicalPersonalEntity = (0, autoRemember_1.getPersonalMemoryEntity)();
|
|
2258
|
+
const personalFallbacks = new Set((0, autoRemember_1.getPersonalRecallEntities)().filter((e) => e !== canonicalPersonalEntity));
|
|
2259
|
+
const [canonicalPersonalType, canonicalPersonalId] = canonicalPersonalEntity.split('/', 2);
|
|
2260
|
+
const remappedFacts = personalFallbacks.size === 0 ? observed.facts : observed.facts.map((fact) => {
|
|
2261
|
+
const slashIdx2 = fact.entityKey.indexOf('/', fact.entityKey.indexOf('/') + 1);
|
|
2262
|
+
const entityPath = slashIdx2 === -1 ? fact.entityKey : fact.entityKey.slice(0, slashIdx2);
|
|
2263
|
+
if (!personalFallbacks.has(entityPath))
|
|
2264
|
+
return fact;
|
|
2265
|
+
const remainder = slashIdx2 === -1 ? '' : fact.entityKey.slice(slashIdx2);
|
|
2266
|
+
return { ...fact, entityKey: `${canonicalPersonalType}/${canonicalPersonalId}${remainder}` };
|
|
2267
|
+
});
|
|
2268
|
+
const structuredFacts = (0, hostMemoryFormatting_1.assignStructuredFactIds)(remappedFacts);
|
|
2269
|
+
watchedEntitiesChanged = this.updateWatchedEntities(observed.entitiesResolved?.map((entry) => entry.canonicalEntity) ?? []) || watchedEntitiesChanged;
|
|
2270
|
+
this.markSharedStateObserved(observeEntityHints.length > 0 ? observeEntityHints : freshState.entities);
|
|
921
2271
|
let reason = 'memory_needed_injected';
|
|
922
|
-
const shouldInject =
|
|
2272
|
+
const shouldInject = structuredFacts.length > 0;
|
|
2273
|
+
let searchSuggestion;
|
|
923
2274
|
if (!shouldInject) {
|
|
924
2275
|
const allAlreadyInContext = observed.totalFound > 0 && observed.alreadyPresent >= observed.totalFound;
|
|
925
2276
|
reason = allAlreadyInContext ? 'memory_needed_but_in_context' : 'memory_needed_no_facts';
|
|
2277
|
+
if (reason === 'memory_needed_no_facts') {
|
|
2278
|
+
const terms = tokenize(latestMessage).slice(0, 6);
|
|
2279
|
+
const alternativeEntities = (observed.entitiesResolved ?? [])
|
|
2280
|
+
.map((e) => e.canonicalEntity)
|
|
2281
|
+
.filter(Boolean);
|
|
2282
|
+
searchSuggestion = {
|
|
2283
|
+
hint: terms.length > 0
|
|
2284
|
+
? `No facts found via attend. Try iranti_search with: ${terms.join(' ')}`
|
|
2285
|
+
: 'No facts found via attend. Try iranti_search with a different query or entity path.',
|
|
2286
|
+
suggestedTerms: terms,
|
|
2287
|
+
alternativeEntities,
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
926
2290
|
}
|
|
927
2291
|
else if (forceInject) {
|
|
928
2292
|
reason = 'forced';
|
|
929
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
|
+
: [];
|
|
930
2305
|
const attendResult = {
|
|
931
2306
|
...observed,
|
|
932
|
-
|
|
2307
|
+
facts: structuredFacts,
|
|
2308
|
+
shouldInject: structuredFacts.length > 0,
|
|
933
2309
|
reason,
|
|
934
2310
|
decision,
|
|
935
2311
|
bootstrap,
|
|
2312
|
+
searchSuggestion,
|
|
2313
|
+
complianceWarning,
|
|
2314
|
+
compliance,
|
|
2315
|
+
memoryAttributions,
|
|
2316
|
+
usageGuidance: buildUsageGuidance('attend'),
|
|
936
2317
|
};
|
|
937
2318
|
if (input.suppressEvents !== true) {
|
|
938
2319
|
(0, staffEventRegistry_1.getStaffEventEmitter)().emit({
|
|
939
2320
|
staffComponent: 'Attendant',
|
|
940
2321
|
actionType: 'attend_completed',
|
|
941
2322
|
agentId: this.agentId,
|
|
942
|
-
source:
|
|
943
|
-
reason:
|
|
944
|
-
level: '
|
|
945
|
-
metadata: {
|
|
2323
|
+
source: this.eventSource,
|
|
2324
|
+
reason: shouldInject ? 'memory_injected' : 'memory_not_injected',
|
|
2325
|
+
level: 'audit',
|
|
2326
|
+
metadata: this.buildEventMetadata({
|
|
946
2327
|
contextCallCount: this.contextCallCount,
|
|
947
|
-
sessionId: this.sessionStarted,
|
|
948
2328
|
shouldInject,
|
|
949
2329
|
attendReason: reason,
|
|
950
|
-
|
|
2330
|
+
injectionId: memoryAttributions[0]?.injectionId ?? null,
|
|
2331
|
+
advisoryDecisionMethod: decision.method,
|
|
2332
|
+
advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
|
|
2333
|
+
freshStateEntities: freshState.entities,
|
|
2334
|
+
freshStateKeys: freshState.priorityKeys,
|
|
2335
|
+
}),
|
|
2336
|
+
});
|
|
2337
|
+
(0, staffEventRegistry_1.getStaffEventEmitter)().emit({
|
|
2338
|
+
staffComponent: 'Attendant',
|
|
2339
|
+
actionType: shouldInject ? 'memory_injected' : 'memory_not_injected',
|
|
2340
|
+
agentId: this.agentId,
|
|
2341
|
+
source: this.eventSource,
|
|
2342
|
+
reason,
|
|
2343
|
+
level: 'audit',
|
|
2344
|
+
metadata: this.buildEventMetadata({
|
|
2345
|
+
shouldInject,
|
|
2346
|
+
factCount: observed.facts.length,
|
|
2347
|
+
injectionId: memoryAttributions[0]?.injectionId ?? null,
|
|
2348
|
+
injectedKeys: observed.facts.map((fact) => fact.entityKey),
|
|
2349
|
+
injectedEntryIds: observed.facts
|
|
2350
|
+
.map((fact) => fact.knowledgeEntryId)
|
|
2351
|
+
.filter((value) => typeof value === 'number'),
|
|
2352
|
+
entitiesResolved: observed.entitiesResolved?.map((entry) => entry.canonicalEntity) ?? [],
|
|
2353
|
+
alreadyPresent: observed.alreadyPresent,
|
|
2354
|
+
totalFound: observed.totalFound,
|
|
2355
|
+
advisoryPriorityKeys: this.advisoryLearningProfile?.priorityKeys ?? [],
|
|
2356
|
+
freshStateEntities: freshState.entities,
|
|
2357
|
+
freshStateKeys: freshState.priorityKeys,
|
|
2358
|
+
}),
|
|
951
2359
|
});
|
|
952
2360
|
}
|
|
2361
|
+
if (this.brief) {
|
|
2362
|
+
this.brief = {
|
|
2363
|
+
...this.brief,
|
|
2364
|
+
compliance,
|
|
2365
|
+
briefGeneratedAt: this.complianceUpdatedAt,
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
if (watchedEntitiesChanged || this.brief?.compliance !== compliance || memoryAttributions.length > 0) {
|
|
2369
|
+
await this.persistState();
|
|
2370
|
+
}
|
|
953
2371
|
(0, metrics_1.timeEnd)('attendant.attend_ms', t0);
|
|
954
2372
|
return attendResult;
|
|
955
2373
|
}
|
|
956
2374
|
// Context Window Observation
|
|
957
2375
|
async observe(input) {
|
|
958
2376
|
const t0 = (0, metrics_1.timeStart)();
|
|
2377
|
+
this.setLedgerContext(input.ledgerContext);
|
|
959
2378
|
const maxFacts = input.maxFacts ?? 5;
|
|
960
2379
|
const currentContext = input.currentContext ?? '';
|
|
961
|
-
const entityHints =
|
|
962
|
-
? input.entityHints.filter((hint) => typeof hint === 'string' && hint.trim().length > 0)
|
|
963
|
-
: [];
|
|
2380
|
+
const entityHints = this.resolveObserveEntityHints(input.entityHints, currentContext);
|
|
964
2381
|
const requestedPriorityKeys = Array.isArray(input.priorityKeys)
|
|
965
2382
|
? input.priorityKeys
|
|
966
2383
|
.filter((key) => typeof key === 'string' && key.trim().length > 0)
|
|
@@ -971,13 +2388,12 @@ class AttendantInstance {
|
|
|
971
2388
|
staffComponent: 'Attendant',
|
|
972
2389
|
actionType: 'observe_completed',
|
|
973
2390
|
agentId: this.agentId,
|
|
974
|
-
source:
|
|
975
|
-
reason:
|
|
976
|
-
level: '
|
|
977
|
-
metadata: {
|
|
2391
|
+
source: this.eventSource,
|
|
2392
|
+
reason: 'no_observation_context',
|
|
2393
|
+
level: 'audit',
|
|
2394
|
+
metadata: this.buildEventMetadata({
|
|
978
2395
|
observeType: 'empty_context',
|
|
979
|
-
|
|
980
|
-
},
|
|
2396
|
+
}),
|
|
981
2397
|
});
|
|
982
2398
|
(0, metrics_1.timeEnd)('attendant.observe_ms', t0);
|
|
983
2399
|
return {
|
|
@@ -985,6 +2401,7 @@ class AttendantInstance {
|
|
|
985
2401
|
entitiesDetected: [],
|
|
986
2402
|
alreadyPresent: 0,
|
|
987
2403
|
totalFound: 0,
|
|
2404
|
+
usageGuidance: buildUsageGuidance('observe'),
|
|
988
2405
|
entitiesResolved: [],
|
|
989
2406
|
debug: {
|
|
990
2407
|
skipped: 'empty_context',
|
|
@@ -1120,13 +2537,12 @@ ${detectionWindow}`,
|
|
|
1120
2537
|
staffComponent: 'Attendant',
|
|
1121
2538
|
actionType: 'observe_completed',
|
|
1122
2539
|
agentId: this.agentId,
|
|
1123
|
-
source:
|
|
1124
|
-
reason:
|
|
1125
|
-
level: '
|
|
1126
|
-
metadata: {
|
|
2540
|
+
source: this.eventSource,
|
|
2541
|
+
reason: 'no_entity_candidates',
|
|
2542
|
+
level: 'audit',
|
|
2543
|
+
metadata: this.buildEventMetadata({
|
|
1127
2544
|
observeType: 'no_candidates',
|
|
1128
|
-
|
|
1129
|
-
},
|
|
2545
|
+
}),
|
|
1130
2546
|
});
|
|
1131
2547
|
(0, metrics_1.timeEnd)('attendant.observe_ms', t0);
|
|
1132
2548
|
return {
|
|
@@ -1134,6 +2550,7 @@ ${detectionWindow}`,
|
|
|
1134
2550
|
entitiesDetected: [],
|
|
1135
2551
|
alreadyPresent: 0,
|
|
1136
2552
|
totalFound: 0,
|
|
2553
|
+
usageGuidance: buildUsageGuidance('observe'),
|
|
1137
2554
|
entitiesResolved: [],
|
|
1138
2555
|
debug: {
|
|
1139
2556
|
contextLength: currentContext.length,
|
|
@@ -1228,37 +2645,51 @@ ${detectionWindow}`,
|
|
|
1228
2645
|
const allEntries = await (0, queries_1.findEntriesByEntity)(resolvedInfo.entityType, resolvedInfo.entityId);
|
|
1229
2646
|
// Priority keys first
|
|
1230
2647
|
const policyPriorityKeys = policy.observeKeyPriority?.[resolvedInfo.entityType] ?? [];
|
|
1231
|
-
const priorityKeys = new Set([...policyPriorityKeys, ...requestedPriorityKeys]);
|
|
2648
|
+
const priorityKeys = new Set(expandContinuityPriorityKeys([...policyPriorityKeys, ...requestedPriorityKeys]));
|
|
1232
2649
|
const priorityEntries = allEntries.filter((e) => priorityKeys.has(e.key));
|
|
1233
2650
|
const remainingEntries = allEntries
|
|
1234
2651
|
.filter((e) => !priorityKeys.has(e.key))
|
|
1235
|
-
.sort((a, b) =>
|
|
2652
|
+
.sort((a, b) => {
|
|
2653
|
+
const checkpointPenalty = (entryKey) => entryKey.startsWith('checkpoint_') ? 1 : 0;
|
|
2654
|
+
return (checkpointPenalty(a.key) - checkpointPenalty(b.key)
|
|
2655
|
+
|| b.confidence - a.confidence
|
|
2656
|
+
|| a.key.localeCompare(b.key));
|
|
2657
|
+
});
|
|
1236
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
|
+
}
|
|
1237
2665
|
for (const entry of selectedEntries) {
|
|
1238
2666
|
allFacts.push({
|
|
1239
|
-
entityKey: `${resolvedInfo.entityType}/${resolvedInfo.entityId}/${entry.key}`,
|
|
2667
|
+
entityKey: `${resolvedInfo.entityType}/${resolvedInfo.entityId}/${normalizeContinuityKey(entry.key)}`,
|
|
1240
2668
|
summary: entry.valueSummary,
|
|
1241
2669
|
value: entry.valueRaw,
|
|
1242
2670
|
confidence: entry.confidence,
|
|
1243
2671
|
source: entry.source,
|
|
2672
|
+
lastUpdated: entry.updatedAt.toISOString(),
|
|
1244
2673
|
entryId: entry.id,
|
|
1245
2674
|
});
|
|
1246
2675
|
}
|
|
1247
2676
|
}
|
|
1248
|
-
// Step 3 — filter out facts already present in context
|
|
2677
|
+
// Step 3 — filter out facts already present in context (skipped when forceInject)
|
|
1249
2678
|
const contextLower = currentContext.toLowerCase();
|
|
1250
2679
|
let alreadyPresent = 0;
|
|
1251
2680
|
const newFacts = [];
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
2681
|
+
if (input.skipContextFilter) {
|
|
2682
|
+
newFacts.push(...allFacts);
|
|
2683
|
+
}
|
|
2684
|
+
else {
|
|
2685
|
+
for (const fact of allFacts) {
|
|
2686
|
+
const alreadyInContext = factAlreadyPresentInContext(contextLower, fact);
|
|
2687
|
+
if (alreadyInContext) {
|
|
2688
|
+
alreadyPresent++;
|
|
2689
|
+
}
|
|
2690
|
+
else {
|
|
2691
|
+
newFacts.push(fact);
|
|
2692
|
+
}
|
|
1262
2693
|
}
|
|
1263
2694
|
}
|
|
1264
2695
|
// Step 4 — return top facts by confidence
|
|
@@ -1270,27 +2701,29 @@ ${detectionWindow}`,
|
|
|
1270
2701
|
staffComponent: 'Attendant',
|
|
1271
2702
|
actionType: 'observe_completed',
|
|
1272
2703
|
agentId: this.agentId,
|
|
1273
|
-
source:
|
|
1274
|
-
reason:
|
|
1275
|
-
level: '
|
|
1276
|
-
metadata: {
|
|
2704
|
+
source: this.eventSource,
|
|
2705
|
+
reason: topFacts.length > 0 ? 'facts_retrieved' : 'no_new_facts',
|
|
2706
|
+
level: 'audit',
|
|
2707
|
+
metadata: this.buildEventMetadata({
|
|
1277
2708
|
observeType: 'facts_retrieved',
|
|
1278
2709
|
factsCount: topFacts.length,
|
|
1279
|
-
|
|
1280
|
-
},
|
|
2710
|
+
}),
|
|
1281
2711
|
});
|
|
1282
2712
|
(0, metrics_1.timeEnd)('attendant.observe_ms', t0);
|
|
1283
2713
|
return {
|
|
1284
|
-
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,
|
|
1285
2716
|
entityKey,
|
|
1286
2717
|
summary,
|
|
1287
2718
|
value,
|
|
1288
2719
|
confidence,
|
|
1289
2720
|
source,
|
|
1290
|
-
|
|
2721
|
+
lastUpdated,
|
|
2722
|
+
}))),
|
|
1291
2723
|
entitiesDetected: Array.from(entitiesDetected),
|
|
1292
2724
|
alreadyPresent,
|
|
1293
2725
|
totalFound: allFacts.length,
|
|
2726
|
+
usageGuidance: buildUsageGuidance('observe'),
|
|
1294
2727
|
entitiesResolved,
|
|
1295
2728
|
debug: {
|
|
1296
2729
|
contextLength: currentContext.length,
|
|
@@ -1339,6 +2772,10 @@ ${detectionWindow}`,
|
|
|
1339
2772
|
explanation: heuristic.explanation,
|
|
1340
2773
|
};
|
|
1341
2774
|
}
|
|
2775
|
+
const advisoryDecision = this.buildAdvisoryMemoryDecision(input.latestMessage);
|
|
2776
|
+
if (advisoryDecision) {
|
|
2777
|
+
return advisoryDecision;
|
|
2778
|
+
}
|
|
1342
2779
|
const contextWindow = input.currentContext.length <= MEMORY_DECISION_CONTEXT_WINDOW_CHARS
|
|
1343
2780
|
? input.currentContext
|
|
1344
2781
|
: input.currentContext.slice(-MEMORY_DECISION_CONTEXT_WINDOW_CHARS);
|
|
@@ -1357,8 +2794,9 @@ Return ONLY valid JSON with this exact shape:
|
|
|
1357
2794
|
{"needsMemory":true,"confidence":0.81,"reason":"short_reason"}
|
|
1358
2795
|
|
|
1359
2796
|
Rules:
|
|
1360
|
-
- needsMemory=true when the
|
|
1361
|
-
- needsMemory=
|
|
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.
|
|
1362
2800
|
- confidence is a float from 0 to 1.`,
|
|
1363
2801
|
},
|
|
1364
2802
|
], 128);
|
|
@@ -1371,6 +2809,61 @@ Rules:
|
|
|
1371
2809
|
explanation: parsed.reason,
|
|
1372
2810
|
};
|
|
1373
2811
|
}
|
|
2812
|
+
return this.buildParseFailureFallbackDecision(input);
|
|
2813
|
+
}
|
|
2814
|
+
buildParseFailureFallbackDecision(input) {
|
|
2815
|
+
const normalized = normalizeMessage(input.latestMessage);
|
|
2816
|
+
if (!normalized || MEMORY_NEED_NEGATIVE_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
2817
|
+
return {
|
|
2818
|
+
needed: false,
|
|
2819
|
+
confidence: 0.5,
|
|
2820
|
+
method: 'heuristic',
|
|
2821
|
+
explanation: 'classification_parse_failed_default_false',
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
// Re-run the heuristic against the strengthened positive patterns. Since the heuristic
|
|
2825
|
+
// was already called in decideMemoryNeed() and returned null (ambiguous), we reach here
|
|
2826
|
+
// only for very short messages (≤20 chars) with no prior pattern matches. The updated
|
|
2827
|
+
// MEMORY_NEED_POSITIVE_PATTERNS now catch most imperative and technical cues, so a second
|
|
2828
|
+
// pass here picks up any stragglers added after the initial call.
|
|
2829
|
+
const heuristicResult = heuristicMemoryNeed(input.latestMessage);
|
|
2830
|
+
if (heuristicResult.needed !== null) {
|
|
2831
|
+
return {
|
|
2832
|
+
needed: heuristicResult.needed,
|
|
2833
|
+
confidence: heuristicResult.confidence,
|
|
2834
|
+
method: 'heuristic',
|
|
2835
|
+
explanation: heuristicResult.needed
|
|
2836
|
+
? 'classification_parse_failed_heuristic_true'
|
|
2837
|
+
: 'classification_parse_failed_heuristic_false',
|
|
2838
|
+
};
|
|
2839
|
+
}
|
|
2840
|
+
const substantivePrompt = normalized.length >= 12
|
|
2841
|
+
|| normalized.split(/\s+/).filter(Boolean).length >= 3
|
|
2842
|
+
|| /[?]$/.test(normalized);
|
|
2843
|
+
const hasScopedContext = input.entityHintCount > 0 || input.currentContext.trim().length > 0;
|
|
2844
|
+
const hasProjectStateCue = MEMORY_PARSE_FAILURE_PROJECT_CUE_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
2845
|
+
if (substantivePrompt && (hasScopedContext || hasProjectStateCue)) {
|
|
2846
|
+
return {
|
|
2847
|
+
needed: true,
|
|
2848
|
+
confidence: 0.55,
|
|
2849
|
+
method: 'heuristic',
|
|
2850
|
+
explanation: 'classification_parse_failed_default_true',
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
// Final safety net: for any non-trivial message (≥5 chars) where the LLM parse failed,
|
|
2854
|
+
// default to injecting memory rather than silently skipping. A false positive (unnecessary
|
|
2855
|
+
// injection) is far cheaper than a false negative (missing context causes wrong output).
|
|
2856
|
+
// Only true single-word acks/greetings — already caught by MEMORY_NEED_NEGATIVE_PATTERNS
|
|
2857
|
+
// above — should reach this point with a truly empty normalized form; everything else
|
|
2858
|
+
// gets injection.
|
|
2859
|
+
if (normalized.length > 0) {
|
|
2860
|
+
return {
|
|
2861
|
+
needed: true,
|
|
2862
|
+
confidence: 0.5,
|
|
2863
|
+
method: 'heuristic',
|
|
2864
|
+
explanation: 'classification_parse_failed_safe_default_true',
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
1374
2867
|
return {
|
|
1375
2868
|
needed: false,
|
|
1376
2869
|
confidence: 0.5,
|
|
@@ -1378,11 +2871,37 @@ Rules:
|
|
|
1378
2871
|
explanation: 'classification_parse_failed_default_false',
|
|
1379
2872
|
};
|
|
1380
2873
|
}
|
|
2874
|
+
buildAdvisoryMemoryDecision(latestMessage) {
|
|
2875
|
+
const profile = this.advisoryLearningProfile;
|
|
2876
|
+
const normalized = normalizeMessage(latestMessage);
|
|
2877
|
+
if (!profile?.preferMemoryForAmbiguousTurns || !normalized) {
|
|
2878
|
+
return null;
|
|
2879
|
+
}
|
|
2880
|
+
if (MEMORY_NEED_NEGATIVE_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
2881
|
+
return null;
|
|
2882
|
+
}
|
|
2883
|
+
if (normalized.length < 5) {
|
|
2884
|
+
return null;
|
|
2885
|
+
}
|
|
2886
|
+
if (!messageHasAdvisoryCue(normalized, profile.matchedTaskType ?? this.brief?.inferredTaskType ?? null)) {
|
|
2887
|
+
return null;
|
|
2888
|
+
}
|
|
2889
|
+
return {
|
|
2890
|
+
needed: true,
|
|
2891
|
+
confidence: profile.scopesUsed.includes('task') ? 0.78 : 0.7,
|
|
2892
|
+
method: 'advisory',
|
|
2893
|
+
explanation: `advisory_${profile.scopesUsed[0] ?? 'global'}_learning`,
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
1381
2896
|
resolveAttendEntityHints(entityHints, latestMessage) {
|
|
1382
2897
|
const explicit = Array.isArray(entityHints)
|
|
1383
2898
|
? entityHints.filter((hint) => typeof hint === 'string' && hint.trim().length > 0)
|
|
1384
2899
|
: [];
|
|
2900
|
+
const explicitFromMessage = extractExactEntityReferences(latestMessage);
|
|
1385
2901
|
const scope = (0, autoRemember_1.classifyMemoryScope)(latestMessage);
|
|
2902
|
+
if (explicitFromMessage.length > 0) {
|
|
2903
|
+
return explicitFromMessage;
|
|
2904
|
+
}
|
|
1386
2905
|
if (explicit.length > 0) {
|
|
1387
2906
|
return explicit;
|
|
1388
2907
|
}
|
|
@@ -1397,6 +2916,215 @@ Rules:
|
|
|
1397
2916
|
}
|
|
1398
2917
|
return [];
|
|
1399
2918
|
}
|
|
2919
|
+
resolveObserveEntityHints(entityHints, currentContext) {
|
|
2920
|
+
const explicit = Array.isArray(entityHints)
|
|
2921
|
+
? entityHints.filter((hint) => typeof hint === 'string' && hint.trim().length > 0)
|
|
2922
|
+
: [];
|
|
2923
|
+
if (explicit.length > 0) {
|
|
2924
|
+
return explicit;
|
|
2925
|
+
}
|
|
2926
|
+
const explicitFromContext = extractExactEntityReferences(currentContext);
|
|
2927
|
+
if (explicitFromContext.length > 0) {
|
|
2928
|
+
return explicitFromContext;
|
|
2929
|
+
}
|
|
2930
|
+
const scope = (0, autoRemember_1.classifyMemoryScope)(currentContext);
|
|
2931
|
+
if (scope && this.brief) {
|
|
2932
|
+
const watched = normalizeWatchedEntities([
|
|
2933
|
+
...(this.brief?.watchedEntities ?? []),
|
|
2934
|
+
...(this.sessionCheckpoint?.checkpoint.entityTargets ?? []),
|
|
2935
|
+
]);
|
|
2936
|
+
if (watched.length > 0) {
|
|
2937
|
+
return watched;
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
if (scope === 'personal') {
|
|
2941
|
+
return (0, autoRemember_1.getPersonalRecallEntities)();
|
|
2942
|
+
}
|
|
2943
|
+
if (scope === 'project') {
|
|
2944
|
+
const configured = (0, autoRemember_1.getProjectMemoryEntity)();
|
|
2945
|
+
if (configured) {
|
|
2946
|
+
return [configured];
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
return [];
|
|
2950
|
+
}
|
|
2951
|
+
async detectRelevantFreshState(entityHints, latestMessage) {
|
|
2952
|
+
const pending = this.consumePendingFreshState(entityHints, latestMessage);
|
|
2953
|
+
if (pending.hasFreshState) {
|
|
2954
|
+
return pending;
|
|
2955
|
+
}
|
|
2956
|
+
const sinceRaw = this.sharedStateObservedAt?.trim() || this.brief?.briefGeneratedAt?.trim();
|
|
2957
|
+
if (!sinceRaw) {
|
|
2958
|
+
return { hasFreshState: false, priorityKeys: [], entities: [] };
|
|
2959
|
+
}
|
|
2960
|
+
const since = new Date(sinceRaw);
|
|
2961
|
+
if (Number.isNaN(since.getTime())) {
|
|
2962
|
+
return { hasFreshState: false, priorityKeys: [], entities: [] };
|
|
2963
|
+
}
|
|
2964
|
+
const targetHints = entityHints.length > 0
|
|
2965
|
+
? entityHints
|
|
2966
|
+
: shouldUseWatchedEntitiesForPrompt(latestMessage)
|
|
2967
|
+
? (this.brief?.watchedEntities ?? [])
|
|
2968
|
+
: [];
|
|
2969
|
+
if (targetHints.length === 0) {
|
|
2970
|
+
return { hasFreshState: false, priorityKeys: [], entities: [] };
|
|
2971
|
+
}
|
|
2972
|
+
const resolvedEntities = await this.expandRelevantFreshTargets(targetHints.slice(0, 5));
|
|
2973
|
+
const entities = [];
|
|
2974
|
+
const priorityKeys = [];
|
|
2975
|
+
const seenKeys = new Set();
|
|
2976
|
+
for (const [canonicalEntity, resolved] of resolvedEntities) {
|
|
2977
|
+
const entries = await (0, queries_1.findEntriesByEntity)(resolved.entityType, resolved.entityId);
|
|
2978
|
+
const freshEntries = entries
|
|
2979
|
+
.filter((entry) => entry.updatedAt > since)
|
|
2980
|
+
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
2981
|
+
if (freshEntries.length === 0)
|
|
2982
|
+
continue;
|
|
2983
|
+
entities.push(canonicalEntity);
|
|
2984
|
+
for (const entry of freshEntries) {
|
|
2985
|
+
if (seenKeys.has(entry.key))
|
|
2986
|
+
continue;
|
|
2987
|
+
seenKeys.add(entry.key);
|
|
2988
|
+
priorityKeys.push(entry.key);
|
|
2989
|
+
if (priorityKeys.length >= 8) {
|
|
2990
|
+
break;
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
if (priorityKeys.length >= 8) {
|
|
2994
|
+
break;
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
return {
|
|
2998
|
+
hasFreshState: entities.length > 0,
|
|
2999
|
+
priorityKeys,
|
|
3000
|
+
entities,
|
|
3001
|
+
};
|
|
3002
|
+
}
|
|
3003
|
+
async expandRelevantFreshTargets(targetHints) {
|
|
3004
|
+
const resolvedEntities = new Map();
|
|
3005
|
+
for (const hint of targetHints) {
|
|
3006
|
+
const resolved = await this.resolveFreshEntityTarget(hint);
|
|
3007
|
+
if (!resolved)
|
|
3008
|
+
continue;
|
|
3009
|
+
resolvedEntities.set(resolved.canonicalEntity, resolved);
|
|
3010
|
+
const related = await (0, relationships_1.getRelated)(resolved.entityType, resolved.entityId);
|
|
3011
|
+
for (const neighbor of related.slice(0, 6)) {
|
|
3012
|
+
const relatedEntity = `${neighbor.entityType}/${neighbor.entityId}`;
|
|
3013
|
+
const relatedResolved = await this.resolveFreshEntityTarget(relatedEntity);
|
|
3014
|
+
if (!relatedResolved)
|
|
3015
|
+
continue;
|
|
3016
|
+
resolvedEntities.set(relatedResolved.canonicalEntity, relatedResolved);
|
|
3017
|
+
if (resolvedEntities.size >= 12) {
|
|
3018
|
+
return resolvedEntities;
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
return resolvedEntities;
|
|
3023
|
+
}
|
|
3024
|
+
async resolveFreshEntityTarget(hint) {
|
|
3025
|
+
try {
|
|
3026
|
+
const parsed = (0, entity_resolution_1.parseEntityString)(hint);
|
|
3027
|
+
try {
|
|
3028
|
+
const resolved = await (0, entity_resolution_1.resolveEntity)({
|
|
3029
|
+
entityType: parsed.entityType,
|
|
3030
|
+
entityId: parsed.entityId,
|
|
3031
|
+
rawName: hint,
|
|
3032
|
+
aliases: [hint, parsed.entityId],
|
|
3033
|
+
source: 'attend_refresh',
|
|
3034
|
+
confidence: 100,
|
|
3035
|
+
createIfMissing: false,
|
|
3036
|
+
});
|
|
3037
|
+
return {
|
|
3038
|
+
canonicalEntity: resolved.canonicalEntity,
|
|
3039
|
+
entityType: resolved.entityType,
|
|
3040
|
+
entityId: resolved.entityId,
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
catch {
|
|
3044
|
+
return {
|
|
3045
|
+
canonicalEntity: `${parsed.entityType}/${parsed.entityId}`,
|
|
3046
|
+
entityType: parsed.entityType,
|
|
3047
|
+
entityId: parsed.entityId,
|
|
3048
|
+
};
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
catch {
|
|
3052
|
+
return null;
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
updateWatchedEntities(candidates) {
|
|
3056
|
+
if (!this.brief)
|
|
3057
|
+
return false;
|
|
3058
|
+
const next = normalizeWatchedEntities([
|
|
3059
|
+
...(this.brief.watchedEntities ?? []),
|
|
3060
|
+
...candidates,
|
|
3061
|
+
]);
|
|
3062
|
+
const changed = JSON.stringify(next) !== JSON.stringify(this.brief.watchedEntities ?? []);
|
|
3063
|
+
this.brief.watchedEntities = next;
|
|
3064
|
+
return changed;
|
|
3065
|
+
}
|
|
3066
|
+
isWatchingEntity(entity) {
|
|
3067
|
+
const normalized = entity.trim();
|
|
3068
|
+
if (!normalized)
|
|
3069
|
+
return false;
|
|
3070
|
+
if ((this.brief?.watchedEntities ?? []).includes(normalized)) {
|
|
3071
|
+
return true;
|
|
3072
|
+
}
|
|
3073
|
+
return (this.sessionCheckpoint?.checkpoint.entityTargets ?? []).includes(normalized);
|
|
3074
|
+
}
|
|
3075
|
+
notifySharedEntityUpdated(entity, key) {
|
|
3076
|
+
const normalizedEntity = entity.trim();
|
|
3077
|
+
const normalizedKey = key.trim();
|
|
3078
|
+
if (!normalizedEntity || !normalizedKey)
|
|
3079
|
+
return;
|
|
3080
|
+
if (!this.isWatchingEntity(normalizedEntity))
|
|
3081
|
+
return;
|
|
3082
|
+
const existing = this.pendingSharedStateInvalidations.get(normalizedEntity) ?? new Set();
|
|
3083
|
+
existing.add(normalizedKey);
|
|
3084
|
+
this.pendingSharedStateInvalidations.set(normalizedEntity, existing);
|
|
3085
|
+
}
|
|
3086
|
+
consumePendingFreshState(entityHints, latestMessage) {
|
|
3087
|
+
const targetHints = entityHints.length > 0
|
|
3088
|
+
? entityHints
|
|
3089
|
+
: shouldUseWatchedEntitiesForPrompt(latestMessage)
|
|
3090
|
+
? (this.brief?.watchedEntities ?? [])
|
|
3091
|
+
: [];
|
|
3092
|
+
if (targetHints.length === 0) {
|
|
3093
|
+
return { hasFreshState: false, priorityKeys: [], entities: [] };
|
|
3094
|
+
}
|
|
3095
|
+
const entities = [];
|
|
3096
|
+
const priorityKeys = [];
|
|
3097
|
+
const seenKeys = new Set();
|
|
3098
|
+
for (const entity of targetHints) {
|
|
3099
|
+
const keys = this.pendingSharedStateInvalidations.get(entity);
|
|
3100
|
+
if (!keys || keys.size === 0)
|
|
3101
|
+
continue;
|
|
3102
|
+
entities.push(entity);
|
|
3103
|
+
for (const key of keys) {
|
|
3104
|
+
if (seenKeys.has(key))
|
|
3105
|
+
continue;
|
|
3106
|
+
seenKeys.add(key);
|
|
3107
|
+
priorityKeys.push(key);
|
|
3108
|
+
if (priorityKeys.length >= 8)
|
|
3109
|
+
break;
|
|
3110
|
+
}
|
|
3111
|
+
if (priorityKeys.length >= 8)
|
|
3112
|
+
break;
|
|
3113
|
+
}
|
|
3114
|
+
return {
|
|
3115
|
+
hasFreshState: entities.length > 0,
|
|
3116
|
+
priorityKeys,
|
|
3117
|
+
entities,
|
|
3118
|
+
};
|
|
3119
|
+
}
|
|
3120
|
+
markSharedStateObserved(entities) {
|
|
3121
|
+
if (entities.length === 0)
|
|
3122
|
+
return;
|
|
3123
|
+
this.sharedStateObservedAt = new Date().toISOString();
|
|
3124
|
+
for (const entity of entities) {
|
|
3125
|
+
this.pendingSharedStateInvalidations.delete(entity);
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
1400
3128
|
parseMemoryDecision(raw) {
|
|
1401
3129
|
try {
|
|
1402
3130
|
const cleaned = raw.replace(/```json|```/g, '').trim();
|
|
@@ -1502,6 +3230,11 @@ If nothing is relevant, return: none`,
|
|
|
1502
3230
|
async persistState() {
|
|
1503
3231
|
if (!this.brief)
|
|
1504
3232
|
return;
|
|
3233
|
+
this.brief = {
|
|
3234
|
+
...this.brief,
|
|
3235
|
+
compliance: this.buildComplianceState(),
|
|
3236
|
+
pendingMemoryAttributions: this.pendingMemoryAttributions.map((entry) => ({ ...entry })),
|
|
3237
|
+
};
|
|
1505
3238
|
await (0, client_1.getDb)().knowledgeEntry.upsert({
|
|
1506
3239
|
where: {
|
|
1507
3240
|
entityType_entityId_key: {
|
|
@@ -1536,6 +3269,16 @@ If nothing is relevant, return: none`,
|
|
|
1536
3269
|
this.sessionStarted = state.sessionStarted;
|
|
1537
3270
|
this.contextCallCount = state.contextCallCount ?? 0;
|
|
1538
3271
|
this.sessionCheckpoint = state.sessionCheckpoint ?? null;
|
|
3272
|
+
this.advisoryLearningProfile = null;
|
|
3273
|
+
this.sharedStateObservedAt = state.briefGeneratedAt;
|
|
3274
|
+
this.attendsWithoutPersist = state.compliance?.counters.attendsWithoutPersist ?? 0;
|
|
3275
|
+
this.consecutivePreResponseWithoutPost = state.compliance?.counters.consecutivePreResponseWithoutPost ?? 0;
|
|
3276
|
+
this.consecutiveUnusedMemoryInjections = state.compliance?.counters.consecutiveUnusedMemoryInjections ?? 0;
|
|
3277
|
+
this.lastAttendPhase = state.compliance?.counters.lastAttendPhase ?? undefined;
|
|
3278
|
+
this.complianceUpdatedAt = state.compliance?.lastUpdated ?? state.briefGeneratedAt;
|
|
3279
|
+
this.pendingMemoryAttributions = Array.isArray(state.pendingMemoryAttributions)
|
|
3280
|
+
? state.pendingMemoryAttributions.map((entry) => ({ ...entry }))
|
|
3281
|
+
: [];
|
|
1539
3282
|
this.brief = state;
|
|
1540
3283
|
return state;
|
|
1541
3284
|
}
|