@biaoo/tiangong-wiki 0.3.2 → 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.
@@ -9,8 +9,12 @@ import { buildVaultWorkflowPrompt, ensureWorkflowArtifactSet, getWorkflowArtifac
9
9
  import { readWorkflowResult } from "./workflow-result.js";
10
10
  import { AppError } from "../utils/errors.js";
11
11
  import { readTextFileSync } from "../utils/fs.js";
12
- import { toOffsetIso } from "../utils/time.js";
12
+ import { addSeconds, toOffsetIso } from "../utils/time.js";
13
13
  const INLINE_WORKFLOW_ATTEMPTS = 2;
14
+ const MAX_QUEUE_ERROR_RETRIES = 3;
15
+ const QUEUE_FULL_RETRY_DELAY_SECONDS = 300;
16
+ const WORKFLOW_TIMEOUT_RETRY_DELAY_SECONDS = 120;
17
+ const NON_RETRYABLE_QUEUE_ERROR_CODES = new Set(["config_error", "invalid_request"]);
14
18
  function buildFileIdFilterClause(filterFileIds) {
15
19
  if (!filterFileIds || filterFileIds.length === 0) {
16
20
  return { clause: "", params: [] };
@@ -20,6 +24,16 @@ function buildFileIdFilterClause(filterFileIds) {
20
24
  params: filterFileIds,
21
25
  };
22
26
  }
27
+ function buildExcludedFileIdClause(excludedFileIds) {
28
+ const params = Array.from(excludedFileIds ?? []).filter((value) => value.trim().length > 0);
29
+ if (params.length === 0) {
30
+ return { clause: "", params: [] };
31
+ }
32
+ return {
33
+ clause: ` AND vault_processing_queue.file_id NOT IN (${params.map(() => "?").join(", ")})`,
34
+ params,
35
+ };
36
+ }
23
37
  function parseOptionalStringArray(value) {
24
38
  if (Array.isArray(value)) {
25
39
  return value
@@ -43,9 +57,11 @@ function parseOptionalStringArray(value) {
43
57
  }
44
58
  }
45
59
  function mapQueueRow(row) {
60
+ const attempts = Number(row.attempts ?? 0);
61
+ const status = row.status;
46
62
  return {
47
63
  fileId: String(row.fileId),
48
- status: row.status,
64
+ status,
49
65
  priority: Number(row.priority ?? 0),
50
66
  queuedAt: String(row.queuedAt),
51
67
  claimedAt: typeof row.claimedAt === "string" ? row.claimedAt : null,
@@ -53,13 +69,15 @@ function mapQueueRow(row) {
53
69
  processedAt: typeof row.processedAt === "string" ? row.processedAt : null,
54
70
  resultPageId: typeof row.resultPageId === "string" ? row.resultPageId : null,
55
71
  errorMessage: typeof row.errorMessage === "string" ? row.errorMessage : null,
56
- attempts: Number(row.attempts ?? 0),
72
+ attempts,
57
73
  threadId: typeof row.threadId === "string" ? row.threadId : null,
58
74
  workflowVersion: typeof row.workflowVersion === "string" ? row.workflowVersion : null,
59
75
  decision: typeof row.decision === "string" ? row.decision : null,
60
76
  resultManifestPath: typeof row.resultManifestPath === "string" ? row.resultManifestPath : null,
61
77
  lastErrorAt: typeof row.lastErrorAt === "string" ? row.lastErrorAt : null,
78
+ lastErrorCode: typeof row.lastErrorCode === "string" ? row.lastErrorCode : null,
62
79
  retryAfter: typeof row.retryAfter === "string" ? row.retryAfter : null,
80
+ autoRetryExhausted: status === "error" && attempts > MAX_QUEUE_ERROR_RETRIES,
63
81
  createdPageIds: parseOptionalStringArray(row.createdPageIds),
64
82
  updatedPageIds: parseOptionalStringArray(row.updatedPageIds),
65
83
  appliedTypeNames: parseOptionalStringArray(row.appliedTypeNames),
@@ -72,8 +90,20 @@ function mapQueueRow(row) {
72
90
  filePath: typeof row.filePath === "string" ? row.filePath : undefined,
73
91
  };
74
92
  }
75
- function claimQueueItems(db, limit, filterFileIds) {
76
- const filter = buildFileIdFilterClause(filterFileIds);
93
+ function claimQueueItems(db, limit, options = {}) {
94
+ const filter = buildFileIdFilterClause(options.filterFileIds);
95
+ const exclude = buildExcludedFileIdClause(options.excludeFileIds);
96
+ const manualClaim = Boolean(options.filterFileIds && options.filterFileIds.length > 0);
97
+ const errorEligibility = manualClaim
98
+ ? "vault_processing_queue.status = 'error'"
99
+ : [
100
+ "vault_processing_queue.status = 'error'",
101
+ `vault_processing_queue.attempts <= ${MAX_QUEUE_ERROR_RETRIES}`,
102
+ `COALESCE(vault_processing_queue.last_error_code, '') NOT IN (${Array.from(NON_RETRYABLE_QUEUE_ERROR_CODES)
103
+ .map((code) => `'${code}'`)
104
+ .join(", ")})`,
105
+ "(vault_processing_queue.retry_after IS NULL OR julianday(vault_processing_queue.retry_after) <= julianday(?))",
106
+ ].join("\n AND ");
77
107
  const select = db.prepare(`
78
108
  SELECT
79
109
  file_id AS fileId,
@@ -91,6 +121,7 @@ function claimQueueItems(db, limit, filterFileIds) {
91
121
  decision,
92
122
  result_manifest_path AS resultManifestPath,
93
123
  last_error_at AS lastErrorAt,
124
+ last_error_code AS lastErrorCode,
94
125
  retry_after AS retryAfter,
95
126
  created_page_ids AS createdPageIds,
96
127
  updated_page_ids AS updatedPageIds,
@@ -104,7 +135,12 @@ function claimQueueItems(db, limit, filterFileIds) {
104
135
  vault_files.file_path AS filePath
105
136
  FROM vault_processing_queue
106
137
  LEFT JOIN vault_files ON vault_files.id = vault_processing_queue.file_id
107
- WHERE status IN ('pending', 'error')${filter.clause}
138
+ WHERE (
139
+ vault_processing_queue.status = 'pending'
140
+ OR (
141
+ ${errorEligibility}
142
+ )
143
+ )${filter.clause}${exclude.clause}
108
144
  ORDER BY priority DESC, queued_at ASC
109
145
  LIMIT ?
110
146
  `);
@@ -114,12 +150,16 @@ function claimQueueItems(db, limit, filterFileIds) {
114
150
  status = 'processing',
115
151
  claimed_at = @claimed_at,
116
152
  started_at = @started_at,
117
- error_message = NULL
153
+ error_message = NULL,
154
+ retry_after = NULL
118
155
  WHERE file_id = @file_id AND status IN ('pending', 'error')
119
156
  `);
120
157
  return db.transaction((claimLimit, claimFilterParams) => {
121
158
  const startedAt = toOffsetIso();
122
- const items = select.all(...claimFilterParams, claimLimit).map(mapQueueRow);
159
+ const selectParams = manualClaim
160
+ ? [...claimFilterParams, claimLimit]
161
+ : [startedAt, ...claimFilterParams, claimLimit];
162
+ const items = select.all(...selectParams).map(mapQueueRow);
123
163
  for (const item of items) {
124
164
  markProcessing.run({
125
165
  file_id: item.fileId,
@@ -132,7 +172,7 @@ function claimQueueItems(db, limit, filterFileIds) {
132
172
  claimedAt: startedAt,
133
173
  startedAt,
134
174
  }));
135
- })(limit, filter.params);
175
+ })(limit, [...filter.params, ...exclude.params]);
136
176
  }
137
177
  function fetchQueueItemsByStatus(db, status) {
138
178
  const rows = db.prepare(`
@@ -152,6 +192,7 @@ function fetchQueueItemsByStatus(db, status) {
152
192
  decision,
153
193
  result_manifest_path AS resultManifestPath,
154
194
  last_error_at AS lastErrorAt,
195
+ last_error_code AS lastErrorCode,
155
196
  retry_after AS retryAfter,
156
197
  created_page_ids AS createdPageIds,
157
198
  updated_page_ids AS updatedPageIds,
@@ -188,6 +229,7 @@ function fetchQueueItemByFileId(db, fileId) {
188
229
  decision,
189
230
  result_manifest_path AS resultManifestPath,
190
231
  last_error_at AS lastErrorAt,
232
+ last_error_code AS lastErrorCode,
191
233
  retry_after AS retryAfter,
192
234
  created_page_ids AS createdPageIds,
193
235
  updated_page_ids AS updatedPageIds,
@@ -262,10 +304,112 @@ function serializeArray(value) {
262
304
  function formatManifestLogFields(manifest) {
263
305
  return `decision=${manifest.decision} skills=${manifest.skillsUsed.join(",") || "-"} created=${manifest.createdPageIds.join(",") || "-"} updated=${manifest.updatedPageIds.join(",") || "-"} proposed=${manifest.proposedTypes.map((item) => item.name).join(",") || "-"}`;
264
306
  }
265
- function applyWorkflowManifest(db, fileId, manifest, resultManifestPath) {
307
+ function extractErrorDetailsCode(error) {
308
+ if (!(error instanceof AppError)) {
309
+ return null;
310
+ }
311
+ if (typeof error.details !== "object" || error.details === null || Array.isArray(error.details)) {
312
+ return null;
313
+ }
314
+ const code = error.details.code;
315
+ return typeof code === "string" && code.trim() ? code.trim() : null;
316
+ }
317
+ function inferWorkflowErrorCode(message) {
318
+ const normalized = message.toLowerCase();
319
+ if (normalized.includes("queue_full") || normalized.includes("write queue is full")) {
320
+ return "queue_full";
321
+ }
322
+ if (normalized.includes("timed out")) {
323
+ return "workflow_timeout";
324
+ }
325
+ return null;
326
+ }
327
+ function buildRetryAfter(seconds) {
328
+ return toOffsetIso(addSeconds(new Date(), seconds));
329
+ }
330
+ function buildQueueFailureState(message, options = {}) {
331
+ const inferredCode = inferWorkflowErrorCode(message);
332
+ const errorCode = inferredCode ?? options.explicitCode ?? (options.errorType === "config" ? "config_error" : null);
333
+ if (errorCode && NON_RETRYABLE_QUEUE_ERROR_CODES.has(errorCode)) {
334
+ return {
335
+ errorCode,
336
+ retryAfter: null,
337
+ autoRetryEligible: false,
338
+ };
339
+ }
340
+ if (errorCode === "queue_full") {
341
+ return {
342
+ errorCode,
343
+ retryAfter: buildRetryAfter(QUEUE_FULL_RETRY_DELAY_SECONDS),
344
+ autoRetryEligible: true,
345
+ };
346
+ }
347
+ if (errorCode === "workflow_timeout") {
348
+ return {
349
+ errorCode,
350
+ retryAfter: buildRetryAfter(WORKFLOW_TIMEOUT_RETRY_DELAY_SECONDS),
351
+ autoRetryEligible: true,
352
+ };
353
+ }
354
+ return {
355
+ errorCode,
356
+ retryAfter: null,
357
+ autoRetryEligible: true,
358
+ };
359
+ }
360
+ function formatQueueErrorMessage(message, autoRetryExhausted) {
361
+ const autoRetrySuffix = autoRetryExhausted
362
+ ? ` Auto retry limit reached after ${MAX_QUEUE_ERROR_RETRIES} retries; use manual retry or requeue after the vault file changes.`
363
+ : "";
364
+ return `${message}${autoRetrySuffix}`.slice(0, 1_000);
365
+ }
366
+ function applyWorkflowManifest(db, fileId, manifest, resultManifestPath, currentAttempts) {
266
367
  const resultPageId = manifest.createdPageIds[0] ?? manifest.updatedPageIds[0] ?? null;
267
368
  const status = manifest.status;
268
369
  const processedAt = toOffsetIso();
370
+ if (status === "error") {
371
+ const failureState = buildQueueFailureState(manifest.reason);
372
+ const nextAttempts = currentAttempts + 1;
373
+ const autoRetryExhausted = failureState.autoRetryEligible && nextAttempts > MAX_QUEUE_ERROR_RETRIES;
374
+ db.prepare(`
375
+ UPDATE vault_processing_queue
376
+ SET
377
+ status = 'error',
378
+ processed_at = @processed_at,
379
+ result_page_id = @result_page_id,
380
+ error_message = @error_message,
381
+ attempts = attempts + 1,
382
+ workflow_version = @workflow_version,
383
+ decision = @decision,
384
+ result_manifest_path = @result_manifest_path,
385
+ last_error_at = @last_error_at,
386
+ last_error_code = @last_error_code,
387
+ retry_after = @retry_after,
388
+ created_page_ids = @created_page_ids,
389
+ updated_page_ids = @updated_page_ids,
390
+ applied_type_names = @applied_type_names,
391
+ proposed_type_names = @proposed_type_names,
392
+ skills_used = @skills_used
393
+ WHERE file_id = @file_id
394
+ `).run({
395
+ file_id: fileId,
396
+ processed_at: processedAt,
397
+ result_page_id: resultPageId,
398
+ error_message: formatQueueErrorMessage(manifest.reason, autoRetryExhausted),
399
+ workflow_version: CODEX_WORKFLOW_VERSION,
400
+ decision: manifest.decision,
401
+ result_manifest_path: resultManifestPath,
402
+ last_error_at: processedAt,
403
+ last_error_code: failureState.errorCode,
404
+ retry_after: autoRetryExhausted ? null : failureState.retryAfter,
405
+ created_page_ids: serializeArray(manifest.createdPageIds),
406
+ updated_page_ids: serializeArray(manifest.updatedPageIds),
407
+ applied_type_names: serializeArray(manifest.appliedTypeNames),
408
+ proposed_type_names: serializeArray(manifest.proposedTypes.map((item) => item.name)),
409
+ skills_used: serializeArray(manifest.skillsUsed),
410
+ });
411
+ return { status, pageId: resultPageId };
412
+ }
269
413
  db.prepare(`
270
414
  UPDATE vault_processing_queue
271
415
  SET
@@ -277,6 +421,7 @@ function applyWorkflowManifest(db, fileId, manifest, resultManifestPath) {
277
421
  decision = @decision,
278
422
  result_manifest_path = @result_manifest_path,
279
423
  last_error_at = NULL,
424
+ last_error_code = NULL,
280
425
  retry_after = NULL,
281
426
  created_page_ids = @created_page_ids,
282
427
  updated_page_ids = @updated_page_ids,
@@ -425,16 +570,20 @@ function updateQueueWorkflowError(db, fileId, payload) {
425
570
  thread_id = COALESCE(@thread_id, thread_id),
426
571
  workflow_version = @workflow_version,
427
572
  result_manifest_path = COALESCE(@result_manifest_path, result_manifest_path),
428
- last_error_at = @last_error_at
573
+ last_error_at = @last_error_at,
574
+ last_error_code = @last_error_code,
575
+ retry_after = @retry_after
429
576
  WHERE file_id = @file_id
430
577
  `).run({
431
578
  file_id: fileId,
432
579
  processed_at: processedAt,
433
- error_message: payload.errorMessage.slice(0, 1_000),
580
+ error_message: formatQueueErrorMessage(payload.errorMessage, payload.autoRetryExhausted === true),
434
581
  thread_id: payload.threadId ?? null,
435
582
  workflow_version: CODEX_WORKFLOW_VERSION,
436
583
  result_manifest_path: payload.resultManifestPath ?? null,
437
584
  last_error_at: processedAt,
585
+ last_error_code: payload.errorCode ?? null,
586
+ retry_after: payload.autoRetryExhausted ? null : payload.retryAfter ?? null,
438
587
  });
439
588
  }
440
589
  function prepareCodexWorkflowInput(paths, item, file, localFilePath, env, allowTemplateEvolution) {
@@ -484,10 +633,189 @@ function prepareCodexWorkflowInput(paths, item, file, localFilePath, env, allowT
484
633
  },
485
634
  };
486
635
  }
636
+ async function processClaimedQueueItem(input) {
637
+ const { db, env, paths, item, workflowRunner, templateEvolution, maxWorkflowAttempts, workflowTimeoutMs } = input;
638
+ input.log?.(`${item.fileId}: start processing attempt=${item.attempts + 1} queuedAt=${item.queuedAt} thread=${item.threadId ?? "-"}`);
639
+ const file = fetchVaultFile(db, item.fileId);
640
+ if (!file) {
641
+ updateQueueStatus(db, item.fileId, {
642
+ status: "error",
643
+ processedAt: toOffsetIso(),
644
+ errorMessage: `Vault file missing from index: ${item.fileId}`,
645
+ incrementAttempts: true,
646
+ });
647
+ input.log?.(`${item.fileId}: error thread=- result=- message=Vault file missing from index`);
648
+ return {
649
+ status: "error",
650
+ item: {
651
+ fileId: item.fileId,
652
+ status: "error",
653
+ reason: "Vault file missing from index",
654
+ },
655
+ };
656
+ }
657
+ let threadId = item.threadId ?? null;
658
+ let resultManifestPath = null;
659
+ try {
660
+ const localFilePath = await ensureLocalVaultFile(file, paths.vaultPath, env);
661
+ const { artifacts, input: workflowInput } = prepareCodexWorkflowInput(paths, item, file, localFilePath, env, templateEvolution.canApply);
662
+ resultManifestPath = artifacts.resultPath;
663
+ let finalOutcome = null;
664
+ let lastWorkflowError;
665
+ for (let attempt = 1; attempt <= maxWorkflowAttempts; attempt += 1) {
666
+ try {
667
+ const mode = threadId ? "resume" : "start";
668
+ const workflowController = new AbortController();
669
+ let loggedStartedThreadId = null;
670
+ const attemptInput = {
671
+ ...workflowInput,
672
+ signal: workflowController.signal,
673
+ onThreadStarted: (startedThreadId) => {
674
+ if (loggedStartedThreadId === startedThreadId) {
675
+ return;
676
+ }
677
+ loggedStartedThreadId = startedThreadId;
678
+ threadId = startedThreadId;
679
+ updateQueueWorkflowTracking(db, item.fileId, {
680
+ threadId: startedThreadId,
681
+ resultManifestPath: artifacts.resultPath,
682
+ });
683
+ input.log?.(`${item.fileId}: workflow started mode=${mode} attempt=${attempt}/${maxWorkflowAttempts} thread=${startedThreadId} result=${artifacts.resultPath}`);
684
+ },
685
+ };
686
+ input.log?.(`${item.fileId}: launching workflow mode=${mode} attempt=${attempt}/${maxWorkflowAttempts} timeout=${Math.ceil(workflowTimeoutMs / 1000)}s result=${artifacts.resultPath}`);
687
+ const handle = threadId
688
+ ? await runWithWorkflowTimeout("resumeWorkflow", workflowTimeoutMs, workflowController, () => workflowRunner.resumeWorkflow(threadId, attemptInput))
689
+ : await runWithWorkflowTimeout("startWorkflow", workflowTimeoutMs, workflowController, () => workflowRunner.startWorkflow(attemptInput));
690
+ threadId = handle.threadId;
691
+ if (loggedStartedThreadId !== handle.threadId) {
692
+ loggedStartedThreadId = handle.threadId;
693
+ input.log?.(`${item.fileId}: workflow started mode=${mode} attempt=${attempt}/${maxWorkflowAttempts} thread=${handle.threadId} result=${artifacts.resultPath}`);
694
+ }
695
+ updateQueueWorkflowTracking(db, item.fileId, {
696
+ threadId: handle.threadId,
697
+ resultManifestPath: artifacts.resultPath,
698
+ });
699
+ input.log?.(`${item.fileId}: waiting for workflow result thread=${handle.threadId} attempt=${attempt}/${maxWorkflowAttempts} result=${artifacts.resultPath}`);
700
+ const collectController = new AbortController();
701
+ const manifest = await runWithWorkflowTimeout("collectResult", workflowTimeoutMs, collectController, () => workflowRunner.collectResult(handle, {
702
+ ...workflowInput,
703
+ signal: collectController.signal,
704
+ }));
705
+ assertTemplateEvolutionAllowed(manifest, templateEvolution);
706
+ finalOutcome = {
707
+ outcome: applyWorkflowManifest(db, item.fileId, manifest, artifacts.resultPath, item.attempts),
708
+ manifest,
709
+ handleThreadId: handle.threadId,
710
+ };
711
+ break;
712
+ }
713
+ catch (error) {
714
+ lastWorkflowError = error;
715
+ threadId = readPersistedWorkflowThreadId(artifacts.queueItemPath) ?? threadId;
716
+ if (threadId) {
717
+ updateQueueWorkflowTracking(db, item.fileId, {
718
+ threadId,
719
+ resultManifestPath: artifacts.resultPath,
720
+ });
721
+ }
722
+ const recoveredManifest = shouldAttemptManifestRecovery(error)
723
+ ? readRecoverableWorkflowResult(artifacts.resultPath, threadId)
724
+ : null;
725
+ if (recoveredManifest) {
726
+ assertTemplateEvolutionAllowed(recoveredManifest, templateEvolution);
727
+ finalOutcome = {
728
+ outcome: applyWorkflowManifest(db, item.fileId, recoveredManifest, artifacts.resultPath, item.attempts),
729
+ manifest: recoveredManifest,
730
+ handleThreadId: recoveredManifest.threadId,
731
+ };
732
+ input.log?.(`${item.fileId}: recovered persisted workflow result status=${recoveredManifest.status} thread=${recoveredManifest.threadId} ${formatManifestLogFields(recoveredManifest)} result=${artifacts.resultPath} message=${formatWorkflowError(error)}`);
733
+ break;
734
+ }
735
+ if (!shouldRetryWorkflowAttempt(error, attempt, maxWorkflowAttempts)) {
736
+ throw error;
737
+ }
738
+ input.log?.(`${item.fileId}: retrying workflow attempt ${attempt + 1}/${maxWorkflowAttempts} thread=${threadId ?? "-"} result=${artifacts.resultPath} message=${formatWorkflowError(error)}`);
739
+ }
740
+ }
741
+ if (!finalOutcome) {
742
+ throw (lastWorkflowError ?? new AppError("Workflow completed without a result", "runtime"));
743
+ }
744
+ input.log?.(`${item.fileId}: ${finalOutcome.outcome.status} thread=${finalOutcome.handleThreadId} ${formatManifestLogFields(finalOutcome.manifest)} result=${artifacts.resultPath}`);
745
+ return {
746
+ status: finalOutcome.outcome.status,
747
+ item: {
748
+ fileId: item.fileId,
749
+ status: finalOutcome.outcome.status,
750
+ pageId: finalOutcome.outcome.pageId,
751
+ reason: finalOutcome.manifest.reason,
752
+ threadId: finalOutcome.handleThreadId,
753
+ decision: finalOutcome.manifest.decision,
754
+ skillsUsed: finalOutcome.manifest.skillsUsed,
755
+ createdPageIds: finalOutcome.manifest.createdPageIds,
756
+ updatedPageIds: finalOutcome.manifest.updatedPageIds,
757
+ proposedTypeNames: finalOutcome.manifest.proposedTypes.map((entry) => entry.name),
758
+ resultManifestPath: artifacts.resultPath,
759
+ },
760
+ };
761
+ }
762
+ catch (error) {
763
+ const recoveredManifest = shouldAttemptManifestRecovery(error)
764
+ ? readRecoverableWorkflowResult(resultManifestPath, threadId)
765
+ : null;
766
+ if (recoveredManifest && resultManifestPath) {
767
+ assertTemplateEvolutionAllowed(recoveredManifest, templateEvolution);
768
+ const recoveredOutcome = applyWorkflowManifest(db, item.fileId, recoveredManifest, resultManifestPath, item.attempts);
769
+ input.log?.(`${item.fileId}: recovered persisted workflow result after terminal failure status=${recoveredOutcome.status} thread=${recoveredManifest.threadId} ${formatManifestLogFields(recoveredManifest)} result=${resultManifestPath} message=${formatWorkflowError(error)}`);
770
+ return {
771
+ status: recoveredOutcome.status,
772
+ item: {
773
+ fileId: item.fileId,
774
+ status: recoveredOutcome.status,
775
+ pageId: recoveredOutcome.pageId,
776
+ reason: recoveredManifest.reason,
777
+ threadId: recoveredManifest.threadId,
778
+ decision: recoveredManifest.decision,
779
+ skillsUsed: recoveredManifest.skillsUsed,
780
+ createdPageIds: recoveredManifest.createdPageIds,
781
+ updatedPageIds: recoveredManifest.updatedPageIds,
782
+ proposedTypeNames: recoveredManifest.proposedTypes.map((entry) => entry.name),
783
+ resultManifestPath,
784
+ },
785
+ };
786
+ }
787
+ const message = formatWorkflowError(error);
788
+ const failureState = buildQueueFailureState(message, {
789
+ explicitCode: extractErrorDetailsCode(error),
790
+ errorType: error instanceof AppError ? error.type : null,
791
+ });
792
+ const autoRetryExhausted = failureState.autoRetryEligible && item.attempts >= MAX_QUEUE_ERROR_RETRIES;
793
+ updateQueueWorkflowError(db, item.fileId, {
794
+ errorMessage: message,
795
+ errorCode: failureState.errorCode,
796
+ retryAfter: failureState.retryAfter,
797
+ threadId,
798
+ resultManifestPath,
799
+ autoRetryExhausted,
800
+ });
801
+ input.log?.(`${item.fileId}: error thread=${threadId ?? "-"} result=${resultManifestPath ?? "-"} message=${message}${autoRetryExhausted ? ` autoRetryLimit=${MAX_QUEUE_ERROR_RETRIES}` : ""}`);
802
+ return {
803
+ status: "error",
804
+ item: {
805
+ fileId: item.fileId,
806
+ status: "error",
807
+ pageId: item.resultPageId ?? null,
808
+ reason: message,
809
+ threadId,
810
+ resultManifestPath,
811
+ },
812
+ };
813
+ }
814
+ }
487
815
  export function getVaultQueueSnapshot(env = process.env, status) {
488
816
  const paths = resolveRuntimePaths(env);
489
817
  const config = loadConfig(paths.configPath);
490
- const { db } = openDb(paths.dbPath, config, Number.parseInt(env.EMBEDDING_DIMENSIONS ?? "384", 10) || 384);
818
+ const { db } = openDb(paths.dbPath, config, Number.parseInt(env.EMBEDDING_DIMENSIONS ?? "384", 10) || 384, paths.packageRoot);
491
819
  try {
492
820
  const items = fetchQueueItemsByStatus(db, status);
493
821
  const counts = db.prepare(`
@@ -515,7 +843,7 @@ export function getVaultQueueSnapshot(env = process.env, status) {
515
843
  export function getVaultQueueItem(env = process.env, fileId) {
516
844
  const paths = resolveRuntimePaths(env);
517
845
  const config = loadConfig(paths.configPath);
518
- const { db } = openDb(paths.dbPath, config, Number.parseInt(env.EMBEDDING_DIMENSIONS ?? "384", 10) || 384);
846
+ const { db } = openDb(paths.dbPath, config, Number.parseInt(env.EMBEDDING_DIMENSIONS ?? "384", 10) || 384, paths.packageRoot);
519
847
  try {
520
848
  return fetchQueueItemByFileId(db, fileId);
521
849
  }
@@ -537,9 +865,8 @@ export async function processVaultQueueBatch(env = process.env, options = {}) {
537
865
  }
538
866
  const paths = resolveRuntimePaths(env);
539
867
  const config = loadConfig(paths.configPath);
540
- const { db } = openDb(paths.dbPath, config, Number.parseInt(env.EMBEDDING_DIMENSIONS ?? "384", 10) || 384);
868
+ const { db } = openDb(paths.dbPath, config, Number.parseInt(env.EMBEDDING_DIMENSIONS ?? "384", 10) || 384, paths.packageRoot);
541
869
  try {
542
- const items = claimQueueItems(db, options.maxItems ?? agentSettings.batchSize, options.filterFileIds);
543
870
  const result = {
544
871
  enabled: true,
545
872
  processed: 0,
@@ -552,9 +879,10 @@ export async function processVaultQueueBatch(env = process.env, options = {}) {
552
879
  const templateEvolution = resolveTemplateEvolutionSettings(env);
553
880
  const maxWorkflowAttempts = isInlineRetryCapable(workflowRunner) ? INLINE_WORKFLOW_ATTEMPTS : 1;
554
881
  const workflowTimeoutMs = agentSettings.workflowTimeoutSeconds * 1000;
555
- if (items.length > 0) {
556
- options.log?.(`claimed ${items.length} items: ${items.map((item) => item.fileId).join(", ")}`);
557
- }
882
+ const workerSlots = Math.max(0, options.maxItems ?? agentSettings.batchSize);
883
+ const attemptedFileIds = new Set();
884
+ const orderedItems = [];
885
+ let nextSequence = 0;
558
886
  const countOutcome = (status) => {
559
887
  if (status === "done") {
560
888
  result.done += 1;
@@ -567,169 +895,61 @@ export async function processVaultQueueBatch(env = process.env, options = {}) {
567
895
  }
568
896
  result.processed += 1;
569
897
  };
570
- for (const item of items) {
571
- options.log?.(`${item.fileId}: start processing attempt=${item.attempts + 1} queuedAt=${item.queuedAt} thread=${item.threadId ?? "-"}`);
572
- const file = fetchVaultFile(db, item.fileId);
573
- if (!file) {
574
- updateQueueStatus(db, item.fileId, {
575
- status: "error",
576
- processedAt: toOffsetIso(),
577
- errorMessage: `Vault file missing from index: ${item.fileId}`,
578
- incrementAttempts: true,
579
- });
580
- countOutcome("error");
581
- options.log?.(`${item.fileId}: error thread=- result=- message=Vault file missing from index`);
582
- result.items.push({
583
- fileId: item.fileId,
584
- status: "error",
585
- reason: "Vault file missing from index",
586
- });
587
- continue;
898
+ const claimNextQueueItem = () => {
899
+ if (options.shouldStop?.() === true) {
900
+ return null;
588
901
  }
589
- let threadId = item.threadId ?? null;
590
- let resultManifestPath = null;
591
- try {
592
- const localFilePath = await ensureLocalVaultFile(file, paths.vaultPath, env);
593
- const { artifacts, input } = prepareCodexWorkflowInput(paths, item, file, localFilePath, env, templateEvolution.canApply);
594
- resultManifestPath = artifacts.resultPath;
595
- let finalOutcome = null;
596
- let lastWorkflowError;
597
- for (let attempt = 1; attempt <= maxWorkflowAttempts; attempt += 1) {
598
- try {
599
- const mode = threadId ? "resume" : "start";
600
- const workflowController = new AbortController();
601
- let loggedStartedThreadId = null;
602
- const attemptInput = {
603
- ...input,
604
- signal: workflowController.signal,
605
- onThreadStarted: (startedThreadId) => {
606
- if (loggedStartedThreadId === startedThreadId) {
607
- return;
608
- }
609
- loggedStartedThreadId = startedThreadId;
610
- threadId = startedThreadId;
611
- updateQueueWorkflowTracking(db, item.fileId, {
612
- threadId: startedThreadId,
613
- resultManifestPath: artifacts.resultPath,
614
- });
615
- options.log?.(`${item.fileId}: workflow started mode=${mode} attempt=${attempt}/${maxWorkflowAttempts} thread=${startedThreadId} result=${artifacts.resultPath}`);
616
- },
617
- };
618
- options.log?.(`${item.fileId}: launching workflow mode=${mode} attempt=${attempt}/${maxWorkflowAttempts} timeout=${agentSettings.workflowTimeoutSeconds}s result=${artifacts.resultPath}`);
619
- const handle = threadId
620
- ? await runWithWorkflowTimeout("resumeWorkflow", workflowTimeoutMs, workflowController, () => workflowRunner.resumeWorkflow(threadId, attemptInput))
621
- : await runWithWorkflowTimeout("startWorkflow", workflowTimeoutMs, workflowController, () => workflowRunner.startWorkflow(attemptInput));
622
- threadId = handle.threadId;
623
- if (loggedStartedThreadId !== handle.threadId) {
624
- loggedStartedThreadId = handle.threadId;
625
- options.log?.(`${item.fileId}: workflow started mode=${mode} attempt=${attempt}/${maxWorkflowAttempts} thread=${handle.threadId} result=${artifacts.resultPath}`);
626
- }
627
- updateQueueWorkflowTracking(db, item.fileId, {
628
- threadId: handle.threadId,
629
- resultManifestPath: artifacts.resultPath,
630
- });
631
- options.log?.(`${item.fileId}: waiting for workflow result thread=${handle.threadId} attempt=${attempt}/${maxWorkflowAttempts} result=${artifacts.resultPath}`);
632
- const collectController = new AbortController();
633
- const manifest = await runWithWorkflowTimeout("collectResult", workflowTimeoutMs, collectController, () => workflowRunner.collectResult(handle, {
634
- ...input,
635
- signal: collectController.signal,
636
- }));
637
- assertTemplateEvolutionAllowed(manifest, templateEvolution);
638
- finalOutcome = {
639
- outcome: applyWorkflowManifest(db, item.fileId, manifest, artifacts.resultPath),
640
- manifest,
641
- handleThreadId: handle.threadId,
642
- };
643
- break;
644
- }
645
- catch (error) {
646
- lastWorkflowError = error;
647
- threadId = readPersistedWorkflowThreadId(artifacts.queueItemPath) ?? threadId;
648
- if (threadId) {
649
- updateQueueWorkflowTracking(db, item.fileId, {
650
- threadId,
651
- resultManifestPath: artifacts.resultPath,
652
- });
653
- }
654
- const recoveredManifest = shouldAttemptManifestRecovery(error)
655
- ? readRecoverableWorkflowResult(artifacts.resultPath, threadId)
656
- : null;
657
- if (recoveredManifest) {
658
- assertTemplateEvolutionAllowed(recoveredManifest, templateEvolution);
659
- finalOutcome = {
660
- outcome: applyWorkflowManifest(db, item.fileId, recoveredManifest, artifacts.resultPath),
661
- manifest: recoveredManifest,
662
- handleThreadId: recoveredManifest.threadId,
663
- };
664
- options.log?.(`${item.fileId}: recovered persisted workflow result status=${recoveredManifest.status} thread=${recoveredManifest.threadId} ${formatManifestLogFields(recoveredManifest)} result=${artifacts.resultPath} message=${formatWorkflowError(error)}`);
665
- break;
666
- }
667
- if (!shouldRetryWorkflowAttempt(error, attempt, maxWorkflowAttempts)) {
668
- throw error;
669
- }
670
- options.log?.(`${item.fileId}: retrying workflow attempt ${attempt + 1}/${maxWorkflowAttempts} thread=${threadId ?? "-"} result=${artifacts.resultPath} message=${formatWorkflowError(error)}`);
671
- }
672
- }
673
- if (!finalOutcome) {
674
- throw (lastWorkflowError ?? new AppError("Workflow completed without a result", "runtime"));
675
- }
676
- options.log?.(`${item.fileId}: ${finalOutcome.outcome.status} thread=${finalOutcome.handleThreadId} ${formatManifestLogFields(finalOutcome.manifest)} result=${artifacts.resultPath}`);
677
- countOutcome(finalOutcome.outcome.status);
678
- result.items.push({
679
- fileId: item.fileId,
680
- status: finalOutcome.outcome.status,
681
- pageId: finalOutcome.outcome.pageId,
682
- reason: finalOutcome.manifest.reason,
683
- threadId: finalOutcome.handleThreadId,
684
- decision: finalOutcome.manifest.decision,
685
- skillsUsed: finalOutcome.manifest.skillsUsed,
686
- createdPageIds: finalOutcome.manifest.createdPageIds,
687
- updatedPageIds: finalOutcome.manifest.updatedPageIds,
688
- proposedTypeNames: finalOutcome.manifest.proposedTypes.map((entry) => entry.name),
689
- resultManifestPath: artifacts.resultPath,
690
- });
902
+ const remainingFilterFileIds = options.filterFileIds?.filter((fileId) => !attemptedFileIds.has(fileId));
903
+ if (options.filterFileIds && remainingFilterFileIds?.length === 0) {
904
+ return null;
691
905
  }
692
- catch (error) {
693
- const recoveredManifest = shouldAttemptManifestRecovery(error)
694
- ? readRecoverableWorkflowResult(resultManifestPath, threadId)
695
- : null;
696
- if (recoveredManifest && resultManifestPath) {
697
- assertTemplateEvolutionAllowed(recoveredManifest, templateEvolution);
698
- const recoveredOutcome = applyWorkflowManifest(db, item.fileId, recoveredManifest, resultManifestPath);
699
- options.log?.(`${item.fileId}: recovered persisted workflow result after terminal failure status=${recoveredOutcome.status} thread=${recoveredManifest.threadId} ${formatManifestLogFields(recoveredManifest)} result=${resultManifestPath} message=${formatWorkflowError(error)}`);
700
- countOutcome(recoveredOutcome.status);
701
- result.items.push({
702
- fileId: item.fileId,
703
- status: recoveredOutcome.status,
704
- pageId: recoveredOutcome.pageId,
705
- reason: recoveredManifest.reason,
706
- threadId: recoveredManifest.threadId,
707
- decision: recoveredManifest.decision,
708
- skillsUsed: recoveredManifest.skillsUsed,
709
- createdPageIds: recoveredManifest.createdPageIds,
710
- updatedPageIds: recoveredManifest.updatedPageIds,
711
- proposedTypeNames: recoveredManifest.proposedTypes.map((entry) => entry.name),
712
- resultManifestPath,
713
- });
714
- continue;
906
+ const item = claimQueueItems(db, 1, {
907
+ filterFileIds: remainingFilterFileIds,
908
+ excludeFileIds: attemptedFileIds,
909
+ })[0];
910
+ if (!item) {
911
+ return null;
912
+ }
913
+ attemptedFileIds.add(item.fileId);
914
+ options.log?.(`claimed 1 items: ${item.fileId}`);
915
+ return {
916
+ sequence: nextSequence++,
917
+ item,
918
+ };
919
+ };
920
+ const workerCount = options.filterFileIds
921
+ ? Math.min(workerSlots, options.filterFileIds.length)
922
+ : workerSlots;
923
+ const workers = Array.from({ length: workerCount }, async () => {
924
+ while (true) {
925
+ const claimed = claimNextQueueItem();
926
+ if (!claimed) {
927
+ return;
715
928
  }
716
- const message = formatWorkflowError(error);
717
- updateQueueWorkflowError(db, item.fileId, {
718
- errorMessage: message,
719
- threadId,
720
- resultManifestPath,
929
+ const processed = await processClaimedQueueItem({
930
+ db,
931
+ env,
932
+ paths,
933
+ item: claimed.item,
934
+ log: options.log,
935
+ workflowRunner,
936
+ templateEvolution,
937
+ maxWorkflowAttempts,
938
+ workflowTimeoutMs,
721
939
  });
722
- options.log?.(`${item.fileId}: error thread=${threadId ?? "-"} result=${resultManifestPath ?? "-"} message=${message}`);
723
- countOutcome("error");
724
- result.items.push({
725
- fileId: item.fileId,
726
- status: "error",
727
- pageId: item.resultPageId ?? null,
728
- reason: message,
729
- threadId,
730
- resultManifestPath,
940
+ countOutcome(processed.status);
941
+ orderedItems.push({
942
+ sequence: claimed.sequence,
943
+ item: processed.item,
731
944
  });
732
945
  }
946
+ });
947
+ await Promise.all(workers);
948
+ result.items = orderedItems
949
+ .sort((left, right) => left.sequence - right.sequence)
950
+ .map((entry) => entry.item);
951
+ if (result.items.length > 0) {
952
+ options.log?.(`processed ${result.items.length} queue items with workerPool=${workerCount}`);
733
953
  }
734
954
  return result;
735
955
  }