@psiclawops/hypermem 0.7.0 → 0.8.1
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/ARCHITECTURE.md +30 -38
- package/README.md +83 -35
- package/dist/background-indexer.d.ts +14 -3
- package/dist/background-indexer.d.ts.map +1 -1
- package/dist/background-indexer.js +126 -18
- package/dist/budget-policy.d.ts +22 -0
- package/dist/budget-policy.d.ts.map +1 -0
- package/dist/budget-policy.js +27 -0
- package/dist/cache.d.ts +11 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/compositor-utils.d.ts +31 -0
- package/dist/compositor-utils.d.ts.map +1 -0
- package/dist/compositor-utils.js +47 -0
- package/dist/compositor.d.ts +163 -1
- package/dist/compositor.d.ts.map +1 -1
- package/dist/compositor.js +862 -130
- package/dist/content-hash.d.ts +43 -0
- package/dist/content-hash.d.ts.map +1 -0
- package/dist/content-hash.js +75 -0
- package/dist/context-store.d.ts +54 -0
- package/dist/context-store.d.ts.map +1 -1
- package/dist/context-store.js +102 -0
- package/dist/contradiction-audit-store.d.ts +54 -0
- package/dist/contradiction-audit-store.d.ts.map +1 -0
- package/dist/contradiction-audit-store.js +88 -0
- package/dist/contradiction-resolution-policy.d.ts +21 -0
- package/dist/contradiction-resolution-policy.d.ts.map +1 -0
- package/dist/contradiction-resolution-policy.js +17 -0
- package/dist/degradation.d.ts +102 -0
- package/dist/degradation.d.ts.map +1 -0
- package/dist/degradation.js +141 -0
- package/dist/dreaming-promoter.d.ts +38 -0
- package/dist/dreaming-promoter.d.ts.map +1 -1
- package/dist/dreaming-promoter.js +68 -2
- package/dist/index.d.ts +68 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +402 -26
- package/dist/knowledge-lint.d.ts +2 -0
- package/dist/knowledge-lint.d.ts.map +1 -1
- package/dist/knowledge-lint.js +40 -1
- package/dist/library-schema.d.ts +7 -2
- package/dist/library-schema.d.ts.map +1 -1
- package/dist/library-schema.js +236 -1
- package/dist/message-store.d.ts +64 -1
- package/dist/message-store.d.ts.map +1 -1
- package/dist/message-store.js +137 -1
- package/dist/open-domain.js +1 -1
- package/dist/proactive-pass.d.ts +2 -2
- package/dist/proactive-pass.d.ts.map +1 -1
- package/dist/proactive-pass.js +66 -12
- package/dist/replay-recovery.d.ts +29 -0
- package/dist/replay-recovery.d.ts.map +1 -0
- package/dist/replay-recovery.js +82 -0
- package/dist/reranker.d.ts +95 -0
- package/dist/reranker.d.ts.map +1 -0
- package/dist/reranker.js +308 -0
- package/dist/schema.d.ts +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +46 -1
- package/dist/session-flusher.d.ts +2 -2
- package/dist/session-flusher.d.ts.map +1 -1
- package/dist/session-flusher.js +1 -1
- package/dist/temporal-store.js +2 -2
- package/dist/tool-artifact-store.d.ts +98 -0
- package/dist/tool-artifact-store.d.ts.map +1 -0
- package/dist/tool-artifact-store.js +244 -0
- package/dist/topic-detector.js +2 -2
- package/dist/topic-store.d.ts +6 -0
- package/dist/topic-store.d.ts.map +1 -1
- package/dist/topic-store.js +39 -0
- package/dist/types.d.ts +233 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-store.d.ts +2 -1
- package/dist/vector-store.d.ts.map +1 -1
- package/dist/vector-store.js +3 -0
- package/dist/version.d.ts +10 -10
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +10 -10
- package/package.json +6 -4
package/dist/schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,qBAAqB,
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAgJxC;;GAEG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA0K9C;AAED,OAAO,EAAE,qBAAqB,IAAI,cAAc,EAAE,CAAC"}
|
package/dist/schema.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Write-heavy, temporal, rotatable.
|
|
6
6
|
* Contains ONLY conversation data — structured knowledge lives in library.db.
|
|
7
7
|
*/
|
|
8
|
-
export const LATEST_SCHEMA_VERSION =
|
|
8
|
+
export const LATEST_SCHEMA_VERSION = 10;
|
|
9
9
|
function nowIso() {
|
|
10
10
|
return new Date().toISOString();
|
|
11
11
|
}
|
|
@@ -250,6 +250,51 @@ export function migrate(db) {
|
|
|
250
250
|
db.prepare('INSERT OR IGNORE INTO schema_version (version, applied_at) VALUES (?, ?)')
|
|
251
251
|
.run(8, nowIso());
|
|
252
252
|
}
|
|
253
|
+
// v8 → v9: Tool Artifact Store — durable storage for full tool result payloads.
|
|
254
|
+
// Transcript stubs carry an artifactId pointer; raw payload lives here so
|
|
255
|
+
// wave-guard degradation is never lossy. See specs/TOOL_ARTIFACT_STORE.md.
|
|
256
|
+
if (currentVersion < 9) {
|
|
257
|
+
db.exec(`
|
|
258
|
+
CREATE TABLE IF NOT EXISTS tool_artifacts (
|
|
259
|
+
id TEXT PRIMARY KEY,
|
|
260
|
+
content_hash TEXT NOT NULL,
|
|
261
|
+
agent_id TEXT NOT NULL,
|
|
262
|
+
session_key TEXT NOT NULL,
|
|
263
|
+
conversation_id INTEGER REFERENCES conversations(id),
|
|
264
|
+
message_id INTEGER REFERENCES messages(id),
|
|
265
|
+
turn_id TEXT,
|
|
266
|
+
tool_call_id TEXT,
|
|
267
|
+
tool_name TEXT NOT NULL,
|
|
268
|
+
is_error INTEGER NOT NULL DEFAULT 0,
|
|
269
|
+
content_type TEXT NOT NULL DEFAULT 'text/plain',
|
|
270
|
+
size_bytes INTEGER NOT NULL,
|
|
271
|
+
token_estimate INTEGER NOT NULL,
|
|
272
|
+
payload TEXT NOT NULL,
|
|
273
|
+
summary TEXT,
|
|
274
|
+
created_at TEXT NOT NULL,
|
|
275
|
+
last_used_at TEXT NOT NULL,
|
|
276
|
+
ref_count INTEGER NOT NULL DEFAULT 1
|
|
277
|
+
)
|
|
278
|
+
`);
|
|
279
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_tool_artifacts_hash ON tool_artifacts(content_hash)');
|
|
280
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_tool_artifacts_session ON tool_artifacts(agent_id, session_key, created_at DESC)');
|
|
281
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_tool_artifacts_turn ON tool_artifacts(turn_id)');
|
|
282
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_tool_artifacts_tool_call ON tool_artifacts(tool_call_id)');
|
|
283
|
+
db.prepare('INSERT OR IGNORE INTO schema_version (version, applied_at) VALUES (?, ?)')
|
|
284
|
+
.run(9, nowIso());
|
|
285
|
+
}
|
|
286
|
+
// v9 → v10: Retention sweep — add is_sensitive flag to tool_artifacts.
|
|
287
|
+
// Enables differential TTL: sensitive artifacts expire sooner than standard ones.
|
|
288
|
+
// See: ToolArtifactStore.sweep() and ToolArtifactRetentionPolicy.
|
|
289
|
+
if (currentVersion < 10) {
|
|
290
|
+
const taCols = db.prepare('PRAGMA table_info(tool_artifacts)').all()
|
|
291
|
+
.map(r => r.name);
|
|
292
|
+
if (!taCols.includes('is_sensitive')) {
|
|
293
|
+
db.exec('ALTER TABLE tool_artifacts ADD COLUMN is_sensitive INTEGER NOT NULL DEFAULT 0');
|
|
294
|
+
}
|
|
295
|
+
db.prepare('INSERT OR IGNORE INTO schema_version (version, applied_at) VALUES (?, ?)')
|
|
296
|
+
.run(10, nowIso());
|
|
297
|
+
}
|
|
253
298
|
}
|
|
254
299
|
export { LATEST_SCHEMA_VERSION as SCHEMA_VERSION };
|
|
255
300
|
//# sourceMappingURL=schema.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* session-flusher.ts
|
|
3
3
|
*
|
|
4
|
-
* Provides a clean, operator-safe way to flush a session's hot cache
|
|
4
|
+
* Provides a clean, operator-safe way to flush a session's hot cache
|
|
5
5
|
* without touching long-term memory (facts, vectors, episodes, knowledge graph).
|
|
6
6
|
*
|
|
7
7
|
* Used to implement the /fresh slash command — lets users start a new unwarmed
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { CacheLayer } from './cache.js';
|
|
14
14
|
export interface FlushSessionOptions {
|
|
15
|
-
/** If true, also clears topic-level
|
|
15
|
+
/** If true, also clears topic-level hot-cache entries for this session */
|
|
16
16
|
includeTopics?: boolean;
|
|
17
17
|
}
|
|
18
18
|
export interface FlushSessionResult {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-flusher.d.ts","sourceRoot":"","sources":["../src/session-flusher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,WAAW,mBAAmB;IAClC,
|
|
1
|
+
{"version":3,"file":"session-flusher.d.ts","sourceRoot":"","sources":["../src/session-flusher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,WAAW,mBAAmB;IAClC,0EAA0E;IAC1E,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,iCAAiC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,uBAAuB;IACvB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,mBAAwB,GAC7B,OAAO,CAAC,kBAAkB,CAAC,CA2B7B;AAED;;GAEG;AACH,qBAAa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBADP,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,MAAM;IAGlC;;OAEG;IACG,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAGzF"}
|
package/dist/session-flusher.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* session-flusher.ts
|
|
3
3
|
*
|
|
4
|
-
* Provides a clean, operator-safe way to flush a session's hot cache
|
|
4
|
+
* Provides a clean, operator-safe way to flush a session's hot cache
|
|
5
5
|
* without touching long-term memory (facts, vectors, episodes, knowledge graph).
|
|
6
6
|
*
|
|
7
7
|
* Used to implement the /fresh slash command — lets users start a new unwarmed
|
package/dist/temporal-store.js
CHANGED
|
@@ -59,11 +59,11 @@ export class TemporalStore {
|
|
|
59
59
|
const limit = opts.limit ?? 20;
|
|
60
60
|
const order = opts.order ?? 'DESC';
|
|
61
61
|
const minConf = opts.minConfidence ?? 0;
|
|
62
|
-
const params = [];
|
|
62
|
+
const params = [minConf];
|
|
63
63
|
const conditions = [
|
|
64
64
|
'f.superseded_by IS NULL',
|
|
65
65
|
'f.decay_score < 0.8',
|
|
66
|
-
|
|
66
|
+
't.confidence >= ?',
|
|
67
67
|
];
|
|
68
68
|
if (opts.agentId) {
|
|
69
69
|
conditions.push('t.agent_id = ?');
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Artifact Store
|
|
3
|
+
*
|
|
4
|
+
* Durable, addressable storage for full tool result payloads. Schema v9.
|
|
5
|
+
*
|
|
6
|
+
* Why: the wave-guard at ingest time replaces oversized tool result payloads
|
|
7
|
+
* with a small stub for pressure relief. Before this store existed, the full
|
|
8
|
+
* payload was discarded. Now the stub in the transcript carries an
|
|
9
|
+
* `artifactId` pointing at the durable copy here, and hydration is an
|
|
10
|
+
* explicit decision, not an automatic transcript rewrite.
|
|
11
|
+
*
|
|
12
|
+
* Retention is deliberately independent of transcript eviction. Artifacts
|
|
13
|
+
* outlive the messages that referenced them, and GC is a separate concern
|
|
14
|
+
* (Phase 2).
|
|
15
|
+
*
|
|
16
|
+
* See: specs/TOOL_ARTIFACT_STORE.md
|
|
17
|
+
*/
|
|
18
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
19
|
+
export interface ToolArtifactRetentionPolicy {
|
|
20
|
+
/** TTL in ms for non-sensitive artifacts (e.g. 7 * 24 * 60 * 60 * 1000). */
|
|
21
|
+
standardTtlMs: number;
|
|
22
|
+
/** TTL in ms for sensitive artifacts. Should be <= standardTtlMs. */
|
|
23
|
+
sensitiveTtlMs: number;
|
|
24
|
+
/** Optional: max artifacts to keep per (agent_id, session_key). Excess removed oldest-first. */
|
|
25
|
+
maxPerSession?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface ToolArtifactRecord {
|
|
28
|
+
id: string;
|
|
29
|
+
contentHash: string;
|
|
30
|
+
agentId: string;
|
|
31
|
+
sessionKey: string;
|
|
32
|
+
conversationId: number | null;
|
|
33
|
+
messageId: number | null;
|
|
34
|
+
turnId: string | null;
|
|
35
|
+
toolCallId: string | null;
|
|
36
|
+
toolName: string;
|
|
37
|
+
isError: boolean;
|
|
38
|
+
contentType: string;
|
|
39
|
+
sizeBytes: number;
|
|
40
|
+
tokenEstimate: number;
|
|
41
|
+
payload: string;
|
|
42
|
+
summary: string | null;
|
|
43
|
+
createdAt: string;
|
|
44
|
+
lastUsedAt: string;
|
|
45
|
+
refCount: number;
|
|
46
|
+
isSensitive: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface PutToolArtifactInput {
|
|
49
|
+
agentId: string;
|
|
50
|
+
sessionKey: string;
|
|
51
|
+
conversationId?: number;
|
|
52
|
+
messageId?: number;
|
|
53
|
+
turnId?: string;
|
|
54
|
+
toolCallId?: string;
|
|
55
|
+
toolName: string;
|
|
56
|
+
isError?: boolean;
|
|
57
|
+
contentType?: string;
|
|
58
|
+
payload: string;
|
|
59
|
+
summary?: string;
|
|
60
|
+
isSensitive?: boolean;
|
|
61
|
+
}
|
|
62
|
+
export declare class ToolArtifactStore {
|
|
63
|
+
private readonly db;
|
|
64
|
+
constructor(db: DatabaseSync);
|
|
65
|
+
/**
|
|
66
|
+
* Insert a new artifact, or dedupe against an existing one in the same
|
|
67
|
+
* (agentId, sessionKey) scope. Dedupe is scoped to a session so distinct
|
|
68
|
+
* sessions can't leak artifact ids across each other even when payload
|
|
69
|
+
* content is identical.
|
|
70
|
+
*/
|
|
71
|
+
put(input: PutToolArtifactInput): ToolArtifactRecord;
|
|
72
|
+
/** Alias for get(id) — explicit name used by the compositor hydration pass. */
|
|
73
|
+
getById(id: string): ToolArtifactRecord | null;
|
|
74
|
+
get(id: string): ToolArtifactRecord | null;
|
|
75
|
+
getByHash(agentId: string, sessionKey: string, contentHash: string): ToolArtifactRecord | null;
|
|
76
|
+
listByTurn(sessionKey: string, turnId: string): ToolArtifactRecord[];
|
|
77
|
+
listByToolCall(toolCallId: string): ToolArtifactRecord[];
|
|
78
|
+
listRecent(agentId: string, sessionKey: string, limit?: number): ToolArtifactRecord[];
|
|
79
|
+
/** Update last_used_at — call this when hydration actually surfaces the payload. */
|
|
80
|
+
touch(id: string): void;
|
|
81
|
+
/**
|
|
82
|
+
* GC sweep: delete artifacts that exceed their TTL or per-session count cap.
|
|
83
|
+
* Returns total rows deleted.
|
|
84
|
+
*
|
|
85
|
+
* Sensitive artifacts use sensitiveTtlMs (shorter); standard artifacts use
|
|
86
|
+
* standardTtlMs. Optional maxPerSession bounds row count per (agent, session)
|
|
87
|
+
* using a ROW_NUMBER() window query — oldest last_used_at removed first.
|
|
88
|
+
*/
|
|
89
|
+
sweep(policy: ToolArtifactRetentionPolicy): number;
|
|
90
|
+
/**
|
|
91
|
+
* Delete artifacts whose last_used_at is older than the ISO cutoff.
|
|
92
|
+
* Returns the number of rows deleted. Phase 1: manual invocation only.
|
|
93
|
+
*/
|
|
94
|
+
deleteOlderThan(isoCutoff: string): number;
|
|
95
|
+
/** Debug / ops: row count across a session. */
|
|
96
|
+
count(agentId?: string, sessionKey?: string): number;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=tool-artifact-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-artifact-store.d.ts","sourceRoot":"","sources":["../src/tool-artifact-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA0BhD,MAAM,WAAW,2BAA2B;IAC1C,4EAA4E;IAC5E,aAAa,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,cAAc,EAAE,MAAM,CAAC;IACvB,gGAAgG;IAChG,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AA0BD,qBAAa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,YAAY;IAE7C;;;;;OAKG;IACH,GAAG,CAAC,KAAK,EAAE,oBAAoB,GAAG,kBAAkB;IA+FpD,+EAA+E;IAC/E,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI;IAI9C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI;IAO1C,SAAS,CACP,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAClB,kBAAkB,GAAG,IAAI;IAY5B,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAWpE,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAWxD,UAAU,CACR,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,KAAK,GAAE,MAAW,GACjB,kBAAkB,EAAE;IAYvB,oFAAoF;IACpF,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAMvB;;;;;;;OAOG;IACH,KAAK,CAAC,MAAM,EAAE,2BAA2B,GAAG,MAAM;IAsClD;;;OAGG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAO1C,+CAA+C;IAC/C,KAAK,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM;CAcrD"}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Artifact Store
|
|
3
|
+
*
|
|
4
|
+
* Durable, addressable storage for full tool result payloads. Schema v9.
|
|
5
|
+
*
|
|
6
|
+
* Why: the wave-guard at ingest time replaces oversized tool result payloads
|
|
7
|
+
* with a small stub for pressure relief. Before this store existed, the full
|
|
8
|
+
* payload was discarded. Now the stub in the transcript carries an
|
|
9
|
+
* `artifactId` pointing at the durable copy here, and hydration is an
|
|
10
|
+
* explicit decision, not an automatic transcript rewrite.
|
|
11
|
+
*
|
|
12
|
+
* Retention is deliberately independent of transcript eviction. Artifacts
|
|
13
|
+
* outlive the messages that referenced them, and GC is a separate concern
|
|
14
|
+
* (Phase 2).
|
|
15
|
+
*
|
|
16
|
+
* See: specs/TOOL_ARTIFACT_STORE.md
|
|
17
|
+
*/
|
|
18
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
19
|
+
/**
|
|
20
|
+
* Simple char/4 heuristic matching src/compositor.ts estimateTokens().
|
|
21
|
+
* Kept local to avoid a cyclic import and keep the store self-contained.
|
|
22
|
+
*/
|
|
23
|
+
function estimateTokens(text) {
|
|
24
|
+
if (!text)
|
|
25
|
+
return 0;
|
|
26
|
+
return Math.ceil(text.length / 4);
|
|
27
|
+
}
|
|
28
|
+
function nowIso() {
|
|
29
|
+
return new Date().toISOString();
|
|
30
|
+
}
|
|
31
|
+
function newArtifactId() {
|
|
32
|
+
// 24-char lowercase id. Deliberately NOT a UUID — shorter, fits the stub
|
|
33
|
+
// length cap in src/degradation.ts (DEGRADATION_LIMITS.artifactId = 64).
|
|
34
|
+
return 'art_' + randomBytes(10).toString('hex');
|
|
35
|
+
}
|
|
36
|
+
function sha256Hex(s) {
|
|
37
|
+
return createHash('sha256').update(s).digest('hex');
|
|
38
|
+
}
|
|
39
|
+
function rowToRecord(row) {
|
|
40
|
+
return {
|
|
41
|
+
id: row.id,
|
|
42
|
+
contentHash: row.content_hash,
|
|
43
|
+
agentId: row.agent_id,
|
|
44
|
+
sessionKey: row.session_key,
|
|
45
|
+
conversationId: row.conversation_id ?? null,
|
|
46
|
+
messageId: row.message_id ?? null,
|
|
47
|
+
turnId: row.turn_id ?? null,
|
|
48
|
+
toolCallId: row.tool_call_id ?? null,
|
|
49
|
+
toolName: row.tool_name,
|
|
50
|
+
isError: row.is_error === 1,
|
|
51
|
+
contentType: row.content_type,
|
|
52
|
+
sizeBytes: row.size_bytes,
|
|
53
|
+
tokenEstimate: row.token_estimate,
|
|
54
|
+
payload: row.payload,
|
|
55
|
+
summary: row.summary ?? null,
|
|
56
|
+
createdAt: row.created_at,
|
|
57
|
+
lastUsedAt: row.last_used_at,
|
|
58
|
+
refCount: row.ref_count,
|
|
59
|
+
isSensitive: row.is_sensitive === 1,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export class ToolArtifactStore {
|
|
63
|
+
db;
|
|
64
|
+
constructor(db) {
|
|
65
|
+
this.db = db;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Insert a new artifact, or dedupe against an existing one in the same
|
|
69
|
+
* (agentId, sessionKey) scope. Dedupe is scoped to a session so distinct
|
|
70
|
+
* sessions can't leak artifact ids across each other even when payload
|
|
71
|
+
* content is identical.
|
|
72
|
+
*/
|
|
73
|
+
put(input) {
|
|
74
|
+
const payload = input.payload ?? '';
|
|
75
|
+
const contentHash = sha256Hex(payload);
|
|
76
|
+
const sizeBytes = Buffer.byteLength(payload, 'utf8');
|
|
77
|
+
const tokenEstimate = estimateTokens(payload);
|
|
78
|
+
const contentType = input.contentType ?? 'text/plain';
|
|
79
|
+
const isError = input.isError ? 1 : 0;
|
|
80
|
+
const isSensitive = input.isSensitive ? 1 : 0;
|
|
81
|
+
// Dedupe within (agentId, sessionKey) — same hash bumps ref_count and
|
|
82
|
+
// updates last_used_at, returning the existing record.
|
|
83
|
+
const existing = this.db
|
|
84
|
+
.prepare(`SELECT * FROM tool_artifacts
|
|
85
|
+
WHERE agent_id = ? AND session_key = ? AND content_hash = ?
|
|
86
|
+
LIMIT 1`)
|
|
87
|
+
.get(input.agentId, input.sessionKey, contentHash);
|
|
88
|
+
if (existing) {
|
|
89
|
+
const ts = nowIso();
|
|
90
|
+
this.db
|
|
91
|
+
.prepare(`UPDATE tool_artifacts
|
|
92
|
+
SET ref_count = ref_count + 1,
|
|
93
|
+
last_used_at = ?
|
|
94
|
+
WHERE id = ?`)
|
|
95
|
+
.run(ts, existing.id);
|
|
96
|
+
return rowToRecord({ ...existing, ref_count: existing.ref_count + 1, last_used_at: ts, is_sensitive: existing.is_sensitive ?? 0 });
|
|
97
|
+
}
|
|
98
|
+
const id = newArtifactId();
|
|
99
|
+
const ts = nowIso();
|
|
100
|
+
this.db
|
|
101
|
+
.prepare(`INSERT INTO tool_artifacts (
|
|
102
|
+
id, content_hash, agent_id, session_key,
|
|
103
|
+
conversation_id, message_id, turn_id, tool_call_id,
|
|
104
|
+
tool_name, is_error, content_type,
|
|
105
|
+
size_bytes, token_estimate, payload, summary,
|
|
106
|
+
created_at, last_used_at, ref_count, is_sensitive
|
|
107
|
+
) VALUES (?, ?, ?, ?,
|
|
108
|
+
?, ?, ?, ?,
|
|
109
|
+
?, ?, ?,
|
|
110
|
+
?, ?, ?, ?,
|
|
111
|
+
?, ?, 1, ?)`)
|
|
112
|
+
.run(id, contentHash, input.agentId, input.sessionKey, input.conversationId ?? null, input.messageId ?? null, input.turnId ?? null, input.toolCallId ?? null, input.toolName, isError, contentType, sizeBytes, tokenEstimate, payload, input.summary ?? null, ts, ts, isSensitive);
|
|
113
|
+
return {
|
|
114
|
+
id,
|
|
115
|
+
contentHash,
|
|
116
|
+
agentId: input.agentId,
|
|
117
|
+
sessionKey: input.sessionKey,
|
|
118
|
+
conversationId: input.conversationId ?? null,
|
|
119
|
+
messageId: input.messageId ?? null,
|
|
120
|
+
turnId: input.turnId ?? null,
|
|
121
|
+
toolCallId: input.toolCallId ?? null,
|
|
122
|
+
toolName: input.toolName,
|
|
123
|
+
isError: input.isError ?? false,
|
|
124
|
+
contentType,
|
|
125
|
+
sizeBytes,
|
|
126
|
+
tokenEstimate,
|
|
127
|
+
payload,
|
|
128
|
+
summary: input.summary ?? null,
|
|
129
|
+
createdAt: ts,
|
|
130
|
+
lastUsedAt: ts,
|
|
131
|
+
refCount: 1,
|
|
132
|
+
isSensitive: input.isSensitive ?? false,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** Alias for get(id) — explicit name used by the compositor hydration pass. */
|
|
136
|
+
getById(id) {
|
|
137
|
+
return this.get(id);
|
|
138
|
+
}
|
|
139
|
+
get(id) {
|
|
140
|
+
const row = this.db
|
|
141
|
+
.prepare('SELECT * FROM tool_artifacts WHERE id = ? LIMIT 1')
|
|
142
|
+
.get(id);
|
|
143
|
+
return row ? rowToRecord(row) : null;
|
|
144
|
+
}
|
|
145
|
+
getByHash(agentId, sessionKey, contentHash) {
|
|
146
|
+
const row = this.db
|
|
147
|
+
.prepare(`SELECT * FROM tool_artifacts
|
|
148
|
+
WHERE agent_id = ? AND session_key = ? AND content_hash = ?
|
|
149
|
+
ORDER BY created_at DESC
|
|
150
|
+
LIMIT 1`)
|
|
151
|
+
.get(agentId, sessionKey, contentHash);
|
|
152
|
+
return row ? rowToRecord(row) : null;
|
|
153
|
+
}
|
|
154
|
+
listByTurn(sessionKey, turnId) {
|
|
155
|
+
const rows = this.db
|
|
156
|
+
.prepare(`SELECT * FROM tool_artifacts
|
|
157
|
+
WHERE session_key = ? AND turn_id = ?
|
|
158
|
+
ORDER BY created_at ASC`)
|
|
159
|
+
.all(sessionKey, turnId);
|
|
160
|
+
return rows.map(rowToRecord);
|
|
161
|
+
}
|
|
162
|
+
listByToolCall(toolCallId) {
|
|
163
|
+
const rows = this.db
|
|
164
|
+
.prepare(`SELECT * FROM tool_artifacts
|
|
165
|
+
WHERE tool_call_id = ?
|
|
166
|
+
ORDER BY created_at ASC`)
|
|
167
|
+
.all(toolCallId);
|
|
168
|
+
return rows.map(rowToRecord);
|
|
169
|
+
}
|
|
170
|
+
listRecent(agentId, sessionKey, limit = 20) {
|
|
171
|
+
const rows = this.db
|
|
172
|
+
.prepare(`SELECT * FROM tool_artifacts
|
|
173
|
+
WHERE agent_id = ? AND session_key = ?
|
|
174
|
+
ORDER BY created_at DESC
|
|
175
|
+
LIMIT ?`)
|
|
176
|
+
.all(agentId, sessionKey, limit);
|
|
177
|
+
return rows.map(rowToRecord);
|
|
178
|
+
}
|
|
179
|
+
/** Update last_used_at — call this when hydration actually surfaces the payload. */
|
|
180
|
+
touch(id) {
|
|
181
|
+
this.db
|
|
182
|
+
.prepare('UPDATE tool_artifacts SET last_used_at = ? WHERE id = ?')
|
|
183
|
+
.run(nowIso(), id);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* GC sweep: delete artifacts that exceed their TTL or per-session count cap.
|
|
187
|
+
* Returns total rows deleted.
|
|
188
|
+
*
|
|
189
|
+
* Sensitive artifacts use sensitiveTtlMs (shorter); standard artifacts use
|
|
190
|
+
* standardTtlMs. Optional maxPerSession bounds row count per (agent, session)
|
|
191
|
+
* using a ROW_NUMBER() window query — oldest last_used_at removed first.
|
|
192
|
+
*/
|
|
193
|
+
sweep(policy) {
|
|
194
|
+
const now = Date.now();
|
|
195
|
+
const standardCutoff = new Date(now - policy.standardTtlMs).toISOString();
|
|
196
|
+
const sensitiveCutoff = new Date(now - policy.sensitiveTtlMs).toISOString();
|
|
197
|
+
const ttlResult = this.db
|
|
198
|
+
.prepare(`DELETE FROM tool_artifacts
|
|
199
|
+
WHERE (is_sensitive = 0 AND last_used_at < ?)
|
|
200
|
+
OR (is_sensitive = 1 AND last_used_at < ?)`)
|
|
201
|
+
.run(standardCutoff, sensitiveCutoff);
|
|
202
|
+
let deleted = ttlResult.changes ?? 0;
|
|
203
|
+
if (policy.maxPerSession != null) {
|
|
204
|
+
const boundResult = this.db
|
|
205
|
+
.prepare(`DELETE FROM tool_artifacts
|
|
206
|
+
WHERE id IN (
|
|
207
|
+
SELECT id FROM (
|
|
208
|
+
SELECT id,
|
|
209
|
+
ROW_NUMBER() OVER (
|
|
210
|
+
PARTITION BY agent_id, session_key
|
|
211
|
+
ORDER BY last_used_at DESC
|
|
212
|
+
) AS rn
|
|
213
|
+
FROM tool_artifacts
|
|
214
|
+
)
|
|
215
|
+
WHERE rn > ?
|
|
216
|
+
)`)
|
|
217
|
+
.run(policy.maxPerSession);
|
|
218
|
+
deleted += boundResult.changes ?? 0;
|
|
219
|
+
}
|
|
220
|
+
return deleted;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Delete artifacts whose last_used_at is older than the ISO cutoff.
|
|
224
|
+
* Returns the number of rows deleted. Phase 1: manual invocation only.
|
|
225
|
+
*/
|
|
226
|
+
deleteOlderThan(isoCutoff) {
|
|
227
|
+
const result = this.db
|
|
228
|
+
.prepare('DELETE FROM tool_artifacts WHERE last_used_at < ?')
|
|
229
|
+
.run(isoCutoff);
|
|
230
|
+
return Number(result.changes ?? 0);
|
|
231
|
+
}
|
|
232
|
+
/** Debug / ops: row count across a session. */
|
|
233
|
+
count(agentId, sessionKey) {
|
|
234
|
+
if (agentId && sessionKey) {
|
|
235
|
+
const row = this.db
|
|
236
|
+
.prepare('SELECT COUNT(*) AS n FROM tool_artifacts WHERE agent_id = ? AND session_key = ?')
|
|
237
|
+
.get(agentId, sessionKey);
|
|
238
|
+
return row?.n ?? 0;
|
|
239
|
+
}
|
|
240
|
+
const row = this.db.prepare('SELECT COUNT(*) AS n FROM tool_artifacts').get();
|
|
241
|
+
return row?.n ?? 0;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=tool-artifact-store.js.map
|
package/dist/topic-detector.js
CHANGED
|
@@ -234,11 +234,11 @@ export function detectTopicShift(incomingMessage, recentMessages, currentTopicId
|
|
|
234
234
|
const contextText = contextWindow
|
|
235
235
|
.map(m => m.textContent ?? '')
|
|
236
236
|
.join(' ');
|
|
237
|
-
if (incomingEntities.size
|
|
237
|
+
if (incomingEntities.size >= 2 && contextText.length > 0) {
|
|
238
238
|
const contextEntities = extractEntities(contextText);
|
|
239
239
|
const similarity = jaccardSimilarity(incomingEntities, contextEntities);
|
|
240
240
|
// Low similarity + no continuation marker → likely a new topic
|
|
241
|
-
if (similarity < 0.
|
|
241
|
+
if (similarity < 0.15) {
|
|
242
242
|
const name = extractTopicName(text);
|
|
243
243
|
return { topicId: null, isNewTopic: true, confidence: 0.6, topicName: name };
|
|
244
244
|
}
|
package/dist/topic-store.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { DatabaseSync } from 'node:sqlite';
|
|
9
9
|
import type { Topic, TopicStatus } from './types.js';
|
|
10
|
+
export declare function normalizeTopicName(name: string): string;
|
|
10
11
|
export declare class TopicStore {
|
|
11
12
|
private readonly db;
|
|
12
13
|
constructor(db: DatabaseSync);
|
|
@@ -14,6 +15,11 @@ export declare class TopicStore {
|
|
|
14
15
|
* Create a new topic.
|
|
15
16
|
*/
|
|
16
17
|
create(agentId: string, name: string, description?: string, visibility?: string): Topic;
|
|
18
|
+
/**
|
|
19
|
+
* Find an existing topic by name (case-insensitive) or create a new one.
|
|
20
|
+
* Prevents duplicate topics for the same logical concept.
|
|
21
|
+
*/
|
|
22
|
+
findOrCreate(agentId: string, rawName: string, description?: string, visibility?: string): Topic;
|
|
17
23
|
/**
|
|
18
24
|
* Touch a topic — update activity tracking.
|
|
19
25
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topic-store.d.ts","sourceRoot":"","sources":["../src/topic-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"topic-store.d.ts","sourceRoot":"","sources":["../src/topic-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAmBrD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGvD;AAqBD,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,YAAY;IAE7C;;OAEG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK;IAwBvF;;;OAGG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK;IAkBhG;;OAEG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,GAAE,MAAU,GAAG,IAAI;IAY3E;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,KAAK,EAAE;IAWvD;;OAEG;IACH,MAAM,CACJ,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAC9C,KAAK,EAAE;IAoBV;;OAEG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,KAAK,EAAE;IAanE;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,iBAAiB,GAAE,MAAW,GAAG,MAAM;IAYpE;;OAEG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,eAAe,GAAE,MAAU,GAAG,MAAM;CAWnE"}
|
package/dist/topic-store.js
CHANGED
|
@@ -5,6 +5,26 @@
|
|
|
5
5
|
* can span multiple sessions and channels.
|
|
6
6
|
* Lives in the central library DB.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a topic name for dedup purposes.
|
|
10
|
+
* Preserves casing of known product names; lowercases everything else.
|
|
11
|
+
*/
|
|
12
|
+
const KNOWN_NAMES = {
|
|
13
|
+
hypermem: 'HyperMem',
|
|
14
|
+
hyperbuilder: 'HyperBuilder',
|
|
15
|
+
clawcanvas: 'ClawCanvas',
|
|
16
|
+
clawdash: 'ClawDash',
|
|
17
|
+
clawdispatch: 'ClawDispatch',
|
|
18
|
+
clawtext: 'ClawText',
|
|
19
|
+
clawtomation: 'ClawTomation',
|
|
20
|
+
clawcouncil: 'ClawCouncil',
|
|
21
|
+
openclaw: 'OpenClaw',
|
|
22
|
+
clawhub: 'ClawHub',
|
|
23
|
+
};
|
|
24
|
+
export function normalizeTopicName(name) {
|
|
25
|
+
const lower = name.trim().toLowerCase();
|
|
26
|
+
return KNOWN_NAMES[lower] ?? name.trim();
|
|
27
|
+
}
|
|
8
28
|
function nowIso() {
|
|
9
29
|
return new Date().toISOString();
|
|
10
30
|
}
|
|
@@ -50,6 +70,25 @@ export class TopicStore {
|
|
|
50
70
|
updatedAt: now,
|
|
51
71
|
};
|
|
52
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Find an existing topic by name (case-insensitive) or create a new one.
|
|
75
|
+
* Prevents duplicate topics for the same logical concept.
|
|
76
|
+
*/
|
|
77
|
+
findOrCreate(agentId, rawName, description, visibility) {
|
|
78
|
+
const name = normalizeTopicName(rawName);
|
|
79
|
+
const existing = this.db.prepare(`
|
|
80
|
+
SELECT * FROM topics
|
|
81
|
+
WHERE agent_id = ?
|
|
82
|
+
AND lower(name) = lower(?)
|
|
83
|
+
AND status != 'closed'
|
|
84
|
+
ORDER BY updated_at DESC
|
|
85
|
+
LIMIT 1
|
|
86
|
+
`).get(agentId, name);
|
|
87
|
+
if (existing) {
|
|
88
|
+
return parseTopicRow(existing);
|
|
89
|
+
}
|
|
90
|
+
return this.create(agentId, name, description, visibility);
|
|
91
|
+
}
|
|
53
92
|
/**
|
|
54
93
|
* Touch a topic — update activity tracking.
|
|
55
94
|
*/
|