dsh-tdai-memory 0.2.13 → 0.3.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 -21
- package/README.md +15 -13
- package/README.zh.md +9 -7
- package/THIRD-PARTY-NOTICES.md +20 -20
- package/client.js +21 -4
- package/cordis.patch.yml +8 -8
- package/index.js +36 -14
- package/package.json +77 -78
- package/recall-inject.js +86 -86
- package/vendor/tdai/adapters/index.js +15 -15
- package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -83
- package/vendor/tdai/adapters/openclaw/index.js +5 -5
- package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -69
- package/vendor/tdai/adapters/standalone/host-adapter.js +58 -58
- package/vendor/tdai/adapters/standalone/index.js +5 -5
- package/vendor/tdai/adapters/standalone/llm-runner.js +34 -6
- package/vendor/tdai/config.js +3 -0
- package/vendor/tdai/core/conversation/l0-recorder.js +420 -420
- package/vendor/tdai/core/hooks/auto-capture.js +236 -236
- package/vendor/tdai/core/hooks/auto-recall.js +640 -640
- package/vendor/tdai/core/index.js +8 -8
- package/vendor/tdai/core/persona/persona-generator.js +184 -184
- package/vendor/tdai/core/persona/persona-trigger.js +94 -94
- package/vendor/tdai/core/profile/profile-sync.js +200 -200
- package/vendor/tdai/core/prompts/l1-dedup.js +72 -72
- package/vendor/tdai/core/prompts/l1-extraction.js +34 -34
- package/vendor/tdai/core/prompts/persona-generation.js +35 -35
- package/vendor/tdai/core/prompts/scene-extraction.js +45 -45
- package/vendor/tdai/core/record/l1-dedup.js +318 -318
- package/vendor/tdai/core/record/l1-extractor.js +379 -379
- package/vendor/tdai/core/record/l1-reader.js +178 -178
- package/vendor/tdai/core/record/l1-writer.js +141 -141
- package/vendor/tdai/core/report/reporter.js +86 -86
- package/vendor/tdai/core/scene/filename-normalizer.js +161 -161
- package/vendor/tdai/core/scene/scene-extractor.js +410 -410
- package/vendor/tdai/core/scene/scene-format.js +53 -53
- package/vendor/tdai/core/scene/scene-index.js +82 -82
- package/vendor/tdai/core/scene/scene-navigation.js +57 -57
- package/vendor/tdai/core/seed/input.js +404 -404
- package/vendor/tdai/core/seed/seed-runtime.js +4 -0
- package/vendor/tdai/core/seed/types.js +9 -9
- package/vendor/tdai/core/store/bm25-client.js +117 -117
- package/vendor/tdai/core/store/bm25-local.js +68 -68
- package/vendor/tdai/core/store/embedding.js +632 -632
- package/vendor/tdai/core/store/factory.js +95 -95
- package/vendor/tdai/core/store/search-utils.js +54 -54
- package/vendor/tdai/core/store/sqlite.js +1632 -1632
- package/vendor/tdai/core/store/tcvdb-client.js +221 -221
- package/vendor/tdai/core/store/tcvdb.js +1047 -1047
- package/vendor/tdai/core/store/types.js +17 -17
- package/vendor/tdai/core/tdai-core.js +4 -0
- package/vendor/tdai/core/tools/conversation-search.js +199 -199
- package/vendor/tdai/core/tools/memory-search.js +207 -207
- package/vendor/tdai/core/types.js +12 -12
- package/vendor/tdai/utils/backup.js +189 -189
- package/vendor/tdai/utils/checkpoint.js +360 -360
- package/vendor/tdai/utils/clean-context-runner.js +472 -472
- package/vendor/tdai/utils/ensure-hook-policy.js +213 -213
- package/vendor/tdai/utils/env.js +13 -13
- package/vendor/tdai/utils/managed-timer.js +127 -127
- package/vendor/tdai/utils/manifest.js +91 -91
- package/vendor/tdai/utils/memory-cleaner.js +300 -300
- package/vendor/tdai/utils/no-think-fetch.js +109 -109
- package/vendor/tdai/utils/openclaw-state-dir.js +26 -26
- package/vendor/tdai/utils/pipeline-factory.js +517 -517
- package/vendor/tdai/utils/pipeline-manager.js +892 -892
- package/vendor/tdai/utils/sanitize.js +360 -360
- package/vendor/tdai/utils/serial-queue.js +103 -103
- package/vendor/tdai/utils/session-filter.js +88 -88
- package/vendor/tdai/utils/session-header-fetch.js +88 -0
- package/vendor/tdai/utils/text-utils.js +29 -29
- package/vendor/tdai/utils/time.js +252 -252
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SerialQueue: a lightweight task queue with concurrency=1.
|
|
3
|
-
*
|
|
4
|
-
* Equivalent to `new PQueue({ concurrency: 1 })` but with zero external
|
|
5
|
-
* dependencies. Supports:
|
|
6
|
-
* - Serial execution (FIFO)
|
|
7
|
-
* - `add(fn)` to enqueue a task (returns the task's result promise)
|
|
8
|
-
* - `onIdle()` to wait until all queued tasks have completed
|
|
9
|
-
* - `pause()` / `start()` to suspend/resume execution
|
|
10
|
-
* - `size` to check pending task count
|
|
11
|
-
* - Optional debug logger for enqueue/dequeue/complete diagnostics
|
|
12
|
-
*/
|
|
13
|
-
export class SerialQueue {
|
|
14
|
-
/** Human-readable name for logging / diagnostics. */
|
|
15
|
-
name;
|
|
16
|
-
queue = [];
|
|
17
|
-
running = false;
|
|
18
|
-
paused = false;
|
|
19
|
-
idleResolvers = [];
|
|
20
|
-
/** Optional debug logger — receives diagnostic messages for enqueue/dequeue/complete. */
|
|
21
|
-
debugFn;
|
|
22
|
-
constructor(name = "unnamed") {
|
|
23
|
-
this.name = name;
|
|
24
|
-
}
|
|
25
|
-
/** Set a debug logger for queue diagnostics. */
|
|
26
|
-
setDebugLogger(fn) {
|
|
27
|
-
this.debugFn = fn;
|
|
28
|
-
}
|
|
29
|
-
/** Number of tasks waiting to be executed. */
|
|
30
|
-
get size() {
|
|
31
|
-
return this.queue.length;
|
|
32
|
-
}
|
|
33
|
-
/** Whether a task is currently executing. */
|
|
34
|
-
get pending() {
|
|
35
|
-
return this.running;
|
|
36
|
-
}
|
|
37
|
-
/** Whether the queue is idle (no queued tasks and nothing running). */
|
|
38
|
-
get idle() {
|
|
39
|
-
return this.queue.length === 0 && !this.running;
|
|
40
|
-
}
|
|
41
|
-
/** Add a task to the queue. Returns the task's result promise. */
|
|
42
|
-
add(task) {
|
|
43
|
-
return new Promise((resolve, reject) => {
|
|
44
|
-
this.queue.push({
|
|
45
|
-
task: task,
|
|
46
|
-
resolve: resolve,
|
|
47
|
-
reject,
|
|
48
|
-
});
|
|
49
|
-
this.debugFn?.(`[queue:${this.name}] enqueued, pending=${this.queue.length}, running=${this.running}`);
|
|
50
|
-
this.drain();
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
/** Pause the queue. Currently running task will finish, but no new tasks start. */
|
|
54
|
-
pause() {
|
|
55
|
-
this.paused = true;
|
|
56
|
-
}
|
|
57
|
-
/** Resume the queue after pause(). */
|
|
58
|
-
start() {
|
|
59
|
-
this.paused = false;
|
|
60
|
-
this.drain();
|
|
61
|
-
}
|
|
62
|
-
/** Returns a promise that resolves when all queued tasks have completed. */
|
|
63
|
-
onIdle() {
|
|
64
|
-
if (this.queue.length === 0 && !this.running) {
|
|
65
|
-
return Promise.resolve();
|
|
66
|
-
}
|
|
67
|
-
return new Promise((resolve) => {
|
|
68
|
-
this.idleResolvers.push(resolve);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
/** Clear all pending (not yet started) tasks. */
|
|
72
|
-
clear() {
|
|
73
|
-
for (const entry of this.queue) {
|
|
74
|
-
entry.reject(new Error("Queue cleared"));
|
|
75
|
-
}
|
|
76
|
-
this.queue = [];
|
|
77
|
-
}
|
|
78
|
-
drain() {
|
|
79
|
-
if (this.running || this.paused || this.queue.length === 0)
|
|
80
|
-
return;
|
|
81
|
-
const entry = this.queue.shift();
|
|
82
|
-
this.running = true;
|
|
83
|
-
this.debugFn?.(`[queue:${this.name}] dequeued, starting execution (remaining=${this.queue.length})`);
|
|
84
|
-
entry
|
|
85
|
-
.task()
|
|
86
|
-
.then((result) => entry.resolve(result))
|
|
87
|
-
.catch((err) => entry.reject(err))
|
|
88
|
-
.finally(() => {
|
|
89
|
-
this.running = false;
|
|
90
|
-
this.debugFn?.(`[queue:${this.name}] task completed (remaining=${this.queue.length})`);
|
|
91
|
-
if (this.queue.length === 0) {
|
|
92
|
-
// Notify idle waiters
|
|
93
|
-
const resolvers = this.idleResolvers;
|
|
94
|
-
this.idleResolvers = [];
|
|
95
|
-
for (const resolve of resolvers)
|
|
96
|
-
resolve();
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
this.drain();
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* SerialQueue: a lightweight task queue with concurrency=1.
|
|
3
|
+
*
|
|
4
|
+
* Equivalent to `new PQueue({ concurrency: 1 })` but with zero external
|
|
5
|
+
* dependencies. Supports:
|
|
6
|
+
* - Serial execution (FIFO)
|
|
7
|
+
* - `add(fn)` to enqueue a task (returns the task's result promise)
|
|
8
|
+
* - `onIdle()` to wait until all queued tasks have completed
|
|
9
|
+
* - `pause()` / `start()` to suspend/resume execution
|
|
10
|
+
* - `size` to check pending task count
|
|
11
|
+
* - Optional debug logger for enqueue/dequeue/complete diagnostics
|
|
12
|
+
*/
|
|
13
|
+
export class SerialQueue {
|
|
14
|
+
/** Human-readable name for logging / diagnostics. */
|
|
15
|
+
name;
|
|
16
|
+
queue = [];
|
|
17
|
+
running = false;
|
|
18
|
+
paused = false;
|
|
19
|
+
idleResolvers = [];
|
|
20
|
+
/** Optional debug logger — receives diagnostic messages for enqueue/dequeue/complete. */
|
|
21
|
+
debugFn;
|
|
22
|
+
constructor(name = "unnamed") {
|
|
23
|
+
this.name = name;
|
|
24
|
+
}
|
|
25
|
+
/** Set a debug logger for queue diagnostics. */
|
|
26
|
+
setDebugLogger(fn) {
|
|
27
|
+
this.debugFn = fn;
|
|
28
|
+
}
|
|
29
|
+
/** Number of tasks waiting to be executed. */
|
|
30
|
+
get size() {
|
|
31
|
+
return this.queue.length;
|
|
32
|
+
}
|
|
33
|
+
/** Whether a task is currently executing. */
|
|
34
|
+
get pending() {
|
|
35
|
+
return this.running;
|
|
36
|
+
}
|
|
37
|
+
/** Whether the queue is idle (no queued tasks and nothing running). */
|
|
38
|
+
get idle() {
|
|
39
|
+
return this.queue.length === 0 && !this.running;
|
|
40
|
+
}
|
|
41
|
+
/** Add a task to the queue. Returns the task's result promise. */
|
|
42
|
+
add(task) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
this.queue.push({
|
|
45
|
+
task: task,
|
|
46
|
+
resolve: resolve,
|
|
47
|
+
reject,
|
|
48
|
+
});
|
|
49
|
+
this.debugFn?.(`[queue:${this.name}] enqueued, pending=${this.queue.length}, running=${this.running}`);
|
|
50
|
+
this.drain();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** Pause the queue. Currently running task will finish, but no new tasks start. */
|
|
54
|
+
pause() {
|
|
55
|
+
this.paused = true;
|
|
56
|
+
}
|
|
57
|
+
/** Resume the queue after pause(). */
|
|
58
|
+
start() {
|
|
59
|
+
this.paused = false;
|
|
60
|
+
this.drain();
|
|
61
|
+
}
|
|
62
|
+
/** Returns a promise that resolves when all queued tasks have completed. */
|
|
63
|
+
onIdle() {
|
|
64
|
+
if (this.queue.length === 0 && !this.running) {
|
|
65
|
+
return Promise.resolve();
|
|
66
|
+
}
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
this.idleResolvers.push(resolve);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** Clear all pending (not yet started) tasks. */
|
|
72
|
+
clear() {
|
|
73
|
+
for (const entry of this.queue) {
|
|
74
|
+
entry.reject(new Error("Queue cleared"));
|
|
75
|
+
}
|
|
76
|
+
this.queue = [];
|
|
77
|
+
}
|
|
78
|
+
drain() {
|
|
79
|
+
if (this.running || this.paused || this.queue.length === 0)
|
|
80
|
+
return;
|
|
81
|
+
const entry = this.queue.shift();
|
|
82
|
+
this.running = true;
|
|
83
|
+
this.debugFn?.(`[queue:${this.name}] dequeued, starting execution (remaining=${this.queue.length})`);
|
|
84
|
+
entry
|
|
85
|
+
.task()
|
|
86
|
+
.then((result) => entry.resolve(result))
|
|
87
|
+
.catch((err) => entry.reject(err))
|
|
88
|
+
.finally(() => {
|
|
89
|
+
this.running = false;
|
|
90
|
+
this.debugFn?.(`[queue:${this.name}] task completed (remaining=${this.queue.length})`);
|
|
91
|
+
if (this.queue.length === 0) {
|
|
92
|
+
// Notify idle waiters
|
|
93
|
+
const resolvers = this.idleResolvers;
|
|
94
|
+
this.idleResolvers = [];
|
|
95
|
+
for (const resolve of resolvers)
|
|
96
|
+
resolve();
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
this.drain();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session filtering for memory-tdai.
|
|
3
|
-
*
|
|
4
|
-
* Decides whether a session should be ignored by the memory plugin
|
|
5
|
-
* (capture, recall, pipeline scheduling). All skip rules are compiled
|
|
6
|
-
* into a flat list of matchers at construction time — zero per-call overhead.
|
|
7
|
-
*/
|
|
8
|
-
// ============================
|
|
9
|
-
// Non-interactive trigger detection
|
|
10
|
-
// ============================
|
|
11
|
-
const SKIP_TRIGGERS = new Set(["cron", "heartbeat", "automation", "schedule"]);
|
|
12
|
-
/**
|
|
13
|
-
* Returns true when the hook was fired by a non-interactive trigger
|
|
14
|
-
* (heartbeat, cron job, automation, etc.) — these produce no meaningful
|
|
15
|
-
* user conversation and should not be captured or counted.
|
|
16
|
-
*/
|
|
17
|
-
export function isNonInteractiveTrigger(trigger, sessionKey) {
|
|
18
|
-
if (trigger && SKIP_TRIGGERS.has(trigger.toLowerCase()))
|
|
19
|
-
return true;
|
|
20
|
-
if (sessionKey) {
|
|
21
|
-
if (/:cron:/i.test(sessionKey) || /:heartbeat:/i.test(sessionKey))
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
// ============================
|
|
27
|
-
// Built-in skip rules (always active)
|
|
28
|
-
// ============================
|
|
29
|
-
/**
|
|
30
|
-
* Hard-coded matchers that identify internal / non-user sessions.
|
|
31
|
-
* These are always applied regardless of user configuration.
|
|
32
|
-
*/
|
|
33
|
-
const BUILTIN_MATCHERS = [
|
|
34
|
-
// Scene extraction runner sessions
|
|
35
|
-
(key) => key.includes(":memory-scene-extract-"),
|
|
36
|
-
// OpenClaw subagent sessions
|
|
37
|
-
(key) => key.includes(":subagent:"),
|
|
38
|
-
// Temporary / internal utility sessions (e.g. temp:slug-generator)
|
|
39
|
-
(key) => key.startsWith("temp:"),
|
|
40
|
-
];
|
|
41
|
-
// ============================
|
|
42
|
-
// Glob → matcher compiler
|
|
43
|
-
// ============================
|
|
44
|
-
/**
|
|
45
|
-
* Turn a simple glob pattern (only `*` supported) into a matcher
|
|
46
|
-
* that tests the full sessionKey.
|
|
47
|
-
*
|
|
48
|
-
* Since sessionKeys look like `agent:<agentId>:...`, we match the
|
|
49
|
-
* glob against the whole key so users can write patterns like
|
|
50
|
-
* `bench-judge-*` (matched anywhere) or more specific ones.
|
|
51
|
-
*/
|
|
52
|
-
function globToMatcher(pattern) {
|
|
53
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
54
|
-
const re = new RegExp(escaped);
|
|
55
|
-
return (key) => re.test(key);
|
|
56
|
-
}
|
|
57
|
-
// ============================
|
|
58
|
-
// SessionFilter
|
|
59
|
-
// ============================
|
|
60
|
-
/**
|
|
61
|
-
* Unified filter: construct once at plugin startup, then call
|
|
62
|
-
* `shouldSkip(sessionKey)` or `shouldSkipCtx(ctx)` at each gate.
|
|
63
|
-
*/
|
|
64
|
-
export class SessionFilter {
|
|
65
|
-
matchers;
|
|
66
|
-
constructor(excludeAgents = []) {
|
|
67
|
-
// Merge built-in rules + user-configured exclude patterns into one flat list
|
|
68
|
-
const userMatchers = excludeAgents
|
|
69
|
-
.map((p) => p.trim())
|
|
70
|
-
.filter((p) => p.length > 0)
|
|
71
|
-
.map(globToMatcher);
|
|
72
|
-
this.matchers = [...BUILTIN_MATCHERS, ...userMatchers];
|
|
73
|
-
}
|
|
74
|
-
/** Should this sessionKey be skipped? */
|
|
75
|
-
shouldSkip(sessionKey) {
|
|
76
|
-
return this.matchers.some((m) => m(sessionKey));
|
|
77
|
-
}
|
|
78
|
-
/** Should this hook context be skipped? */
|
|
79
|
-
shouldSkipCtx(ctx) {
|
|
80
|
-
if (!ctx.sessionKey)
|
|
81
|
-
return true;
|
|
82
|
-
if (ctx.sessionId?.startsWith("memory-"))
|
|
83
|
-
return true;
|
|
84
|
-
if (isNonInteractiveTrigger(ctx.trigger, ctx.sessionKey))
|
|
85
|
-
return true;
|
|
86
|
-
return this.shouldSkip(ctx.sessionKey);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Session filtering for memory-tdai.
|
|
3
|
+
*
|
|
4
|
+
* Decides whether a session should be ignored by the memory plugin
|
|
5
|
+
* (capture, recall, pipeline scheduling). All skip rules are compiled
|
|
6
|
+
* into a flat list of matchers at construction time — zero per-call overhead.
|
|
7
|
+
*/
|
|
8
|
+
// ============================
|
|
9
|
+
// Non-interactive trigger detection
|
|
10
|
+
// ============================
|
|
11
|
+
const SKIP_TRIGGERS = new Set(["cron", "heartbeat", "automation", "schedule"]);
|
|
12
|
+
/**
|
|
13
|
+
* Returns true when the hook was fired by a non-interactive trigger
|
|
14
|
+
* (heartbeat, cron job, automation, etc.) — these produce no meaningful
|
|
15
|
+
* user conversation and should not be captured or counted.
|
|
16
|
+
*/
|
|
17
|
+
export function isNonInteractiveTrigger(trigger, sessionKey) {
|
|
18
|
+
if (trigger && SKIP_TRIGGERS.has(trigger.toLowerCase()))
|
|
19
|
+
return true;
|
|
20
|
+
if (sessionKey) {
|
|
21
|
+
if (/:cron:/i.test(sessionKey) || /:heartbeat:/i.test(sessionKey))
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
// ============================
|
|
27
|
+
// Built-in skip rules (always active)
|
|
28
|
+
// ============================
|
|
29
|
+
/**
|
|
30
|
+
* Hard-coded matchers that identify internal / non-user sessions.
|
|
31
|
+
* These are always applied regardless of user configuration.
|
|
32
|
+
*/
|
|
33
|
+
const BUILTIN_MATCHERS = [
|
|
34
|
+
// Scene extraction runner sessions
|
|
35
|
+
(key) => key.includes(":memory-scene-extract-"),
|
|
36
|
+
// OpenClaw subagent sessions
|
|
37
|
+
(key) => key.includes(":subagent:"),
|
|
38
|
+
// Temporary / internal utility sessions (e.g. temp:slug-generator)
|
|
39
|
+
(key) => key.startsWith("temp:"),
|
|
40
|
+
];
|
|
41
|
+
// ============================
|
|
42
|
+
// Glob → matcher compiler
|
|
43
|
+
// ============================
|
|
44
|
+
/**
|
|
45
|
+
* Turn a simple glob pattern (only `*` supported) into a matcher
|
|
46
|
+
* that tests the full sessionKey.
|
|
47
|
+
*
|
|
48
|
+
* Since sessionKeys look like `agent:<agentId>:...`, we match the
|
|
49
|
+
* glob against the whole key so users can write patterns like
|
|
50
|
+
* `bench-judge-*` (matched anywhere) or more specific ones.
|
|
51
|
+
*/
|
|
52
|
+
function globToMatcher(pattern) {
|
|
53
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
54
|
+
const re = new RegExp(escaped);
|
|
55
|
+
return (key) => re.test(key);
|
|
56
|
+
}
|
|
57
|
+
// ============================
|
|
58
|
+
// SessionFilter
|
|
59
|
+
// ============================
|
|
60
|
+
/**
|
|
61
|
+
* Unified filter: construct once at plugin startup, then call
|
|
62
|
+
* `shouldSkip(sessionKey)` or `shouldSkipCtx(ctx)` at each gate.
|
|
63
|
+
*/
|
|
64
|
+
export class SessionFilter {
|
|
65
|
+
matchers;
|
|
66
|
+
constructor(excludeAgents = []) {
|
|
67
|
+
// Merge built-in rules + user-configured exclude patterns into one flat list
|
|
68
|
+
const userMatchers = excludeAgents
|
|
69
|
+
.map((p) => p.trim())
|
|
70
|
+
.filter((p) => p.length > 0)
|
|
71
|
+
.map(globToMatcher);
|
|
72
|
+
this.matchers = [...BUILTIN_MATCHERS, ...userMatchers];
|
|
73
|
+
}
|
|
74
|
+
/** Should this sessionKey be skipped? */
|
|
75
|
+
shouldSkip(sessionKey) {
|
|
76
|
+
return this.matchers.some((m) => m(sessionKey));
|
|
77
|
+
}
|
|
78
|
+
/** Should this hook context be skipped? */
|
|
79
|
+
shouldSkipCtx(ctx) {
|
|
80
|
+
if (!ctx.sessionKey)
|
|
81
|
+
return true;
|
|
82
|
+
if (ctx.sessionId?.startsWith("memory-"))
|
|
83
|
+
return true;
|
|
84
|
+
if (isNonInteractiveTrigger(ctx.trigger, ctx.sessionKey))
|
|
85
|
+
return true;
|
|
86
|
+
return this.shouldSkip(ctx.sessionKey);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch wrapper that injects a stable session-id header (`x-opencode-session`)
|
|
3
|
+
* into outgoing LLM requests.
|
|
4
|
+
*
|
|
5
|
+
* OpenCode Go (and other OpenAI-compatible gateways) require every request to
|
|
6
|
+
* carry a stable per-conversation id; requests without it started erroring
|
|
7
|
+
* after 2026-09-06. The value is:
|
|
8
|
+
*
|
|
9
|
+
* 1. An explicit configured `sessionId`, when set;
|
|
10
|
+
* 2. Otherwise a persistent per-instance random id. The id is generated once
|
|
11
|
+
* and stored under the TDAI data dir (`<dataDir>/session-id`), so it
|
|
12
|
+
* survives restarts — one stable id per plugin instance, which is exactly
|
|
13
|
+
* what the gateway asks for when the client has no real conversation.
|
|
14
|
+
*
|
|
15
|
+
* Embedding and other non-chat traffic still gets the header (harmless, and
|
|
16
|
+
* gateways may require it on every request). When disabled, returns
|
|
17
|
+
* `globalThis.fetch` unchanged.
|
|
18
|
+
*/
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
22
|
+
|
|
23
|
+
const SESSION_ID_FILE = "session-id";
|
|
24
|
+
|
|
25
|
+
let cachedId = "";
|
|
26
|
+
/** Load-or-create a persistent per-instance session id under `dataDir`. */
|
|
27
|
+
export async function resolvePersistentSessionId(dataDir) {
|
|
28
|
+
if (cachedId)
|
|
29
|
+
return cachedId;
|
|
30
|
+
if (!dataDir) {
|
|
31
|
+
cachedId = `tdai-memory-${randomUUID()}`;
|
|
32
|
+
return cachedId;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const file = join(dataDir, SESSION_ID_FILE);
|
|
36
|
+
const existing = await readFile(file, "utf8").catch(() => "");
|
|
37
|
+
const trimmed = existing.trim();
|
|
38
|
+
if (trimmed) {
|
|
39
|
+
cachedId = trimmed;
|
|
40
|
+
return cachedId;
|
|
41
|
+
}
|
|
42
|
+
const fresh = `tdai-memory-${randomUUID()}`;
|
|
43
|
+
await mkdir(dataDir, { recursive: true });
|
|
44
|
+
await writeFile(file, fresh, "utf8");
|
|
45
|
+
cachedId = fresh;
|
|
46
|
+
return cachedId;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Any persistence failure degrades to a per-process id.
|
|
50
|
+
cachedId = `tdai-memory-${randomUUID()}`;
|
|
51
|
+
return cachedId;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build a session-id header value for a request.
|
|
57
|
+
* @param {object} opts { send, headerName, sessionId, persistentId }
|
|
58
|
+
* @returns {Record<string,string>|undefined} header object, or undefined when disabled.
|
|
59
|
+
*/
|
|
60
|
+
export function sessionHeaderOf({ send, headerName, sessionId, persistentId }) {
|
|
61
|
+
if (send === false)
|
|
62
|
+
return undefined;
|
|
63
|
+
const id = (sessionId && String(sessionId).trim()) || persistentId || "";
|
|
64
|
+
if (!id)
|
|
65
|
+
return undefined;
|
|
66
|
+
return { [headerName || "x-opencode-session"]: id };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a fetch wrapper that injects the session-id header into every request.
|
|
71
|
+
* @param {object} opts { send, headerName, sessionId, persistentId }
|
|
72
|
+
* @returns {typeof globalThis.fetch}
|
|
73
|
+
*/
|
|
74
|
+
export function createSessionHeaderFetch({ send, headerName, sessionId, persistentId }) {
|
|
75
|
+
if (send === false)
|
|
76
|
+
return globalThis.fetch;
|
|
77
|
+
const header = sessionHeaderOf({ send, headerName, sessionId, persistentId });
|
|
78
|
+
if (!header)
|
|
79
|
+
return globalThis.fetch;
|
|
80
|
+
return (async (input, init) => {
|
|
81
|
+
const next = init ? { ...init } : {};
|
|
82
|
+
next.headers = new Headers(init?.headers);
|
|
83
|
+
for (const [k, v] of Object.entries(header)) {
|
|
84
|
+
next.headers.set(k, v);
|
|
85
|
+
}
|
|
86
|
+
return globalThis.fetch(input, next);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared text utility functions for the memory-tdai plugin.
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* Extract meaningful words from text (supports CJK and Latin).
|
|
6
|
-
*
|
|
7
|
-
* Used by both auto-recall (keyword search) and l1-dedup (keyword candidate recall).
|
|
8
|
-
* Extracted to a shared module to prevent implementation drift.
|
|
9
|
-
*/
|
|
10
|
-
export function extractWords(text) {
|
|
11
|
-
const words = new Set();
|
|
12
|
-
// Latin words (2+ chars)
|
|
13
|
-
const latinWords = text.toLowerCase().match(/[a-z0-9]{2,}/g);
|
|
14
|
-
if (latinWords) {
|
|
15
|
-
for (const w of latinWords)
|
|
16
|
-
words.add(w);
|
|
17
|
-
}
|
|
18
|
-
// CJK characters (each char as a "word", plus 2-gram)
|
|
19
|
-
const cjkChars = text.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g);
|
|
20
|
-
if (cjkChars) {
|
|
21
|
-
for (const c of cjkChars)
|
|
22
|
-
words.add(c);
|
|
23
|
-
// 2-grams for better matching
|
|
24
|
-
for (let i = 0; i < cjkChars.length - 1; i++) {
|
|
25
|
-
words.add(cjkChars[i] + cjkChars[i + 1]);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return words;
|
|
29
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Shared text utility functions for the memory-tdai plugin.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Extract meaningful words from text (supports CJK and Latin).
|
|
6
|
+
*
|
|
7
|
+
* Used by both auto-recall (keyword search) and l1-dedup (keyword candidate recall).
|
|
8
|
+
* Extracted to a shared module to prevent implementation drift.
|
|
9
|
+
*/
|
|
10
|
+
export function extractWords(text) {
|
|
11
|
+
const words = new Set();
|
|
12
|
+
// Latin words (2+ chars)
|
|
13
|
+
const latinWords = text.toLowerCase().match(/[a-z0-9]{2,}/g);
|
|
14
|
+
if (latinWords) {
|
|
15
|
+
for (const w of latinWords)
|
|
16
|
+
words.add(w);
|
|
17
|
+
}
|
|
18
|
+
// CJK characters (each char as a "word", plus 2-gram)
|
|
19
|
+
const cjkChars = text.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g);
|
|
20
|
+
if (cjkChars) {
|
|
21
|
+
for (const c of cjkChars)
|
|
22
|
+
words.add(c);
|
|
23
|
+
// 2-grams for better matching
|
|
24
|
+
for (let i = 0; i < cjkChars.length - 1; i++) {
|
|
25
|
+
words.add(cjkChars[i] + cjkChars[i + 1]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return words;
|
|
29
|
+
}
|