@syntesseraai/opencode-feature-factory 0.3.3 → 0.3.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.
@@ -23,32 +23,51 @@ import { getMemoriesDir, storeMemories } from './memory-service.js';
23
23
  function getErrorMessage(error) {
24
24
  return error instanceof Error ? error.message : String(error);
25
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
+ }
26
53
  function recordFailure(stats, failure) {
27
54
  let message;
28
- let rawError;
29
55
  switch (failure.scope) {
30
56
  case 'project':
31
57
  message = `No OpenCode project found for directory: ${failure.directory}`;
32
58
  break;
33
59
  case 'message':
34
60
  message = `Error processing message ${failure.messageID}: ${getErrorMessage(failure.error)}`;
35
- rawError = failure.error;
36
61
  break;
37
62
  case 'session':
38
63
  message = `Error processing session ${failure.sessionID}: ${getErrorMessage(failure.error)}`;
39
- rawError = failure.error;
40
64
  break;
41
65
  case 'extraction':
42
66
  message = `Extraction failed: ${getErrorMessage(failure.error)}`;
43
- rawError = failure.error;
44
67
  break;
45
68
  }
46
69
  stats.errors.push(message);
47
- if (rawError !== undefined) {
48
- console.error('[local-recall-daemon]', message, rawError);
49
- return;
50
- }
51
- console.error('[local-recall-daemon]', message);
70
+ return message;
52
71
  }
53
72
  // ────────────────────────────────────────────────────────────
54
73
  // Helpers
@@ -99,7 +118,14 @@ export async function runExtraction(directory) {
99
118
  // Find project for this directory
100
119
  const project = await findProject(directory);
101
120
  if (!project) {
102
- recordFailure(stats, { scope: 'project', directory });
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
+ }
103
129
  return stats;
104
130
  }
105
131
  // Ensure local-recall directories exist
@@ -203,11 +229,13 @@ export async function runExtraction(directory) {
203
229
  }
204
230
  }
205
231
  catch (err) {
206
- recordFailure(stats, {
232
+ const failure = {
207
233
  scope: 'session',
208
234
  sessionID: session.id,
209
235
  error: err,
210
- });
236
+ };
237
+ const failureMessage = recordFailure(stats, failure);
238
+ newProcessedEntries.push(toProcessedFailureEntry(failure, failureMessage));
211
239
  }
212
240
  }
213
241
  // Batch store all new memories
@@ -221,7 +249,14 @@ export async function runExtraction(directory) {
221
249
  }
222
250
  }
223
251
  catch (err) {
224
- recordFailure(stats, { scope: 'extraction', error: 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
+ }
225
260
  }
226
261
  return stats;
227
262
  }
@@ -11,6 +11,9 @@
11
11
  import { createHash } from 'node:crypto';
12
12
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
13
13
  import { join, dirname } from 'node:path';
14
+ function isProcessedMessageEntry(entry) {
15
+ return 'messageID' in entry && 'contentHash' in entry;
16
+ }
14
17
  function getLogPath(directory) {
15
18
  return join(directory, 'ff-memories', 'processed.json');
16
19
  }
@@ -48,7 +51,7 @@ export async function readProcessedLog(directory) {
48
51
  */
49
52
  export async function isProcessed(directory, messageID) {
50
53
  const log = await readProcessedLog(directory);
51
- return log.some((entry) => entry.messageID === messageID);
54
+ return log.some((entry) => isProcessedMessageEntry(entry) && entry.messageID === messageID);
52
55
  }
53
56
  /**
54
57
  * Check if a content hash has already been processed.
@@ -56,7 +59,7 @@ export async function isProcessed(directory, messageID) {
56
59
  */
57
60
  export async function isContentProcessed(directory, hash) {
58
61
  const log = await readProcessedLog(directory);
59
- return log.some((entry) => entry.contentHash === hash);
62
+ return log.some((entry) => isProcessedMessageEntry(entry) && entry.contentHash === hash);
60
63
  }
61
64
  /**
62
65
  * Mark messages as processed by appending entries to the log.
@@ -72,11 +75,11 @@ export async function markProcessed(directory, entries) {
72
75
  * Get the set of already-processed message IDs for fast lookup.
73
76
  */
74
77
  export function getProcessedMessageIDs(log) {
75
- return new Set(log.map((e) => e.messageID));
78
+ return new Set(log.filter(isProcessedMessageEntry).map((e) => e.messageID));
76
79
  }
77
80
  /**
78
81
  * Get the set of already-processed content hashes for fast lookup.
79
82
  */
80
83
  export function getProcessedHashes(log) {
81
- return new Set(log.map((e) => e.contentHash));
84
+ return new Set(log.filter(isProcessedMessageEntry).map((e) => e.contentHash));
82
85
  }
@@ -45,6 +45,10 @@ async function dirExists(dirPath) {
45
45
  return false;
46
46
  }
47
47
  }
48
+ function getPartStartTime(part) {
49
+ const start = part.time?.start;
50
+ return typeof start === 'number' ? start : Number.POSITIVE_INFINITY;
51
+ }
48
52
  // ── Project Reader ──────────────────────────────────────────────
49
53
  /**
50
54
  * Find the project record whose worktree matches `directory`.
@@ -131,7 +135,13 @@ export async function listParts(messageID) {
131
135
  if (!(await dirExists(partDir)))
132
136
  return [];
133
137
  const parts = await readAllJsonInDir(partDir);
134
- return parts.sort((a, b) => a.time.start - b.time.start);
138
+ return parts.sort((a, b) => {
139
+ const timeDelta = getPartStartTime(a) - getPartStartTime(b);
140
+ if (timeDelta !== 0) {
141
+ return timeDelta;
142
+ }
143
+ return a.id.localeCompare(b.id);
144
+ });
135
145
  }
136
146
  /**
137
147
  * Get a single part by ID.
@@ -54,10 +54,10 @@ export interface OCPart {
54
54
  messageID: string;
55
55
  type: string;
56
56
  text?: string;
57
- synthetic: boolean;
58
- time: {
59
- start: number;
60
- end: number;
57
+ synthetic?: boolean;
58
+ time?: {
59
+ start?: number;
60
+ end?: number;
61
61
  };
62
62
  }
63
63
  /** A memory extracted from a conversation turn */
@@ -119,24 +119,34 @@ export interface ExtractionResult {
119
119
  /** Where the extraction came from — used to generate logical IDs */
120
120
  source: 'session' | 'thinking';
121
121
  }
122
- interface ProcessedEntryBase {
122
+ interface ProcessedMessageEntryBase {
123
123
  messageID: string;
124
124
  processedAt: number;
125
125
  /** SHA-256 hex hash of the concatenated extracted bodies for content-level idempotency */
126
126
  contentHash: string;
127
127
  }
128
128
  /** Tracks which messages have already been processed */
129
- export type ProcessedEntry = (ProcessedEntryBase & {
129
+ export type ProcessedMessageEntry = (ProcessedMessageEntryBase & {
130
130
  status: 'success';
131
131
  memoriesCreated: number;
132
132
  failure?: undefined;
133
- }) | (ProcessedEntryBase & {
133
+ }) | (ProcessedMessageEntryBase & {
134
134
  status: 'failed';
135
135
  memoriesCreated: 0;
136
136
  failure: string;
137
- }) | (ProcessedEntryBase & {
137
+ }) | (ProcessedMessageEntryBase & {
138
138
  status?: undefined;
139
139
  memoriesCreated: number;
140
140
  failure?: undefined;
141
141
  });
142
+ /** Persistent daemon-level failures that are not tied to a specific message record. */
143
+ export type ProcessedFailureEntry = {
144
+ kind: 'failure';
145
+ scope: 'project' | 'session' | 'extraction';
146
+ processedAt: number;
147
+ failure: string;
148
+ directory?: string;
149
+ sessionID?: string;
150
+ };
151
+ export type ProcessedEntry = ProcessedMessageEntry | ProcessedFailureEntry;
142
152
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@syntesseraai/opencode-feature-factory",
4
- "version": "0.3.3",
4
+ "version": "0.3.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",