@biaoo/tiangong-wiki 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core/db.js CHANGED
@@ -177,6 +177,7 @@ function ensureBaseTables(db, embeddingDimensions) {
177
177
  decision: "TEXT",
178
178
  result_manifest_path: "TEXT",
179
179
  last_error_at: "TEXT",
180
+ last_error_code: "TEXT",
180
181
  retry_after: "TEXT",
181
182
  created_page_ids: "TEXT",
182
183
  updated_page_ids: "TEXT",
@@ -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,6 +633,185 @@ 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);
@@ -539,7 +867,6 @@ export async function processVaultQueueBatch(env = process.env, options = {}) {
539
867
  const config = loadConfig(paths.configPath);
540
868
  const { db } = openDb(paths.dbPath, config, Number.parseInt(env.EMBEDDING_DIMENSIONS ?? "384", 10) || 384);
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
  }
@@ -441,6 +441,7 @@ export function syncVaultIndex(db, currentFiles, syncId) {
441
441
  decision,
442
442
  result_manifest_path,
443
443
  last_error_at,
444
+ last_error_code,
444
445
  retry_after,
445
446
  created_page_ids,
446
447
  updated_page_ids,
@@ -468,6 +469,7 @@ export function syncVaultIndex(db, currentFiles, syncId) {
468
469
  NULL,
469
470
  NULL,
470
471
  NULL,
472
+ NULL,
471
473
  NULL
472
474
  )
473
475
  ON CONFLICT(file_id) DO UPDATE SET
@@ -493,6 +495,10 @@ export function syncVaultIndex(db, currentFiles, syncId) {
493
495
  WHEN vault_processing_queue.status = 'processing' THEN vault_processing_queue.error_message
494
496
  ELSE NULL
495
497
  END,
498
+ attempts = CASE
499
+ WHEN vault_processing_queue.status = 'processing' THEN vault_processing_queue.attempts
500
+ ELSE 0
501
+ END,
496
502
  thread_id = CASE
497
503
  WHEN vault_processing_queue.status = 'processing' THEN vault_processing_queue.thread_id
498
504
  ELSE NULL
@@ -513,6 +519,10 @@ export function syncVaultIndex(db, currentFiles, syncId) {
513
519
  WHEN vault_processing_queue.status = 'processing' THEN vault_processing_queue.last_error_at
514
520
  ELSE NULL
515
521
  END,
522
+ last_error_code = CASE
523
+ WHEN vault_processing_queue.status = 'processing' THEN vault_processing_queue.last_error_code
524
+ ELSE NULL
525
+ END,
516
526
  retry_after = CASE
517
527
  WHEN vault_processing_queue.status = 'processing' THEN vault_processing_queue.retry_after
518
528
  ELSE NULL
@@ -511,6 +511,7 @@ export async function runDaemonServer(options) {
511
511
  while (!stopping) {
512
512
  const batchResult = await processVaultQueueBatch(env, {
513
513
  log: (message) => logInfo(`queue ${message}`),
514
+ shouldStop: () => stopping,
514
515
  });
515
516
  if (!batchResult.enabled) {
516
517
  break;
@@ -254,6 +254,7 @@ function buildQueueTiming(item) {
254
254
  startedAt: item.startedAt ?? null,
255
255
  processedAt: item.processedAt,
256
256
  lastErrorAt: item.lastErrorAt ?? null,
257
+ lastErrorCode: item.lastErrorCode ?? null,
257
258
  retryAfter: item.retryAfter ?? null,
258
259
  queueAgeMs: Number.isFinite(queuedAt) ? now - queuedAt : null,
259
260
  waitDurationMs: Number.isFinite(claimedAt) && Number.isFinite(queuedAt) ? claimedAt - queuedAt : null,
@@ -271,8 +272,10 @@ function buildQueueListItem(item) {
271
272
  status: item.status,
272
273
  priority: item.priority,
273
274
  attempts: item.attempts,
275
+ autoRetryExhausted: item.autoRetryExhausted ?? false,
274
276
  resultPageId: item.resultPageId,
275
277
  errorMessage: item.errorMessage,
278
+ lastErrorCode: item.lastErrorCode ?? null,
276
279
  threadId: item.threadId ?? null,
277
280
  decision: item.decision ?? null,
278
281
  workflowVersion: item.workflowVersion ?? null,
@@ -790,11 +793,13 @@ export function retryDashboardQueueItem(env = process.env, fileId) {
790
793
  processed_at = NULL,
791
794
  result_page_id = NULL,
792
795
  error_message = NULL,
796
+ attempts = 0,
793
797
  thread_id = NULL,
794
798
  workflow_version = NULL,
795
799
  decision = NULL,
796
800
  result_manifest_path = NULL,
797
801
  last_error_at = NULL,
802
+ last_error_code = NULL,
798
803
  retry_after = NULL,
799
804
  created_page_ids = NULL,
800
805
  updated_page_ids = NULL,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biaoo/tiangong-wiki",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Local-first wiki index and query engine for Markdown knowledge pages (Tiangong Wiki).",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -87,12 +87,14 @@ The agent uses [Codex SDK](https://www.npmjs.com/package/@openai/codex-sdk) to p
87
87
  | `WIKI_AGENT_BASE_URL` | No | LLM API base URL (e.g. `https://api.openai.com/v1`). When set, overrides global Codex config |
88
88
  | `WIKI_AGENT_API_KEY` | If enabled | API key for the LLM provider |
89
89
  | `WIKI_AGENT_MODEL` | No | Model name (e.g. `gpt-5.4`, `Qwen/Qwen3.5-397B-A17B-GPTQ-Int4`) |
90
- | `WIKI_AGENT_BATCH_SIZE` | No | Max concurrent vault items per batch (default: `5`) |
90
+ | `WIKI_AGENT_BATCH_SIZE` | No | Max concurrent vault queue workers per cycle (default: `5`) |
91
91
  | `WIKI_AGENT_SANDBOX_MODE` | No | Codex sandbox mode: `danger-full-access` (default) or `workspace-write` |
92
92
  | `WIKI_PARSER_SKILLS` | No | Comma-separated parser skill list (e.g. `pdf,docx,pptx,xlsx`) |
93
93
 
94
94
  `tiangong-wiki setup` now prompts for `WIKI_AGENT_SANDBOX_MODE` when automatic vault processing is enabled. The default is `danger-full-access`, and the setup wizard highlights that this mode grants full runtime access.
95
95
 
96
+ Queue items that fail workflow execution are auto-retried up to 3 times. After that they remain in `error` until you manually retry them from the dashboard / queue tooling, or a later vault sync requeues the file because the source changed.
97
+
96
98
  ---
97
99
 
98
100
  ## Common Issues