soulbridge 0.0.12 → 0.0.13
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/dist/packages/cli/src/cli.js +17 -0
- package/dist/packages/core/src/index.d.ts +12 -2
- package/dist/packages/core/src/index.js +23 -1
- package/dist/packages/core/src/reflect.d.ts +4 -0
- package/dist/packages/core/src/reflect.js +9 -0
- package/dist/packages/core/src/sqlite.js +55 -0
- package/dist/packages/core/src/store.d.ts +5 -1
- package/dist/packages/core/src/types.d.ts +27 -0
- package/package.json +1 -1
|
@@ -324,6 +324,22 @@ async function main() {
|
|
|
324
324
|
out({ ok: soul.forget(id), id });
|
|
325
325
|
return;
|
|
326
326
|
}
|
|
327
|
+
case 'corrections': {
|
|
328
|
+
const { soul } = open(flags);
|
|
329
|
+
const rows = soul.corrections(flags.limit ? Number(flags.limit) : 10);
|
|
330
|
+
if (rows.length === 0) {
|
|
331
|
+
process.stdout.write('no corrections recorded yet\n');
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
for (const row of rows) {
|
|
335
|
+
const when = new Date(row.at).toISOString().slice(0, 19).replace('T', ' ');
|
|
336
|
+
process.stdout.write(`${when} [${row.rule ?? '-'}]\n`);
|
|
337
|
+
process.stdout.write(` 用户: ${row.userText}\n`);
|
|
338
|
+
if (row.assistantText)
|
|
339
|
+
process.stdout.write(` 当时: ${row.assistantText}\n`);
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
327
343
|
case 'status': {
|
|
328
344
|
const config = ensureConfig();
|
|
329
345
|
const dbPath = resolveDbPath(flags.db);
|
|
@@ -393,6 +409,7 @@ async function main() {
|
|
|
393
409
|
' import --from <steering.md> 导入 AGENTS.md / CLAUDE.md',
|
|
394
410
|
' ingest --file <transcript.jsonl> 会话转写 → 记忆与情绪采样',
|
|
395
411
|
' list [--limit N] 看它记住了什么',
|
|
412
|
+
' corrections [--limit N] 看被纠正过的证据',
|
|
396
413
|
' forget <id> 忘掉一条记忆(墓碑)',
|
|
397
414
|
' status 一眼看它是否在工作',
|
|
398
415
|
' stats 看原始状态(JSON)',
|
|
@@ -8,7 +8,7 @@ export * from './types.ts';
|
|
|
8
8
|
export type { SoulStore } from './store.ts';
|
|
9
9
|
export { openSqlite, SCHEMA_VERSION, AFFECT_RETENTION_MS, DbNewerThanBinaryError } from './sqlite.ts';
|
|
10
10
|
export { buildContext, styleDirectives, DEFAULT_MAX_CHARS, AFFECT_BUDGET, AFFECT_WINDOW_MS, } from './context.ts';
|
|
11
|
-
export { reflect, ruleExtract, nextEmotion } from './reflect.ts';
|
|
11
|
+
export { reflect, ruleExtract, nextEmotion, isCorrection, correctionRule } from './reflect.ts';
|
|
12
12
|
export { rank, scoreMemory, tokenize, relevance } from './scoring.ts';
|
|
13
13
|
export * from './affect/index.ts';
|
|
14
14
|
export { measureStyle } from './style/measure.ts';
|
|
@@ -17,7 +17,7 @@ export * from './storage/config.ts';
|
|
|
17
17
|
export * from './storage/merge.ts';
|
|
18
18
|
export * from './storage/steering.ts';
|
|
19
19
|
export * from './storage/transcript.ts';
|
|
20
|
-
import type { AffectSignal, ContextRequest, Memory, ContextResult, Emotion, LlmFn, MemoryType, ObserveResult, ReflectResult, ScoredMemory, Turn, Visibility } from './types.ts';
|
|
20
|
+
import type { AffectSignal, ContextRequest, Correction, Memory, ContextResult, Emotion, LlmFn, MemoryType, ObserveResult, ReflectResult, ScoredMemory, Turn, Visibility } from './types.ts';
|
|
21
21
|
import type { SoulStore } from './store.ts';
|
|
22
22
|
import type { AffectAnalyzer } from './affect/port.ts';
|
|
23
23
|
export interface SoulOptions {
|
|
@@ -56,6 +56,16 @@ export declare class Soul {
|
|
|
56
56
|
list(limit?: number): Memory[];
|
|
57
57
|
/** Tombstone one memory so it never reaches a prompt again. */
|
|
58
58
|
forget(id: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Record a user correction as evidence (RFC 0003 phase A). Nothing here
|
|
61
|
+
* reaches a prompt; distillation to a strategy is a later phase.
|
|
62
|
+
*/
|
|
63
|
+
recordCorrection(input: {
|
|
64
|
+
userText: string;
|
|
65
|
+
assistantText?: string;
|
|
66
|
+
rule?: string;
|
|
67
|
+
}): string;
|
|
68
|
+
corrections(limit?: number): Correction[];
|
|
59
69
|
/** Verb 3: on demand. */
|
|
60
70
|
recall(query: string, limit?: number): ScoredMemory[];
|
|
61
71
|
/** Pure analysis, no storage. Async because the analyzer may call a model. */
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
export * from './types.js';
|
|
8
8
|
export { openSqlite, SCHEMA_VERSION, AFFECT_RETENTION_MS, DbNewerThanBinaryError } from './sqlite.js';
|
|
9
9
|
export { buildContext, styleDirectives, DEFAULT_MAX_CHARS, AFFECT_BUDGET, AFFECT_WINDOW_MS, } from './context.js';
|
|
10
|
-
export { reflect, ruleExtract, nextEmotion } from './reflect.js';
|
|
10
|
+
export { reflect, ruleExtract, nextEmotion, isCorrection, correctionRule } from './reflect.js';
|
|
11
11
|
export { rank, scoreMemory, tokenize, relevance } from './scoring.js';
|
|
12
12
|
export * from './affect/index.js';
|
|
13
13
|
export { measureStyle } from './style/measure.js';
|
|
@@ -67,6 +67,28 @@ export class Soul {
|
|
|
67
67
|
forget(id) {
|
|
68
68
|
return this.store.forget(id);
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Record a user correction as evidence (RFC 0003 phase A). Nothing here
|
|
72
|
+
* reaches a prompt; distillation to a strategy is a later phase.
|
|
73
|
+
*/
|
|
74
|
+
recordCorrection(input) {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const id = `cor_${now.toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
77
|
+
this.store.saveCorrection({
|
|
78
|
+
id,
|
|
79
|
+
soulId: this.soulId,
|
|
80
|
+
userId: this.userId,
|
|
81
|
+
at: now,
|
|
82
|
+
userText: input.userText,
|
|
83
|
+
assistantText: input.assistantText,
|
|
84
|
+
rule: input.rule,
|
|
85
|
+
deviceId: this.deviceId,
|
|
86
|
+
});
|
|
87
|
+
return id;
|
|
88
|
+
}
|
|
89
|
+
corrections(limit = 20) {
|
|
90
|
+
return this.store.recentCorrections(this.soulId, this.userId, limit);
|
|
91
|
+
}
|
|
70
92
|
/** Verb 3: on demand. */
|
|
71
93
|
recall(query, limit = 5) {
|
|
72
94
|
const memories = this.store.query({ soulId: this.soulId, userId: this.userId });
|
|
@@ -26,6 +26,10 @@ export declare const MAX_STRATEGY_CHARS = 80;
|
|
|
26
26
|
export declare function distillStrategy(text: string): string | null;
|
|
27
27
|
/** Cheap, synchronous, no model. Deliberately conservative: false positives cost trust. */
|
|
28
28
|
export declare const RULES: Rule[];
|
|
29
|
+
/** Is this user turn a correction? Evidence for RFC 0003, not a memory by itself. */
|
|
30
|
+
export declare function isCorrection(text: string): boolean;
|
|
31
|
+
/** Which marker matched, for statistics; null when the turn is not a correction. */
|
|
32
|
+
export declare function correctionRule(text: string): string | null;
|
|
29
33
|
export declare function ruleExtract(turns: Turn[]): Draft[];
|
|
30
34
|
/** Corrections raise tension and lower passion; an ordinary session lets tension decay. */
|
|
31
35
|
export declare function nextEmotion(current: Emotion, turns: Turn[]): Emotion;
|
|
@@ -75,6 +75,15 @@ export const RULES = [
|
|
|
75
75
|
},
|
|
76
76
|
];
|
|
77
77
|
const CORRECTION = /(不对|不是这样|错了|你搞错|wrong|that'?s not right|incorrect)/i;
|
|
78
|
+
/** Is this user turn a correction? Evidence for RFC 0003, not a memory by itself. */
|
|
79
|
+
export function isCorrection(text) {
|
|
80
|
+
return CORRECTION.test(text);
|
|
81
|
+
}
|
|
82
|
+
/** Which marker matched, for statistics; null when the turn is not a correction. */
|
|
83
|
+
export function correctionRule(text) {
|
|
84
|
+
const match = CORRECTION.exec(text);
|
|
85
|
+
return match ? match[0] : null;
|
|
86
|
+
}
|
|
78
87
|
const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
79
88
|
const round2 = (n) => Math.round(n * 100) / 100;
|
|
80
89
|
export function ruleExtract(turns) {
|
|
@@ -76,6 +76,17 @@ CREATE TABLE IF NOT EXISTS affect_samples (
|
|
|
76
76
|
);
|
|
77
77
|
CREATE INDEX IF NOT EXISTS idx_affect ON affect_samples(soul_id, user_id, at DESC);
|
|
78
78
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_affect_unique ON affect_samples(soul_id, user_id, at, analyzer);
|
|
79
|
+
CREATE TABLE IF NOT EXISTS corrections (
|
|
80
|
+
id TEXT PRIMARY KEY,
|
|
81
|
+
soul_id TEXT NOT NULL,
|
|
82
|
+
user_id TEXT NOT NULL,
|
|
83
|
+
at INTEGER NOT NULL,
|
|
84
|
+
user_text TEXT NOT NULL,
|
|
85
|
+
assistant_text TEXT,
|
|
86
|
+
rule TEXT,
|
|
87
|
+
device_id TEXT
|
|
88
|
+
);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_corrections ON corrections(soul_id, user_id, at DESC);
|
|
79
90
|
`;
|
|
80
91
|
const sleepSync = (ms) => {
|
|
81
92
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
@@ -123,6 +134,20 @@ function upgradeToV2(db) {
|
|
|
123
134
|
db.exec('UPDATE memories SET updated_at = created_at WHERE updated_at IS NULL');
|
|
124
135
|
db.exec('UPDATE emotions SET updated_at = 0 WHERE updated_at IS NULL');
|
|
125
136
|
}
|
|
137
|
+
/** v2 → v3: corrections evidence table (RFC 0003 phase A). */
|
|
138
|
+
function upgradeToV3(db) {
|
|
139
|
+
db.exec(`CREATE TABLE IF NOT EXISTS corrections (
|
|
140
|
+
id TEXT PRIMARY KEY,
|
|
141
|
+
soul_id TEXT NOT NULL,
|
|
142
|
+
user_id TEXT NOT NULL,
|
|
143
|
+
at INTEGER NOT NULL,
|
|
144
|
+
user_text TEXT NOT NULL,
|
|
145
|
+
assistant_text TEXT,
|
|
146
|
+
rule TEXT,
|
|
147
|
+
device_id TEXT
|
|
148
|
+
)`);
|
|
149
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_corrections ON corrections(soul_id, user_id, at DESC)');
|
|
150
|
+
}
|
|
126
151
|
function backupBeforeUpgrade(file, from) {
|
|
127
152
|
if (file === ':memory:' || !existsSync(file))
|
|
128
153
|
return;
|
|
@@ -144,6 +169,8 @@ export function openSqlite(file) {
|
|
|
144
169
|
backupBeforeUpgrade(file, version);
|
|
145
170
|
if (version < 2)
|
|
146
171
|
upgradeToV2(db);
|
|
172
|
+
if (version < 3)
|
|
173
|
+
upgradeToV3(db);
|
|
147
174
|
setVersion(db, SCHEMA_VERSION);
|
|
148
175
|
}
|
|
149
176
|
const stmtPut = db.prepare(`
|
|
@@ -260,6 +287,34 @@ export function openSqlite(file) {
|
|
|
260
287
|
const result = withRetry(() => db.prepare('DELETE FROM affect_samples WHERE at < ?').run(beforeMs));
|
|
261
288
|
return Number(result.changes ?? 0);
|
|
262
289
|
},
|
|
290
|
+
saveCorrection(correction) {
|
|
291
|
+
withRetry(() => db
|
|
292
|
+
.prepare(`INSERT INTO corrections (id, soul_id, user_id, at, user_text, assistant_text, rule, device_id)
|
|
293
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
294
|
+
.run(correction.id, correction.soulId, correction.userId, correction.at, correction.userText, correction.assistantText ?? null, correction.rule ?? null, correction.deviceId ?? null));
|
|
295
|
+
},
|
|
296
|
+
recentCorrections(soulId, userId, limit) {
|
|
297
|
+
const rows = db
|
|
298
|
+
.prepare(`SELECT * FROM corrections WHERE soul_id = ? AND user_id = ?
|
|
299
|
+
ORDER BY at DESC LIMIT ?`)
|
|
300
|
+
.all(soulId, userId, limit);
|
|
301
|
+
return rows.map((row) => ({
|
|
302
|
+
id: String(row.id),
|
|
303
|
+
soulId: String(row.soul_id),
|
|
304
|
+
userId: String(row.user_id),
|
|
305
|
+
at: Number(row.at),
|
|
306
|
+
userText: String(row.user_text),
|
|
307
|
+
assistantText: row.assistant_text == null ? undefined : String(row.assistant_text),
|
|
308
|
+
rule: row.rule == null ? undefined : String(row.rule),
|
|
309
|
+
deviceId: row.device_id == null ? undefined : String(row.device_id),
|
|
310
|
+
}));
|
|
311
|
+
},
|
|
312
|
+
countCorrections(soulId, userId) {
|
|
313
|
+
const row = db
|
|
314
|
+
.prepare('SELECT COUNT(*) AS n FROM corrections WHERE soul_id = ? AND user_id = ?')
|
|
315
|
+
.get(soulId, userId);
|
|
316
|
+
return Number(row.n);
|
|
317
|
+
},
|
|
263
318
|
count(soulId, userId) {
|
|
264
319
|
const row = db
|
|
265
320
|
.prepare(`SELECT COUNT(*) AS n FROM memories
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The only port. Every added method is a storage engine's opinion leaking into
|
|
3
3
|
* the domain, so the surface stays deliberately small.
|
|
4
4
|
*/
|
|
5
|
-
import type { AffectSignal, Emotion, Identity, Memory, QuerySpec } from './types.ts';
|
|
5
|
+
import type { AffectSignal, Correction, Emotion, Identity, Memory, QuerySpec } from './types.ts';
|
|
6
6
|
export interface SoulStore {
|
|
7
7
|
ensureSoul(identity: Identity): void;
|
|
8
8
|
identity(soulId: string): Identity | null;
|
|
@@ -24,6 +24,10 @@ export interface SoulStore {
|
|
|
24
24
|
recentAffect(soulId: string, userId: string, sinceMs: number): AffectSignal[];
|
|
25
25
|
/** Drop samples older than `beforeMs`; returns rows removed. */
|
|
26
26
|
purgeAffect(beforeMs: number): number;
|
|
27
|
+
/** Evidence, not memory: a user correction plus the reply it targeted. */
|
|
28
|
+
saveCorrection(correction: Correction): void;
|
|
29
|
+
recentCorrections(soulId: string, userId: string, limit: number): Correction[];
|
|
30
|
+
countCorrections(soulId: string, userId: string): number;
|
|
27
31
|
count(soulId: string, userId: string): number;
|
|
28
32
|
close(): void;
|
|
29
33
|
}
|
|
@@ -148,3 +148,30 @@ export interface AffectSignal {
|
|
|
148
148
|
export declare const EMPTY_AFFECT: AffectSignal;
|
|
149
149
|
/** Wire version of the four verbs. Additive changes bump MINOR (ADR 0005). */
|
|
150
150
|
export declare const PROTOCOL_VERSION = "0.2";
|
|
151
|
+
/**
|
|
152
|
+
* Evidence, not memory: a user correction captured with the reply it targeted.
|
|
153
|
+
* Stored separately so it never reaches a prompt; distillation (RFC 0003 phase B)
|
|
154
|
+
* is what turns evidence into a strategy.
|
|
155
|
+
*/
|
|
156
|
+
export interface Correction {
|
|
157
|
+
id: string;
|
|
158
|
+
soulId: string;
|
|
159
|
+
userId: string;
|
|
160
|
+
at: number;
|
|
161
|
+
/** The user's own words, complete. */
|
|
162
|
+
userText: string;
|
|
163
|
+
/** The reply the correction targeted, complete; undefined when unknown. */
|
|
164
|
+
assistantText?: string;
|
|
165
|
+
/** Which marker matched (for statistics). */
|
|
166
|
+
rule?: string;
|
|
167
|
+
deviceId?: string;
|
|
168
|
+
}
|
|
169
|
+
export interface CorrectionInput {
|
|
170
|
+
soulId: string;
|
|
171
|
+
userId: string;
|
|
172
|
+
userText: string;
|
|
173
|
+
assistantText?: string;
|
|
174
|
+
rule?: string;
|
|
175
|
+
deviceId?: string;
|
|
176
|
+
at?: number;
|
|
177
|
+
}
|