@syntesseraai/opencode-feature-factory 0.4.3 → 0.4.5

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 +23 -2
  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
@@ -72,9 +72,29 @@ At the start of EVERY building task:
72
72
  9. **Document your context** - Use `ff-agents-update` tool to create `.feature-factory/agents/building-{UUID}.md`
73
73
  10. **Check for existing plans** - Use `ff-plans-list` and `ff-plans-get` to find implementation plans
74
74
 
75
- ## Git Worktrees (Mandatory)
75
+ ## Git Workflow
76
76
 
77
- To prevent conflicts and ensure a clean state, you MUST use git worktrees for your implementation:
77
+ Choose the appropriate git workflow based on the repository's working agreements. Check `AGENTS.md` (or equivalent) in the repository root for explicit guidance.
78
+
79
+ ### How to Detect the Development Model
80
+
81
+ 1. Check for `AGENTS.md` in the repository root for explicit working agreements
82
+ 2. Look for keywords: "trunk-based", "mainline", "direct to main" → use **Trunk-Based**
83
+ 3. Look for keywords: "pull request", "feature branch", "branch protection" → use **Branch-Based**
84
+ 4. **Default:** If no guidance is found, use **trunk-based development** (simpler, less overhead)
85
+
86
+ ### Trunk-Based Development (Direct to Main)
87
+
88
+ If the repository specifies **trunk-based / mainline development** (e.g., "commit directly to the main branch, do not create branches or PRs"):
89
+
90
+ 1. **Work in the existing checkout** — Do NOT create worktrees or feature branches
91
+ 2. **Commit directly to `main`** — Make atomic, well-described commits
92
+ 3. **Run quality checks before committing** — Ensure lint, typecheck, and tests pass
93
+ 4. **Keep commits small and focused** — Each commit should be a logical unit of work
94
+
95
+ ### Branch-Based Development (Worktrees)
96
+
97
+ If the repository uses **branch-based / PR workflows**, use git worktrees to prevent conflicts and ensure a clean state:
78
98
 
79
99
  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
100
  ```bash
@@ -434,6 +454,7 @@ Use ff-severity-classification when making changes:
434
454
 
435
455
  ## Important Notes
436
456
 
457
+ - **GitHub Interactions** - ALWAYS prefer using the `gh` CLI tool via bash for interacting with GitHub (e.g., `gh pr create`, `gh issue view`, etc.) rather than making direct `curl` requests or calling the REST API. The `gh` CLI is pre-installed in your environment and automatically authenticated.
437
458
  - **You can make code changes** - This is the only agent that can edit files
438
459
  - **Always create todos** - Track progress visibly for the user
439
460
  - **Validate frequently** - Don't wait until the end to check quality
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.3",
4
+ "version": "0.4.5",
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
- }