@vainplex/openclaw-cortex 0.1.0
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/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/dist/src/boot-context.d.ts +43 -0
- package/dist/src/boot-context.d.ts.map +1 -0
- package/dist/src/boot-context.js +215 -0
- package/dist/src/boot-context.js.map +1 -0
- package/dist/src/config.d.ts +10 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +100 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/decision-tracker.d.ts +48 -0
- package/dist/src/decision-tracker.d.ts.map +1 -0
- package/dist/src/decision-tracker.js +135 -0
- package/dist/src/decision-tracker.js.map +1 -0
- package/dist/src/hooks.d.ts +7 -0
- package/dist/src/hooks.d.ts.map +1 -0
- package/dist/src/hooks.js +104 -0
- package/dist/src/hooks.js.map +1 -0
- package/dist/src/narrative-generator.d.ts +34 -0
- package/dist/src/narrative-generator.d.ts.map +1 -0
- package/dist/src/narrative-generator.js +153 -0
- package/dist/src/narrative-generator.js.map +1 -0
- package/dist/src/patterns.d.ts +24 -0
- package/dist/src/patterns.d.ts.map +1 -0
- package/dist/src/patterns.js +98 -0
- package/dist/src/patterns.js.map +1 -0
- package/dist/src/pre-compaction.d.ts +21 -0
- package/dist/src/pre-compaction.d.ts.map +1 -0
- package/dist/src/pre-compaction.js +108 -0
- package/dist/src/pre-compaction.js.map +1 -0
- package/dist/src/storage.d.ts +44 -0
- package/dist/src/storage.d.ts.map +1 -0
- package/dist/src/storage.js +126 -0
- package/dist/src/storage.js.map +1 -0
- package/dist/src/thread-tracker.d.ts +75 -0
- package/dist/src/thread-tracker.d.ts.map +1 -0
- package/dist/src/thread-tracker.js +250 -0
- package/dist/src/thread-tracker.js.map +1 -0
- package/dist/src/types.d.ts +202 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +24 -0
- package/dist/src/types.js.map +1 -0
- package/openclaw.plugin.json +154 -0
- package/package.json +48 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patterns.js","sourceRoot":"","sources":["../../src/patterns.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CompactingMessage, PreCompactionResult, PluginLogger, CortexConfig } from "./types.js";
|
|
2
|
+
import { ThreadTracker } from "./thread-tracker.js";
|
|
3
|
+
/**
|
|
4
|
+
* Build a hot snapshot markdown from compacting messages.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildHotSnapshot(messages: CompactingMessage[], maxMessages: number): string;
|
|
7
|
+
/**
|
|
8
|
+
* Pre-Compaction Pipeline — orchestrates all modules before memory compaction.
|
|
9
|
+
*/
|
|
10
|
+
export declare class PreCompaction {
|
|
11
|
+
private readonly workspace;
|
|
12
|
+
private readonly config;
|
|
13
|
+
private readonly logger;
|
|
14
|
+
private readonly threadTracker;
|
|
15
|
+
constructor(workspace: string, config: CortexConfig, logger: PluginLogger, threadTracker: ThreadTracker);
|
|
16
|
+
/**
|
|
17
|
+
* Run the full pre-compaction pipeline.
|
|
18
|
+
*/
|
|
19
|
+
run(compactingMessages?: CompactingMessage[]): PreCompactionResult;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=pre-compaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pre-compaction.d.ts","sourceRoot":"","sources":["../../src/pre-compaction.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,iBAAiB,EACjB,mBAAmB,EACnB,YAAY,EACZ,YAAY,EACb,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAKpD;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,WAAW,EAAE,MAAM,GAClB,MAAM,CAsBR;AAED;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;gBAG5C,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,YAAY,EACpB,aAAa,EAAE,aAAa;IAQ9B;;OAEG;IACH,GAAG,CAAC,kBAAkB,CAAC,EAAE,iBAAiB,EAAE,GAAG,mBAAmB;CA6EnE"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { NarrativeGenerator } from "./narrative-generator.js";
|
|
3
|
+
import { BootContextGenerator } from "./boot-context.js";
|
|
4
|
+
import { saveText, rebootDir, ensureRebootDir } from "./storage.js";
|
|
5
|
+
/**
|
|
6
|
+
* Build a hot snapshot markdown from compacting messages.
|
|
7
|
+
*/
|
|
8
|
+
export function buildHotSnapshot(messages, maxMessages) {
|
|
9
|
+
const now = new Date().toISOString().slice(0, 19) + "Z";
|
|
10
|
+
const parts = [
|
|
11
|
+
`# Hot Snapshot — ${now}`,
|
|
12
|
+
"## Last conversation before compaction",
|
|
13
|
+
"",
|
|
14
|
+
];
|
|
15
|
+
const recent = messages.slice(-maxMessages);
|
|
16
|
+
if (recent.length > 0) {
|
|
17
|
+
parts.push("**Recent messages:**");
|
|
18
|
+
for (const msg of recent) {
|
|
19
|
+
const content = msg.content.trim();
|
|
20
|
+
const short = content.length > 200 ? content.slice(0, 200) + "..." : content;
|
|
21
|
+
parts.push(`- [${msg.role}] ${short}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
parts.push("(No recent messages captured)");
|
|
26
|
+
}
|
|
27
|
+
parts.push("");
|
|
28
|
+
return parts.join("\n");
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Pre-Compaction Pipeline — orchestrates all modules before memory compaction.
|
|
32
|
+
*/
|
|
33
|
+
export class PreCompaction {
|
|
34
|
+
workspace;
|
|
35
|
+
config;
|
|
36
|
+
logger;
|
|
37
|
+
threadTracker;
|
|
38
|
+
constructor(workspace, config, logger, threadTracker) {
|
|
39
|
+
this.workspace = workspace;
|
|
40
|
+
this.config = config;
|
|
41
|
+
this.logger = logger;
|
|
42
|
+
this.threadTracker = threadTracker;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Run the full pre-compaction pipeline.
|
|
46
|
+
*/
|
|
47
|
+
run(compactingMessages) {
|
|
48
|
+
const warnings = [];
|
|
49
|
+
const now = new Date().toISOString();
|
|
50
|
+
let messagesSnapshotted = 0;
|
|
51
|
+
ensureRebootDir(this.workspace, this.logger);
|
|
52
|
+
// 1. Flush thread tracker state
|
|
53
|
+
try {
|
|
54
|
+
this.threadTracker.flush();
|
|
55
|
+
this.logger.info("[cortex] Pre-compaction: thread state flushed");
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
warnings.push(`Thread flush failed: ${err}`);
|
|
59
|
+
this.logger.warn(`[cortex] Pre-compaction: thread flush failed: ${err}`);
|
|
60
|
+
}
|
|
61
|
+
// 2. Build and write hot snapshot
|
|
62
|
+
try {
|
|
63
|
+
const messages = compactingMessages ?? [];
|
|
64
|
+
messagesSnapshotted = Math.min(messages.length, this.config.preCompaction.maxSnapshotMessages);
|
|
65
|
+
const snapshot = buildHotSnapshot(messages, this.config.preCompaction.maxSnapshotMessages);
|
|
66
|
+
const snapshotPath = join(rebootDir(this.workspace), "hot-snapshot.md");
|
|
67
|
+
const ok = saveText(snapshotPath, snapshot, this.logger);
|
|
68
|
+
if (!ok)
|
|
69
|
+
warnings.push("Hot snapshot write failed");
|
|
70
|
+
this.logger.info(`[cortex] Pre-compaction: hot snapshot (${messagesSnapshotted} messages)`);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
warnings.push(`Hot snapshot failed: ${err}`);
|
|
74
|
+
this.logger.warn(`[cortex] Pre-compaction: hot snapshot failed: ${err}`);
|
|
75
|
+
}
|
|
76
|
+
// 3. Generate narrative
|
|
77
|
+
try {
|
|
78
|
+
if (this.config.narrative.enabled) {
|
|
79
|
+
const narrative = new NarrativeGenerator(this.workspace, this.logger);
|
|
80
|
+
narrative.write();
|
|
81
|
+
this.logger.info("[cortex] Pre-compaction: narrative generated");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
warnings.push(`Narrative generation failed: ${err}`);
|
|
86
|
+
this.logger.warn(`[cortex] Pre-compaction: narrative generation failed: ${err}`);
|
|
87
|
+
}
|
|
88
|
+
// 4. Generate boot context
|
|
89
|
+
try {
|
|
90
|
+
if (this.config.bootContext.enabled) {
|
|
91
|
+
const boot = new BootContextGenerator(this.workspace, this.config.bootContext, this.logger);
|
|
92
|
+
boot.write();
|
|
93
|
+
this.logger.info("[cortex] Pre-compaction: boot context generated");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
warnings.push(`Boot context generation failed: ${err}`);
|
|
98
|
+
this.logger.warn(`[cortex] Pre-compaction: boot context generation failed: ${err}`);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
success: warnings.length === 0,
|
|
102
|
+
timestamp: now,
|
|
103
|
+
messagesSnapshotted,
|
|
104
|
+
warnings,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=pre-compaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pre-compaction.js","sourceRoot":"","sources":["../../src/pre-compaction.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { PluginLogger } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the reboot directory path.
|
|
4
|
+
* Does NOT create it — use ensureRebootDir() for that.
|
|
5
|
+
*/
|
|
6
|
+
export declare function rebootDir(workspace: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Ensure the memory/reboot/ directory exists.
|
|
9
|
+
* Returns false if creation fails (read-only workspace).
|
|
10
|
+
*/
|
|
11
|
+
export declare function ensureRebootDir(workspace: string, logger: PluginLogger): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Check if the workspace is writable.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isWritable(workspace: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Load a JSON file. Returns empty object on any failure.
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadJson<T = Record<string, unknown>>(filePath: string): T;
|
|
20
|
+
/**
|
|
21
|
+
* Atomically write JSON to a file.
|
|
22
|
+
* Writes to .tmp first, then renames. This prevents partial writes on crash.
|
|
23
|
+
* Returns false on failure (read-only filesystem).
|
|
24
|
+
*/
|
|
25
|
+
export declare function saveJson(filePath: string, data: unknown, logger: PluginLogger): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Load a text file. Returns empty string on failure.
|
|
28
|
+
*/
|
|
29
|
+
export declare function loadText(filePath: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Write a text file atomically.
|
|
32
|
+
* Returns false on failure.
|
|
33
|
+
*/
|
|
34
|
+
export declare function saveText(filePath: string, content: string, logger: PluginLogger): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Get file modification time as ISO string. Returns null if file doesn't exist.
|
|
37
|
+
*/
|
|
38
|
+
export declare function getFileMtime(filePath: string): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Check if a file is older than the given number of hours.
|
|
41
|
+
* Returns true if the file doesn't exist.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isFileOlderThan(filePath: string, hours: number): boolean;
|
|
44
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/storage.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;GAGG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAShF;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAarD;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,CAOzE;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAWvF;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMjD;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAWzF;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO5D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAKxE"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, renameSync, mkdirSync, accessSync, statSync } from "node:fs";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the reboot directory path.
|
|
6
|
+
* Does NOT create it — use ensureRebootDir() for that.
|
|
7
|
+
*/
|
|
8
|
+
export function rebootDir(workspace) {
|
|
9
|
+
return join(workspace, "memory", "reboot");
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Ensure the memory/reboot/ directory exists.
|
|
13
|
+
* Returns false if creation fails (read-only workspace).
|
|
14
|
+
*/
|
|
15
|
+
export function ensureRebootDir(workspace, logger) {
|
|
16
|
+
const dir = rebootDir(workspace);
|
|
17
|
+
try {
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
logger.warn(`[cortex] Cannot create ${dir}: ${err}`);
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Check if the workspace is writable.
|
|
28
|
+
*/
|
|
29
|
+
export function isWritable(workspace) {
|
|
30
|
+
try {
|
|
31
|
+
accessSync(join(workspace, "memory"), constants.W_OK);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// memory/ might not exist yet — check workspace itself
|
|
36
|
+
try {
|
|
37
|
+
accessSync(workspace, constants.W_OK);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Load a JSON file. Returns empty object on any failure.
|
|
47
|
+
*/
|
|
48
|
+
export function loadJson(filePath) {
|
|
49
|
+
try {
|
|
50
|
+
const content = readFileSync(filePath, "utf-8");
|
|
51
|
+
return JSON.parse(content);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Atomically write JSON to a file.
|
|
59
|
+
* Writes to .tmp first, then renames. This prevents partial writes on crash.
|
|
60
|
+
* Returns false on failure (read-only filesystem).
|
|
61
|
+
*/
|
|
62
|
+
export function saveJson(filePath, data, logger) {
|
|
63
|
+
try {
|
|
64
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
65
|
+
const tmpPath = filePath + ".tmp";
|
|
66
|
+
writeFileSync(tmpPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
67
|
+
renameSync(tmpPath, filePath);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
logger.warn(`[cortex] Failed to write ${filePath}: ${err}`);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Load a text file. Returns empty string on failure.
|
|
77
|
+
*/
|
|
78
|
+
export function loadText(filePath) {
|
|
79
|
+
try {
|
|
80
|
+
return readFileSync(filePath, "utf-8");
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Write a text file atomically.
|
|
88
|
+
* Returns false on failure.
|
|
89
|
+
*/
|
|
90
|
+
export function saveText(filePath, content, logger) {
|
|
91
|
+
try {
|
|
92
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
93
|
+
const tmpPath = filePath + ".tmp";
|
|
94
|
+
writeFileSync(tmpPath, content, "utf-8");
|
|
95
|
+
renameSync(tmpPath, filePath);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
logger.warn(`[cortex] Failed to write ${filePath}: ${err}`);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get file modification time as ISO string. Returns null if file doesn't exist.
|
|
105
|
+
*/
|
|
106
|
+
export function getFileMtime(filePath) {
|
|
107
|
+
try {
|
|
108
|
+
const stat = statSync(filePath);
|
|
109
|
+
return stat.mtime.toISOString();
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Check if a file is older than the given number of hours.
|
|
117
|
+
* Returns true if the file doesn't exist.
|
|
118
|
+
*/
|
|
119
|
+
export function isFileOlderThan(filePath, hours) {
|
|
120
|
+
const mtime = getFileMtime(filePath);
|
|
121
|
+
if (!mtime)
|
|
122
|
+
return true;
|
|
123
|
+
const ageMs = Date.now() - new Date(mtime).getTime();
|
|
124
|
+
return ageMs > hours * 60 * 60 * 1000;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Thread, ThreadSignals, PluginLogger } from "./types.js";
|
|
2
|
+
import type { PatternLanguage } from "./patterns.js";
|
|
3
|
+
export type ThreadTrackerConfig = {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
pruneDays: number;
|
|
6
|
+
maxThreads: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Check if text matches a thread via word overlap (≥ minOverlap words from title in text).
|
|
10
|
+
* Words shorter than 3 characters are excluded.
|
|
11
|
+
*/
|
|
12
|
+
export declare function matchesThread(thread: Thread, text: string, minOverlap?: number): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Extract thread-related signals from message text.
|
|
15
|
+
*/
|
|
16
|
+
export declare function extractSignals(text: string, language: PatternLanguage): ThreadSignals;
|
|
17
|
+
/**
|
|
18
|
+
* Thread Tracker — manages conversation thread state.
|
|
19
|
+
*/
|
|
20
|
+
export declare class ThreadTracker {
|
|
21
|
+
private threads;
|
|
22
|
+
private dirty;
|
|
23
|
+
private writeable;
|
|
24
|
+
private eventsProcessed;
|
|
25
|
+
private lastEventTimestamp;
|
|
26
|
+
private sessionMood;
|
|
27
|
+
private readonly filePath;
|
|
28
|
+
private readonly config;
|
|
29
|
+
private readonly language;
|
|
30
|
+
private readonly logger;
|
|
31
|
+
constructor(workspace: string, config: ThreadTrackerConfig, language: PatternLanguage, logger: PluginLogger);
|
|
32
|
+
/** Create new threads from topic signals. */
|
|
33
|
+
private createFromTopics;
|
|
34
|
+
/** Close threads matching closure signals. */
|
|
35
|
+
private closeMatching;
|
|
36
|
+
/** Append decisions to matching threads. */
|
|
37
|
+
private applyDecisions;
|
|
38
|
+
/** Update waiting_for on matching threads. */
|
|
39
|
+
private applyWaits;
|
|
40
|
+
/** Update mood on active threads matching content. */
|
|
41
|
+
private applyMood;
|
|
42
|
+
/**
|
|
43
|
+
* Process a message: extract signals, update threads, persist.
|
|
44
|
+
*/
|
|
45
|
+
processMessage(content: string, sender: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Prune closed threads older than pruneDays and enforce maxThreads cap.
|
|
48
|
+
*/
|
|
49
|
+
private pruneAndCap;
|
|
50
|
+
/**
|
|
51
|
+
* Attempt to persist current state to disk.
|
|
52
|
+
*/
|
|
53
|
+
private persist;
|
|
54
|
+
/**
|
|
55
|
+
* Build the ThreadsData object for serialization.
|
|
56
|
+
*/
|
|
57
|
+
private buildData;
|
|
58
|
+
/**
|
|
59
|
+
* Force-flush state to disk. Called by pre-compaction.
|
|
60
|
+
*/
|
|
61
|
+
flush(): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Get current thread list (in-memory).
|
|
64
|
+
*/
|
|
65
|
+
getThreads(): Thread[];
|
|
66
|
+
/**
|
|
67
|
+
* Get current session mood.
|
|
68
|
+
*/
|
|
69
|
+
getSessionMood(): string;
|
|
70
|
+
/**
|
|
71
|
+
* Get events processed count.
|
|
72
|
+
*/
|
|
73
|
+
getEventsProcessed(): number;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=thread-tracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thread-tracker.d.ts","sourceRoot":"","sources":["../../src/thread-tracker.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,MAAM,EAEN,aAAa,EAEb,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGrD,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,SAAI,GAAG,OAAO,CAanF;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,aAAa,CAwCrF;AAaD;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,kBAAkB,CAAM;IAChC,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;gBAGpC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,mBAAmB,EAC3B,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,YAAY;IAgBtB,6CAA6C;IAC7C,OAAO,CAAC,gBAAgB;IAexB,8CAA8C;IAC9C,OAAO,CAAC,aAAa;IAUrB,4CAA4C;IAC5C,OAAO,CAAC,cAAc;IActB,8CAA8C;IAC9C,OAAO,CAAC,UAAU;IAWlB,sDAAsD;IACtD,OAAO,CAAC,SAAS;IASjB;;OAEG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAsBrD;;OAEG;IACH,OAAO,CAAC,WAAW;IAsBnB;;OAEG;IACH,OAAO,CAAC,OAAO;IAWf;;OAEG;IACH,OAAO,CAAC,SAAS;IAcjB;;OAEG;IACH,KAAK,IAAI,OAAO;IAKhB;;OAEG;IACH,UAAU,IAAI,MAAM,EAAE;IAItB;;OAEG;IACH,cAAc,IAAI,MAAM;IAIxB;;OAEG;IACH,kBAAkB,IAAI,MAAM;CAG7B"}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getPatterns, detectMood, HIGH_IMPACT_KEYWORDS } from "./patterns.js";
|
|
4
|
+
import { loadJson, saveJson, rebootDir, ensureRebootDir } from "./storage.js";
|
|
5
|
+
/**
|
|
6
|
+
* Check if text matches a thread via word overlap (≥ minOverlap words from title in text).
|
|
7
|
+
* Words shorter than 3 characters are excluded.
|
|
8
|
+
*/
|
|
9
|
+
export function matchesThread(thread, text, minOverlap = 2) {
|
|
10
|
+
const threadWords = new Set(thread.title.toLowerCase().split(/\s+/).filter(w => w.length > 2));
|
|
11
|
+
const textWords = new Set(text.toLowerCase().split(/\s+/).filter(w => w.length > 2));
|
|
12
|
+
let overlap = 0;
|
|
13
|
+
for (const word of threadWords) {
|
|
14
|
+
if (textWords.has(word))
|
|
15
|
+
overlap++;
|
|
16
|
+
}
|
|
17
|
+
return overlap >= minOverlap;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Extract thread-related signals from message text.
|
|
21
|
+
*/
|
|
22
|
+
export function extractSignals(text, language) {
|
|
23
|
+
const patterns = getPatterns(language);
|
|
24
|
+
const signals = { decisions: [], closures: [], waits: [], topics: [] };
|
|
25
|
+
for (const pattern of patterns.decision) {
|
|
26
|
+
const globalPattern = new RegExp(pattern.source, "gi");
|
|
27
|
+
let match;
|
|
28
|
+
while ((match = globalPattern.exec(text)) !== null) {
|
|
29
|
+
const start = Math.max(0, match.index - 50);
|
|
30
|
+
const end = Math.min(text.length, match.index + match[0].length + 100);
|
|
31
|
+
signals.decisions.push(text.slice(start, end).trim());
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const pattern of patterns.close) {
|
|
35
|
+
if (pattern.test(text)) {
|
|
36
|
+
signals.closures.push(true);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const pattern of patterns.wait) {
|
|
40
|
+
const globalPattern = new RegExp(pattern.source, "gi");
|
|
41
|
+
let match;
|
|
42
|
+
while ((match = globalPattern.exec(text)) !== null) {
|
|
43
|
+
const end = Math.min(text.length, match.index + match[0].length + 80);
|
|
44
|
+
signals.waits.push(text.slice(match.index, end).trim());
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const pattern of patterns.topic) {
|
|
48
|
+
const globalPattern = new RegExp(pattern.source, "gi");
|
|
49
|
+
let match;
|
|
50
|
+
while ((match = globalPattern.exec(text)) !== null) {
|
|
51
|
+
if (match[1]) {
|
|
52
|
+
signals.topics.push(match[1].trim());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return signals;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Infer thread priority from content.
|
|
60
|
+
*/
|
|
61
|
+
function inferPriority(text) {
|
|
62
|
+
const lower = text.toLowerCase();
|
|
63
|
+
for (const kw of HIGH_IMPACT_KEYWORDS) {
|
|
64
|
+
if (lower.includes(kw))
|
|
65
|
+
return "high";
|
|
66
|
+
}
|
|
67
|
+
return "medium";
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Thread Tracker — manages conversation thread state.
|
|
71
|
+
*/
|
|
72
|
+
export class ThreadTracker {
|
|
73
|
+
threads = [];
|
|
74
|
+
dirty = false;
|
|
75
|
+
writeable = true;
|
|
76
|
+
eventsProcessed = 0;
|
|
77
|
+
lastEventTimestamp = "";
|
|
78
|
+
sessionMood = "neutral";
|
|
79
|
+
filePath;
|
|
80
|
+
config;
|
|
81
|
+
language;
|
|
82
|
+
logger;
|
|
83
|
+
constructor(workspace, config, language, logger) {
|
|
84
|
+
this.config = config;
|
|
85
|
+
this.language = language;
|
|
86
|
+
this.logger = logger;
|
|
87
|
+
this.filePath = join(rebootDir(workspace), "threads.json");
|
|
88
|
+
// Ensure directory exists
|
|
89
|
+
ensureRebootDir(workspace, logger);
|
|
90
|
+
// Load existing state
|
|
91
|
+
const data = loadJson(this.filePath);
|
|
92
|
+
this.threads = Array.isArray(data.threads) ? data.threads : [];
|
|
93
|
+
this.sessionMood = data.session_mood ?? "neutral";
|
|
94
|
+
}
|
|
95
|
+
/** Create new threads from topic signals. */
|
|
96
|
+
createFromTopics(topics, sender, mood, now) {
|
|
97
|
+
for (const topic of topics) {
|
|
98
|
+
const exists = this.threads.some(t => t.title.toLowerCase() === topic.toLowerCase() || matchesThread(t, topic));
|
|
99
|
+
if (!exists) {
|
|
100
|
+
this.threads.push({
|
|
101
|
+
id: randomUUID(), title: topic, status: "open",
|
|
102
|
+
priority: inferPriority(topic), summary: `Topic detected from ${sender}`,
|
|
103
|
+
decisions: [], waiting_for: null, mood, last_activity: now, created: now,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Close threads matching closure signals. */
|
|
109
|
+
closeMatching(content, closures, now) {
|
|
110
|
+
if (closures.length === 0)
|
|
111
|
+
return;
|
|
112
|
+
for (const thread of this.threads) {
|
|
113
|
+
if (thread.status === "open" && matchesThread(thread, content)) {
|
|
114
|
+
thread.status = "closed";
|
|
115
|
+
thread.last_activity = now;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Append decisions to matching threads. */
|
|
120
|
+
applyDecisions(decisions, now) {
|
|
121
|
+
for (const ctx of decisions) {
|
|
122
|
+
for (const thread of this.threads) {
|
|
123
|
+
if (thread.status === "open" && matchesThread(thread, ctx)) {
|
|
124
|
+
const short = ctx.slice(0, 100);
|
|
125
|
+
if (!thread.decisions.includes(short)) {
|
|
126
|
+
thread.decisions.push(short);
|
|
127
|
+
thread.last_activity = now;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Update waiting_for on matching threads. */
|
|
134
|
+
applyWaits(waits, content, now) {
|
|
135
|
+
for (const waitCtx of waits) {
|
|
136
|
+
for (const thread of this.threads) {
|
|
137
|
+
if (thread.status === "open" && matchesThread(thread, content)) {
|
|
138
|
+
thread.waiting_for = waitCtx.slice(0, 100);
|
|
139
|
+
thread.last_activity = now;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Update mood on active threads matching content. */
|
|
145
|
+
applyMood(mood, content) {
|
|
146
|
+
if (mood === "neutral")
|
|
147
|
+
return;
|
|
148
|
+
for (const thread of this.threads) {
|
|
149
|
+
if (thread.status === "open" && matchesThread(thread, content)) {
|
|
150
|
+
thread.mood = mood;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Process a message: extract signals, update threads, persist.
|
|
156
|
+
*/
|
|
157
|
+
processMessage(content, sender) {
|
|
158
|
+
if (!content)
|
|
159
|
+
return;
|
|
160
|
+
const signals = extractSignals(content, this.language);
|
|
161
|
+
const mood = detectMood(content);
|
|
162
|
+
const now = new Date().toISOString();
|
|
163
|
+
this.eventsProcessed++;
|
|
164
|
+
this.lastEventTimestamp = now;
|
|
165
|
+
if (mood !== "neutral")
|
|
166
|
+
this.sessionMood = mood;
|
|
167
|
+
this.createFromTopics(signals.topics, sender, mood, now);
|
|
168
|
+
this.closeMatching(content, signals.closures, now);
|
|
169
|
+
this.applyDecisions(signals.decisions, now);
|
|
170
|
+
this.applyWaits(signals.waits, content, now);
|
|
171
|
+
this.applyMood(mood, content);
|
|
172
|
+
this.dirty = true;
|
|
173
|
+
this.pruneAndCap();
|
|
174
|
+
this.persist();
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Prune closed threads older than pruneDays and enforce maxThreads cap.
|
|
178
|
+
*/
|
|
179
|
+
pruneAndCap() {
|
|
180
|
+
const cutoff = new Date(Date.now() - this.config.pruneDays * 24 * 60 * 60 * 1000).toISOString();
|
|
181
|
+
// Remove closed threads older than cutoff
|
|
182
|
+
this.threads = this.threads.filter(t => !(t.status === "closed" && t.last_activity < cutoff));
|
|
183
|
+
// Enforce maxThreads cap — remove oldest closed threads first
|
|
184
|
+
if (this.threads.length > this.config.maxThreads) {
|
|
185
|
+
const open = this.threads.filter(t => t.status === "open");
|
|
186
|
+
const closed = this.threads
|
|
187
|
+
.filter(t => t.status === "closed")
|
|
188
|
+
.sort((a, b) => a.last_activity.localeCompare(b.last_activity));
|
|
189
|
+
const budget = this.config.maxThreads - open.length;
|
|
190
|
+
this.threads = [...open, ...closed.slice(Math.max(0, closed.length - budget))];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Attempt to persist current state to disk.
|
|
195
|
+
*/
|
|
196
|
+
persist() {
|
|
197
|
+
if (!this.writeable)
|
|
198
|
+
return;
|
|
199
|
+
const ok = saveJson(this.filePath, this.buildData(), this.logger);
|
|
200
|
+
if (!ok) {
|
|
201
|
+
this.writeable = false;
|
|
202
|
+
this.logger.warn("[cortex] Workspace not writable — running in-memory only");
|
|
203
|
+
}
|
|
204
|
+
if (ok)
|
|
205
|
+
this.dirty = false;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Build the ThreadsData object for serialization.
|
|
209
|
+
*/
|
|
210
|
+
buildData() {
|
|
211
|
+
return {
|
|
212
|
+
version: 2,
|
|
213
|
+
updated: new Date().toISOString(),
|
|
214
|
+
threads: this.threads,
|
|
215
|
+
integrity: {
|
|
216
|
+
last_event_timestamp: this.lastEventTimestamp || new Date().toISOString(),
|
|
217
|
+
events_processed: this.eventsProcessed,
|
|
218
|
+
source: "hooks",
|
|
219
|
+
},
|
|
220
|
+
session_mood: this.sessionMood,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Force-flush state to disk. Called by pre-compaction.
|
|
225
|
+
*/
|
|
226
|
+
flush() {
|
|
227
|
+
if (!this.dirty)
|
|
228
|
+
return true;
|
|
229
|
+
return saveJson(this.filePath, this.buildData(), this.logger);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get current thread list (in-memory).
|
|
233
|
+
*/
|
|
234
|
+
getThreads() {
|
|
235
|
+
return [...this.threads];
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get current session mood.
|
|
239
|
+
*/
|
|
240
|
+
getSessionMood() {
|
|
241
|
+
return this.sessionMood;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Get events processed count.
|
|
245
|
+
*/
|
|
246
|
+
getEventsProcessed() {
|
|
247
|
+
return this.eventsProcessed;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=thread-tracker.js.map
|