memorix 1.9.4 → 1.9.6
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 +75 -1
- package/README.md +12 -6
- package/README.zh-CN.md +12 -6
- package/dist/cli/index.js +2533 -585
- package/dist/index.d.ts +15 -8
- package/dist/index.js +1201 -284
- package/dist/maintenance-runner.js +387 -216
- package/dist/media-audio-runner.js +300 -129
- package/dist/media-video-runner.js +300 -129
- package/dist/memcode-runtime/CHANGELOG.md +75 -1
- package/dist/sdk.d.ts +1 -0
- package/dist/sdk.js +1882 -998
- package/dist/types.d.ts +1 -1
- package/dist/vector-backfill-runner.js +300 -129
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +47 -3
- package/docs/API_REFERENCE.md +39 -1
- package/docs/CONFIGURATION.md +58 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/DOCKER.md +23 -3
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/KNOWN_ISSUES_AND_ROADMAP.md +61 -1
- package/docs/MULTI-DEVICE-SYNC-RESEARCH.md +4 -0
- package/docs/PERFORMANCE.md +46 -2
- package/docs/README.md +2 -1
- package/docs/SETUP.md +56 -1
- package/package.json +3 -2
- package/plugins/antigravity/memorix/plugin.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/hermes/memorix/plugin.yaml +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/capability-map.ts +1 -0
- package/src/cli/command-guide.ts +15 -3
- package/src/cli/commands/agent-integrations.ts +57 -0
- package/src/cli/commands/background.ts +11 -2
- package/src/cli/commands/context.ts +2 -0
- package/src/cli/commands/continuity.ts +155 -0
- package/src/cli/commands/init.ts +5 -2
- package/src/cli/commands/serve-http.ts +435 -84
- package/src/cli/commands/setup.ts +77 -10
- package/src/cli/commands/sync-store.ts +47 -12
- package/src/cli/commands/sync.ts +26 -0
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/ConfigureView.tsx +2 -0
- package/src/codegraph/auto-context.ts +17 -0
- package/src/config/legacy-loader.ts +2 -2
- package/src/config/resolved-config.ts +8 -5
- package/src/config/yaml-loader.ts +20 -10
- package/src/dashboard/server.ts +10 -12
- package/src/embedding/api-provider.ts +4 -2
- package/src/embedding/fastembed-provider.ts +47 -14
- package/src/embedding/transformers-provider.ts +29 -7
- package/src/evaluation/memory-quality-gate.ts +90 -0
- package/src/hooks/official-skills.ts +2 -0
- package/src/integrations/registry.ts +1 -1
- package/src/knowledge/context-assembly.ts +1 -0
- package/src/knowledge/context-receipt.ts +15 -0
- package/src/knowledge/task-continuity.ts +413 -0
- package/src/knowledge/workset.ts +77 -0
- package/src/llm/provider.ts +2 -1
- package/src/memory/graph.ts +10 -8
- package/src/memory/session.ts +1 -1
- package/src/orchestrate/coordinator.ts +60 -0
- package/src/orchestrate/effect-ledger.ts +160 -0
- package/src/orchestrate/pipeline-trace.ts +40 -6
- package/src/runtime/maintenance-jobs.ts +12 -6
- package/src/sdk.ts +24 -0
- package/src/server/mcp-binding-store.ts +14 -3
- package/src/server/modern-mcp-bridge.ts +153 -27
- package/src/server/tool-profile.ts +3 -2
- package/src/server.ts +131 -7
- package/src/store/file-lock.ts +28 -3
- package/src/store/graph-store.ts +178 -46
- package/src/store/persistence.ts +14 -9
- package/src/store/sqlite-db.ts +150 -7
- package/src/store/sqlite-store.ts +32 -27
- package/src/sync/engine.ts +9 -7
- package/src/sync/namespace.ts +23 -0
- package/src/sync/policy.ts +19 -4
- package/src/sync/store-port.ts +43 -13
- package/src/sync/types.ts +2 -0
- package/src/team/team-store.ts +62 -27
- package/src/types.ts +1 -1
- package/src/workspace/mcp-adapters/grok.ts +32 -0
|
@@ -24,6 +24,7 @@ import { classifyError, resetBackoff, type RecoveryAction } from './error-recove
|
|
|
24
24
|
import { calculatePipelineCost, isBudgetExceeded, formatCostSummary } from './cost-tracker.js';
|
|
25
25
|
import { writeTaskEvidence, writePipelineSummary, type TaskEvidence } from './evidence.js';
|
|
26
26
|
import { TaskToolTracker } from './permission.js';
|
|
27
|
+
import { AgentEffectLedger } from './effect-ledger.js';
|
|
27
28
|
import { storeVerifiedFix, storeFixExhausted, searchKnownFixes, searchLessons, storeTaskCompletion, storePipelineSummary as memStorePipelineSummary, type BridgeConfig, DEFAULT_BRIDGE_CONFIG } from './memorix-bridge.js';
|
|
28
29
|
|
|
29
30
|
// ── Types ──────────────────────────────────────────────────────────
|
|
@@ -232,10 +233,12 @@ export async function runCoordinationLoop(config: CoordinatorConfig): Promise<Co
|
|
|
232
233
|
|
|
233
234
|
// Phase 6g: Pipeline tracing
|
|
234
235
|
let traceDb: ReturnType<typeof teamStore.getDb> | null = null;
|
|
236
|
+
let effectLedger: AgentEffectLedger | null = null;
|
|
235
237
|
try {
|
|
236
238
|
traceDb = teamStore.getDb();
|
|
237
239
|
resetTraceCache();
|
|
238
240
|
initTraceTable(traceDb);
|
|
241
|
+
if (pipelineId) effectLedger = new AgentEffectLedger(traceDb);
|
|
239
242
|
} catch { /* best-effort: tracing is non-critical */ }
|
|
240
243
|
|
|
241
244
|
// Phase 6i: Cleanup orphan worktrees from previous crashed runs
|
|
@@ -567,6 +570,31 @@ export async function runCoordinationLoop(config: CoordinatorConfig): Promise<Co
|
|
|
567
570
|
for await (const msg of agentProcess.messages!) {
|
|
568
571
|
if (msg.type === 'tool_use') {
|
|
569
572
|
dispatch.toolCount++;
|
|
573
|
+
const effectKey = msg.callId ?? `seq:${dispatch.toolCount}`;
|
|
574
|
+
const effect = effectLedger?.observe({
|
|
575
|
+
pipelineId: pipelineId!,
|
|
576
|
+
taskId: dispatch.taskId,
|
|
577
|
+
attempt: dispatch.attempt,
|
|
578
|
+
effectKey,
|
|
579
|
+
tool: msg.tool ?? 'unknown',
|
|
580
|
+
...(msg.callId ? { callId: msg.callId } : {}),
|
|
581
|
+
...(msg.input ? { input: msg.input } : {}),
|
|
582
|
+
});
|
|
583
|
+
if (effect && traceDb && pipelineId) {
|
|
584
|
+
try {
|
|
585
|
+
writeTrace(traceDb, {
|
|
586
|
+
pipelineId,
|
|
587
|
+
timestamp: Date.now(),
|
|
588
|
+
type: 'effect',
|
|
589
|
+
taskId: dispatch.taskId,
|
|
590
|
+
agent: dispatch.adapterName,
|
|
591
|
+
detail: `observed tool=${effect.tool} risk=${effect.riskTier} replay=${effect.replayPolicy} effect=${effect.effectKey}`,
|
|
592
|
+
attempt: dispatch.attempt,
|
|
593
|
+
riskTier: effect.riskTier,
|
|
594
|
+
effectKey: effect.effectKey,
|
|
595
|
+
});
|
|
596
|
+
} catch { /* effect trace is best-effort */ }
|
|
597
|
+
}
|
|
570
598
|
// Phase 7: Record tool usage for risk profiling
|
|
571
599
|
if (msg.tool) tracker.record(msg.tool);
|
|
572
600
|
emit('agent:tool_use', `[${adapter.name}] tool #${dispatch.toolCount}: ${msg.tool ?? 'unknown'}`, {
|
|
@@ -587,6 +615,16 @@ export async function runCoordinationLoop(config: CoordinatorConfig): Promise<Co
|
|
|
587
615
|
} catch { /* trace is best-effort */ }
|
|
588
616
|
}
|
|
589
617
|
}
|
|
618
|
+
if (msg.type === 'tool_result' && msg.callId && effectLedger && pipelineId) {
|
|
619
|
+
effectLedger.settle({
|
|
620
|
+
pipelineId,
|
|
621
|
+
taskId: dispatch.taskId,
|
|
622
|
+
attempt: dispatch.attempt,
|
|
623
|
+
effectKey: msg.callId,
|
|
624
|
+
status: 'succeeded',
|
|
625
|
+
detail: 'tool result observed',
|
|
626
|
+
});
|
|
627
|
+
}
|
|
590
628
|
// Accumulate text for planner output extraction (avoids ring buffer truncation)
|
|
591
629
|
if (msg.type === 'text' && msg.content) {
|
|
592
630
|
dispatch.accumulatedText += msg.content;
|
|
@@ -671,6 +709,9 @@ export async function runCoordinationLoop(config: CoordinatorConfig): Promise<Co
|
|
|
671
709
|
if (result.sessionId) {
|
|
672
710
|
taskSessionIds.set(dispatch.taskId, result.sessionId);
|
|
673
711
|
}
|
|
712
|
+
if (effectLedger && pipelineId) {
|
|
713
|
+
effectLedger.markAttemptUnknown(pipelineId, dispatch.taskId, dispatch.attempt);
|
|
714
|
+
}
|
|
674
715
|
|
|
675
716
|
// ── 方案 A: Orchestrator owns task lifecycle ──
|
|
676
717
|
// Agent does NOT call team_task. Orchestrator infers outcome from exit code.
|
|
@@ -755,6 +796,25 @@ export async function runCoordinationLoop(config: CoordinatorConfig): Promise<Co
|
|
|
755
796
|
for await (const msg of fixProcess.messages!) {
|
|
756
797
|
if (msg.type === 'tool_use') {
|
|
757
798
|
fixDispatch.toolCount++;
|
|
799
|
+
effectLedger?.observe({
|
|
800
|
+
pipelineId: pipelineId!,
|
|
801
|
+
taskId: fixDispatch.taskId,
|
|
802
|
+
attempt: fixDispatch.attempt,
|
|
803
|
+
effectKey: msg.callId ?? `seq:${fixDispatch.toolCount}`,
|
|
804
|
+
tool: msg.tool ?? 'unknown',
|
|
805
|
+
...(msg.callId ? { callId: msg.callId } : {}),
|
|
806
|
+
...(msg.input ? { input: msg.input } : {}),
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
if (msg.type === 'tool_result' && msg.callId && effectLedger && pipelineId) {
|
|
810
|
+
effectLedger.settle({
|
|
811
|
+
pipelineId,
|
|
812
|
+
taskId: fixDispatch.taskId,
|
|
813
|
+
attempt: fixDispatch.attempt,
|
|
814
|
+
effectKey: msg.callId,
|
|
815
|
+
status: 'succeeded',
|
|
816
|
+
detail: 'tool result observed',
|
|
817
|
+
});
|
|
758
818
|
}
|
|
759
819
|
if (msg.type === 'text' && msg.content) {
|
|
760
820
|
fixDispatch.accumulatedText += msg.content;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable, privacy-safe records for agent tool effects.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately not a claim of exactly-once tool delivery. It records
|
|
5
|
+
* what the coordinator observed, whether a result arrived, and what replay
|
|
6
|
+
* policy a future recovery path must apply before repeating the effect.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
10
|
+
import { sanitizeCredentials } from '../memory/secret-filter.js';
|
|
11
|
+
import { classifyTool, type RiskTier } from './permission.js';
|
|
12
|
+
|
|
13
|
+
export type AgentEffectStatus = 'observed' | 'succeeded' | 'failed' | 'unknown';
|
|
14
|
+
export type AgentEffectReplayPolicy = 'safe' | 'requires-idempotency-key';
|
|
15
|
+
|
|
16
|
+
export interface AgentEffectRecord {
|
|
17
|
+
id: string;
|
|
18
|
+
pipelineId: string;
|
|
19
|
+
taskId: string;
|
|
20
|
+
attempt: number;
|
|
21
|
+
effectKey: string;
|
|
22
|
+
tool: string;
|
|
23
|
+
riskTier: RiskTier;
|
|
24
|
+
replayPolicy: AgentEffectReplayPolicy;
|
|
25
|
+
status: AgentEffectStatus;
|
|
26
|
+
callId?: string;
|
|
27
|
+
idempotencyKey: string;
|
|
28
|
+
inputFingerprint?: string;
|
|
29
|
+
detail?: string;
|
|
30
|
+
startedAt: number;
|
|
31
|
+
finishedAt?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function fingerprintEffectInput(input: unknown): string | undefined {
|
|
35
|
+
if (input === undefined) return undefined;
|
|
36
|
+
try {
|
|
37
|
+
const safe = sanitizeCredentials(JSON.stringify(input));
|
|
38
|
+
return createHash('sha256').update(safe).digest('hex').slice(0, 24);
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function rowToEffect(row: any): AgentEffectRecord {
|
|
45
|
+
return {
|
|
46
|
+
id: String(row.id),
|
|
47
|
+
pipelineId: String(row.pipeline_id),
|
|
48
|
+
taskId: String(row.task_id),
|
|
49
|
+
attempt: Number(row.attempt),
|
|
50
|
+
effectKey: String(row.effect_key),
|
|
51
|
+
tool: String(row.tool),
|
|
52
|
+
riskTier: row.risk_tier as RiskTier,
|
|
53
|
+
replayPolicy: row.replay_policy as AgentEffectReplayPolicy,
|
|
54
|
+
status: row.status as AgentEffectStatus,
|
|
55
|
+
...(row.call_id ? { callId: String(row.call_id) } : {}),
|
|
56
|
+
idempotencyKey: String(row.idempotency_key),
|
|
57
|
+
...(row.input_fingerprint ? { inputFingerprint: String(row.input_fingerprint) } : {}),
|
|
58
|
+
...(row.detail ? { detail: String(row.detail) } : {}),
|
|
59
|
+
startedAt: Number(row.started_at),
|
|
60
|
+
...(row.finished_at ? { finishedAt: Number(row.finished_at) } : {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class AgentEffectLedger {
|
|
65
|
+
constructor(private readonly db: any) {}
|
|
66
|
+
|
|
67
|
+
observe(input: {
|
|
68
|
+
pipelineId: string;
|
|
69
|
+
taskId: string;
|
|
70
|
+
attempt: number;
|
|
71
|
+
effectKey: string;
|
|
72
|
+
tool: string;
|
|
73
|
+
callId?: string;
|
|
74
|
+
input?: unknown;
|
|
75
|
+
}): AgentEffectRecord {
|
|
76
|
+
const existing = this.db.prepare(`
|
|
77
|
+
SELECT * FROM agent_effects
|
|
78
|
+
WHERE pipeline_id = ? AND task_id = ? AND attempt = ? AND effect_key = ?
|
|
79
|
+
`).get(input.pipelineId, input.taskId, input.attempt, input.effectKey);
|
|
80
|
+
if (existing) return rowToEffect(existing);
|
|
81
|
+
|
|
82
|
+
const riskTier = classifyTool(input.tool);
|
|
83
|
+
const replayPolicy: AgentEffectReplayPolicy = riskTier === 'safe'
|
|
84
|
+
? 'safe'
|
|
85
|
+
: 'requires-idempotency-key';
|
|
86
|
+
const idempotencyKey = `${input.pipelineId}:${input.taskId}:${input.attempt}:${input.effectKey}`;
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
const record: AgentEffectRecord = {
|
|
89
|
+
id: randomUUID(),
|
|
90
|
+
pipelineId: input.pipelineId,
|
|
91
|
+
taskId: input.taskId,
|
|
92
|
+
attempt: input.attempt,
|
|
93
|
+
effectKey: input.effectKey,
|
|
94
|
+
tool: input.tool.slice(0, 200),
|
|
95
|
+
riskTier,
|
|
96
|
+
replayPolicy,
|
|
97
|
+
status: 'observed',
|
|
98
|
+
...(input.callId ? { callId: input.callId.slice(0, 200) } : {}),
|
|
99
|
+
idempotencyKey,
|
|
100
|
+
...(fingerprintEffectInput(input.input) ? { inputFingerprint: fingerprintEffectInput(input.input) } : {}),
|
|
101
|
+
startedAt: now,
|
|
102
|
+
};
|
|
103
|
+
this.db.prepare(`
|
|
104
|
+
INSERT OR IGNORE INTO agent_effects (
|
|
105
|
+
id, pipeline_id, task_id, attempt, effect_key, tool, risk_tier,
|
|
106
|
+
replay_policy, status, call_id, idempotency_key, input_fingerprint,
|
|
107
|
+
detail, started_at, finished_at
|
|
108
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
109
|
+
`).run(
|
|
110
|
+
record.id, record.pipelineId, record.taskId, record.attempt, record.effectKey,
|
|
111
|
+
record.tool, record.riskTier, record.replayPolicy, record.status,
|
|
112
|
+
record.callId ?? null, record.idempotencyKey, record.inputFingerprint ?? null,
|
|
113
|
+
null, record.startedAt, null,
|
|
114
|
+
);
|
|
115
|
+
return rowToEffect(this.db.prepare(`
|
|
116
|
+
SELECT * FROM agent_effects
|
|
117
|
+
WHERE pipeline_id = ? AND task_id = ? AND attempt = ? AND effect_key = ?
|
|
118
|
+
`).get(input.pipelineId, input.taskId, input.attempt, input.effectKey));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
settle(input: {
|
|
122
|
+
pipelineId: string;
|
|
123
|
+
taskId: string;
|
|
124
|
+
attempt: number;
|
|
125
|
+
effectKey: string;
|
|
126
|
+
status: Exclude<AgentEffectStatus, 'observed'>;
|
|
127
|
+
detail?: string;
|
|
128
|
+
}): AgentEffectRecord | undefined {
|
|
129
|
+
const detail = input.detail ? sanitizeCredentials(input.detail).slice(0, 500) : undefined;
|
|
130
|
+
this.db.prepare(`
|
|
131
|
+
UPDATE agent_effects
|
|
132
|
+
SET status = ?, detail = ?, finished_at = ?
|
|
133
|
+
WHERE pipeline_id = ? AND task_id = ? AND attempt = ? AND effect_key = ?
|
|
134
|
+
`).run(
|
|
135
|
+
input.status, detail ?? null, Date.now(), input.pipelineId, input.taskId,
|
|
136
|
+
input.attempt, input.effectKey,
|
|
137
|
+
);
|
|
138
|
+
const row = this.db.prepare(`
|
|
139
|
+
SELECT * FROM agent_effects
|
|
140
|
+
WHERE pipeline_id = ? AND task_id = ? AND attempt = ? AND effect_key = ?
|
|
141
|
+
`).get(input.pipelineId, input.taskId, input.attempt, input.effectKey);
|
|
142
|
+
return row ? rowToEffect(row) : undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
markAttemptUnknown(pipelineId: string, taskId: string, attempt: number): number {
|
|
146
|
+
const result = this.db.prepare(`
|
|
147
|
+
UPDATE agent_effects
|
|
148
|
+
SET status = 'unknown', detail = COALESCE(detail, 'No matching tool result observed'), finished_at = ?
|
|
149
|
+
WHERE pipeline_id = ? AND task_id = ? AND attempt = ? AND status = 'observed'
|
|
150
|
+
`).run(Date.now(), pipelineId, taskId, attempt);
|
|
151
|
+
return Number(result.changes ?? 0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
list(pipelineId: string, taskId?: string): AgentEffectRecord[] {
|
|
155
|
+
const rows = taskId
|
|
156
|
+
? this.db.prepare('SELECT * FROM agent_effects WHERE pipeline_id = ? AND task_id = ? ORDER BY started_at ASC, rowid ASC').all(pipelineId, taskId)
|
|
157
|
+
: this.db.prepare('SELECT * FROM agent_effects WHERE pipeline_id = ? ORDER BY started_at ASC, rowid ASC').all(pipelineId);
|
|
158
|
+
return rows.map(rowToEffect);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type Database from 'better-sqlite3';
|
|
9
|
+
import { sanitizeCredentials } from '../memory/secret-filter.js';
|
|
9
10
|
|
|
10
11
|
// ── Types ──────────────────────────────────────────────────────────
|
|
11
12
|
|
|
12
13
|
export type TraceEventType =
|
|
13
14
|
| 'plan' | 'materialize' | 'dispatch' | 'complete' | 'fail'
|
|
14
15
|
| 'retry' | 'timeout' | 'stale' | 'replan' | 'worktree:create'
|
|
15
|
-
| 'worktree:merge' | 'worktree:cleanup' | 'pipeline:start' | 'pipeline:end'
|
|
16
|
+
| 'worktree:merge' | 'worktree:cleanup' | 'pipeline:start' | 'pipeline:end'
|
|
17
|
+
| 'effect';
|
|
16
18
|
|
|
17
19
|
export interface TraceEvent {
|
|
18
20
|
pipelineId: string;
|
|
@@ -22,6 +24,10 @@ export interface TraceEvent {
|
|
|
22
24
|
agent?: string;
|
|
23
25
|
detail: string;
|
|
24
26
|
durationMs?: number;
|
|
27
|
+
attempt?: number;
|
|
28
|
+
riskTier?: 'safe' | 'moderate' | 'dangerous';
|
|
29
|
+
effectKey?: string;
|
|
30
|
+
memoryRefs?: string[];
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
// ── Schema ─────────────────────────────────────────────────────────
|
|
@@ -35,7 +41,11 @@ const CREATE_TABLE_SQL = `
|
|
|
35
41
|
task_id TEXT,
|
|
36
42
|
agent TEXT,
|
|
37
43
|
detail TEXT NOT NULL,
|
|
38
|
-
duration_ms INTEGER
|
|
44
|
+
duration_ms INTEGER,
|
|
45
|
+
attempt INTEGER,
|
|
46
|
+
risk_tier TEXT,
|
|
47
|
+
effect_key TEXT,
|
|
48
|
+
memory_refs TEXT
|
|
39
49
|
)
|
|
40
50
|
`;
|
|
41
51
|
|
|
@@ -48,6 +58,9 @@ const CREATE_INDEX_SQL = `
|
|
|
48
58
|
|
|
49
59
|
export function initTraceTable(db: Database.Database): void {
|
|
50
60
|
db.exec(CREATE_TABLE_SQL);
|
|
61
|
+
for (const column of ['attempt INTEGER', 'risk_tier TEXT', 'effect_key TEXT', 'memory_refs TEXT']) {
|
|
62
|
+
try { db.exec(`ALTER TABLE pipeline_traces ADD COLUMN ${column}`); } catch { /* already exists */ }
|
|
63
|
+
}
|
|
51
64
|
db.exec(CREATE_INDEX_SQL);
|
|
52
65
|
}
|
|
53
66
|
|
|
@@ -58,8 +71,10 @@ let _insertStmt: Database.Statement | null = null;
|
|
|
58
71
|
export function writeTrace(db: Database.Database, event: TraceEvent): void {
|
|
59
72
|
if (!_insertStmt) {
|
|
60
73
|
_insertStmt = db.prepare(`
|
|
61
|
-
INSERT INTO pipeline_traces (
|
|
62
|
-
|
|
74
|
+
INSERT INTO pipeline_traces (
|
|
75
|
+
pipeline_id, timestamp, type, task_id, agent, detail, duration_ms,
|
|
76
|
+
attempt, risk_tier, effect_key, memory_refs
|
|
77
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
63
78
|
`);
|
|
64
79
|
}
|
|
65
80
|
_insertStmt.run(
|
|
@@ -68,8 +83,12 @@ export function writeTrace(db: Database.Database, event: TraceEvent): void {
|
|
|
68
83
|
event.type,
|
|
69
84
|
event.taskId ?? null,
|
|
70
85
|
event.agent ?? null,
|
|
71
|
-
event.detail,
|
|
86
|
+
sanitizeCredentials(event.detail).slice(0, 2_000),
|
|
72
87
|
event.durationMs ?? null,
|
|
88
|
+
event.attempt ?? null,
|
|
89
|
+
event.riskTier ?? null,
|
|
90
|
+
event.effectKey ?? null,
|
|
91
|
+
event.memoryRefs ? JSON.stringify(event.memoryRefs.slice(0, 16)) : null,
|
|
73
92
|
);
|
|
74
93
|
}
|
|
75
94
|
|
|
@@ -81,7 +100,8 @@ export function getTraces(db: Database.Database, pipelineId: string): TraceEvent
|
|
|
81
100
|
).all(pipelineId) as Array<{
|
|
82
101
|
pipeline_id: string; timestamp: number; type: string;
|
|
83
102
|
task_id: string | null; agent: string | null; detail: string;
|
|
84
|
-
duration_ms: number | null;
|
|
103
|
+
duration_ms: number | null; attempt: number | null; risk_tier: string | null;
|
|
104
|
+
effect_key: string | null; memory_refs: string | null;
|
|
85
105
|
}>;
|
|
86
106
|
|
|
87
107
|
return rows.map(r => ({
|
|
@@ -92,9 +112,23 @@ export function getTraces(db: Database.Database, pipelineId: string): TraceEvent
|
|
|
92
112
|
agent: r.agent ?? undefined,
|
|
93
113
|
detail: r.detail,
|
|
94
114
|
durationMs: r.duration_ms ?? undefined,
|
|
115
|
+
attempt: r.attempt ?? undefined,
|
|
116
|
+
riskTier: r.risk_tier as TraceEvent['riskTier'] ?? undefined,
|
|
117
|
+
effectKey: r.effect_key ?? undefined,
|
|
118
|
+
memoryRefs: parseMemoryRefs(r.memory_refs),
|
|
95
119
|
}));
|
|
96
120
|
}
|
|
97
121
|
|
|
122
|
+
function parseMemoryRefs(value: string | null): string[] | undefined {
|
|
123
|
+
if (!value) return undefined;
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(value);
|
|
126
|
+
return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === 'string') : undefined;
|
|
127
|
+
} catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
98
132
|
// ── Prune ──────────────────────────────────────────────────────────
|
|
99
133
|
|
|
100
134
|
/**
|
|
@@ -92,6 +92,7 @@ const DEFAULT_LEASE_MS = 30_000;
|
|
|
92
92
|
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
93
93
|
const MAX_ERROR_LENGTH = 1_000;
|
|
94
94
|
const DEFAULT_COMPLETED_HISTORY_RETENTION_MS = 7 * 24 * 60 * 60 * 1_000;
|
|
95
|
+
export const DEFAULT_FAILED_HISTORY_RETENTION_MS = 30 * 24 * 60 * 60 * 1_000;
|
|
95
96
|
|
|
96
97
|
function parsePayload(raw: unknown): Record<string, unknown> {
|
|
97
98
|
if (typeof raw !== 'string' || !raw) return {};
|
|
@@ -292,20 +293,25 @@ export class MaintenanceJobStore {
|
|
|
292
293
|
|
|
293
294
|
/**
|
|
294
295
|
* Completed jobs are useful recent operator history, not permanent data.
|
|
295
|
-
* Failed
|
|
296
|
-
*
|
|
296
|
+
* Failed diagnostics are retained longer, but are still bounded so a
|
|
297
|
+
* permanently failing job kind cannot grow the database forever.
|
|
297
298
|
*/
|
|
298
|
-
pruneCompletedHistory(options: { now?: number; maxAgeMs?: number } = {}): number {
|
|
299
|
+
pruneCompletedHistory(options: { now?: number; maxAgeMs?: number; failedMaxAgeMs?: number } = {}): number {
|
|
299
300
|
const now = options.now ?? Date.now();
|
|
300
301
|
const maxAgeMs = Number.isFinite(options.maxAgeMs)
|
|
301
302
|
? Math.max(0, Math.floor(options.maxAgeMs!))
|
|
302
303
|
: DEFAULT_COMPLETED_HISTORY_RETENTION_MS;
|
|
304
|
+
const failedMaxAgeMs = Number.isFinite(options.failedMaxAgeMs)
|
|
305
|
+
? Math.max(0, Math.floor(options.failedMaxAgeMs!))
|
|
306
|
+
: DEFAULT_FAILED_HISTORY_RETENTION_MS;
|
|
303
307
|
const result = this.db.prepare(`
|
|
304
308
|
DELETE FROM maintenance_jobs
|
|
305
|
-
WHERE status = 'completed'
|
|
309
|
+
WHERE (status = 'completed'
|
|
306
310
|
AND completed_at IS NOT NULL
|
|
307
|
-
AND completed_at < ?
|
|
308
|
-
|
|
311
|
+
AND completed_at < ?)
|
|
312
|
+
OR (status = 'failed'
|
|
313
|
+
AND updated_at < ?)
|
|
314
|
+
`).run(now - maxAgeMs, now - failedMaxAgeMs);
|
|
309
315
|
return Number(result.changes ?? 0);
|
|
310
316
|
}
|
|
311
317
|
|
package/src/sdk.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
ObservationReader,
|
|
29
29
|
RetrievalQuality,
|
|
30
30
|
} from './types.js';
|
|
31
|
+
import path from 'node:path';
|
|
31
32
|
import {
|
|
32
33
|
canManageObservation,
|
|
33
34
|
canReadObservation,
|
|
@@ -101,6 +102,9 @@ export interface ResolveResult {
|
|
|
101
102
|
notFound: number[];
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
const activeSdkClients = new Map<string, number>();
|
|
106
|
+
let activeSdkDataDir: string | null = null;
|
|
107
|
+
|
|
104
108
|
/**
|
|
105
109
|
* A lightweight, self-contained memory client for reading and writing
|
|
106
110
|
* Memorix observations without MCP overhead.
|
|
@@ -124,6 +128,7 @@ export class MemoryClient {
|
|
|
124
128
|
private _oramaStore!: typeof import('./store/orama-store.js');
|
|
125
129
|
private _obsStore!: typeof import('./store/obs-store.js');
|
|
126
130
|
private _freshness!: typeof import('./memory/freshness.js');
|
|
131
|
+
private _sdkRefRegistered = false;
|
|
127
132
|
|
|
128
133
|
/** @internal — use createMemoryClient() instead */
|
|
129
134
|
constructor(projectId: string, projectRoot: string, dataDir: string) {
|
|
@@ -147,6 +152,12 @@ export class MemoryClient {
|
|
|
147
152
|
* Called by createMemoryClient(). Do not call directly.
|
|
148
153
|
*/
|
|
149
154
|
async _init(silent: boolean): Promise<void> {
|
|
155
|
+
const normalizedDataDir = path.resolve(this._dataDir);
|
|
156
|
+
if (activeSdkDataDir && activeSdkDataDir !== normalizedDataDir) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
'[memorix-sdk] Multiple active clients with different data directories are not supported in one process; use separate processes.',
|
|
159
|
+
);
|
|
160
|
+
}
|
|
150
161
|
// Suppress logs if requested
|
|
151
162
|
const originalError = console.error;
|
|
152
163
|
if (silent) {
|
|
@@ -167,6 +178,9 @@ export class MemoryClient {
|
|
|
167
178
|
|
|
168
179
|
// Prepare search index (hydrate Orama from SQLite)
|
|
169
180
|
await this._observations.prepareSearchIndex();
|
|
181
|
+
activeSdkDataDir = normalizedDataDir;
|
|
182
|
+
activeSdkClients.set(normalizedDataDir, (activeSdkClients.get(normalizedDataDir) ?? 0) + 1);
|
|
183
|
+
this._sdkRefRegistered = true;
|
|
170
184
|
} finally {
|
|
171
185
|
if (silent) {
|
|
172
186
|
console.error = originalError;
|
|
@@ -298,6 +312,16 @@ export class MemoryClient {
|
|
|
298
312
|
async close(): Promise<void> {
|
|
299
313
|
if (this._closed) return;
|
|
300
314
|
this._closed = true;
|
|
315
|
+
const normalizedDataDir = path.resolve(this._dataDir);
|
|
316
|
+
const currentRefs = activeSdkClients.get(normalizedDataDir) ?? 0;
|
|
317
|
+
const remainingRefs = Math.max(0, currentRefs - (this._sdkRefRegistered ? 1 : 0));
|
|
318
|
+
if (remainingRefs > 0) {
|
|
319
|
+
activeSdkClients.set(normalizedDataDir, remainingRefs);
|
|
320
|
+
this._sdkRefRegistered = false;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
activeSdkClients.delete(normalizedDataDir);
|
|
324
|
+
if (activeSdkDataDir === normalizedDataDir) activeSdkDataDir = null;
|
|
301
325
|
try {
|
|
302
326
|
this._obsStore.resetObservationStore();
|
|
303
327
|
await this._oramaStore.resetDb();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { getDatabase } from '../store/sqlite-db.js';
|
|
3
3
|
|
|
4
|
+
export const DEFAULT_MCP_BINDING_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
5
|
+
|
|
4
6
|
export interface McpProjectBinding {
|
|
5
7
|
handleId: string;
|
|
6
8
|
projectId: string;
|
|
@@ -38,6 +40,7 @@ export class McpBindingStore {
|
|
|
38
40
|
|
|
39
41
|
create(input: { projectId: string; projectRoot: string; dataDir: string; ttlMs?: number }): McpProjectBinding {
|
|
40
42
|
const now = new Date();
|
|
43
|
+
const ttlMs = input.ttlMs ?? DEFAULT_MCP_BINDING_TTL_MS;
|
|
41
44
|
const binding: McpProjectBinding = {
|
|
42
45
|
handleId: `mxh_${randomUUID().replace(/-/g, '')}`,
|
|
43
46
|
projectId: input.projectId,
|
|
@@ -45,7 +48,7 @@ export class McpBindingStore {
|
|
|
45
48
|
dataDir: input.dataDir,
|
|
46
49
|
createdAt: now.toISOString(),
|
|
47
50
|
lastUsedAt: now.toISOString(),
|
|
48
|
-
...(
|
|
51
|
+
...(ttlMs > 0 ? { expiresAt: new Date(now.getTime() + ttlMs).toISOString() } : {}),
|
|
49
52
|
};
|
|
50
53
|
this.requireDb().prepare(`
|
|
51
54
|
INSERT INTO mcp_bindings (handle_id, project_id, project_root, data_dir, created_at, last_used_at, expires_at)
|
|
@@ -69,8 +72,16 @@ export class McpBindingStore {
|
|
|
69
72
|
const binding = this.get(handleId);
|
|
70
73
|
if (!binding) return undefined;
|
|
71
74
|
const lastUsedAt = new Date().toISOString();
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
const expiresAt = new Date(Date.now() + DEFAULT_MCP_BINDING_TTL_MS).toISOString();
|
|
76
|
+
this.requireDb().prepare(`UPDATE mcp_bindings SET last_used_at = ?, expires_at = ? WHERE handle_id = ?`).run(lastUsedAt, expiresAt, handleId);
|
|
77
|
+
return { ...binding, lastUsedAt, expiresAt };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
cleanupExpired(now = Date.now()): number {
|
|
81
|
+
const result = this.requireDb().prepare(
|
|
82
|
+
`DELETE FROM mcp_bindings WHERE expires_at IS NOT NULL AND expires_at <= ?`,
|
|
83
|
+
).run(new Date(now).toISOString());
|
|
84
|
+
return Number(result.changes ?? 0);
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
/** Reuse the most recent valid extension handle for a verified project root. */
|