@syntesseraai/opencode-feature-factory 0.4.2 → 0.4.4

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.
Files changed (48) hide show
  1. package/agents/building.md +9 -6
  2. package/package.json +2 -2
  3. package/dist/discovery.test.d.ts +0 -10
  4. package/dist/discovery.test.js +0 -97
  5. package/dist/local-recall/daemon-controller.d.ts +0 -51
  6. package/dist/local-recall/daemon-controller.js +0 -166
  7. package/dist/local-recall/daemon.d.ts +0 -35
  8. package/dist/local-recall/daemon.js +0 -262
  9. package/dist/local-recall/index-state.d.ts +0 -14
  10. package/dist/local-recall/index-state.js +0 -76
  11. package/dist/local-recall/index.d.ts +0 -20
  12. package/dist/local-recall/index.js +0 -27
  13. package/dist/local-recall/mcp-server.d.ts +0 -34
  14. package/dist/local-recall/mcp-server.js +0 -194
  15. package/dist/local-recall/mcp-stdio-server.d.ts +0 -4
  16. package/dist/local-recall/mcp-stdio-server.js +0 -225
  17. package/dist/local-recall/mcp-tools.d.ts +0 -103
  18. package/dist/local-recall/mcp-tools.js +0 -187
  19. package/dist/local-recall/memory-service.d.ts +0 -32
  20. package/dist/local-recall/memory-service.js +0 -156
  21. package/dist/local-recall/model-router.d.ts +0 -23
  22. package/dist/local-recall/model-router.js +0 -41
  23. package/dist/local-recall/processed-log.d.ts +0 -41
  24. package/dist/local-recall/processed-log.js +0 -85
  25. package/dist/local-recall/prompt-injection.d.ts +0 -2
  26. package/dist/local-recall/prompt-injection.js +0 -194
  27. package/dist/local-recall/session-extractor.d.ts +0 -19
  28. package/dist/local-recall/session-extractor.js +0 -172
  29. package/dist/local-recall/storage-reader.d.ts +0 -40
  30. package/dist/local-recall/storage-reader.js +0 -157
  31. package/dist/local-recall/thinking-extractor.d.ts +0 -16
  32. package/dist/local-recall/thinking-extractor.js +0 -132
  33. package/dist/local-recall/types.d.ts +0 -152
  34. package/dist/local-recall/types.js +0 -7
  35. package/dist/local-recall/vector/embedding-provider.d.ts +0 -37
  36. package/dist/local-recall/vector/embedding-provider.js +0 -184
  37. package/dist/local-recall/vector/orama-index.d.ts +0 -39
  38. package/dist/local-recall/vector/orama-index.js +0 -408
  39. package/dist/local-recall/vector/types.d.ts +0 -33
  40. package/dist/local-recall/vector/types.js +0 -1
  41. package/dist/output.test.d.ts +0 -8
  42. package/dist/output.test.js +0 -205
  43. package/dist/plugins/ff-reviews-delete-plugin.d.ts +0 -2
  44. package/dist/plugins/ff-reviews-delete-plugin.js +0 -32
  45. package/dist/quality-gate-config.test.d.ts +0 -9
  46. package/dist/quality-gate-config.test.js +0 -164
  47. package/dist/stop-quality-gate.test.d.ts +0 -8
  48. package/dist/stop-quality-gate.test.js +0 -549
@@ -76,18 +76,21 @@ At the start of EVERY building task:
76
76
 
77
77
  To prevent conflicts and ensure a clean state, you MUST use git worktrees for your implementation:
78
78
 
79
- 1. **Create Worktree:** Before starting code modifications, create a dedicated worktree outside the main directory:
79
+ 1. **Create Worktree:** Before starting code modifications, create a dedicated worktree in a writable root (avoid `../` paths that may resolve outside CI workspace mounts):
80
80
  ```bash
81
- git worktree add ../ff-build-{UUID} -b feature/ff-build-{UUID}
81
+ WORKTREE_ROOT="${FF_WORKTREE_ROOT:-$PWD/.feature-factory/worktrees}"
82
+ mkdir -p "$WORKTREE_ROOT"
83
+ WORKTREE_PATH="$WORKTREE_ROOT/ff-build-{UUID}"
84
+ git worktree add "$WORKTREE_PATH" -b "feature/ff-build-{UUID}"
82
85
  ```
83
86
  2. **Use the Worktree:**
84
- - When using the `bash` tool, always set the `workdir` parameter to the absolute path of your worktree.
85
- - When using `edit`, `write`, or `read` tools, ensure the `filePath` points to the files inside your worktree (e.g., replace the base project path with the path to your worktree).
87
+ - When using the `bash` tool, always set the `workdir` parameter to the absolute path in `$WORKTREE_PATH`.
88
+ - When using `edit`, `write`, or `read` tools, ensure the `filePath` points to files inside `$WORKTREE_PATH`.
86
89
  3. **Commit & Push:** Commit your changes and push the branch from within the worktree.
87
90
  4. **Cleanup:** After your work is pushed, remove the worktree:
88
91
  ```bash
89
- git worktree remove ../ff-build-{UUID} --force
90
- git branch -D feature/ff-build-{UUID}
92
+ git worktree remove "$WORKTREE_PATH" --force
93
+ git branch -D "feature/ff-build-{UUID}"
91
94
  ```
92
95
 
93
96
  ## File Management Tools
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@syntesseraai/opencode-feature-factory",
4
- "version": "0.4.2",
4
+ "version": "0.4.4",
5
5
  "type": "module",
6
6
  "description": "OpenCode plugin for Feature Factory agents - provides sub-agents and skills for validation, review, security, and architecture assessment",
7
7
  "license": "MIT",
8
8
  "main": "./dist/index.js",
9
9
  "types": "./dist/index.d.ts",
10
10
  "bin": {
11
- "ff-deploy": "./bin/ff-deploy.js"
11
+ "ff-deploy": "bin/ff-deploy.js"
12
12
  },
13
13
  "files": [
14
14
  "dist",
@@ -1,10 +0,0 @@
1
- /**
2
- * Unit tests for discovery module
3
- *
4
- * Tests focus on the pure function:
5
- * - buildRunCommand: builds package manager-specific run commands
6
- *
7
- * Note: Most functions in discovery.ts require shell access ($) so they're
8
- * integration-tested elsewhere. This file tests the pure utility functions.
9
- */
10
- export {};
@@ -1,97 +0,0 @@
1
- /**
2
- * Unit tests for discovery module
3
- *
4
- * Tests focus on the pure function:
5
- * - buildRunCommand: builds package manager-specific run commands
6
- *
7
- * Note: Most functions in discovery.ts require shell access ($) so they're
8
- * integration-tested elsewhere. This file tests the pure utility functions.
9
- */
10
- // Re-implement buildRunCommand for testing since it's not exported
11
- function buildRunCommand(pm, script) {
12
- switch (pm) {
13
- case 'pnpm':
14
- return `pnpm -s run ${script}`;
15
- case 'bun':
16
- return `bun run ${script}`;
17
- case 'yarn':
18
- return `yarn -s ${script}`;
19
- case 'npm':
20
- return `npm run -s ${script}`;
21
- default:
22
- return `npm run -s ${script}`;
23
- }
24
- }
25
- describe('buildRunCommand', () => {
26
- describe('pnpm', () => {
27
- it('should build correct pnpm command', () => {
28
- expect(buildRunCommand('pnpm', 'lint')).toBe('pnpm -s run lint');
29
- });
30
- it('should build correct pnpm command for build script', () => {
31
- expect(buildRunCommand('pnpm', 'build')).toBe('pnpm -s run build');
32
- });
33
- it('should build correct pnpm command for test script', () => {
34
- expect(buildRunCommand('pnpm', 'test')).toBe('pnpm -s run test');
35
- });
36
- it('should handle scripts with colons', () => {
37
- expect(buildRunCommand('pnpm', 'lint:ci')).toBe('pnpm -s run lint:ci');
38
- });
39
- });
40
- describe('bun', () => {
41
- it('should build correct bun command', () => {
42
- expect(buildRunCommand('bun', 'lint')).toBe('bun run lint');
43
- });
44
- it('should build correct bun command for build script', () => {
45
- expect(buildRunCommand('bun', 'build')).toBe('bun run build');
46
- });
47
- it('should build correct bun command for test script', () => {
48
- expect(buildRunCommand('bun', 'test')).toBe('bun run test');
49
- });
50
- });
51
- describe('yarn', () => {
52
- it('should build correct yarn command', () => {
53
- expect(buildRunCommand('yarn', 'lint')).toBe('yarn -s lint');
54
- });
55
- it('should build correct yarn command for build script', () => {
56
- expect(buildRunCommand('yarn', 'build')).toBe('yarn -s build');
57
- });
58
- it('should build correct yarn command for test script', () => {
59
- expect(buildRunCommand('yarn', 'test')).toBe('yarn -s test');
60
- });
61
- });
62
- describe('npm', () => {
63
- it('should build correct npm command', () => {
64
- expect(buildRunCommand('npm', 'lint')).toBe('npm run -s lint');
65
- });
66
- it('should build correct npm command for build script', () => {
67
- expect(buildRunCommand('npm', 'build')).toBe('npm run -s build');
68
- });
69
- it('should build correct npm command for test script', () => {
70
- expect(buildRunCommand('npm', 'test')).toBe('npm run -s test');
71
- });
72
- });
73
- describe('edge cases', () => {
74
- it('should handle script names with hyphens', () => {
75
- expect(buildRunCommand('npm', 'type-check')).toBe('npm run -s type-check');
76
- });
77
- it('should handle script names with underscores', () => {
78
- expect(buildRunCommand('pnpm', 'lint_fix')).toBe('pnpm -s run lint_fix');
79
- });
80
- it('should handle long script names', () => {
81
- expect(buildRunCommand('yarn', 'test:unit:coverage')).toBe('yarn -s test:unit:coverage');
82
- });
83
- });
84
- });
85
- /**
86
- * PackageManager type validation tests
87
- * These ensure the type constraints are working correctly
88
- */
89
- describe('PackageManager type', () => {
90
- it('should accept valid package manager values', () => {
91
- const validManagers = ['pnpm', 'bun', 'yarn', 'npm'];
92
- validManagers.forEach((pm) => {
93
- expect(['pnpm', 'bun', 'yarn', 'npm']).toContain(pm);
94
- });
95
- });
96
- });
97
- export {};
@@ -1,51 +0,0 @@
1
- import { type ExtractionStats } from './daemon.js';
2
- import { OramaMemoryIndex } from './vector/orama-index.js';
3
- export interface DaemonRunReport {
4
- fullRebuild: boolean;
5
- extraction: ExtractionStats | null;
6
- upserted: number;
7
- removed: number;
8
- documents: number;
9
- durationMs: number;
10
- completedAt: string;
11
- }
12
- export interface DaemonStatus {
13
- running: boolean;
14
- processing: boolean;
15
- intervalMs: number;
16
- pending: boolean;
17
- pendingReason: string | null;
18
- lastRun: DaemonRunReport | null;
19
- lastError: string | null;
20
- }
21
- interface ControllerOptions {
22
- directory: string;
23
- index: OramaMemoryIndex;
24
- intervalMs?: number;
25
- extractionEnabled?: boolean;
26
- }
27
- export declare class LocalRecallDaemonController {
28
- private readonly directory;
29
- private readonly index;
30
- private readonly extractionEnabled;
31
- private intervalMs;
32
- private timer;
33
- private running;
34
- private processing;
35
- private pending;
36
- private pendingFullRebuild;
37
- private pendingReason;
38
- private lastRun;
39
- private lastError;
40
- constructor(options: ControllerOptions);
41
- start(intervalMs?: number): DaemonStatus;
42
- stop(): DaemonStatus;
43
- requestRun(reason?: string, fullRebuild?: boolean): void;
44
- runNow(reason?: string, fullRebuild?: boolean): Promise<DaemonStatus>;
45
- rebuild(): Promise<DaemonStatus>;
46
- getStatus(): DaemonStatus;
47
- private runLoop;
48
- private runCycle;
49
- private waitForIdle;
50
- }
51
- export {};
@@ -1,166 +0,0 @@
1
- import { runExtraction } from './daemon.js';
2
- import { computeMemoryHash, readIndexState, writeIndexState } from './index-state.js';
3
- import { listAllMemories } from './memory-service.js';
4
- const DEFAULT_INTERVAL_MS = 15_000;
5
- export class LocalRecallDaemonController {
6
- directory;
7
- index;
8
- extractionEnabled;
9
- intervalMs;
10
- timer = null;
11
- running = false;
12
- processing = false;
13
- pending = false;
14
- pendingFullRebuild = false;
15
- pendingReason = null;
16
- lastRun = null;
17
- lastError = null;
18
- constructor(options) {
19
- this.directory = options.directory;
20
- this.index = options.index;
21
- this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
22
- this.extractionEnabled = options.extractionEnabled ?? true;
23
- }
24
- start(intervalMs) {
25
- if (intervalMs && intervalMs > 0) {
26
- this.intervalMs = intervalMs;
27
- }
28
- if (this.running) {
29
- return this.getStatus();
30
- }
31
- this.running = true;
32
- this.timer = setInterval(() => {
33
- this.requestRun('interval');
34
- }, this.intervalMs);
35
- this.requestRun('startup');
36
- return this.getStatus();
37
- }
38
- stop() {
39
- this.running = false;
40
- if (this.timer) {
41
- clearInterval(this.timer);
42
- this.timer = null;
43
- }
44
- return this.getStatus();
45
- }
46
- requestRun(reason = 'manual', fullRebuild = false) {
47
- this.pending = true;
48
- this.pendingReason = reason;
49
- if (fullRebuild) {
50
- this.pendingFullRebuild = true;
51
- }
52
- void this.runLoop();
53
- }
54
- async runNow(reason = 'manual', fullRebuild = false) {
55
- this.requestRun(reason, fullRebuild);
56
- await this.waitForIdle();
57
- return this.getStatus();
58
- }
59
- async rebuild() {
60
- return this.runNow('rebuild', true);
61
- }
62
- getStatus() {
63
- return {
64
- running: this.running,
65
- processing: this.processing,
66
- intervalMs: this.intervalMs,
67
- pending: this.pending,
68
- pendingReason: this.pendingReason,
69
- lastRun: this.lastRun,
70
- lastError: this.lastError,
71
- };
72
- }
73
- async runLoop() {
74
- if (this.processing) {
75
- return;
76
- }
77
- this.processing = true;
78
- try {
79
- while (this.pending) {
80
- const fullRebuild = this.pendingFullRebuild;
81
- this.pending = false;
82
- this.pendingFullRebuild = false;
83
- this.pendingReason = null;
84
- await this.runCycle(fullRebuild);
85
- }
86
- }
87
- finally {
88
- this.processing = false;
89
- }
90
- }
91
- async runCycle(fullRebuild) {
92
- const startedAt = Date.now();
93
- this.lastError = null;
94
- try {
95
- const extraction = this.extractionEnabled ? await runExtraction(this.directory) : null;
96
- const allMemories = await listAllMemories(this.directory);
97
- const now = Date.now();
98
- let upserted = 0;
99
- let removed = 0;
100
- if (fullRebuild) {
101
- upserted = await this.index.rebuild(allMemories);
102
- }
103
- else {
104
- const previousState = await readIndexState(this.directory);
105
- const nextEntries = {};
106
- const changed = [];
107
- for (const memory of allMemories) {
108
- const hash = computeMemoryHash(memory);
109
- nextEntries[memory.id] = {
110
- hash,
111
- updatedAt: now,
112
- };
113
- const previous = previousState.entries[memory.id];
114
- if (!previous || previous.hash !== hash) {
115
- changed.push(memory);
116
- }
117
- }
118
- const removedIDs = Object.keys(previousState.entries).filter((id) => !nextEntries[id]);
119
- if (changed.length > 0) {
120
- upserted = await this.index.upsertMemories(changed);
121
- }
122
- if (removedIDs.length > 0) {
123
- removed = await this.index.removeMemories(removedIDs);
124
- }
125
- }
126
- await writeIndexState(this.directory, {
127
- version: 1,
128
- entries: Object.fromEntries(allMemories.map((memory) => [
129
- memory.id,
130
- {
131
- hash: computeMemoryHash(memory),
132
- updatedAt: now,
133
- },
134
- ])),
135
- updatedAt: now,
136
- });
137
- const documents = this.index.getStatus().documents;
138
- this.lastRun = {
139
- fullRebuild,
140
- extraction,
141
- upserted,
142
- removed,
143
- documents,
144
- durationMs: Date.now() - startedAt,
145
- completedAt: new Date().toISOString(),
146
- };
147
- }
148
- catch (error) {
149
- this.lastError = error instanceof Error ? error.message : String(error);
150
- this.lastRun = {
151
- fullRebuild,
152
- extraction: null,
153
- upserted: 0,
154
- removed: 0,
155
- documents: this.index.getStatus().documents,
156
- durationMs: Date.now() - startedAt,
157
- completedAt: new Date().toISOString(),
158
- };
159
- }
160
- }
161
- async waitForIdle() {
162
- while (this.processing || this.pending) {
163
- await new Promise((resolve) => setTimeout(resolve, 25));
164
- }
165
- }
166
- }
@@ -1,35 +0,0 @@
1
- /**
2
- * daemon.ts — Background extraction daemon for local-recall.
3
- *
4
- * Scans OpenCode session storage for unprocessed assistant and thinking messages,
5
- * runs the extraction pipeline (session + thinking extractors),
6
- * and stores resulting memories with logical IDs and content-hash
7
- * idempotency.
8
- *
9
- * Logical ID conventions:
10
- * session memories → session-<sessionID>-<messageID>-<index>
11
- * thinking memories → thinking-<sessionID>-<messageID>-<index>
12
- *
13
- * Idempotency is dual-layer:
14
- * 1. Message-ID check (fast skip for already-processed messages)
15
- * 2. Content-hash check (skips duplicate content across edits/replays)
16
- */
17
- export interface ExtractionStats {
18
- sessionsScanned: number;
19
- messagesScanned: number;
20
- messagesSkipped: number;
21
- newMemories: number;
22
- errors: string[];
23
- }
24
- /**
25
- * Run a full extraction pass for the project rooted at `directory`.
26
- *
27
- * 1. Find the OpenCode project matching `directory`
28
- * 2. Load existing processed log for fast membership checks
29
- * 3. Iterate sessions → messages (assistant + thinking)
30
- * 4. Skip by message-ID *and* content-hash (dual idempotency)
31
- * 5. Run session + thinking extractors
32
- * 6. Assign logical IDs (session-<sid>-<msgid>-N / thinking-<sid>-<msgid>-N)
33
- * 7. Store new memories & update processed log
34
- */
35
- export declare function runExtraction(directory: string): Promise<ExtractionStats>;
@@ -1,262 +0,0 @@
1
- /**
2
- * daemon.ts — Background extraction daemon for local-recall.
3
- *
4
- * Scans OpenCode session storage for unprocessed assistant and thinking messages,
5
- * runs the extraction pipeline (session + thinking extractors),
6
- * and stores resulting memories with logical IDs and content-hash
7
- * idempotency.
8
- *
9
- * Logical ID conventions:
10
- * session memories → session-<sessionID>-<messageID>-<index>
11
- * thinking memories → thinking-<sessionID>-<messageID>-<index>
12
- *
13
- * Idempotency is dual-layer:
14
- * 1. Message-ID check (fast skip for already-processed messages)
15
- * 2. Content-hash check (skips duplicate content across edits/replays)
16
- */
17
- import * as fs from 'node:fs/promises';
18
- import { findProject, listSessions, listMessages, listParts } from './storage-reader.js';
19
- import { extractFromMessage } from './session-extractor.js';
20
- import { extractThinkingFromMessage } from './thinking-extractor.js';
21
- import { readProcessedLog, getProcessedMessageIDs, getProcessedHashes, markProcessed, contentHash, } from './processed-log.js';
22
- import { getMemoriesDir, storeMemories } from './memory-service.js';
23
- function getErrorMessage(error) {
24
- return error instanceof Error ? error.message : String(error);
25
- }
26
- function toProcessedFailureEntry(failure, failureMessage) {
27
- switch (failure.scope) {
28
- case 'project':
29
- return {
30
- kind: 'failure',
31
- scope: 'project',
32
- processedAt: Date.now(),
33
- failure: failureMessage,
34
- directory: failure.directory,
35
- };
36
- case 'session':
37
- return {
38
- kind: 'failure',
39
- scope: 'session',
40
- processedAt: Date.now(),
41
- failure: failureMessage,
42
- sessionID: failure.sessionID,
43
- };
44
- case 'extraction':
45
- return {
46
- kind: 'failure',
47
- scope: 'extraction',
48
- processedAt: Date.now(),
49
- failure: failureMessage,
50
- };
51
- }
52
- }
53
- function recordFailure(stats, failure) {
54
- let message;
55
- switch (failure.scope) {
56
- case 'project':
57
- message = `No OpenCode project found for directory: ${failure.directory}`;
58
- break;
59
- case 'message':
60
- message = `Error processing message ${failure.messageID}: ${getErrorMessage(failure.error)}`;
61
- break;
62
- case 'session':
63
- message = `Error processing session ${failure.sessionID}: ${getErrorMessage(failure.error)}`;
64
- break;
65
- case 'extraction':
66
- message = `Extraction failed: ${getErrorMessage(failure.error)}`;
67
- break;
68
- }
69
- stats.errors.push(message);
70
- return message;
71
- }
72
- // ────────────────────────────────────────────────────────────
73
- // Helpers
74
- // ────────────────────────────────────────────────────────────
75
- /**
76
- * Build a deterministic content hash for a message based on its
77
- * concatenated part text. This allows us to detect duplicate
78
- * processing even when message IDs change.
79
- */
80
- async function buildMessageContentHash(messageID) {
81
- try {
82
- const parts = await listParts(messageID);
83
- const textParts = parts
84
- .filter((p) => p.type === 'text' || p.type === 'reasoning' || p.type === 'thinking')
85
- .map((p) => p.text ?? '')
86
- .join('\n');
87
- // Hash content only (no messageID) so dedupe works across ID changes
88
- return contentHash(textParts);
89
- }
90
- catch {
91
- // Fallback: hash the message ID itself (best effort)
92
- return contentHash(messageID);
93
- }
94
- }
95
- // ────────────────────────────────────────────────────────────
96
- // Main extraction loop
97
- // ────────────────────────────────────────────────────────────
98
- /**
99
- * Run a full extraction pass for the project rooted at `directory`.
100
- *
101
- * 1. Find the OpenCode project matching `directory`
102
- * 2. Load existing processed log for fast membership checks
103
- * 3. Iterate sessions → messages (assistant + thinking)
104
- * 4. Skip by message-ID *and* content-hash (dual idempotency)
105
- * 5. Run session + thinking extractors
106
- * 6. Assign logical IDs (session-<sid>-<msgid>-N / thinking-<sid>-<msgid>-N)
107
- * 7. Store new memories & update processed log
108
- */
109
- export async function runExtraction(directory) {
110
- const stats = {
111
- sessionsScanned: 0,
112
- messagesScanned: 0,
113
- messagesSkipped: 0,
114
- newMemories: 0,
115
- errors: [],
116
- };
117
- try {
118
- // Find project for this directory
119
- const project = await findProject(directory);
120
- if (!project) {
121
- const failure = { scope: 'project', directory };
122
- const failureMessage = recordFailure(stats, failure);
123
- try {
124
- await markProcessed(directory, [toProcessedFailureEntry(failure, failureMessage)]);
125
- }
126
- catch (persistErr) {
127
- stats.errors.push(`Failed to persist processing failure: ${getErrorMessage(persistErr)}`);
128
- }
129
- return stats;
130
- }
131
- // Ensure local-recall directories exist
132
- await fs.mkdir(getMemoriesDir(directory), { recursive: true });
133
- // Pre-load processed log for fast lookups
134
- const existingLog = await readProcessedLog(directory);
135
- const processedMsgIDs = getProcessedMessageIDs(existingLog);
136
- const processedHashes = getProcessedHashes(existingLog);
137
- // Iterate sessions
138
- const sessions = await listSessions(project.id);
139
- stats.sessionsScanned = sessions.length;
140
- const newProcessedEntries = [];
141
- const allNewMemories = [];
142
- for (const session of sessions) {
143
- try {
144
- const messages = await listMessages(session.id);
145
- for (const message of messages) {
146
- stats.messagesScanned++;
147
- // Only process assistant and thinking messages
148
- if (message.role !== 'assistant' && message.role !== 'thinking') {
149
- stats.messagesSkipped++;
150
- continue;
151
- }
152
- // Layer 1: skip by message ID
153
- if (processedMsgIDs.has(message.id)) {
154
- stats.messagesSkipped++;
155
- continue;
156
- }
157
- // Layer 2: skip by content hash
158
- const msgHash = await buildMessageContentHash(message.id);
159
- if (processedHashes.has(msgHash)) {
160
- stats.messagesSkipped++;
161
- continue;
162
- }
163
- try {
164
- const input = {
165
- sessionID: session.id,
166
- messageID: message.id,
167
- };
168
- // Run both extractors in parallel
169
- const [sessionResults, thinkingResults] = await Promise.all([
170
- extractFromMessage(input),
171
- extractThinkingFromMessage(input),
172
- ]);
173
- // Convert ExtractionResult → Memory with logical IDs
174
- const sessionMemories = sessionResults.map((r, idx) => ({
175
- id: `session-${session.id}-${message.id}-${idx}`,
176
- sessionID: r.sessionID,
177
- messageID: r.messageID,
178
- category: r.category,
179
- title: r.title,
180
- body: r.body,
181
- tags: r.tags,
182
- importance: r.importance,
183
- createdAt: message.time?.created ?? Date.now(),
184
- extractedBy: 'local-recall-daemon',
185
- }));
186
- const thinkingMemories = thinkingResults.map((r, idx) => ({
187
- id: `thinking-${session.id}-${message.id}-${idx}`,
188
- sessionID: r.sessionID,
189
- messageID: r.messageID,
190
- category: r.category,
191
- title: r.title,
192
- body: r.body,
193
- tags: r.tags,
194
- importance: r.importance,
195
- createdAt: message.time?.created ?? Date.now(),
196
- extractedBy: 'local-recall-daemon',
197
- }));
198
- const memories = [...sessionMemories, ...thinkingMemories];
199
- allNewMemories.push(...memories);
200
- // Add to fast-lookup sets so subsequent messages in this
201
- // pass are also deduplicated
202
- processedMsgIDs.add(message.id);
203
- processedHashes.add(msgHash);
204
- // Mark as processed with content hash
205
- newProcessedEntries.push({
206
- status: 'success',
207
- messageID: message.id,
208
- contentHash: msgHash,
209
- processedAt: Date.now(),
210
- memoriesCreated: memories.length,
211
- });
212
- }
213
- catch (err) {
214
- recordFailure(stats, {
215
- scope: 'message',
216
- messageID: message.id,
217
- error: err,
218
- });
219
- // Still mark as processed to avoid re-trying broken messages
220
- newProcessedEntries.push({
221
- status: 'failed',
222
- messageID: message.id,
223
- contentHash: msgHash,
224
- processedAt: Date.now(),
225
- memoriesCreated: 0,
226
- failure: getErrorMessage(err),
227
- });
228
- }
229
- }
230
- }
231
- catch (err) {
232
- const failure = {
233
- scope: 'session',
234
- sessionID: session.id,
235
- error: err,
236
- };
237
- const failureMessage = recordFailure(stats, failure);
238
- newProcessedEntries.push(toProcessedFailureEntry(failure, failureMessage));
239
- }
240
- }
241
- // Batch store all new memories
242
- if (allNewMemories.length > 0) {
243
- await storeMemories(directory, allNewMemories);
244
- stats.newMemories = allNewMemories.length;
245
- }
246
- // Batch update processed log
247
- if (newProcessedEntries.length > 0) {
248
- await markProcessed(directory, newProcessedEntries);
249
- }
250
- }
251
- catch (err) {
252
- const failure = { scope: 'extraction', error: err };
253
- const failureMessage = recordFailure(stats, failure);
254
- try {
255
- await markProcessed(directory, [toProcessedFailureEntry(failure, failureMessage)]);
256
- }
257
- catch (persistErr) {
258
- stats.errors.push(`Failed to persist processing failure: ${getErrorMessage(persistErr)}`);
259
- }
260
- }
261
- return stats;
262
- }
@@ -1,14 +0,0 @@
1
- import type { Memory } from './types.js';
2
- export interface IndexStateEntry {
3
- hash: string;
4
- updatedAt: number;
5
- }
6
- export interface IndexState {
7
- version: 1;
8
- entries: Record<string, IndexStateEntry>;
9
- updatedAt: number;
10
- }
11
- export declare function getIndexStatePath(directory: string): string;
12
- export declare function readIndexState(directory: string): Promise<IndexState>;
13
- export declare function writeIndexState(directory: string, state: IndexState): Promise<void>;
14
- export declare function computeMemoryHash(memory: Memory): string;