audrey 2.0.0 → 2.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/CHANGELOG.md +63 -0
- package/README.md +64 -60
- package/SECURITY.md +2 -3
- package/dist/mcp-server/config.d.ts +1 -1
- package/dist/mcp-server/config.d.ts.map +1 -1
- package/dist/mcp-server/config.js +31 -3
- package/dist/mcp-server/config.js.map +1 -1
- package/dist/mcp-server/index.d.ts +6 -2
- package/dist/mcp-server/index.d.ts.map +1 -1
- package/dist/mcp-server/index.js +147 -16
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/mcp-server/tool-schemas.d.ts +46 -0
- package/dist/mcp-server/tool-schemas.d.ts.map +1 -1
- package/dist/mcp-server/tool-schemas.js +20 -1
- package/dist/mcp-server/tool-schemas.js.map +1 -1
- package/dist/src/action-key.d.ts.map +1 -1
- package/dist/src/action-key.js +7 -7
- package/dist/src/action-key.js.map +1 -1
- package/dist/src/audrey.d.ts +7 -4
- package/dist/src/audrey.d.ts.map +1 -1
- package/dist/src/audrey.js +210 -57
- package/dist/src/audrey.js.map +1 -1
- package/dist/src/autopilot.d.ts +1 -1
- package/dist/src/autopilot.d.ts.map +1 -1
- package/dist/src/autopilot.js +47 -224
- package/dist/src/autopilot.js.map +1 -1
- package/dist/src/capsule.d.ts +4 -17
- package/dist/src/capsule.d.ts.map +1 -1
- package/dist/src/capsule.js +21 -20
- package/dist/src/capsule.js.map +1 -1
- package/dist/src/chatgpt-auth.d.ts +77 -0
- package/dist/src/chatgpt-auth.d.ts.map +1 -0
- package/dist/src/chatgpt-auth.js +887 -0
- package/dist/src/chatgpt-auth.js.map +1 -0
- package/dist/src/chatgpt-llm.d.ts +18 -0
- package/dist/src/chatgpt-llm.d.ts.map +1 -0
- package/dist/src/chatgpt-llm.js +157 -0
- package/dist/src/chatgpt-llm.js.map +1 -0
- package/dist/src/consolidate.d.ts.map +1 -1
- package/dist/src/consolidate.js +30 -2
- package/dist/src/consolidate.js.map +1 -1
- package/dist/src/controller.d.ts +7 -1
- package/dist/src/controller.d.ts.map +1 -1
- package/dist/src/controller.js +88 -14
- package/dist/src/controller.js.map +1 -1
- package/dist/src/db.d.ts +1 -0
- package/dist/src/db.d.ts.map +1 -1
- package/dist/src/db.js +126 -45
- package/dist/src/db.js.map +1 -1
- package/dist/src/embedding.d.ts.map +1 -1
- package/dist/src/embedding.js +14 -1
- package/dist/src/embedding.js.map +1 -1
- package/dist/src/encode.d.ts +14 -0
- package/dist/src/encode.d.ts.map +1 -1
- package/dist/src/encode.js +56 -17
- package/dist/src/encode.js.map +1 -1
- package/dist/src/events.d.ts +17 -0
- package/dist/src/events.d.ts.map +1 -1
- package/dist/src/events.js +130 -3
- package/dist/src/events.js.map +1 -1
- package/dist/src/export.d.ts +1 -0
- package/dist/src/export.d.ts.map +1 -1
- package/dist/src/export.js +9 -2
- package/dist/src/export.js.map +1 -1
- package/dist/src/forget.d.ts.map +1 -1
- package/dist/src/forget.js +6 -2
- package/dist/src/forget.js.map +1 -1
- package/dist/src/fts.d.ts +2 -2
- package/dist/src/fts.d.ts.map +1 -1
- package/dist/src/fts.js +4 -4
- package/dist/src/fts.js.map +1 -1
- package/dist/src/grounding.d.ts +1 -1
- package/dist/src/grounding.d.ts.map +1 -1
- package/dist/src/grounding.js +24 -11
- package/dist/src/grounding.js.map +1 -1
- package/dist/src/hybrid-recall.d.ts +5 -8
- package/dist/src/hybrid-recall.d.ts.map +1 -1
- package/dist/src/hybrid-recall.js +25 -57
- package/dist/src/hybrid-recall.js.map +1 -1
- package/dist/src/import.d.ts +40 -3
- package/dist/src/import.d.ts.map +1 -1
- package/dist/src/import.js +101 -42
- package/dist/src/import.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/llm-shared.d.ts +10 -0
- package/dist/src/llm-shared.d.ts.map +1 -0
- package/dist/src/llm-shared.js +20 -0
- package/dist/src/llm-shared.js.map +1 -0
- package/dist/src/llm.d.ts +2 -1
- package/dist/src/llm.d.ts.map +1 -1
- package/dist/src/llm.js +13 -22
- package/dist/src/llm.js.map +1 -1
- package/dist/src/migrate.d.ts.map +1 -1
- package/dist/src/migrate.js +34 -40
- package/dist/src/migrate.js.map +1 -1
- package/dist/src/preflight.d.ts +2 -0
- package/dist/src/preflight.d.ts.map +1 -1
- package/dist/src/preflight.js +5 -1
- package/dist/src/preflight.js.map +1 -1
- package/dist/src/project-memory.d.ts +14 -0
- package/dist/src/project-memory.d.ts.map +1 -0
- package/dist/src/project-memory.js +135 -0
- package/dist/src/project-memory.js.map +1 -0
- package/dist/src/project.d.ts +2 -0
- package/dist/src/project.d.ts.map +1 -1
- package/dist/src/project.js +12 -0
- package/dist/src/project.js.map +1 -1
- package/dist/src/promote.d.ts.map +1 -1
- package/dist/src/promote.js +2 -26
- package/dist/src/promote.js.map +1 -1
- package/dist/src/recall-scoring.d.ts +14 -0
- package/dist/src/recall-scoring.d.ts.map +1 -0
- package/dist/src/recall-scoring.js +167 -0
- package/dist/src/recall-scoring.js.map +1 -0
- package/dist/src/recall.d.ts.map +1 -1
- package/dist/src/recall.js +31 -168
- package/dist/src/recall.js.map +1 -1
- package/dist/src/rollback.d.ts +12 -0
- package/dist/src/rollback.d.ts.map +1 -1
- package/dist/src/rollback.js +118 -25
- package/dist/src/rollback.js.map +1 -1
- package/dist/src/routes.d.ts.map +1 -1
- package/dist/src/routes.js +102 -33
- package/dist/src/routes.js.map +1 -1
- package/dist/src/rules-compiler.d.ts +4 -0
- package/dist/src/rules-compiler.d.ts.map +1 -1
- package/dist/src/rules-compiler.js +34 -2
- package/dist/src/rules-compiler.js.map +1 -1
- package/dist/src/server.d.ts +2 -0
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +96 -38
- package/dist/src/server.js.map +1 -1
- package/dist/src/shell-command.d.ts.map +1 -1
- package/dist/src/shell-command.js +24 -7
- package/dist/src/shell-command.js.map +1 -1
- package/dist/src/success-checks.d.ts +41 -0
- package/dist/src/success-checks.d.ts.map +1 -0
- package/dist/src/success-checks.js +164 -0
- package/dist/src/success-checks.js.map +1 -0
- package/dist/src/types.d.ts +7 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/validate.d.ts.map +1 -1
- package/dist/src/validate.js +23 -9
- package/dist/src/validate.js.map +1 -1
- package/package.json +7 -10
package/dist/src/audrey.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { createDatabase, closeDatabase, liveRowClause, refreshVectorRow } from './db.js';
|
|
3
4
|
import { createEmbeddingProvider } from './embedding.js';
|
|
4
5
|
import { createLLMProvider } from './llm.js';
|
|
5
|
-
import { encodeEpisode } from './encode.js';
|
|
6
|
+
import { encodeEpisode, assertEncodeInput } from './encode.js';
|
|
6
7
|
import { redact } from './redact.js';
|
|
7
8
|
import { recall as recallFn, recallStream as recallStreamFn } from './recall.js';
|
|
8
9
|
import { validateMemory } from './validate.js';
|
|
@@ -27,15 +28,15 @@ import { buildPreflight } from './preflight.js';
|
|
|
27
28
|
import { buildReflexReport } from './reflexes.js';
|
|
28
29
|
import { beforeAction as guardBeforeAction, afterAction as guardAfterAction, } from './controller.js';
|
|
29
30
|
import { findPromotionCandidates, } from './promote.js';
|
|
30
|
-
import { renderAllRules } from './rules-compiler.js';
|
|
31
|
-
import { insertEvent } from './events.js';
|
|
31
|
+
import { renderAllRules, legacyClaudeRuleRelativePath, claudeRuleHasProvenance, } from './rules-compiler.js';
|
|
32
|
+
import { insertEvent, recordPromotion } from './events.js';
|
|
32
33
|
import { requireAgent, resolveMemoryScope, isContainedIn } from './utils.js';
|
|
33
|
-
import { mkdirSync, writeFileSync,
|
|
34
|
+
import { mkdirSync, writeFileSync, readFileSync, readdirSync, lstatSync, openSync, closeSync, fsyncSync, linkSync, rmSync, } from 'node:fs';
|
|
34
35
|
import { dirname, join, resolve as pathResolve, delimiter as pathDelimiter } from 'node:path';
|
|
35
36
|
import { ProfileRecorder } from './profile.js';
|
|
36
37
|
import { performance } from 'node:perf_hooks';
|
|
37
38
|
import { recordAnchors, verifyAnchors, } from './grounding.js';
|
|
38
|
-
import {
|
|
39
|
+
import { checkoutRoot } from './project.js';
|
|
39
40
|
/**
|
|
40
41
|
* Thrown by `Audrey.validate()` when a structural lineage check fails. Carries a
|
|
41
42
|
* stable `code` so HTTP and MCP callers can branch on the failure type without
|
|
@@ -52,16 +53,31 @@ export class ValidateLineageError extends Error {
|
|
|
52
53
|
function roundMs(value) {
|
|
53
54
|
return Math.round(value * 1000) / 1000;
|
|
54
55
|
}
|
|
56
|
+
/** Create and fill a file only if it does not exist; false when it already does. */
|
|
57
|
+
function writeExclusively(path, body) {
|
|
58
|
+
let fd;
|
|
59
|
+
try {
|
|
60
|
+
fd = openSync(path, 'wx', 0o600);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error.code === 'EEXIST')
|
|
64
|
+
return false;
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
writeFileSync(fd, body, 'utf8');
|
|
69
|
+
fsyncSync(fd);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
closeSync(fd);
|
|
73
|
+
rmSync(path, { force: true });
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
closeSync(fd);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
55
79
|
function validateEncodeParams(params) {
|
|
56
|
-
|
|
57
|
-
throw new Error('content must be a non-empty string');
|
|
58
|
-
}
|
|
59
|
-
if (params.salience !== undefined && (params.salience < 0 || params.salience > 1)) {
|
|
60
|
-
throw new Error('salience must be between 0 and 1');
|
|
61
|
-
}
|
|
62
|
-
if (params.tags !== undefined && !Array.isArray(params.tags)) {
|
|
63
|
-
throw new Error('tags must be an array');
|
|
64
|
-
}
|
|
80
|
+
assertEncodeInput(params);
|
|
65
81
|
}
|
|
66
82
|
const TRUTH_RESOLUTIONS = new Set([
|
|
67
83
|
'a_wins',
|
|
@@ -188,6 +204,7 @@ export class Audrey extends EventEmitter {
|
|
|
188
204
|
defaultRetrievalMode;
|
|
189
205
|
autoReflect;
|
|
190
206
|
_migrationPending;
|
|
207
|
+
_migrationPromise = null;
|
|
191
208
|
_autoConsolidateTimer;
|
|
192
209
|
_closed;
|
|
193
210
|
_postEncodeQueue;
|
|
@@ -197,7 +214,7 @@ export class Audrey extends EventEmitter {
|
|
|
197
214
|
_warmupDurationMs;
|
|
198
215
|
_lastRecallCheckAt;
|
|
199
216
|
_lastRecallErrors;
|
|
200
|
-
constructor({ dataDir = './audrey-data', agent = 'default', embedding = { provider: 'mock', dimensions: 64 }, llm, confidence = {}, consolidation = {}, decay = {}, interference = {}, context = {}, affect = {}, autoReflect = false, } = {}) {
|
|
217
|
+
constructor({ dataDir = './audrey-data', sharedStore, agent = 'default', embedding = { provider: 'mock', dimensions: 64 }, llm, confidence = {}, consolidation = {}, decay = {}, interference = {}, context = {}, affect = {}, autoReflect = false, } = {}) {
|
|
201
218
|
super();
|
|
202
219
|
const dormantThreshold = decay.dormantThreshold ?? 0.1;
|
|
203
220
|
if (dormantThreshold < 0 || dormantThreshold > 1) {
|
|
@@ -212,6 +229,7 @@ export class Audrey extends EventEmitter {
|
|
|
212
229
|
this.embeddingProvider = createEmbeddingProvider(embedding);
|
|
213
230
|
const { db, migrated } = createDatabase(dataDir, {
|
|
214
231
|
dimensions: this.embeddingProvider.dimensions,
|
|
232
|
+
sharedStore,
|
|
215
233
|
});
|
|
216
234
|
this.db = db;
|
|
217
235
|
this._migrationPending = migrated;
|
|
@@ -276,9 +294,15 @@ export class Audrey extends EventEmitter {
|
|
|
276
294
|
async _ensureMigrated() {
|
|
277
295
|
if (!this._migrationPending)
|
|
278
296
|
return;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
297
|
+
this._migrationPromise ??= reembedAll(this.db, this.embeddingProvider)
|
|
298
|
+
.then(counts => {
|
|
299
|
+
this._migrationPending = false;
|
|
300
|
+
this.emit('migration', counts);
|
|
301
|
+
})
|
|
302
|
+
.finally(() => {
|
|
303
|
+
this._migrationPromise = null;
|
|
304
|
+
});
|
|
305
|
+
await this._migrationPromise;
|
|
282
306
|
}
|
|
283
307
|
startEmbeddingWarmup(text = 'warmup') {
|
|
284
308
|
if (this._embeddingWarm)
|
|
@@ -376,9 +400,9 @@ export class Audrey extends EventEmitter {
|
|
|
376
400
|
recordAnchors(this.db, {
|
|
377
401
|
memoryId: id,
|
|
378
402
|
memoryType: 'episodic',
|
|
379
|
-
agent: this.agent,
|
|
403
|
+
agent: requireAgent(params.agent, this.agent),
|
|
380
404
|
content: params.content,
|
|
381
|
-
projectRoot:
|
|
405
|
+
projectRoot: checkoutRoot(cwd),
|
|
382
406
|
});
|
|
383
407
|
});
|
|
384
408
|
await this._runPostEncodeStage('validation', async () => {
|
|
@@ -517,7 +541,7 @@ export class Audrey extends EventEmitter {
|
|
|
517
541
|
const encodedEmbedding = { vector: encodedVector, buffer: encodedBuffer };
|
|
518
542
|
if (redactionSummary?.redacted)
|
|
519
543
|
this.emit('redaction', { id, ...redactionSummary });
|
|
520
|
-
this.emit('encode', { id, ...params });
|
|
544
|
+
this.emit('encode', { id, ...params, ...sanitizedFields });
|
|
521
545
|
// The background pipeline (grounding, validation, the contradiction
|
|
522
546
|
// prompt sent to the cloud LLM) must only ever see what the row stores.
|
|
523
547
|
// Handing it the raw params would leak a secret redaction just scrubbed.
|
|
@@ -535,10 +559,14 @@ export class Audrey extends EventEmitter {
|
|
|
535
559
|
}
|
|
536
560
|
return id;
|
|
537
561
|
}
|
|
538
|
-
async reflect(turns) {
|
|
562
|
+
async reflect(turns, options = {}) {
|
|
563
|
+
const agent = requireAgent(options.agent, this.agent);
|
|
539
564
|
if (!this.llmProvider)
|
|
540
565
|
return { encoded: 0, memories: [], skipped: 'no llm provider' };
|
|
541
|
-
const prompt = buildReflectionPrompt(turns
|
|
566
|
+
const prompt = buildReflectionPrompt(turns.map(turn => ({
|
|
567
|
+
role: turn.role,
|
|
568
|
+
content: redact(turn.content).text,
|
|
569
|
+
})));
|
|
542
570
|
let raw;
|
|
543
571
|
try {
|
|
544
572
|
if (typeof this.llmProvider.complete === 'function') {
|
|
@@ -573,6 +601,7 @@ export class Audrey extends EventEmitter {
|
|
|
573
601
|
try {
|
|
574
602
|
await this.encode({
|
|
575
603
|
content: mem.content,
|
|
604
|
+
agent,
|
|
576
605
|
source: mem.source,
|
|
577
606
|
salience: mem.salience,
|
|
578
607
|
tags: mem.tags,
|
|
@@ -629,7 +658,7 @@ export class Audrey extends EventEmitter {
|
|
|
629
658
|
ids.push(id);
|
|
630
659
|
if (redactionSummary?.redacted)
|
|
631
660
|
this.emit('redaction', { id, ...redactionSummary });
|
|
632
|
-
this.emit('encode', { id, ...paramsList[i] });
|
|
661
|
+
this.emit('encode', { id, ...paramsList[i], ...sanitizedFields });
|
|
633
662
|
const encodedEmbedding = { vector: encodedVector, buffer: encodedBuffer };
|
|
634
663
|
// Same boundary as _encodeInternal: background stages see stored values only.
|
|
635
664
|
const postEncodeParams = sanitizedFields
|
|
@@ -774,9 +803,11 @@ export class Audrey extends EventEmitter {
|
|
|
774
803
|
this.db
|
|
775
804
|
.prepare("UPDATE semantics SET state = 'context_dependent', conditions = ? WHERE id = ?")
|
|
776
805
|
.run(conditions, id);
|
|
806
|
+
refreshVectorRow(this.db, 'semantics', id);
|
|
777
807
|
}
|
|
778
808
|
else {
|
|
779
809
|
this.db.prepare('UPDATE episodes SET superseded_by = NULL WHERE id = ?').run(id);
|
|
810
|
+
refreshVectorRow(this.db, 'episodes', id);
|
|
780
811
|
}
|
|
781
812
|
};
|
|
782
813
|
if (result.resolution === 'a_wins') {
|
|
@@ -897,13 +928,13 @@ export class Audrey extends EventEmitter {
|
|
|
897
928
|
const agentClause = memoryScope === 'agent' ? 'AND agent = ?' : '';
|
|
898
929
|
const agentParam = memoryScope === 'agent' ? [memoryAgent] : [];
|
|
899
930
|
const recent = this.db
|
|
900
|
-
.prepare(`SELECT id, content, source, tags, salience, created_at FROM episodes WHERE "private" = 0 ${agentClause} ORDER BY created_at DESC LIMIT ?`)
|
|
931
|
+
.prepare(`SELECT id, content, source, tags, salience, created_at FROM episodes WHERE "private" = 0 AND superseded_by IS NULL ${agentClause} ORDER BY created_at DESC LIMIT ?`)
|
|
901
932
|
.all(...agentParam, recentLimit);
|
|
902
933
|
const principles = this.db
|
|
903
934
|
.prepare(`SELECT id, content, salience, created_at FROM semantics WHERE state = ? ${agentClause} ORDER BY salience DESC LIMIT ?`)
|
|
904
935
|
.all('active', ...agentParam, principleLimit);
|
|
905
936
|
const identity = this.db
|
|
906
|
-
.prepare(`SELECT id, content, tags, salience, created_at FROM episodes WHERE "private" = 1 ${agentClause} ORDER BY created_at DESC LIMIT ?`)
|
|
937
|
+
.prepare(`SELECT id, content, tags, salience, created_at FROM episodes WHERE "private" = 1 AND superseded_by IS NULL ${agentClause} ORDER BY created_at DESC LIMIT ?`)
|
|
907
938
|
.all(...agentParam, identityLimit);
|
|
908
939
|
const unresolved = this.db
|
|
909
940
|
.prepare(`SELECT id, content, tags, salience, created_at
|
|
@@ -914,12 +945,13 @@ export class Audrey extends EventEmitter {
|
|
|
914
945
|
WHERE json_each.type = 'text' AND json_each.value = 'unresolved'
|
|
915
946
|
)
|
|
916
947
|
AND salience > 0.3
|
|
948
|
+
AND superseded_by IS NULL
|
|
917
949
|
${agentClause}
|
|
918
950
|
ORDER BY created_at DESC
|
|
919
951
|
LIMIT 10`)
|
|
920
952
|
.all(...agentParam);
|
|
921
953
|
const rawAffectRows = this.db
|
|
922
|
-
.prepare(`SELECT affect FROM episodes WHERE affect IS NOT NULL AND affect != '{}' ${agentClause} ORDER BY created_at DESC LIMIT 20`)
|
|
954
|
+
.prepare(`SELECT affect FROM episodes WHERE affect IS NOT NULL AND affect != '{}' AND superseded_by IS NULL ${agentClause} ORDER BY created_at DESC LIMIT 20`)
|
|
923
955
|
.all(...agentParam);
|
|
924
956
|
const affectParsed = rawAffectRows
|
|
925
957
|
.map(r => {
|
|
@@ -1005,7 +1037,7 @@ export class Audrey extends EventEmitter {
|
|
|
1005
1037
|
suggestConsolidationParams() {
|
|
1006
1038
|
return suggestParamsFn(this.db);
|
|
1007
1039
|
}
|
|
1008
|
-
validate(input) {
|
|
1040
|
+
validate(input, deferredNotifications) {
|
|
1009
1041
|
input = { ...input, agent: requireAgent(input.agent, this.agent) };
|
|
1010
1042
|
let preflightMetadata = null;
|
|
1011
1043
|
if (input.preflightEventId) {
|
|
@@ -1073,7 +1105,13 @@ export class Audrey extends EventEmitter {
|
|
|
1073
1105
|
...(input.evidenceIds ? { preflight_evidence_ids: input.evidenceIds } : {}),
|
|
1074
1106
|
},
|
|
1075
1107
|
});
|
|
1076
|
-
|
|
1108
|
+
const notify = () => {
|
|
1109
|
+
this.emit('validate', result);
|
|
1110
|
+
};
|
|
1111
|
+
if (deferredNotifications)
|
|
1112
|
+
deferredNotifications.push(notify);
|
|
1113
|
+
else
|
|
1114
|
+
notify();
|
|
1077
1115
|
}
|
|
1078
1116
|
return result;
|
|
1079
1117
|
}
|
|
@@ -1134,12 +1172,18 @@ export class Audrey extends EventEmitter {
|
|
|
1134
1172
|
async waitForIdle() {
|
|
1135
1173
|
await this._postEncodeQueue;
|
|
1136
1174
|
}
|
|
1137
|
-
observeTool(input) {
|
|
1175
|
+
observeTool(input, deferredNotifications) {
|
|
1138
1176
|
const result = observeTool(this.db, {
|
|
1139
1177
|
...input,
|
|
1140
1178
|
actorAgent: requireAgent(input.actorAgent, this.agent),
|
|
1141
1179
|
});
|
|
1142
|
-
|
|
1180
|
+
const notify = () => {
|
|
1181
|
+
this.emit('tool-observed', result.event);
|
|
1182
|
+
};
|
|
1183
|
+
if (deferredNotifications)
|
|
1184
|
+
deferredNotifications.push(notify);
|
|
1185
|
+
else
|
|
1186
|
+
notify();
|
|
1143
1187
|
return result;
|
|
1144
1188
|
}
|
|
1145
1189
|
listEvents(query = {}) {
|
|
@@ -1228,45 +1272,154 @@ export class Audrey extends EventEmitter {
|
|
|
1228
1272
|
}
|
|
1229
1273
|
const promotedAt = new Date().toISOString();
|
|
1230
1274
|
const docs = renderAllRules(candidates, promotedAt);
|
|
1275
|
+
const resolvedPaths = docs.map(doc => doc.relativePath);
|
|
1231
1276
|
const applied = [];
|
|
1232
1277
|
if (!dryRun) {
|
|
1233
1278
|
for (let i = 0; i < candidates.length; i++) {
|
|
1234
1279
|
const candidate = candidates[i];
|
|
1235
1280
|
const doc = docs[i];
|
|
1236
1281
|
const absolutePath = join(projectDir, doc.relativePath);
|
|
1282
|
+
if (!isContainedIn(projectDir, absolutePath)) {
|
|
1283
|
+
throw new Error('promote: refusing to write a rule outside the project through a symlink');
|
|
1284
|
+
}
|
|
1237
1285
|
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1286
|
+
if (!isContainedIn(projectDir, absolutePath)) {
|
|
1287
|
+
throw new Error('promote: rule destination changed outside the project');
|
|
1288
|
+
}
|
|
1289
|
+
const promotion = {
|
|
1290
|
+
target,
|
|
1243
1291
|
actorAgent: this.agent,
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1292
|
+
memoryIds: [candidate.memory_id],
|
|
1293
|
+
promotedAt,
|
|
1294
|
+
};
|
|
1295
|
+
const preserveExistingRule = (relativePath, mustMatch) => {
|
|
1296
|
+
const existingPath = join(projectDir, relativePath);
|
|
1297
|
+
if (!isContainedIn(projectDir, existingPath)) {
|
|
1298
|
+
throw new Error('promote: refusing to inspect a rule outside the project');
|
|
1299
|
+
}
|
|
1300
|
+
try {
|
|
1301
|
+
const existing = lstatSync(existingPath);
|
|
1302
|
+
if (existing.isSymbolicLink() || !existing.isFile()) {
|
|
1303
|
+
throw new Error('promote: refusing to replace a non-file rule destination');
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
catch (error) {
|
|
1307
|
+
if (error.code === 'ENOENT')
|
|
1308
|
+
return false;
|
|
1309
|
+
throw error;
|
|
1310
|
+
}
|
|
1311
|
+
if (!claudeRuleHasProvenance(readFileSync(existingPath, 'utf8'), candidate)) {
|
|
1312
|
+
if (mustMatch) {
|
|
1313
|
+
throw new Error('promote: refusing to overwrite an existing rule with different provenance');
|
|
1314
|
+
}
|
|
1315
|
+
return false;
|
|
1316
|
+
}
|
|
1317
|
+
recordPromotion(this.db, {
|
|
1318
|
+
...promotion,
|
|
1319
|
+
relativePath,
|
|
1320
|
+
absolutePath: existingPath,
|
|
1321
|
+
});
|
|
1322
|
+
resolvedPaths[i] = relativePath;
|
|
1323
|
+
return true;
|
|
1324
|
+
};
|
|
1325
|
+
if (preserveExistingRule(doc.relativePath, true))
|
|
1326
|
+
continue;
|
|
1327
|
+
const legacyPath = legacyClaudeRuleRelativePath(candidate);
|
|
1328
|
+
const legacyDirectory = legacyPath.slice(0, legacyPath.lastIndexOf('/') + 1);
|
|
1329
|
+
const legacyName = legacyPath.slice(legacyDirectory.length);
|
|
1330
|
+
const legacyStem = legacyName.slice(0, -3);
|
|
1331
|
+
// Older renderAllRules disambiguated colliding truncated IDs with
|
|
1332
|
+
// numbered suffixes. Inspect actual files, rather than guessing how
|
|
1333
|
+
// many suffixes were once written, and require the complete provenance.
|
|
1334
|
+
const legacyNames = readdirSync(dirname(absolutePath), { withFileTypes: true })
|
|
1335
|
+
.filter(entry => entry.isFile())
|
|
1336
|
+
.map(entry => entry.name)
|
|
1337
|
+
.filter(name => name === legacyName ||
|
|
1338
|
+
(name.startsWith(`${legacyStem}-`) &&
|
|
1339
|
+
/^[1-9][0-9]*\.md$/.test(name.slice(legacyStem.length + 1))));
|
|
1340
|
+
if (legacyNames.some(name => preserveExistingRule(legacyDirectory + name, false)))
|
|
1341
|
+
continue;
|
|
1342
|
+
// Publish a complete file only if its destination is still absent.
|
|
1343
|
+
// A racing promotion must never overwrite an existing, edited rule.
|
|
1344
|
+
const temporary = `${absolutePath}.${randomUUID()}.tmp`;
|
|
1345
|
+
try {
|
|
1346
|
+
const fd = openSync(temporary, 'wx', 0o600);
|
|
1347
|
+
try {
|
|
1348
|
+
writeFileSync(fd, doc.body, 'utf8');
|
|
1349
|
+
fsyncSync(fd);
|
|
1350
|
+
}
|
|
1351
|
+
finally {
|
|
1352
|
+
closeSync(fd);
|
|
1353
|
+
}
|
|
1354
|
+
if (!isContainedIn(projectDir, dirname(absolutePath))) {
|
|
1355
|
+
throw new Error('promote: rule directory changed outside the project');
|
|
1356
|
+
}
|
|
1357
|
+
try {
|
|
1358
|
+
linkSync(temporary, absolutePath);
|
|
1359
|
+
}
|
|
1360
|
+
catch (error) {
|
|
1361
|
+
const code = error.code;
|
|
1362
|
+
if (code === 'EEXIST') {
|
|
1363
|
+
if (preserveExistingRule(doc.relativePath, true))
|
|
1364
|
+
continue;
|
|
1365
|
+
throw error;
|
|
1366
|
+
}
|
|
1367
|
+
// Filesystems without hard links (exFAT, some network and container
|
|
1368
|
+
// mounts) keep the no-overwrite guarantee through an exclusive create.
|
|
1369
|
+
if (code !== 'EPERM' && code !== 'ENOTSUP' && code !== 'EXDEV' && code !== 'ENOSYS') {
|
|
1370
|
+
throw error;
|
|
1371
|
+
}
|
|
1372
|
+
if (!writeExclusively(absolutePath, doc.body)) {
|
|
1373
|
+
if (preserveExistingRule(doc.relativePath, true))
|
|
1374
|
+
continue;
|
|
1375
|
+
throw new Error(`promote: rule destination already exists: ${doc.relativePath}`, {
|
|
1376
|
+
cause: error,
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
finally {
|
|
1382
|
+
rmSync(temporary, { force: true });
|
|
1383
|
+
}
|
|
1384
|
+
this.db
|
|
1385
|
+
.transaction(() => {
|
|
1386
|
+
recordPromotion(this.db, {
|
|
1387
|
+
...promotion,
|
|
1388
|
+
relativePath: doc.relativePath,
|
|
1389
|
+
absolutePath,
|
|
1390
|
+
});
|
|
1391
|
+
insertEvent(this.db, {
|
|
1392
|
+
eventType: 'Promotion',
|
|
1393
|
+
source: 'promote-command',
|
|
1394
|
+
actorAgent: this.agent,
|
|
1395
|
+
toolName: target,
|
|
1396
|
+
outcome: 'succeeded',
|
|
1397
|
+
cwd: projectDir,
|
|
1398
|
+
fileFingerprints: [doc.relativePath],
|
|
1399
|
+
redactionState: 'clean',
|
|
1400
|
+
metadata: {
|
|
1401
|
+
memory_ids: [candidate.memory_id],
|
|
1402
|
+
memory_type: candidate.memory_type,
|
|
1403
|
+
candidate_id: candidate.candidate_id,
|
|
1404
|
+
confidence: Number(candidate.confidence.toFixed(3)),
|
|
1405
|
+
evidence_count: candidate.evidence_count,
|
|
1406
|
+
failure_prevented: candidate.failure_prevented,
|
|
1407
|
+
score: Number(candidate.score.toFixed(2)),
|
|
1408
|
+
target,
|
|
1409
|
+
absolute_path: absolutePath,
|
|
1410
|
+
relative_path: doc.relativePath,
|
|
1411
|
+
overwritten: false,
|
|
1412
|
+
},
|
|
1413
|
+
});
|
|
1414
|
+
})
|
|
1415
|
+
.immediate();
|
|
1263
1416
|
applied.push({
|
|
1264
1417
|
candidate_id: candidate.candidate_id,
|
|
1265
1418
|
memory_id: candidate.memory_id,
|
|
1266
1419
|
target,
|
|
1267
1420
|
relative_path: doc.relativePath,
|
|
1268
1421
|
absolute_path: absolutePath,
|
|
1269
|
-
overwritten,
|
|
1422
|
+
overwritten: false,
|
|
1270
1423
|
});
|
|
1271
1424
|
}
|
|
1272
1425
|
}
|
|
@@ -1277,7 +1430,7 @@ export class Audrey extends EventEmitter {
|
|
|
1277
1430
|
promoted_at: promotedAt,
|
|
1278
1431
|
candidates: candidates.map((c, i) => ({
|
|
1279
1432
|
...c,
|
|
1280
|
-
rendered_path:
|
|
1433
|
+
rendered_path: resolvedPaths[i],
|
|
1281
1434
|
})),
|
|
1282
1435
|
applied,
|
|
1283
1436
|
};
|