memorix 1.1.13 → 1.2.0

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 (88) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +3 -2
  3. package/README.zh-CN.md +3 -2
  4. package/dist/cli/index.js +36313 -31189
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/dashboard/static/app.js +50 -30
  7. package/dist/index.js +5348 -674
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +3661 -293
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +19 -0
  13. package/dist/sdk.d.ts +1 -1
  14. package/dist/sdk.js +5346 -672
  15. package/dist/sdk.js.map +1 -1
  16. package/docs/1.2.0-CLAIM-LEDGER.md +72 -0
  17. package/docs/1.2.0-CODE-STATE.md +61 -0
  18. package/docs/1.2.0-DEVELOPMENT-CHARTER.md +255 -0
  19. package/docs/1.2.0-DYNAMIC-LIFECYCLE.md +71 -0
  20. package/docs/1.2.0-EVALUATION-HARNESS.md +51 -0
  21. package/docs/1.2.0-IMPLEMENTATION-PLAN.md +554 -0
  22. package/docs/1.2.0-KNOWLEDGE-WORKFLOW-RESEARCH.md +205 -0
  23. package/docs/1.2.0-KNOWLEDGE-WORKSPACE.md +68 -0
  24. package/docs/1.2.0-PRODUCT-STORY.md +234 -0
  25. package/docs/1.2.0-PROVIDER-QUALITY.md +189 -0
  26. package/docs/1.2.0-WORKFLOW-INHERITANCE.md +101 -0
  27. package/docs/1.2.0-WORKSET-RETRIEVAL.md +80 -0
  28. package/docs/AGENT_OPERATOR_PLAYBOOK.md +6 -3
  29. package/docs/API_REFERENCE.md +25 -6
  30. package/docs/CONFIGURATION.md +21 -3
  31. package/docs/README.md +17 -2
  32. package/docs/dev-log/progress.txt +120 -40
  33. package/llms-full.txt +16 -2
  34. package/llms.txt +9 -4
  35. package/package.json +3 -2
  36. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  37. package/src/cli/capability-map.ts +1 -0
  38. package/src/cli/commands/codegraph.ts +112 -9
  39. package/src/cli/commands/context.ts +2 -0
  40. package/src/cli/commands/doctor.ts +73 -4
  41. package/src/cli/commands/knowledge.ts +282 -0
  42. package/src/cli/commands/serve-http.ts +12 -1
  43. package/src/cli/index.ts +3 -1
  44. package/src/cli/tui/App.tsx +1 -1
  45. package/src/cli/tui/Panels.tsx +8 -8
  46. package/src/cli/tui/theme.ts +1 -1
  47. package/src/cli/tui/views/GraphView.tsx +8 -7
  48. package/src/cli/tui/views/KnowledgeView.tsx +9 -9
  49. package/src/codegraph/auto-context.ts +171 -9
  50. package/src/codegraph/code-state.ts +95 -0
  51. package/src/codegraph/context-pack.ts +82 -1
  52. package/src/codegraph/external-provider.ts +581 -0
  53. package/src/codegraph/lite-provider.ts +64 -19
  54. package/src/codegraph/project-context.ts +9 -1
  55. package/src/codegraph/store.ts +154 -6
  56. package/src/codegraph/types.ts +117 -0
  57. package/src/config/resolved-config.ts +28 -0
  58. package/src/config/toml-loader.ts +3 -0
  59. package/src/config/yaml-loader.ts +6 -0
  60. package/src/dashboard/server.ts +15 -1
  61. package/src/evaluation/workset-evaluation.ts +120 -0
  62. package/src/hooks/handler.ts +48 -6
  63. package/src/knowledge/claim-store.ts +267 -0
  64. package/src/knowledge/claims.ts +537 -0
  65. package/src/knowledge/markdown.ts +129 -0
  66. package/src/knowledge/types.ts +157 -0
  67. package/src/knowledge/wiki.ts +524 -0
  68. package/src/knowledge/workflow-store.ts +168 -0
  69. package/src/knowledge/workflow-types.ts +95 -0
  70. package/src/knowledge/workflows.ts +743 -0
  71. package/src/knowledge/workset.ts +515 -0
  72. package/src/knowledge/workspace-store.ts +220 -0
  73. package/src/knowledge/workspace-types.ts +106 -0
  74. package/src/knowledge/workspace.ts +220 -0
  75. package/src/memory/observations.ts +19 -0
  76. package/src/runtime/control-plane-maintenance.ts +5 -0
  77. package/src/runtime/isolated-maintenance.ts +5 -0
  78. package/src/runtime/lifecycle-status.ts +102 -0
  79. package/src/runtime/lifecycle.ts +107 -0
  80. package/src/runtime/maintenance-jobs.ts +5 -0
  81. package/src/runtime/project-maintenance.ts +190 -0
  82. package/src/server/tool-profile.ts +3 -2
  83. package/src/server.ts +354 -14
  84. package/src/store/file-lock.ts +24 -4
  85. package/src/store/sqlite-db.ts +307 -0
  86. package/src/wiki/generator.ts +4 -2
  87. package/src/wiki/knowledge-graph.ts +7 -4
  88. package/src/wiki/types.ts +16 -4
package/src/server.ts CHANGED
@@ -469,10 +469,11 @@ export async function createMemorixServer(
469
469
  let maintenanceWorker: { stop(): void } | null = null;
470
470
  const startProjectMaintenanceWorker = async (autoCleanup = false): Promise<void> => {
471
471
  maintenanceWorker?.stop();
472
- const [{ MaintenanceJobStore, MaintenanceJobWorker }, maintenance, observations] = await Promise.all([
472
+ const [{ MaintenanceJobStore, MaintenanceJobWorker }, maintenance, observations, lifecycle] = await Promise.all([
473
473
  import('./runtime/maintenance-jobs.js'),
474
474
  import('./runtime/project-maintenance.js'),
475
475
  import('./memory/observations.js'),
476
+ import('./runtime/lifecycle.js'),
476
477
  ]);
477
478
  const queue = new MaintenanceJobStore(projectDir);
478
479
  const vectorStatus = observations.getVectorStatus(project.id);
@@ -509,11 +510,12 @@ export async function createMemorixServer(
509
510
  || !Number.isFinite(indexedAt)
510
511
  || Date.now() - indexedAt > 10 * 60_000;
511
512
  if (needsRefresh) {
512
- queue.enqueue({
513
+ lifecycle.enqueueCodegraphRefresh({
514
+ dataDir: projectDir,
513
515
  projectId: project.id,
514
- kind: 'codegraph-refresh',
515
- dedupeKey: 'startup-codegraph-refresh',
516
- payload: { maxFiles: 5_000 },
516
+ source: 'startup',
517
+ maxFiles: 5_000,
518
+ queue,
517
519
  });
518
520
  }
519
521
  } catch {
@@ -1329,10 +1331,12 @@ export async function createMemorixServer(
1329
1331
  },
1330
1332
  { getObservationStore },
1331
1333
  { MaintenanceJobStore },
1334
+ { enqueueCodegraphRefresh },
1332
1335
  ] = await Promise.all([
1333
1336
  import('./codegraph/auto-context.js'),
1334
1337
  import('./store/obs-store.js'),
1335
1338
  import('./runtime/maintenance-jobs.js'),
1339
+ import('./runtime/lifecycle.js'),
1336
1340
  ]);
1337
1341
  const observations = await getObservationStore().loadByProject(project.id, { status: 'active' });
1338
1342
  const context = await buildAutoProjectContext({
@@ -1342,11 +1346,12 @@ export async function createMemorixServer(
1342
1346
  task,
1343
1347
  refresh: refresh ?? 'auto',
1344
1348
  enqueueRefresh: () => {
1345
- new MaintenanceJobStore(projectDir).enqueue({
1349
+ enqueueCodegraphRefresh({
1350
+ dataDir: projectDir,
1346
1351
  projectId: project.id,
1347
- kind: 'codegraph-refresh',
1348
- dedupeKey: 'context-codegraph-refresh',
1349
- payload: { maxFiles: 5_000 },
1352
+ source: 'project-context',
1353
+ maxFiles: 5_000,
1354
+ queue: new MaintenanceJobStore(projectDir),
1350
1355
  });
1351
1356
  },
1352
1357
  });
@@ -1373,13 +1378,24 @@ export async function createMemorixServer(
1373
1378
  const unresolved = requireResolvedProject('show CodeGraph Memory status for the current project');
1374
1379
  if (unresolved) return unresolved;
1375
1380
 
1376
- const { CodeGraphStore } = await import('./codegraph/store.js');
1381
+ const [{ CodeGraphStore }, { getResolvedConfig }, { inspectExternalCodeGraph }] = await Promise.all([
1382
+ import('./codegraph/store.js'),
1383
+ import('./config/resolved-config.js'),
1384
+ import('./codegraph/external-provider.js'),
1385
+ ]);
1377
1386
  const store = new CodeGraphStore();
1378
1387
  await store.init(projectDir);
1379
1388
  const status = store.status(project.id);
1389
+ const codegraphConfig = getResolvedConfig({ projectRoot: project.rootPath }).codegraph;
1390
+ const providerQuality = await inspectExternalCodeGraph({
1391
+ projectRoot: project.rootPath,
1392
+ mode: codegraphConfig.externalContext,
1393
+ command: codegraphConfig.externalCommand,
1394
+ timeoutMs: codegraphConfig.externalTimeoutMs,
1395
+ });
1380
1396
 
1381
1397
  return {
1382
- content: [{ type: 'text' as const, text: JSON.stringify(status, null, 2) }],
1398
+ content: [{ type: 'text' as const, text: JSON.stringify({ ...status, providerQuality: providerQuality.quality }, null, 2) }],
1383
1399
  };
1384
1400
  },
1385
1401
  );
@@ -1405,21 +1421,28 @@ export async function createMemorixServer(
1405
1421
 
1406
1422
  const [
1407
1423
  { CodeGraphStore },
1408
- { assembleContextPackForTask, buildContextPackPrompt },
1424
+ { assembleContextPackForTask, attachTaskWorkset, buildContextPackPrompt },
1409
1425
  { getResolvedConfig },
1426
+ { getExternalCodeGraphContext },
1410
1427
  { getObservationStore },
1428
+ { collectCurrentProjectFacts },
1429
+ { resolveTaskLens },
1411
1430
  ] = await Promise.all([
1412
1431
  import('./codegraph/store.js'),
1413
1432
  import('./codegraph/context-pack.js'),
1414
1433
  import('./config/resolved-config.js'),
1434
+ import('./codegraph/external-provider.js'),
1415
1435
  import('./store/obs-store.js'),
1436
+ import('./codegraph/current-facts.js'),
1437
+ import('./codegraph/task-lens.js'),
1416
1438
  ]);
1417
1439
  const store = new CodeGraphStore();
1418
1440
  await store.init(projectDir);
1419
- const exclude = getResolvedConfig({ projectRoot: project.rootPath }).codegraph.excludePatterns;
1441
+ const codegraphConfig = getResolvedConfig({ projectRoot: project.rootPath }).codegraph;
1442
+ const exclude = codegraphConfig.excludePatterns;
1420
1443
  const observations = await getObservationStore().loadByProject(project.id, { status: 'active' });
1421
1444
  observations.reverse();
1422
- const pack = assembleContextPackForTask({
1445
+ const basePack = assembleContextPackForTask({
1423
1446
  store,
1424
1447
  projectId: project.id,
1425
1448
  task,
@@ -1427,6 +1450,56 @@ export async function createMemorixServer(
1427
1450
  limit: typeof limit === 'number' ? limit : 20,
1428
1451
  exclude,
1429
1452
  });
1453
+ const status = store.status(project.id);
1454
+ const currentFacts = collectCurrentProjectFacts({ project, now: new Date() });
1455
+ const snapshot = status.latestSnapshot;
1456
+ const external = await getExternalCodeGraphContext({
1457
+ projectRoot: project.rootPath,
1458
+ task,
1459
+ exclude,
1460
+ mode: codegraphConfig.externalContext,
1461
+ command: codegraphConfig.externalCommand,
1462
+ timeoutMs: codegraphConfig.externalTimeoutMs,
1463
+ });
1464
+ const worksetFacts: string[] = [];
1465
+ if (currentFacts.packageVersion) worksetFacts.push('Package version: ' + currentFacts.packageVersion);
1466
+ if (currentFacts.latestChangelog) {
1467
+ worksetFacts.push('Latest changelog: ' + currentFacts.latestChangelog.version
1468
+ + (currentFacts.latestChangelog.date ? ' (' + currentFacts.latestChangelog.date + ')' : ''));
1469
+ }
1470
+ worksetFacts.push('Git: ' + (currentFacts.git.branch ? 'branch ' + currentFacts.git.branch + ', ' : '')
1471
+ + (currentFacts.git.dirty ? 'dirty worktree' : 'clean worktree'));
1472
+ const codeState = snapshot
1473
+ ? '- Code state: ' + (snapshot.baseRevision ? snapshot.baseRevision.slice(0, 12) : 'Git unavailable')
1474
+ + ', ' + snapshot.worktreeState + ' worktree'
1475
+ + ', epoch ' + snapshot.sourceEpoch
1476
+ : '- Code state: no completed snapshot yet';
1477
+ const pack = await attachTaskWorkset({
1478
+ pack: basePack,
1479
+ projectId: project.id,
1480
+ dataDir: projectDir,
1481
+ lens: resolveTaskLens(task).id,
1482
+ worktreeDirty: currentFacts.git.dirty,
1483
+ currentFacts: worksetFacts,
1484
+ codeState,
1485
+ ...(snapshot
1486
+ ? {
1487
+ snapshot: {
1488
+ id: snapshot.id,
1489
+ sourceEpoch: snapshot.sourceEpoch,
1490
+ worktreeState: snapshot.worktreeState,
1491
+ incomplete: snapshot.completeness.skippedOversizedFiles > 0
1492
+ || (snapshot.completeness.unreadableFiles ?? 0) > 0
1493
+ || snapshot.completeness.removalScanDeferred,
1494
+ },
1495
+ }
1496
+ : {}),
1497
+ ...(external.outline ? { semanticCode: external.outline } : {}),
1498
+ providerQuality: external.quality,
1499
+ ...(external.caution
1500
+ ? { runtimeCautions: [{ kind: 'external-codegraph-fallback' as const, message: external.caution }] }
1501
+ : {}),
1502
+ });
1430
1503
  const text = buildContextPackPrompt(pack);
1431
1504
 
1432
1505
  return {
@@ -1435,6 +1508,273 @@ export async function createMemorixServer(
1435
1508
  },
1436
1509
  );
1437
1510
 
1511
+ server.registerTool(
1512
+ 'memorix_knowledge',
1513
+ {
1514
+ title: 'Knowledge Workspace',
1515
+ description:
1516
+ 'Manage the reviewable project Knowledge Workspace. Use it only for deliberate knowledge operations: initialize a local or versioned workspace, inspect status, compile source-backed proposals, lint, apply a reviewed proposal, or manage canonical workflows. It is intentionally absent from the default micro/lite profiles.',
1517
+ inputSchema: {
1518
+ action: z.enum([
1519
+ 'workspace_init',
1520
+ 'status',
1521
+ 'compile',
1522
+ 'lint',
1523
+ 'proposal_apply',
1524
+ 'workflow_import',
1525
+ 'workflow_list',
1526
+ 'workflow_select',
1527
+ 'workflow_preview',
1528
+ 'workflow_apply',
1529
+ 'workflow_run',
1530
+ ]).describe('Knowledge operation to perform'),
1531
+ mode: z.enum(['local', 'versioned']).optional().default('local').describe('Workspace mode; versioned writes require an explicit project path during workspace_init'),
1532
+ path: z.string().optional().describe('Explicit versioned workspace path, used only by workspace_init'),
1533
+ proposalId: z.string().optional().describe('Pending proposal id for proposal_apply'),
1534
+ allowManualOverwrite: z.boolean().optional().default(false).describe('Explicitly allow proposal_apply to replace a manually edited page'),
1535
+ workflowId: z.string().optional().describe('Canonical workflow id for workflow preview, apply, or run'),
1536
+ agent: z.string().optional().describe('Target agent for a workflow adapter'),
1537
+ task: z.string().optional().describe('Task text for workflow selection or a workflow run'),
1538
+ outcome: z.enum(['passed', 'failed', 'cancelled', 'in-progress']).optional().describe('Workflow run outcome'),
1539
+ verificationVerdict: z.enum(['passed', 'failed', 'not-run']).optional().describe('Workflow run verification verdict'),
1540
+ failureReason: z.string().optional().describe('Sanitized workflow failure reason'),
1541
+ startingSnapshotId: z.string().optional().describe('Code snapshot id present when a workflow run began'),
1542
+ evidenceIds: z.array(z.string()).max(24).optional().describe('Selected evidence ids for a workflow run'),
1543
+ },
1544
+ },
1545
+ async ({
1546
+ action,
1547
+ mode,
1548
+ path: workspacePath,
1549
+ proposalId,
1550
+ allowManualOverwrite,
1551
+ workflowId,
1552
+ agent,
1553
+ task,
1554
+ outcome,
1555
+ verificationVerdict,
1556
+ failureReason,
1557
+ startingSnapshotId,
1558
+ evidenceIds,
1559
+ }) => {
1560
+ const unresolved = requireResolvedProject('manage the Knowledge Workspace for the current project');
1561
+ if (unresolved) return unresolved;
1562
+
1563
+ const text = (value: unknown, isError = false) => ({
1564
+ content: [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }],
1565
+ ...(isError ? { isError: true as const } : {}),
1566
+ });
1567
+ const modeValue = mode ?? 'local';
1568
+ const requireText = (value: string | undefined, field: string): string | undefined => {
1569
+ const normalized = value?.trim();
1570
+ return normalized ? normalized : undefined;
1571
+ };
1572
+
1573
+ const [
1574
+ { ClaimStore },
1575
+ { CodeGraphStore },
1576
+ { initializeKnowledgeWorkspace, loadKnowledgeWorkspace },
1577
+ { KnowledgeWorkspaceStore },
1578
+ { applyKnowledgeProposal, compileKnowledgeWorkspace, lintKnowledgeWorkspace },
1579
+ { WorkflowStore },
1580
+ {
1581
+ applyWorkflowAdapter,
1582
+ importWindsurfWorkflows,
1583
+ previewWorkflowAdapter,
1584
+ recordWorkflowRun,
1585
+ selectWorkspaceWorkflows,
1586
+ syncCanonicalWorkflows,
1587
+ },
1588
+ ] = await Promise.all([
1589
+ import('./knowledge/claim-store.js'),
1590
+ import('./codegraph/store.js'),
1591
+ import('./knowledge/workspace.js'),
1592
+ import('./knowledge/workspace-store.js'),
1593
+ import('./knowledge/wiki.js'),
1594
+ import('./knowledge/workflow-store.js'),
1595
+ import('./knowledge/workflows.js'),
1596
+ ]);
1597
+
1598
+ if (action === 'workspace_init') {
1599
+ if (modeValue === 'versioned' && !requireText(workspacePath, 'path')) {
1600
+ return text({ error: 'path is required to initialize a versioned Knowledge Workspace.' }, true);
1601
+ }
1602
+ const workspace = await initializeKnowledgeWorkspace({
1603
+ projectId: project.id,
1604
+ dataDir: projectDir,
1605
+ mode: modeValue,
1606
+ ...(modeValue === 'versioned'
1607
+ ? { projectRoot: project.rootPath, rootPath: requireText(workspacePath, 'path')! }
1608
+ : {}),
1609
+ });
1610
+ return text({
1611
+ workspace: {
1612
+ id: workspace.id,
1613
+ mode: workspace.mode,
1614
+ rootPath: workspace.rootPath,
1615
+ status: workspace.status,
1616
+ },
1617
+ next: 'Compile creates reviewable proposals; it does not silently publish pages.',
1618
+ });
1619
+ }
1620
+
1621
+ const workspace = await loadKnowledgeWorkspace({ projectId: project.id, dataDir: projectDir, mode: modeValue });
1622
+ if (!workspace) {
1623
+ return text({ error: 'Knowledge Workspace is not initialized. Run workspace_init first.' }, true);
1624
+ }
1625
+ const claims = new ClaimStore();
1626
+ const workspaceStore = new KnowledgeWorkspaceStore();
1627
+ await Promise.all([claims.init(projectDir), workspaceStore.init(projectDir)]);
1628
+
1629
+ if (action === 'status') {
1630
+ const pages = workspaceStore.listPages(workspace.id);
1631
+ const pending = workspaceStore.listProposals(workspace.id, 'pending');
1632
+ return text({
1633
+ workspace: {
1634
+ id: workspace.id,
1635
+ mode: workspace.mode,
1636
+ rootPath: workspace.rootPath,
1637
+ status: workspace.status,
1638
+ publishedPages: pages.filter(page => page.status === 'active').length,
1639
+ pendingProposals: pending.map(proposal => ({
1640
+ id: proposal.id,
1641
+ targetPath: proposal.targetPath,
1642
+ reason: proposal.reason,
1643
+ createdAt: proposal.createdAt,
1644
+ })),
1645
+ },
1646
+ });
1647
+ }
1648
+
1649
+ if (action === 'compile') {
1650
+ const result = await compileKnowledgeWorkspace({ workspace, claims });
1651
+ return text({
1652
+ proposals: result.proposals.map(proposal => ({
1653
+ id: proposal.id,
1654
+ targetPath: proposal.targetPath,
1655
+ proposalPath: proposal.proposalPath,
1656
+ reason: proposal.reason,
1657
+ })),
1658
+ unchangedPublishedPages: result.published.map(page => page.relativePath),
1659
+ next: result.proposals.length ? 'Review a proposal, then use proposal_apply deliberately.' : undefined,
1660
+ });
1661
+ }
1662
+
1663
+ if (action === 'lint') {
1664
+ const codeStore = new CodeGraphStore();
1665
+ await codeStore.init(projectDir);
1666
+ const result = await lintKnowledgeWorkspace({ workspace, claims, codeStore });
1667
+ return text(result);
1668
+ }
1669
+
1670
+ if (action === 'proposal_apply') {
1671
+ const proposal = requireText(proposalId, 'proposalId');
1672
+ if (!proposal) return text({ error: 'proposalId is required for proposal_apply.' }, true);
1673
+ const result = await applyKnowledgeProposal({
1674
+ workspace,
1675
+ proposalId: proposal,
1676
+ allowManualOverwrite: !!allowManualOverwrite,
1677
+ });
1678
+ return text({
1679
+ proposal: { id: result.proposal.id, status: result.proposal.status },
1680
+ targetPath: result.targetPath,
1681
+ });
1682
+ }
1683
+
1684
+ const workflowStore = new WorkflowStore();
1685
+ await workflowStore.init(projectDir);
1686
+ const projectRoot = workspace.projectRoot ?? project.rootPath;
1687
+
1688
+ if (action === 'workflow_import') {
1689
+ const result = await importWindsurfWorkflows({ workspace, projectRoot });
1690
+ return text({
1691
+ imported: result.imported.map(workflow => ({ id: workflow.id, title: workflow.title, sourcePath: workflow.sourcePath })),
1692
+ skipped: result.skipped,
1693
+ });
1694
+ }
1695
+
1696
+ const synced = await syncCanonicalWorkflows(workspace);
1697
+ if (action === 'workflow_list') {
1698
+ return text({
1699
+ workflows: workflowStore.listWorkflows(workspace.id).map(workflow => ({
1700
+ id: workflow.id,
1701
+ title: workflow.title,
1702
+ status: workflow.status,
1703
+ taskLenses: workflow.taskLenses,
1704
+ sourcePath: workflow.sourcePath,
1705
+ })),
1706
+ parseErrors: synced.errors,
1707
+ });
1708
+ }
1709
+
1710
+ if (action === 'workflow_select') {
1711
+ const selectedTask = requireText(task, 'task');
1712
+ if (!selectedTask) return text({ error: 'task is required for workflow_select.' }, true);
1713
+ const result = await selectWorkspaceWorkflows({ workspace, task: selectedTask });
1714
+ return text({
1715
+ selections: result.selections.map(selection => ({
1716
+ id: selection.workflow.id,
1717
+ title: selection.workflow.title,
1718
+ reasons: selection.reasons,
1719
+ firstPhase: selection.firstPhase.title,
1720
+ cautions: selection.cautions,
1721
+ })),
1722
+ parseErrors: result.errors,
1723
+ });
1724
+ }
1725
+
1726
+ const requestedWorkflowId = requireText(workflowId, 'workflowId');
1727
+ if (!requestedWorkflowId) return text({ error: 'workflowId is required for this workflow action.' }, true);
1728
+ const workflow = workflowStore.getWorkflow(requestedWorkflowId);
1729
+ if (!workflow || workflow.workspaceId !== workspace.id) {
1730
+ return text({ error: 'Workflow was not found for this Knowledge Workspace.' }, true);
1731
+ }
1732
+
1733
+ if (action === 'workflow_preview' || action === 'workflow_apply') {
1734
+ const targetAgent = requireText(agent, 'agent');
1735
+ if (!targetAgent) return text({ error: 'agent is required for a workflow adapter action.' }, true);
1736
+ const result = action === 'workflow_preview'
1737
+ ? await previewWorkflowAdapter({ workflow, projectRoot, agent: targetAgent as any })
1738
+ : await applyWorkflowAdapter({ workflow, projectRoot, agent: targetAgent as any });
1739
+ return text({
1740
+ workflowId: workflow.id,
1741
+ agent: targetAgent,
1742
+ status: result.status,
1743
+ targetPath: result.targetPath,
1744
+ reason: result.reason,
1745
+ ...(action === 'workflow_preview' && result.content ? { content: result.content } : {}),
1746
+ });
1747
+ }
1748
+
1749
+ if (action === 'workflow_run') {
1750
+ const runTask = requireText(task, 'task');
1751
+ if (!runTask) return text({ error: 'task is required for workflow_run.' }, true);
1752
+ if (!outcome) return text({ error: 'outcome is required for workflow_run.' }, true);
1753
+ const result = await recordWorkflowRun({
1754
+ workspace,
1755
+ run: {
1756
+ workflowId: workflow.id,
1757
+ projectId: project.id,
1758
+ task: runTask,
1759
+ outcome,
1760
+ ...(verificationVerdict ? { verificationVerdict } : {}),
1761
+ ...(requireText(failureReason, 'failureReason') ? { failureReason: requireText(failureReason, 'failureReason') } : {}),
1762
+ ...(requireText(startingSnapshotId, 'startingSnapshotId') ? { startingSnapshotId: requireText(startingSnapshotId, 'startingSnapshotId') } : {}),
1763
+ selectedEvidence: [...new Set((evidenceIds ?? []).map(item => item.trim()).filter(Boolean))],
1764
+ },
1765
+ });
1766
+ return text({
1767
+ id: result.id,
1768
+ workflowId: result.workflowId,
1769
+ outcome: result.outcome,
1770
+ verificationVerdict: result.verificationVerdict,
1771
+ });
1772
+ }
1773
+
1774
+ return text({ error: 'Unsupported knowledge action.' }, true);
1775
+ },
1776
+ );
1777
+
1438
1778
  /**
1439
1779
  * memorix_resolve — Mark memories as resolved/completed
1440
1780
  *
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { promises as fs } from 'node:fs';
12
+ import { randomUUID } from 'node:crypto';
12
13
  import path from 'node:path';
13
14
 
14
15
  /** Lock is considered stale after 10 seconds (process crash recovery) */
@@ -17,6 +18,7 @@ const LOCK_STALE_MS = 10_000;
17
18
  const RETRY_INTERVAL_MS = 50;
18
19
  /** Maximum retries before giving up (50ms × 60 = 3 seconds) */
19
20
  const MAX_RETRIES = 60;
21
+ const atomicWriteTails = new Map<string, Promise<void>>();
20
22
 
21
23
  /**
22
24
  * Acquire a lock file atomically.
@@ -93,8 +95,26 @@ export async function withFileLock<T>(projectDir: string, fn: () => Promise<T>):
93
95
  * On most filesystems, rename() is atomic within the same directory,
94
96
  * so readers always see either the old complete file or the new complete file.
95
97
  */
96
- export async function atomicWriteFile(filePath: string, data: string): Promise<void> {
97
- const tmpPath = filePath + `.tmp.${process.pid}`;
98
- await fs.writeFile(tmpPath, data, 'utf-8');
99
- await fs.rename(tmpPath, filePath);
98
+ async function atomicWriteFileOnce(filePath: string, data: string): Promise<void> {
99
+ const tmpPath = filePath + `.tmp.${process.pid}.${randomUUID()}`;
100
+ try {
101
+ await fs.writeFile(tmpPath, data, 'utf-8');
102
+ await fs.rename(tmpPath, filePath);
103
+ } catch (error) {
104
+ await fs.unlink(tmpPath).catch(() => {});
105
+ throw error;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Serialize same-process writes to one target. Windows refuses concurrent
111
+ * replacements of the same path even when each writer has its own temp file.
112
+ */
113
+ export function atomicWriteFile(filePath: string, data: string): Promise<void> {
114
+ const previous = atomicWriteTails.get(filePath) ?? Promise.resolve();
115
+ const next = previous.catch(() => undefined).then(() => atomicWriteFileOnce(filePath, data));
116
+ atomicWriteTails.set(filePath, next);
117
+ return next.finally(() => {
118
+ if (atomicWriteTails.get(filePath) === next) atomicWriteTails.delete(filePath);
119
+ });
100
120
  }