noteconnection 1.8.0 → 1.9.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 (195) hide show
  1. package/README.md +257 -1803
  2. package/dist/src/agent_workspace.contract.parity.test.js +105 -421
  3. package/dist/src/agent_workspace.frontend.test.js +410 -11
  4. package/dist/src/android.knowledgebase.picker.contract.test.js +102 -0
  5. package/dist/src/appimage.patchelf.test.js +113 -0
  6. package/dist/src/appimage.portability.test.js +156 -0
  7. package/dist/src/backend/FileLoader.js +18 -4
  8. package/dist/src/backend/FileLoader.test.js +125 -0
  9. package/dist/src/backend/GraphBuilder.identity.test.js +86 -0
  10. package/dist/src/backend/GraphBuilder.js +66 -18
  11. package/dist/src/backend/GraphBuilder.matching.test.js +127 -0
  12. package/dist/src/backend/GraphMetrics.js +0 -3
  13. package/dist/src/backend/ResourceIdentity.js +155 -0
  14. package/dist/src/backend/ResourceIdentity.test.js +113 -0
  15. package/dist/src/backend/algorithms/CycleDetection.js +0 -1
  16. package/dist/src/backend/algorithms/HybridEngine.js +0 -3
  17. package/dist/src/backend/algorithms/LayoutEngine.js +0 -1
  18. package/dist/src/backend/algorithms/StatisticalAnalyzer.js +0 -4
  19. package/dist/src/backend/algorithms/TopologicalSort.js +0 -2
  20. package/dist/src/backend/build_dag.js +0 -1
  21. package/dist/src/capacitor.bridge.serialization.contract.test.js +3 -0
  22. package/dist/src/capacitor.runtime.contract.test.js +11 -2
  23. package/dist/src/core/Graph.js +138 -42
  24. package/dist/src/core/Graph.test.js +74 -0
  25. package/dist/src/core/NoteConnection.js +2 -2
  26. package/dist/src/core/PathBridge.js +265 -2
  27. package/dist/src/core/ResourceReference.js +17 -0
  28. package/dist/src/desktop.godot.pack.test.js +142 -0
  29. package/dist/src/desktop.runtime.graph.test.js +66 -0
  30. package/dist/src/export/WorkspaceExportBundle.js +44 -0
  31. package/dist/src/export/WorkspaceExportBundle.test.js +61 -0
  32. package/dist/src/foundation.release.evidence.contract.test.js +127 -9
  33. package/dist/src/foundation.sqlite.runtime.contract.test.js +6 -0
  34. package/dist/src/frontend/README.md +257 -1803
  35. package/dist/src/frontend/agent_workspace.js +665 -7
  36. package/dist/src/frontend/index.html +31 -2
  37. package/dist/src/frontend/knowledge_projection_contract.js +230 -0
  38. package/dist/src/frontend/knowledge_projection_store.js +214 -0
  39. package/dist/src/frontend/libs/path_core.js +158 -42
  40. package/dist/src/frontend/locales/en.json +32 -0
  41. package/dist/src/frontend/locales/zh.json +32 -0
  42. package/dist/src/frontend/mobile_budget_runtime.js +33 -0
  43. package/dist/src/frontend/mobile_exact_analyzer.js +393 -0
  44. package/dist/src/frontend/mobile_identity_contract.js +221 -0
  45. package/dist/src/frontend/mobile_semantic_comparator.js +264 -0
  46. package/dist/src/frontend/path.html +6 -2
  47. package/dist/src/frontend/simulationWorker.js +1 -1
  48. package/dist/src/frontend/source_manager.js +110 -4
  49. package/dist/src/frontend/storage_provider.js +661 -71
  50. package/dist/src/frontend/styles.css +107 -13
  51. package/dist/src/frontend/workspace_panes.js +111 -7
  52. package/dist/src/frontend.runtime.assets.contract.test.js +81 -0
  53. package/dist/src/graph.answer.operator.contract.test.js +72 -0
  54. package/dist/src/index.js +6 -2
  55. package/dist/src/index.knowledge-root.test.js +118 -0
  56. package/dist/src/indexing/IndexLifecycle.js +22 -0
  57. package/dist/src/indexing/IndexLifecycle.test.js +24 -0
  58. package/dist/src/knowledge.api.contract.test.js +4 -2
  59. package/dist/src/knowledge_projection.contract.test.js +134 -0
  60. package/dist/src/knowledge_projection_store.test.js +200 -0
  61. package/dist/src/learning/AnswerQualityEvaluation.js +153 -0
  62. package/dist/src/learning/AnswerQualityEvaluation.test.js +118 -0
  63. package/dist/src/learning/KnowledgeLearningPlatform.js +4687 -3892
  64. package/dist/src/learning/KnowledgeLearningPlatform.persistence.test.js +418 -0
  65. package/dist/src/learning/KnowledgeLearningPlatform.test.js +243 -6
  66. package/dist/src/learning/KnowledgeWorkspaceConversationRegression.js +25 -1
  67. package/dist/src/learning/KnowledgeWorkspaceConversationRegression.test.js +13 -5
  68. package/dist/src/learning/MeasurementEvidenceConflicts.test.js +78 -0
  69. package/dist/src/learning/PublicAnswerContract.test.js +85 -0
  70. package/dist/src/learning/PublicEvidenceSelection.test.js +94 -0
  71. package/dist/src/learning/PublicMeasurementDisclosure.test.js +54 -0
  72. package/dist/src/learning/agentConversationExecution.js +211 -0
  73. package/dist/src/learning/agentConversationExecution.test.js +209 -0
  74. package/dist/src/learning/agentConversationSerialization.js +266 -0
  75. package/dist/src/learning/agentConversationSerialization.test.js +165 -0
  76. package/dist/src/learning/agentResponseBudget.js +179 -0
  77. package/dist/src/learning/agentResponseBudget.test.js +97 -0
  78. package/dist/src/learning/agentWorkspaceCapabilityContracts.js +65 -0
  79. package/dist/src/learning/answerReleaseReview.js +1191 -250
  80. package/dist/src/learning/answerReleaseReview.test.js +1293 -7
  81. package/dist/src/learning/answerTaskPlan.js +473 -0
  82. package/dist/src/learning/conversationComposer.js +627 -386
  83. package/dist/src/learning/conversationComposer.test.js +1403 -0
  84. package/dist/src/learning/evidenceContextAssembler.js +312 -120
  85. package/dist/src/learning/graphAnswerCoverage.js +37 -0
  86. package/dist/src/learning/graphAnswerCoverage.test.js +93 -0
  87. package/dist/src/learning/graphAnswerCoverageCalibration.js +59 -0
  88. package/dist/src/learning/graphAnswerCoverageCalibration.test.js +46 -0
  89. package/dist/src/learning/graphAnswerFacts.js +94 -0
  90. package/dist/src/learning/graphAnswerFacts.test.js +39 -0
  91. package/dist/src/learning/graphAnswerPlan.js +908 -0
  92. package/dist/src/learning/graphAnswerPlan.test.js +907 -0
  93. package/dist/src/learning/graphAnswerQualityCalibration.js +255 -0
  94. package/dist/src/learning/graphAnswerQualityCalibration.test.js +72 -0
  95. package/dist/src/learning/graphAnswerQualityPolicy.js +30 -0
  96. package/dist/src/learning/graphClaimMatcher.js +104 -0
  97. package/dist/src/learning/graphConditionedContext.js +109 -0
  98. package/dist/src/learning/graphConditionedContext.test.js +99 -0
  99. package/dist/src/learning/graphContextAssembler.js +3 -13
  100. package/dist/src/learning/graphContextAssembler.test.js +2 -2
  101. package/dist/src/learning/graphExpansionPolicy.js +22 -0
  102. package/dist/src/learning/graphExpansionPolicy.test.js +33 -0
  103. package/dist/src/learning/identityMetadata.test.js +26 -0
  104. package/dist/src/learning/index.js +1 -0
  105. package/dist/src/learning/mobileAnswerProjection.js +104 -0
  106. package/dist/src/learning/mobileAnswerProjection.test.js +102 -0
  107. package/dist/src/learning/queryBackend.js +34 -10
  108. package/dist/src/learning/queryBackend.test.js +21 -0
  109. package/dist/src/learning/ragContextPack.js +115 -16
  110. package/dist/src/learning/ragContextPack.test.js +151 -0
  111. package/dist/src/learning/ragEvidenceQuality.js +96 -0
  112. package/dist/src/learning/ragEvidenceQuality.test.js +29 -0
  113. package/dist/src/learning/ragPublicText.js +67 -4
  114. package/dist/src/learning/ragPublicText.test.js +57 -0
  115. package/dist/src/learning/ragSufficiencyJudge.js +11 -1
  116. package/dist/src/learning/ragSufficiencyProviderJudge.js +16 -26
  117. package/dist/src/learning/ragSufficiencyProviderJudge.test.js +51 -0
  118. package/dist/src/learning/requestNormalization.js +106 -0
  119. package/dist/src/learning/requestNormalization.test.js +101 -0
  120. package/dist/src/learning/store.js +116 -12
  121. package/dist/src/learning/store.test.js +299 -0
  122. package/dist/src/learning/vectorAccelerationAdapter.js +22 -6
  123. package/dist/src/learning/vectorAccelerationAdapter.test.js +30 -0
  124. package/dist/src/learning/workspaceHydration.js +168 -0
  125. package/dist/src/learning/workspaceHydration.test.js +30 -0
  126. package/dist/src/license.policy.contract.test.js +9 -4
  127. package/dist/src/markdown/MarkdownGateway.js +4 -0
  128. package/dist/src/markdown/MarkdownGateway.test.js +59 -0
  129. package/dist/src/middleware/SseResponseWriter.js +122 -0
  130. package/dist/src/middleware/SseResponseWriter.test.js +54 -0
  131. package/dist/src/middleware/auth.js +28 -7
  132. package/dist/src/middleware/auth.test.js +33 -0
  133. package/dist/src/middleware/index.js +2 -1
  134. package/dist/src/mobile.android.evidence.contract.test.js +146 -0
  135. package/dist/src/mobile.android.signing.contract.test.js +66 -0
  136. package/dist/src/mobile.artifact.contract.test.js +144 -0
  137. package/dist/src/mobile.budget.contract.test.js +90 -0
  138. package/dist/src/mobile.canonical-id.readiness.test.js +54 -0
  139. package/dist/src/mobile.graph.parity.contract.test.js +207 -0
  140. package/dist/src/mobile.identity.contract.test.js +66 -0
  141. package/dist/src/mobile.identity.corpus.manifest.test.js +72 -0
  142. package/dist/src/mobile.identity.corpus.test.js +40 -0
  143. package/dist/src/mobile.native.recovery.contract.test.js +64 -0
  144. package/dist/src/mobile.pipeline.test.js +11 -1
  145. package/dist/src/mobile.profile.contract.test.js +225 -0
  146. package/dist/src/mobile.semantic.parity.test.js +92 -0
  147. package/dist/src/mobile_exact_analyzer.test.js +113 -0
  148. package/dist/src/notemd/ContentGenerator.js +4 -9
  149. package/dist/src/notemd/FileProcessor.js +4 -9
  150. package/dist/src/notemd/NotemdService.js +12 -17
  151. package/dist/src/notemd/Translator.js +4 -9
  152. package/dist/src/notemd/progressReporter.js +9 -0
  153. package/dist/src/notemd.server.integration.test.js +119 -4
  154. package/dist/src/notemd.workflow.pipeline.test.js +1 -1
  155. package/dist/src/pathbridge.handshake.contract.test.js +29 -0
  156. package/dist/src/pathbridge.host-adapter.contract.test.js +133 -0
  157. package/dist/src/pathcore.bundle.contract.test.js +8 -0
  158. package/dist/src/platform/ExportProfile.js +28 -0
  159. package/dist/src/platform/MobileBudget.js +11 -0
  160. package/dist/src/platform/PlatformCapabilities.js +14 -0
  161. package/dist/src/platform/PlatformCapabilities.test.js +6 -0
  162. package/dist/src/query_backend.external_http.integration.test.js +2 -2
  163. package/dist/src/release.godot.mirror.contract.test.js +28 -4
  164. package/dist/src/resources/ResourceRegistry.js +40 -0
  165. package/dist/src/resources/ResourceRegistry.test.js +36 -0
  166. package/dist/src/routes/index.js +0 -4
  167. package/dist/src/routes/knowledge.js +46 -33
  168. package/dist/src/routes/knowledgePayload.js +185 -0
  169. package/dist/src/routes/knowledgePayload.test.js +53 -0
  170. package/dist/src/routes/markdown.js +8 -0
  171. package/dist/src/routes/notemd.js +990 -234
  172. package/dist/src/routes/registry.contract.test.js +5 -2
  173. package/dist/src/routes/registry.shadow.contract.test.js +22 -0
  174. package/dist/src/routes/render.js +14 -51
  175. package/dist/src/runtime.capabilities.test.js +18 -2
  176. package/dist/src/server.js +566 -1090
  177. package/dist/src/server.migration.test.js +417 -15
  178. package/dist/src/sidecar.build.lock.contract.test.js +201 -0
  179. package/dist/src/sidecar.freshness.contract.test.js +184 -0
  180. package/dist/src/sidecar.replacement.boundary.contract.test.js +6 -3
  181. package/dist/src/sidecar.supply.readiness.contract.test.js +11 -0
  182. package/dist/src/simulation.worker.offline.test.js +104 -0
  183. package/dist/src/storage.provider.capacitor.content.contract.test.js +4 -1
  184. package/dist/src/storage.provider.contract.test.js +152 -1
  185. package/dist/src/tauri.frontend.build.contract.test.js +8 -0
  186. package/dist/src/tauri.window.evidence.execution.test.js +68 -0
  187. package/dist/src/utils/RuntimePaths.js +6 -0
  188. package/dist/src/utils/RuntimePaths.test.js +27 -0
  189. package/dist/src/windows.installer.build.test.js +109 -0
  190. package/dist/src/windows.installer.payload.test.js +76 -0
  191. package/dist/src/windows.installer.runtime.test.js +69 -0
  192. package/dist/src/workspace/WorkspaceRegistry.js +13 -0
  193. package/package.json +32 -10
  194. package/dist/src/routes/data.js +0 -267
  195. package/dist/src/routes/diagnostics.js +0 -51
@@ -39,12 +39,14 @@ exports.executeNotemdCliCommand = executeNotemdCliCommand;
39
39
  const http = __importStar(require("http"));
40
40
  const fs = __importStar(require("fs"));
41
41
  const path = __importStar(require("path"));
42
+ const os = __importStar(require("os"));
42
43
  const net = __importStar(require("net"));
43
44
  const readline = __importStar(require("readline"));
44
45
  const events_1 = require("events");
45
46
  const child_process_1 = require("child_process");
46
47
  const index_1 = require("./index");
47
48
  const CrashLogger_1 = require("./backend/utils/CrashLogger");
49
+ const ResourceIdentity_1 = require("./backend/ResourceIdentity");
48
50
  const PathBridge_1 = require("./core/PathBridge");
49
51
  const GraphMetrics_1 = require("./backend/GraphMetrics");
50
52
  const LayoutEngine_1 = require("./backend/algorithms/LayoutEngine");
@@ -54,11 +56,18 @@ const reader_renderer_1 = require("./reader_renderer");
54
56
  const native_clipboard_1 = require("./native_clipboard");
55
57
  const notemd_1 = require("./notemd");
56
58
  const AppConfigToml_1 = require("./notemd/AppConfigToml");
57
- const providerTemplates_1 = require("./notemd/providerTemplates");
58
59
  const MarkdownGateway_1 = require("./markdown/MarkdownGateway");
59
60
  const learning_1 = require("./learning");
61
+ const requestNormalization_1 = require("./learning/requestNormalization");
62
+ const agentConversationSerialization_1 = require("./learning/agentConversationSerialization");
63
+ const agentConversationExecution_1 = require("./learning/agentConversationExecution");
64
+ const agentResponseBudget_1 = require("./learning/agentResponseBudget");
65
+ const SseResponseWriter_1 = require("./middleware/SseResponseWriter");
66
+ const mobileAnswerProjection_1 = require("./learning/mobileAnswerProjection");
67
+ const workspaceHydration_1 = require("./learning/workspaceHydration");
60
68
  const routes_1 = require("./routes");
61
69
  const runtimeRunbookRouteOps_1 = require("./routes/runtimeRunbookRouteOps");
70
+ const auth_1 = require("./middleware/auth");
62
71
  const domains_1 = require("./learning/domains");
63
72
  function installBrokenPipeGuard(stream) {
64
73
  if (!stream || stream.__noteConnectionBrokenPipeGuardInstalled) {
@@ -184,6 +193,9 @@ const AGENT_WORKSPACE_DIAGNOSTICS_TRIAGE_TOP_LIMIT = 5;
184
193
  const AGENT_WORKSPACE_DIAGNOSTICS_TRIAGE_TOP_LIMIT_MIN = 1;
185
194
  const AGENT_WORKSPACE_DIAGNOSTICS_TRIAGE_TOP_LIMIT_MAX = 10;
186
195
  const REQUEST_BODY_SPOOL_DIR = path.join(runtimePaths.projectRoot, 'tmp', 'request-bodies');
196
+ const KNOWLEDGE_WORKSPACE_TITLE_PREVIEW_BYTES = 16 * 1024;
197
+ const KNOWLEDGE_WORKSPACE_LARGE_TARGET_FILE_THRESHOLD = 100;
198
+ const KNOWLEDGE_WORKSPACE_MAX_SELECTIVE_HYDRATION_FILES = 32;
187
199
  let KB_ROOT = runtimePaths.kbRoot;
188
200
  let activeBuildKey = null;
189
201
  let activeBuildPromise = null;
@@ -191,7 +203,7 @@ let ACTIVE_GRAPH_TARGET = 'ALL_FOLDERS';
191
203
  let lastRestoreKey = null;
192
204
  let lastRestoreTs = 0;
193
205
  const activeKnowledgeWorkspaceSyncs = new Map();
194
- const SIDECAR_RUNTIME_MANIFEST = path.join(runtimePaths.projectRoot, 'tmp', 'active-sidecar-runtime.json');
206
+ const SIDECAR_RUNTIME_MANIFEST = runtimePaths.runtimeManifestPath;
195
207
  const notemdService = new notemd_1.NotemdService();
196
208
  const notemdLlmClient = new notemd_1.LlmProviderClient();
197
209
  const API_REQUEST_TRACE_MAX_RECORDS = 400;
@@ -4808,12 +4820,22 @@ const knowledgeGraphStore = (0, learning_1.createKnowledgeGraphStore)({
4808
4820
  backend: KNOWLEDGE_GRAPH_STORE_BACKEND,
4809
4821
  filePath: KNOWLEDGE_GRAPH_STORE_PATH,
4810
4822
  graphDbAdapter: KNOWLEDGE_GRAPHDB_ADAPTER,
4823
+ graphDbRequestedProvider: KNOWLEDGE_GRAPHDB_ADAPTER_PROVIDER,
4811
4824
  graphDbFallbackEnabled: KNOWLEDGE_GRAPHDB_FALLBACK_ENABLED,
4812
4825
  graphDbOperationMode: KNOWLEDGE_GRAPHDB_OPERATION_MODE,
4813
4826
  });
4814
4827
  const defaultLocalTutorAdapter = createNotemdTutorAdapter('local');
4815
4828
  const defaultCloudTutorAdapter = createNotemdTutorAdapter('cloud');
4829
+ const SERVER_RESPONSE_LOW_MEMORY_BYTES = 4 * 1024 ** 3;
4830
+ const SERVER_RESPONSE_HIGH_MEMORY_BYTES = 8 * 1024 ** 3;
4831
+ const serverResponseMemoryBytes = os.totalmem();
4832
+ const SERVER_RESPONSE_BUDGET_CAPABILITY = {
4833
+ memoryClass: serverResponseMemoryBytes < SERVER_RESPONSE_LOW_MEMORY_BYTES ? 'low'
4834
+ : serverResponseMemoryBytes >= SERVER_RESPONSE_HIGH_MEMORY_BYTES ? 'high' : 'standard',
4835
+ workload: 'max',
4836
+ };
4816
4837
  const knowledgeLearningPlatform = (0, learning_1.createKnowledgeLearningPlatform)({
4838
+ responseBudgetHostCapability: SERVER_RESPONSE_BUDGET_CAPABILITY,
4817
4839
  store: knowledgeGraphStore,
4818
4840
  learningQualityThresholds: LEARNING_QUALITY_THRESHOLDS,
4819
4841
  studySessionPlanQualityAdaptiveThresholdsEnabled: STUDY_SESSION_PLAN_QUALITY_ADAPTIVE_THRESHOLDS_ENABLED,
@@ -4964,7 +4986,6 @@ const markdownGateway = new MarkdownGateway_1.MarkdownGateway({
4964
4986
  warn: (...args) => warnDiagnostic(...args),
4965
4987
  },
4966
4988
  });
4967
- const NOTEMD_ACTIVE_OPERATIONS = new Map();
4968
4989
  const AGENT_CONVERSATION_TURN_CACHE = new Map();
4969
4990
  const AGENT_CONVERSATION_TURN_CACHE_COUNTERS = {
4970
4991
  cacheHitCount: 0,
@@ -6557,19 +6578,6 @@ function getRawRequestPathname(rawUrl) {
6557
6578
  }
6558
6579
  return requestTarget || '/';
6559
6580
  }
6560
- function extractRequestToken(req) {
6561
- const headerToken = typeof req.headers['x-noteconnection-token'] === 'string'
6562
- ? req.headers['x-noteconnection-token'].trim()
6563
- : '';
6564
- if (headerToken) {
6565
- return headerToken;
6566
- }
6567
- const authHeader = typeof req.headers.authorization === 'string' ? req.headers.authorization.trim() : '';
6568
- if (authHeader.toLowerCase().startsWith('bearer ')) {
6569
- return authHeader.slice(7).trim();
6570
- }
6571
- return '';
6572
- }
6573
6581
  function createRequestId() {
6574
6582
  return `req-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
6575
6583
  }
@@ -6597,7 +6605,7 @@ function isAuthorizedRequest(req) {
6597
6605
  if (!AUTH_TOKEN || !isProtectedRequest(req)) {
6598
6606
  return true;
6599
6607
  }
6600
- return extractRequestToken(req) === AUTH_TOKEN;
6608
+ return (0, auth_1.isRequestTokenAuthorized)(req, AUTH_TOKEN);
6601
6609
  }
6602
6610
  function isGeneratedGraphAsset(filename) {
6603
6611
  return (filename === 'data.js' ||
@@ -6907,6 +6915,18 @@ function classifyApiInputValidationError(error) {
6907
6915
  return null;
6908
6916
  }
6909
6917
  function writeApiErrorResponse(res, error, options) {
6918
+ if (res.destroyed || res.writableEnded)
6919
+ return;
6920
+ if (res.headersSent) {
6921
+ res.destroy();
6922
+ return;
6923
+ }
6924
+ if (error instanceof agentConversationExecution_1.AgentConversationExecutionError || error instanceof agentConversationSerialization_1.AgentConversationSerializationError) {
6925
+ setApiErrorCodeHeader(res, error.code);
6926
+ res.writeHead(error.statusCode, { 'Content-Type': 'application/json' });
6927
+ res.end(JSON.stringify(createApiErrorEnvelope({ error: error.message, errorCode: error.code, requestId: options.requestId })));
6928
+ return;
6929
+ }
6910
6930
  if (writeBodyParseErrorResponse(res, error, { requestId: options.requestId })) {
6911
6931
  return;
6912
6932
  }
@@ -7139,22 +7159,6 @@ function normalizeKnowledgeQueryRequestPayload(payload) {
7139
7159
  : undefined,
7140
7160
  };
7141
7161
  }
7142
- function normalizeAgentConversationRequestPayload(payload) {
7143
- const record = isObjectRecord(payload) ? payload : {};
7144
- const topKValue = parsePositiveIntegerValue(readFirstPresentValue(record, ['topK', 'k', 'limit']));
7145
- const normalizedQueryRequest = normalizeKnowledgeQueryRequestPayload(payload);
7146
- return {
7147
- userId: String(readFirstPresentValue(record, ['userId', 'user_id', 'learnerId']) || '').trim(),
7148
- sessionId: readFirstNonEmptyString(record, ['sessionId', 'session_id']),
7149
- activeTarget: readFirstNonEmptyString(record, ['activeTarget', 'active_target', 'target']),
7150
- message: readFirstNonEmptyString(record, ['message', 'prompt', 'query', 'q', 'text']) || '',
7151
- topK: topKValue > 0 ? topKValue : undefined,
7152
- asOf: readFirstNonEmptyString(record, ['asOf', 'as_of', 'timestamp', 'now']),
7153
- persistMemory: readFirstPresentValue(record, ['persistMemory', 'persist_memory']) !== false,
7154
- memoryNamespace: readFirstNonEmptyString(record, ['memoryNamespace', 'memory_namespace']),
7155
- scope: normalizedQueryRequest.scope,
7156
- };
7157
- }
7158
7162
  function requestAcceptsEventStream(req) {
7159
7163
  const acceptHeader = String(req.headers.accept || '').toLowerCase();
7160
7164
  return acceptHeader.includes('text/event-stream');
@@ -7199,11 +7203,43 @@ function normalizeAgentConversationTopK(value) {
7199
7203
  return Math.max(1, Math.floor(numeric));
7200
7204
  }
7201
7205
  function buildAgentConversationRequestFingerprint(requestPayload) {
7206
+ const scope = requestPayload.scope;
7207
+ const normalizedScope = scope
7208
+ ? {
7209
+ workspaceId: String(scope.workspaceId || '').trim(),
7210
+ corpusId: String(scope.corpusId || '').trim(),
7211
+ documentIds: Array.from(new Set(scope.documentIds || [])).sort(),
7212
+ atomIds: Array.from(new Set(scope.atomIds || [])).sort(),
7213
+ sourcePathPrefixes: Array.from(new Set(scope.sourcePathPrefixes || [])).sort(),
7214
+ languages: Array.from(new Set(scope.languages || [])).sort(),
7215
+ }
7216
+ : null;
7202
7217
  return JSON.stringify({
7203
7218
  userId: String(requestPayload.userId || '').trim(),
7219
+ sessionId: String(requestPayload.sessionId || '').trim(),
7220
+ activeTarget: String(requestPayload.activeTarget || '').trim(),
7204
7221
  message: String(requestPayload.message || '').trim(),
7222
+ answerLanguage: String(requestPayload.answerLanguage || 'auto').trim(),
7223
+ responseMode: String(requestPayload.responseMode || 'slim').trim(),
7224
+ responseBudgetMode: String(requestPayload.responseBudgetMode || 'adaptive').trim(),
7225
+ responseBudgetCapability: requestPayload.responseBudgetCapability
7226
+ ? {
7227
+ memoryClass: String(requestPayload.responseBudgetCapability.memoryClass || '').trim(),
7228
+ workload: String(requestPayload.responseBudgetCapability.workload || '').trim(),
7229
+ maxReportCharsHint: Number.isFinite(Number(requestPayload.responseBudgetCapability.maxReportCharsHint))
7230
+ ? Math.floor(Number(requestPayload.responseBudgetCapability.maxReportCharsHint))
7231
+ : null,
7232
+ maxSerializedBytesHint: Number.isFinite(Number(requestPayload.responseBudgetCapability.maxSerializedBytesHint))
7233
+ ? Math.floor(Number(requestPayload.responseBudgetCapability.maxSerializedBytesHint))
7234
+ : null,
7235
+ }
7236
+ : null,
7237
+ responseProfile: String(requestPayload.responseProfile || 'default').trim(),
7205
7238
  topK: normalizeAgentConversationTopK(requestPayload.topK),
7206
7239
  asOf: String(requestPayload.asOf || '').trim(),
7240
+ persistMemory: requestPayload.persistMemory !== false,
7241
+ memoryNamespace: String(requestPayload.memoryNamespace || '').trim(),
7242
+ scope: normalizedScope,
7207
7243
  });
7208
7244
  }
7209
7245
  function pruneAgentConversationTurnCache(nowMs = Date.now()) {
@@ -7253,8 +7289,13 @@ function getOrCreateAgentConversationTurnCacheRecord(turnId, requestFingerprint)
7253
7289
  return existing;
7254
7290
  }
7255
7291
  AGENT_CONVERSATION_TURN_CACHE_COUNTERS.cacheMissCount += 1;
7292
+ const runningTurns = Array.from(AGENT_CONVERSATION_TURN_CACHE.values()).filter(record => record.status === 'running').length;
7293
+ if (runningTurns >= agentConversationExecution_1.DEFAULT_AGENT_CONVERSATION_EXECUTION_POLICY.maxPendingTurns)
7294
+ throw new agentConversationExecution_1.AgentConversationExecutionError('runtime_turn_capacity');
7256
7295
  const nowMs = Date.now();
7257
7296
  const record = {
7297
+ controller: new AbortController(),
7298
+ consumers: 0,
7258
7299
  turnId,
7259
7300
  requestFingerprint,
7260
7301
  createdAtMs: nowMs,
@@ -7266,6 +7307,31 @@ function getOrCreateAgentConversationTurnCacheRecord(turnId, requestFingerprint)
7266
7307
  pruneAgentConversationTurnCache(nowMs);
7267
7308
  return record;
7268
7309
  }
7310
+ function attachAgentConversationClient(record, req, res) {
7311
+ record.consumers++;
7312
+ let attached = true;
7313
+ const release = () => {
7314
+ if (!attached)
7315
+ return;
7316
+ attached = false;
7317
+ record.consumers--;
7318
+ req.removeListener('aborted', release);
7319
+ res.removeListener('close', release);
7320
+ if (record.status === 'running' && record.consumers === 0) {
7321
+ record.controller.abort(new agentConversationExecution_1.AgentConversationExecutionError('runtime_cancelled'));
7322
+ if (!record.inFlight) {
7323
+ record.status = 'failed';
7324
+ record.failure = { error: 'runtime_cancelled', errorCode: 'runtime_cancelled' };
7325
+ appendAgentConversationTurnCacheEvent(record, { type: 'turn_failed', turnId: record.turnId, emittedAt: new Date().toISOString(), ...record.failure });
7326
+ }
7327
+ }
7328
+ };
7329
+ req.once('aborted', release);
7330
+ res.once('close', release);
7331
+ if (req.aborted || res.destroyed)
7332
+ release();
7333
+ return release;
7334
+ }
7269
7335
  function appendAgentConversationTurnCacheEvent(record, event) {
7270
7336
  if (record.events.length >= AGENT_CONVERSATION_TURN_CACHE_MAX_EVENTS_PER_TURN) {
7271
7337
  record.events.shift();
@@ -7273,12 +7339,58 @@ function appendAgentConversationTurnCacheEvent(record, event) {
7273
7339
  record.events.push(event);
7274
7340
  record.updatedAtMs = Date.now();
7275
7341
  }
7276
- function replayAgentConversationTurnEvents(res, record) {
7342
+ function replayAgentConversationTurnEvents(res, record, requestPayload) {
7277
7343
  AGENT_CONVERSATION_TURN_CACHE_COUNTERS.replayResponseCount += 1;
7278
7344
  AGENT_CONVERSATION_TURN_CACHE_COUNTERS.replayedEventCount += record.events.length;
7279
7345
  for (const event of record.events) {
7280
- writeSseEvent(res, event.type, event);
7346
+ writeSseEvent(res, event.type, projectAgentConversationTurnEvent(event, requestPayload));
7347
+ }
7348
+ }
7349
+ function buildMobileAgentConversationResponse(result) {
7350
+ const projection = result.mobileProjection || (0, mobileAnswerProjection_1.projectAnswerForMobile)(result);
7351
+ const summary = result.summary || {
7352
+ generatedAt: new Date().toISOString(),
7353
+ topK: 0,
7354
+ queryEvidenceCoverageRatioPct: 0,
7355
+ };
7356
+ return {
7357
+ userId: result.userId,
7358
+ sessionId: result.sessionId,
7359
+ assistantMessage: projection.directAnswer,
7360
+ answer: projection.directAnswer,
7361
+ message: projection.directAnswer,
7362
+ responseMode: 'slim',
7363
+ responseProfile: 'mobile_compact',
7364
+ mobileProjection: projection,
7365
+ assistantBlocks: [],
7366
+ knowledgePoints: [],
7367
+ citations: [],
7368
+ recalledMemories: [],
7369
+ memoryActions: [],
7370
+ summary: {
7371
+ generatedAt: summary.generatedAt,
7372
+ topK: summary.topK,
7373
+ returnedKnowledgePoints: projection.route.length,
7374
+ returnedCitations: projection.citations.length,
7375
+ recalledMemoryCount: 0,
7376
+ appliedMemoryCount: 0,
7377
+ queryEvidenceCoverageRatioPct: summary.queryEvidenceCoverageRatioPct,
7378
+ responseBudgetMode: 'adaptive',
7379
+ responseBudgetTier: 'standard',
7380
+ responseTruncated: false,
7381
+ },
7382
+ };
7383
+ }
7384
+ function projectAgentConversationTurnEvent(event, requestPayload) {
7385
+ if (requestPayload?.responseProfile !== 'mobile_compact'
7386
+ || event.type !== 'turn_completed'
7387
+ || !event.result) {
7388
+ return event;
7281
7389
  }
7390
+ return {
7391
+ ...event,
7392
+ result: buildMobileAgentConversationResponse(event.result),
7393
+ };
7282
7394
  }
7283
7395
  function buildAgentConversationInitialTurnEvents(params) {
7284
7396
  return [
@@ -7289,6 +7401,10 @@ function buildAgentConversationInitialTurnEvents(params) {
7289
7401
  request: {
7290
7402
  userId: params.requestPayload.userId,
7291
7403
  topK: params.topK,
7404
+ responseMode: params.requestPayload.responseMode === 'full' ? 'full' : 'slim',
7405
+ responseBudgetMode: params.requestPayload.responseBudgetMode === 'unbounded'
7406
+ ? 'unbounded'
7407
+ : 'adaptive',
7292
7408
  },
7293
7409
  },
7294
7410
  {
@@ -7341,7 +7457,12 @@ async function ensureAgentConversationTurnExecution(record, requestPayload, opti
7341
7457
  const emit = (event) => {
7342
7458
  appendAgentConversationTurnCacheEvent(record, event);
7343
7459
  if (emitLiveEvent) {
7344
- emitLiveEvent(event);
7460
+ try {
7461
+ emitLiveEvent(event);
7462
+ }
7463
+ catch (error) {
7464
+ console.warn('[SSE] Live delivery ended:', error.message);
7465
+ }
7345
7466
  }
7346
7467
  };
7347
7468
  const nowIso = () => new Date().toISOString();
@@ -7355,51 +7476,64 @@ async function ensureAgentConversationTurnExecution(record, requestPayload, opti
7355
7476
  writeLiveEvent: emitLiveEvent,
7356
7477
  });
7357
7478
  try {
7358
- const hydration = await ensureLearningWorkspaceHydratedForConversationRequest(requestPayload);
7359
- emit({
7360
- type: 'capability_result',
7361
- turnId: record.turnId,
7362
- emittedAt: nowIso(),
7363
- stage: 'workspace_readiness_gate',
7364
- summary: {
7365
- hydrated: hydration.hydrated,
7366
- target: hydration.target,
7367
- reason: hydration.reason,
7368
- workspaceId: hydration.scope?.workspaceId || null,
7369
- corpusId: hydration.scope?.corpusId || null,
7370
- },
7371
- });
7372
- const result = await knowledgeLearningPlatform.runAgentConversation(requestPayload);
7373
- record.status = 'completed';
7374
- record.result = result;
7375
- record.failure = undefined;
7376
- AGENT_CONVERSATION_TURN_CACHE_COUNTERS.executionSuccessCount += 1;
7377
- emit({
7378
- type: 'capability_result',
7379
- turnId: record.turnId,
7380
- emittedAt: nowIso(),
7381
- stage: 'query_local_knowledge',
7382
- summary: {
7383
- returnedKnowledgePoints: Number(result.summary.returnedKnowledgePoints || 0),
7384
- queryEvidenceCoverageRatioPct: Number(result.summary.queryEvidenceCoverageRatioPct || 0),
7385
- },
7386
- });
7387
- emit({
7388
- type: 'turn_completed',
7389
- turnId: record.turnId,
7390
- emittedAt: nowIso(),
7391
- result,
7479
+ await agentConversationExecution_1.AgentConversationExecution.run({
7480
+ budget: (0, agentResponseBudget_1.resolveHostedAgentResponseBudget)({
7481
+ responseMode: requestPayload.responseMode,
7482
+ responseBudgetMode: requestPayload.responseBudgetMode,
7483
+ capability: requestPayload.responseBudgetCapability,
7484
+ mobile: requestPayload.responseProfile === 'mobile_compact',
7485
+ }, SERVER_RESPONSE_BUDGET_CAPABILITY),
7486
+ policy: agentConversationExecution_1.DEFAULT_AGENT_CONVERSATION_EXECUTION_POLICY,
7487
+ signal: record.controller.signal,
7488
+ }, async () => {
7489
+ const hydration = await ensureLearningWorkspaceHydratedForConversationRequest(requestPayload);
7490
+ emit({
7491
+ type: 'capability_result',
7492
+ turnId: record.turnId,
7493
+ emittedAt: nowIso(),
7494
+ stage: 'workspace_readiness_gate',
7495
+ summary: {
7496
+ hydrated: hydration.hydrated,
7497
+ target: hydration.target,
7498
+ reason: hydration.reason,
7499
+ workspaceId: hydration.scope?.workspaceId || null,
7500
+ corpusId: hydration.scope?.corpusId || null,
7501
+ },
7502
+ });
7503
+ const rawResult = await knowledgeLearningPlatform.runAgentConversation(requestPayload, record.controller.signal);
7504
+ const result = (0, agentConversationSerialization_1.serializeAgentConversationResponse)(rawResult).result;
7505
+ record.status = 'completed';
7506
+ record.result = result;
7507
+ record.failure = undefined;
7508
+ AGENT_CONVERSATION_TURN_CACHE_COUNTERS.executionSuccessCount += 1;
7509
+ emit({
7510
+ type: 'capability_result',
7511
+ turnId: record.turnId,
7512
+ emittedAt: nowIso(),
7513
+ stage: 'query_local_knowledge',
7514
+ summary: {
7515
+ returnedKnowledgePoints: Number(result.summary.returnedKnowledgePoints || 0),
7516
+ queryEvidenceCoverageRatioPct: Number(result.summary.queryEvidenceCoverageRatioPct || 0),
7517
+ },
7518
+ });
7519
+ emit({
7520
+ type: 'turn_completed',
7521
+ turnId: record.turnId,
7522
+ emittedAt: nowIso(),
7523
+ result,
7524
+ });
7392
7525
  });
7393
7526
  }
7394
7527
  catch (executionError) {
7395
7528
  const validation = classifyApiInputValidationError(executionError);
7529
+ const runtimeError = executionError instanceof agentConversationExecution_1.AgentConversationExecutionError || executionError instanceof agentConversationSerialization_1.AgentConversationSerializationError;
7396
7530
  const failure = {
7397
7531
  error: validation?.error || String(executionError?.message
7398
7532
  || executionError
7399
7533
  || 'unknown_error'),
7400
- errorCode: validation?.errorCode || 'internal_error',
7534
+ errorCode: runtimeError ? executionError.code : validation?.errorCode || 'internal_error',
7401
7535
  };
7402
- if (!validation) {
7536
+ if (!validation && !runtimeError) {
7403
7537
  console.error(executionError);
7404
7538
  CrashLogger_1.CrashLogger.log(executionError, 'API:POST /api/knowledge/conversation [turn-cache]');
7405
7539
  }
@@ -7436,6 +7570,10 @@ function throwAgentConversationCachedFailure(failureLike) {
7436
7570
  error: 'conversation_turn_failed',
7437
7571
  errorCode: 'internal_error',
7438
7572
  };
7573
+ if (failure.errorCode === 'runtime_serialized_bytes_limit')
7574
+ throw new agentConversationSerialization_1.AgentConversationSerializationError();
7575
+ if (failure.errorCode?.startsWith('runtime_'))
7576
+ throw new agentConversationExecution_1.AgentConversationExecutionError(failure.errorCode);
7439
7577
  if (failure.errorCode && failure.errorCode !== 'internal_error') {
7440
7578
  throw new learning_1.InvalidRequestError(failure.error, {
7441
7579
  errorCode: failure.errorCode,
@@ -7620,12 +7758,18 @@ function normalizeKnowledgeDocumentInputPayload(payload) {
7620
7758
  : String(contentRaw ?? '');
7621
7759
  const language = readFirstNonEmptyString(record, ['language', 'lang', 'locale']);
7622
7760
  const updatedAt = readFirstNonEmptyString(record, ['updatedAt', 'updated_at', 'timestamp', 'now']);
7761
+ const sourceUri = readFirstNonEmptyString(record, ['sourceUri', 'source_uri', 'uri']);
7762
+ const revision = readFirstNonEmptyString(record, ['revision', 'sourceRevision', 'source_revision']);
7763
+ const identityAliases = normalizeStringArrayValue(readFirstPresentValue(record, ['identityAliases', 'identity_aliases', 'aliases']));
7623
7764
  if (!documentId && !sourcePath && !content.trim()) {
7624
7765
  return null;
7625
7766
  }
7626
7767
  return {
7627
7768
  documentId,
7628
7769
  sourcePath: sourcePath || '',
7770
+ sourceUri,
7771
+ revision,
7772
+ identityAliases: identityAliases.length > 0 ? identityAliases : undefined,
7629
7773
  content,
7630
7774
  language,
7631
7775
  updatedAt,
@@ -7642,12 +7786,16 @@ function normalizeKnowledgeDocumentDeletePayload(payload) {
7642
7786
  'filepath',
7643
7787
  'file',
7644
7788
  ]);
7645
- if (!documentId && !sourcePath) {
7789
+ const sourceUri = readFirstNonEmptyString(record, ['sourceUri', 'source_uri', 'uri']);
7790
+ const identityAliases = normalizeStringArrayValue(readFirstPresentValue(record, ['identityAliases', 'identity_aliases', 'aliases']));
7791
+ if (!documentId && !sourcePath && !sourceUri && identityAliases.length === 0) {
7646
7792
  return null;
7647
7793
  }
7648
7794
  return {
7649
7795
  documentId,
7650
7796
  sourcePath,
7797
+ sourceUri,
7798
+ identityAliases: identityAliases.length > 0 ? identityAliases : undefined,
7651
7799
  };
7652
7800
  }
7653
7801
  function normalizeKnowledgeIngestOperationPayload(payload) {
@@ -7656,6 +7804,33 @@ function normalizeKnowledgeIngestOperationPayload(payload) {
7656
7804
  const normalizedOp = String(opRaw || '').trim().toLowerCase();
7657
7805
  const documentRaw = readFirstPresentValue(record, ['document', 'payload', 'item']);
7658
7806
  const operationPayload = typeof documentRaw !== 'undefined' ? documentRaw : payload;
7807
+ if (normalizedOp === 'move'
7808
+ || normalizedOp === 'rename') {
7809
+ const moveRecord = isObjectRecord(operationPayload) ? operationPayload : {};
7810
+ const toSourcePath = readFirstNonEmptyString(moveRecord, [
7811
+ 'toSourcePath',
7812
+ 'to_source_path',
7813
+ 'destinationPath',
7814
+ 'destination_path',
7815
+ ]);
7816
+ if (!toSourcePath) {
7817
+ return null;
7818
+ }
7819
+ return {
7820
+ op: 'move',
7821
+ document: {
7822
+ documentId: readFirstNonEmptyString(moveRecord, ['documentId', 'docId', 'id']),
7823
+ fromSourcePath: readFirstNonEmptyString(moveRecord, ['fromSourcePath', 'from_source_path', 'sourcePath', 'source_path']),
7824
+ fromSourceUri: readFirstNonEmptyString(moveRecord, ['fromSourceUri', 'from_source_uri', 'sourceUri', 'source_uri']),
7825
+ fromIdentityAliases: normalizeStringArrayValue(readFirstPresentValue(moveRecord, ['fromIdentityAliases', 'from_identity_aliases', 'fromAliases'])),
7826
+ toSourcePath,
7827
+ toSourceUri: readFirstNonEmptyString(moveRecord, ['toSourceUri', 'to_source_uri', 'destinationUri', 'destination_uri']),
7828
+ toIdentityAliases: normalizeStringArrayValue(readFirstPresentValue(moveRecord, ['toIdentityAliases', 'to_identity_aliases', 'aliases'])),
7829
+ revision: readFirstNonEmptyString(moveRecord, ['revision', 'sourceRevision', 'source_revision']),
7830
+ updatedAt: readFirstNonEmptyString(moveRecord, ['updatedAt', 'updated_at', 'timestamp', 'now']),
7831
+ },
7832
+ };
7833
+ }
7659
7834
  if (normalizedOp === 'delete'
7660
7835
  || normalizedOp === 'remove'
7661
7836
  || normalizedOp === 'del') {
@@ -8848,56 +9023,6 @@ async function persistNotemdSettings(settingsLike) {
8848
9023
  cachedNotemdSettings = cloneNotemdSettings(normalized);
8849
9024
  return cloneNotemdSettings(normalized);
8850
9025
  }
8851
- function extractNotemdWorkspaceState(settings) {
8852
- return {
8853
- filePath: String(settings.workspaceFilePath || '').trim(),
8854
- folderPath: String(settings.workspaceFolderPath || '').trim(),
8855
- outputFilePath: String(settings.workspaceOutputFilePath || '').trim(),
8856
- outputFolderPath: String(settings.workspaceOutputFolderPath || '').trim(),
8857
- };
8858
- }
8859
- function normalizeWorkspaceField(source, keys, fallback) {
8860
- for (const key of keys) {
8861
- if (!Object.prototype.hasOwnProperty.call(source, key)) {
8862
- continue;
8863
- }
8864
- return String(source[key] || '').trim();
8865
- }
8866
- return fallback;
8867
- }
8868
- function applyWorkspacePatchToSettings(settings, workspacePatch) {
8869
- const next = cloneNotemdSettings(settings);
8870
- if (!isObjectRecord(workspacePatch)) {
8871
- return next;
8872
- }
8873
- next.workspaceFilePath = normalizeWorkspaceField(workspacePatch, ['filePath', 'file_path', 'workspaceFilePath', 'workspace_file_path'], next.workspaceFilePath);
8874
- next.workspaceFolderPath = normalizeWorkspaceField(workspacePatch, ['folderPath', 'folder_path', 'workspaceFolderPath', 'workspace_folder_path'], next.workspaceFolderPath);
8875
- next.workspaceOutputFilePath = normalizeWorkspaceField(workspacePatch, ['outputFilePath', 'output_file_path', 'workspaceOutputFilePath', 'workspace_output_file_path'], next.workspaceOutputFilePath);
8876
- next.workspaceOutputFolderPath = normalizeWorkspaceField(workspacePatch, ['outputFolderPath', 'output_folder_path', 'workspaceOutputFolderPath', 'workspace_output_folder_path'], next.workspaceOutputFolderPath);
8877
- return next;
8878
- }
8879
- async function persistNotemdWorkspacePatch(workspacePatch) {
8880
- const settings = await loadNotemdSettings();
8881
- const nextSettings = applyWorkspacePatchToSettings(settings, workspacePatch);
8882
- const persisted = await persistNotemdSettings(nextSettings);
8883
- return extractNotemdWorkspaceState(persisted);
8884
- }
8885
- async function ensureNotemdProviderTemplatesPersisted() {
8886
- const appConfig = await (0, AppConfigToml_1.loadAppConfigToml)();
8887
- const currentNotemdSection = isObjectRecord(appConfig.notemd) ? appConfig.notemd : {};
8888
- const nextNotemdSection = (0, providerTemplates_1.mergeProviderTemplatesIntoNotemdSection)(currentNotemdSection);
8889
- const persisted = JSON.stringify(currentNotemdSection) !== JSON.stringify(nextNotemdSection);
8890
- if (persisted) {
8891
- await (0, AppConfigToml_1.saveAppConfigToml)({
8892
- ...appConfig,
8893
- notemd: nextNotemdSection,
8894
- });
8895
- }
8896
- return {
8897
- configPath: (0, AppConfigToml_1.resolveAppConfigPath)(),
8898
- persisted,
8899
- };
8900
- }
8901
9026
  function clonePathModeSettings(settings) {
8902
9027
  return JSON.parse(JSON.stringify(settings));
8903
9028
  }
@@ -8942,74 +9067,25 @@ async function persistFrontendSettings(settingsLike) {
8942
9067
  cachedFrontendSettings = cloneFrontendSettings(persisted);
8943
9068
  return cloneFrontendSettings(persisted);
8944
9069
  }
8945
- function generateNotemdOperationId() {
8946
- return `notemd-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
8947
- }
8948
- function createNotemdOperation(operationIdCandidate) {
8949
- const requestedId = String(operationIdCandidate || '').trim();
8950
- const operationId = requestedId || generateNotemdOperationId();
8951
- const state = {
8952
- id: operationId,
8953
- controller: new AbortController(),
8954
- status: 'running',
8955
- createdAt: Date.now(),
8956
- updatedAt: Date.now(),
8957
- logs: [],
8958
- };
8959
- NOTEMD_ACTIVE_OPERATIONS.set(operationId, state);
8960
- return state;
8961
- }
8962
- function finalizeNotemdOperation(state, status) {
8963
- state.status = status;
8964
- state.updatedAt = Date.now();
8965
- setTimeout(() => {
8966
- const current = NOTEMD_ACTIVE_OPERATIONS.get(state.id);
8967
- if (current === state && current.status !== 'running') {
8968
- NOTEMD_ACTIVE_OPERATIONS.delete(state.id);
8969
- }
8970
- }, 60000);
8971
- }
9070
+ const sseResponseWriters = new WeakMap();
8972
9071
  function writeSseEvent(res, eventType, payload) {
8973
9072
  if (res.writableEnded || res.destroyed) {
8974
9073
  return;
8975
9074
  }
8976
- try {
8977
- res.write(`event: ${eventType}\n`);
8978
- res.write(`data: ${JSON.stringify(payload)}\n\n`);
8979
- }
8980
- catch (_error) {
8981
- // Ignore stream write failures when client disconnected mid-stream.
9075
+ let writer = sseResponseWriters.get(res);
9076
+ if (!writer) {
9077
+ writer = new SseResponseWriter_1.SseResponseWriter(res);
9078
+ sseResponseWriters.set(res, writer);
8982
9079
  }
9080
+ const serialized = (0, agentConversationSerialization_1.serializeAgentConversationTurnEvent)(eventType, payload);
9081
+ writer.write(`event: ${eventType}\ndata: ${serialized.json}\n\n`);
8983
9082
  }
8984
- function createNotemdReporter(state, res) {
8985
- return {
8986
- report: (eventLike) => {
8987
- const event = {
8988
- ...eventLike,
8989
- operationId: state.id,
8990
- timestamp: Date.now(),
8991
- };
8992
- state.logs.push(event);
8993
- state.updatedAt = event.timestamp;
8994
- if (res) {
8995
- writeSseEvent(res, event.type, event);
8996
- }
8997
- },
8998
- isCancelled: () => state.controller.signal.aborted,
8999
- };
9000
- }
9001
- function shouldStreamNotemdResponse(req) {
9002
- const acceptHeader = typeof req.headers.accept === 'string' ? req.headers.accept : '';
9003
- if (acceptHeader.includes('text/event-stream')) {
9004
- return true;
9005
- }
9006
- try {
9007
- const urlObj = new URL(req.url || '/', `http://${LOOPBACK_HOST}:${PORT}`);
9008
- return urlObj.searchParams.get('stream') === '1';
9009
- }
9010
- catch (_error) {
9011
- return false;
9012
- }
9083
+ async function finishSseResponse(res) {
9084
+ const writer = sseResponseWriters.get(res);
9085
+ if (writer)
9086
+ await writer.finish();
9087
+ else
9088
+ res.end();
9013
9089
  }
9014
9090
  async function resolveNearestExistingAncestor(candidatePath) {
9015
9091
  let current = path.resolve(candidatePath);
@@ -9317,7 +9393,8 @@ async function writeSidecarRuntimeManifest(finalPort) {
9317
9393
  runtimeDataDir: RUNTIME_DATA_DIR,
9318
9394
  generatedAt: new Date().toISOString(),
9319
9395
  pid: process.pid,
9320
- }, null, 2), 'utf8');
9396
+ }, null, 2), { encoding: 'utf8', mode: 0o600 });
9397
+ await fs.promises.chmod(SIDECAR_RUNTIME_MANIFEST, 0o600);
9321
9398
  }
9322
9399
  catch (error) {
9323
9400
  warnDiagnostic('[Sidecar] Failed to write runtime manifest:', error);
@@ -9518,15 +9595,21 @@ function resolveKnowledgeWorkspaceTargetFromConversationRequest(requestPayload)
9518
9595
  return activeTarget;
9519
9596
  }
9520
9597
  function buildKnowledgeDocumentPayloadFromFile(file) {
9521
- const relativePath = path.relative(KB_ROOT, file.filepath).replace(/\\/g, '/');
9522
- const sourcePath = `Knowledge_Base/${relativePath}`.replace(/\/{2,}/g, '/');
9598
+ const sourcePath = buildKnowledgeSourcePathFromFilePath(file.filepath);
9523
9599
  const language = /[\u4e00-\u9fff]/.test(file.content) ? 'zh' : 'en';
9524
9600
  return {
9525
9601
  sourcePath,
9602
+ sourceUri: file.sourceUri,
9603
+ revision: file.revision,
9604
+ identityAliases: file.identityAliases,
9526
9605
  content: file.content,
9527
9606
  language,
9528
9607
  };
9529
9608
  }
9609
+ function buildKnowledgeSourcePathFromFilePath(filePath) {
9610
+ const relativePath = path.relative(KB_ROOT, filePath).replace(/\\/g, '/');
9611
+ return `Knowledge_Base/${relativePath}`.replace(/\/{2,}/g, '/');
9612
+ }
9530
9613
  async function collectMarkdownFilePaths(targetPath) {
9531
9614
  const collected = [];
9532
9615
  const scan = async (currentPath) => {
@@ -9554,26 +9637,7 @@ function normalizeKnowledgeTargetLookupQuery(value) {
9554
9637
  .toLowerCase();
9555
9638
  }
9556
9639
  function deriveKnowledgeTargetLookupQueries(query) {
9557
- const normalized = normalizeKnowledgeTargetLookupQuery(query);
9558
- if (!normalized) {
9559
- return [];
9560
- }
9561
- const candidates = new Set([normalized]);
9562
- const stripped = normalized
9563
- .replace(/^(what is|what are|define|explain|tell me about|what's)\s+/i, '')
9564
- .replace(/^(什么是|解释一下|介绍一下|请解释|请介绍)\s*/i, '')
9565
- .trim();
9566
- if (stripped) {
9567
- candidates.add(stripped);
9568
- candidates.add(stripped.replace(/\s+/g, ''));
9569
- }
9570
- if (normalized.includes('water glass') || stripped === 'waterglass') {
9571
- candidates.add('water glass');
9572
- candidates.add('waterglass');
9573
- candidates.add('水玻璃');
9574
- candidates.add('水杯');
9575
- }
9576
- return Array.from(candidates.values()).filter(Boolean);
9640
+ return (0, workspaceHydration_1.deriveKnowledgeTargetLookupQueries)(query);
9577
9641
  }
9578
9642
  async function findKnowledgeFilesByTitleLikeQueries(targetPath, titleLikeQueries) {
9579
9643
  if (titleLikeQueries.length <= 0) {
@@ -9588,16 +9652,50 @@ async function findKnowledgeFilesByTitleLikeQueries(targetPath, titleLikeQueries
9588
9652
  if (exactMatches.length > 0) {
9589
9653
  return exactMatches;
9590
9654
  }
9591
- return markdownFiles.filter((filePath) => {
9655
+ const fuzzyBasenameMatches = markdownFiles.filter((filePath) => {
9592
9656
  const normalizedBaseName = normalizeKnowledgeTargetLookupQuery(path.basename(filePath, path.extname(filePath)));
9593
9657
  return normalizedQueries.some((query) => normalizedBaseName.includes(query));
9594
9658
  });
9659
+ if (fuzzyBasenameMatches.length > 0) {
9660
+ return fuzzyBasenameMatches;
9661
+ }
9662
+ const previewMatches = [];
9663
+ for (const filePath of markdownFiles) {
9664
+ const preview = await readMarkdownTitlePreview(filePath);
9665
+ if ((0, workspaceHydration_1.markdownPreviewMatchesTitleLikeQueries)({
9666
+ sourcePath: buildKnowledgeSourcePathFromFilePath(filePath),
9667
+ preview,
9668
+ titleLikeQueries: normalizedQueries,
9669
+ })) {
9670
+ previewMatches.push(filePath);
9671
+ }
9672
+ }
9673
+ return previewMatches;
9674
+ }
9675
+ async function readMarkdownTitlePreview(filePath) {
9676
+ const handle = await fs.promises.open(filePath, 'r');
9677
+ try {
9678
+ const buffer = Buffer.allocUnsafe(KNOWLEDGE_WORKSPACE_TITLE_PREVIEW_BYTES);
9679
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
9680
+ return buffer.subarray(0, bytesRead).toString('utf8');
9681
+ }
9682
+ finally {
9683
+ await handle.close();
9684
+ }
9595
9685
  }
9596
9686
  async function buildKnowledgeDocumentPayloadsFromPaths(filePaths) {
9597
9687
  const documents = [];
9598
9688
  for (const filePath of filePaths) {
9599
- const content = await fs.promises.readFile(filePath, 'utf8');
9600
- documents.push(buildKnowledgeDocumentPayloadFromFile({ filepath: filePath, content }));
9689
+ const execution = agentConversationExecution_1.AgentConversationExecution.current();
9690
+ const content = execution ? await execution.readSourceFile(filePath) : await fs.promises.readFile(filePath, 'utf8');
9691
+ if (content === null)
9692
+ continue;
9693
+ const identity = (0, ResourceIdentity_1.createResourceIdentity)(path.relative(KB_ROOT, filePath).replace(/\\/g, '/'), path.basename(filePath, path.extname(filePath)), content);
9694
+ documents.push(buildKnowledgeDocumentPayloadFromFile({
9695
+ filepath: filePath,
9696
+ content,
9697
+ ...identity,
9698
+ }));
9601
9699
  }
9602
9700
  return documents;
9603
9701
  }
@@ -9606,6 +9704,7 @@ async function syncLearningWorkspaceForDocumentPaths(params) {
9606
9704
  const result = await knowledgeLearningPlatform.ingestKnowledge({
9607
9705
  incremental: true,
9608
9706
  documents,
9707
+ deletedDocuments: params.deletedDocuments,
9609
9708
  ingestedAt: new Date().toISOString(),
9610
9709
  relationRecomputeMode: 'incremental',
9611
9710
  });
@@ -9613,6 +9712,7 @@ async function syncLearningWorkspaceForDocumentPaths(params) {
9613
9712
  target: params.target,
9614
9713
  reason: params.reason,
9615
9714
  documentCount: documents.length,
9715
+ deletedDocumentCount: params.deletedDocuments?.length || 0,
9616
9716
  changedDocuments: result.summary.changedDocuments,
9617
9717
  activeAtoms: result.summary.activeAtoms,
9618
9718
  });
@@ -9666,15 +9766,8 @@ async function ensureLearningWorkspaceHydratedForConversationRequest(requestPayl
9666
9766
  const readinessBeforeSync = await knowledgeLearningPlatform.inspectKnowledgeWorkspaceRequest({
9667
9767
  query: requestPayload.message,
9668
9768
  scope: derivedScope,
9769
+ includeSourceInventory: true,
9669
9770
  });
9670
- if (readinessBeforeSync.readiness.status === 'ready') {
9671
- return {
9672
- hydrated: false,
9673
- target,
9674
- scope: derivedScope,
9675
- reason: 'existing_store_ready',
9676
- };
9677
- }
9678
9771
  if (!target || target === 'ALL_FOLDERS') {
9679
9772
  const explicitAllFoldersTarget = String(requestPayload.activeTarget || '').trim().toUpperCase() === 'ALL_FOLDERS';
9680
9773
  if (explicitAllFoldersTarget) {
@@ -9693,26 +9786,91 @@ async function ensureLearningWorkspaceHydratedForConversationRequest(requestPayl
9693
9786
  reason: readinessBeforeSync.readiness.status,
9694
9787
  };
9695
9788
  }
9696
- const targetPath = path.join(KB_ROOT, target);
9789
+ const availableTargets = await collectAvailableTargetsFromPath(KB_ROOT);
9790
+ const resolvedTarget = availableTargets.find((entry) => entry.toLowerCase() === target.toLowerCase()) || target;
9791
+ const targetPath = path.join(KB_ROOT, resolvedTarget);
9792
+ const targetPathExists = await pathExists(targetPath);
9793
+ const targetFiles = await collectMarkdownFilePaths(targetPath);
9794
+ const diskSourcePaths = targetFiles.map(buildKnowledgeSourcePathFromFilePath);
9795
+ const indexedSourcePaths = readinessBeforeSync.sourceInventory?.sourcePaths || [];
9796
+ const inventoryDiff = (0, workspaceHydration_1.buildKnowledgeSourceInventoryDiff)({
9797
+ diskSourcePaths,
9798
+ indexedSourcePaths,
9799
+ });
9800
+ const indexedItemsByPath = new Map((readinessBeforeSync.sourceInventory?.items || []).map((item) => [
9801
+ (0, workspaceHydration_1.normalizeKnowledgeSourcePath)(item.sourcePath),
9802
+ item,
9803
+ ]));
9804
+ // A missing target directory is an unavailable filesystem boundary, not an
9805
+ // authoritative empty corpus. API/mobile callers may legitimately ingest a
9806
+ // document before its host materializes the directory, and deleting those
9807
+ // indexed projections would make the next conversation observe an empty store.
9808
+ const deletedDocuments = targetPathExists
9809
+ ? inventoryDiff.removedSourcePaths
9810
+ .map((sourcePath) => indexedItemsByPath.get((0, workspaceHydration_1.normalizeKnowledgeSourcePath)(sourcePath)))
9811
+ .filter((item) => Boolean(item))
9812
+ .map((item) => ({
9813
+ documentId: item.documentId,
9814
+ sourcePath: item.sourcePath,
9815
+ }))
9816
+ : [];
9817
+ if (readinessBeforeSync.readiness.status === 'ready'
9818
+ && inventoryDiff.addedSourcePaths.length <= 0
9819
+ && deletedDocuments.length <= 0) {
9820
+ return {
9821
+ hydrated: false,
9822
+ target: resolvedTarget,
9823
+ scope: derivedScope,
9824
+ reason: 'existing_store_ready',
9825
+ };
9826
+ }
9697
9827
  const titleLikeQueries = deriveKnowledgeTargetLookupQueries(String(requestPayload.message || ''));
9698
9828
  const candidateFiles = await findKnowledgeFilesByTitleLikeQueries(targetPath, titleLikeQueries);
9699
9829
  if (candidateFiles.length > 0) {
9700
9830
  await syncLearningWorkspaceForDocumentPaths({
9701
- target,
9702
- filePaths: candidateFiles,
9831
+ target: resolvedTarget,
9832
+ filePaths: candidateFiles.slice(0, KNOWLEDGE_WORKSPACE_MAX_SELECTIVE_HYDRATION_FILES),
9833
+ deletedDocuments,
9703
9834
  reason: 'conversation_selective_title_hydration',
9704
9835
  });
9705
9836
  return {
9706
9837
  hydrated: true,
9707
- target,
9838
+ target: resolvedTarget,
9708
9839
  scope: derivedScope,
9709
9840
  reason: 'conversation_selective_title_hydration',
9710
9841
  };
9711
9842
  }
9712
- await syncLearningWorkspaceForTarget(target, 'conversation_auto_hydration');
9843
+ if (deletedDocuments.length > 0 && readinessBeforeSync.readiness.status === 'ready') {
9844
+ await syncLearningWorkspaceForDocumentPaths({
9845
+ target: resolvedTarget,
9846
+ filePaths: [],
9847
+ deletedDocuments,
9848
+ reason: 'conversation_stale_source_reconciliation',
9849
+ });
9850
+ return {
9851
+ hydrated: true,
9852
+ target: resolvedTarget,
9853
+ scope: derivedScope,
9854
+ reason: 'conversation_stale_source_reconciliation',
9855
+ };
9856
+ }
9857
+ if (targetFiles.length > KNOWLEDGE_WORKSPACE_LARGE_TARGET_FILE_THRESHOLD) {
9858
+ return {
9859
+ hydrated: false,
9860
+ target: resolvedTarget,
9861
+ scope: derivedScope,
9862
+ reason: 'conversation_hydration_deferred_large_target',
9863
+ };
9864
+ }
9865
+ await syncLearningWorkspaceForDocumentPaths({
9866
+ target: resolvedTarget,
9867
+ filePaths: targetFiles,
9868
+ deletedDocuments,
9869
+ reason: 'conversation_auto_hydration',
9870
+ });
9713
9871
  return {
9714
9872
  hydrated: true,
9715
- target,
9873
+ target: resolvedTarget,
9716
9874
  scope: derivedScope,
9717
9875
  reason: 'conversation_auto_hydration',
9718
9876
  };
@@ -10130,15 +10288,12 @@ const startServer = async (options = {}) => {
10130
10288
  }
10131
10289
  }
10132
10290
  }
10133
- // --- Strict Registry Mode ---
10134
- // Why: The inline notemd handlers (~1,147 lines) are a legacy fallback from
10135
- // the pre-modularization era. The route registry intercepts all covered routes
10136
- // first (line ~13170), making the inline handlers unreachable in production.
10137
- // This flag lets us verify 100% coverage in CI before safe deletion.
10138
- // Conditional (off by default): integration tests start the server without
10139
- // full registry init and depend on the inline fallback. Once all tests use
10140
- // the registry, this becomes default and inline handlers can be deleted.
10141
- const STRICT_REGISTRY = process.env.NOTE_CONNECTION_STRICT_REGISTRY === '1';
10291
+ const ROUTE_DISPATCH_MODE = String(process.env.NOTE_CONNECTION_ROUTE_DISPATCH_MODE || 'registry')
10292
+ .trim()
10293
+ .toLowerCase() === 'legacy'
10294
+ ? 'legacy'
10295
+ : 'registry';
10296
+ const USE_REGISTRY_DISPATCH = ROUTE_DISPATCH_MODE === 'registry';
10142
10297
  // --- Route Registry (modular dispatch for extracted route groups) ---
10143
10298
  const runtimeRunbookOps = (0, runtimeRunbookRouteOps_1.createRuntimeRunbookRouteOps)({
10144
10299
  buildRuntimePayload: buildKnowledgeRuntimePayload,
@@ -10184,6 +10339,24 @@ const startServer = async (options = {}) => {
10184
10339
  memoryPolicyManager,
10185
10340
  notemdService,
10186
10341
  loadNotemdSettings,
10342
+ notemdHttp: { readJsonBody, writeBodyParseErrorResponse, writeApiErrorResponse, resolvePathWithinKnowledgeBase, writeSseEvent, finishSseResponse, llmClient: notemdLlmClient },
10343
+ executeQueryBackendConfigUpdate: async (payload) => {
10344
+ const requestPayload = normalizeQueryBackendConfigRequestPayload(payload);
10345
+ const result = await knowledgeLearningPlatform.updateQueryBackendConfig(requestPayload);
10346
+ ACTIVE_KNOWLEDGE_QUERY_BACKEND = result.configuredBackend;
10347
+ const baseDiagnostics = knowledgeLearningPlatform.getQueryBackendDiagnostics();
10348
+ const graphvizRuntimeAvailability = await getGraphvizDotRuntimeAvailability();
10349
+ const diagnostics = enrichQueryBackendDiagnosticsWithRendererRuntime(baseDiagnostics, graphvizRuntimeAvailability);
10350
+ return {
10351
+ result,
10352
+ diagnostics,
10353
+ configuredVectorAccelerationProvider: QUERY_VECTOR_ACCELERATION_PROVIDER,
10354
+ configuredVectorAccelerationFailureMode: QUERY_VECTOR_ACCELERATION_FAILURE_MODE,
10355
+ configuredVectorAccelerationRepresentationStrict: QUERY_VECTOR_ACCELERATION_REPRESENTATION_STRICT_ENABLED,
10356
+ queryVectorAnnPrefilterEnabled: QUERY_VECTOR_ANN_PREFILTER_ENABLED,
10357
+ rolloutProfile: buildKnowledgeRuntimeRolloutProfile(),
10358
+ };
10359
+ },
10187
10360
  persistNotemdSettings,
10188
10361
  loadFrontendSettings,
10189
10362
  markdownGateway,
@@ -10199,6 +10372,63 @@ const startServer = async (options = {}) => {
10199
10372
  runtimeRunbookOps,
10200
10373
  getPathBridge: () => pathBridge,
10201
10374
  };
10375
+ // These routes keep their complete state/I/O operation in this server.
10376
+ const serverOwnedApiPaths = new Set([
10377
+ '/api/available-targets',
10378
+ '/api/build',
10379
+ '/api/check-cache',
10380
+ '/api/clipboard/image',
10381
+ '/api/clipboard/image-binary',
10382
+ '/api/content',
10383
+ '/api/folders',
10384
+ '/api/frontend/settings',
10385
+ '/api/kb-path',
10386
+ '/api/knowledge/conversation',
10387
+ '/api/knowledge/conversation-memory/add',
10388
+ '/api/knowledge/conversation-memory/delete',
10389
+ '/api/knowledge/conversation-memory/feedback',
10390
+ '/api/knowledge/conversation-memory/search',
10391
+ '/api/knowledge/conversation/turn-cache/diagnostics',
10392
+ '/api/knowledge/conversation/turn-cache/diagnostics/trend',
10393
+ '/api/knowledge/conversation/turn-cache/diagnostics/trend/export',
10394
+ '/api/knowledge/conversation/turn-cache/diagnostics/trend/index',
10395
+ '/api/knowledge/ingest/guardrails/evaluate',
10396
+ '/api/knowledge/mastery/diagnose',
10397
+ '/api/knowledge/mastery/misconceptions',
10398
+ '/api/knowledge/memory/policy',
10399
+ '/api/knowledge/path',
10400
+ '/api/knowledge/quality/evaluate',
10401
+ '/api/knowledge/quality/snapshot',
10402
+ '/api/knowledge/query',
10403
+ '/api/knowledge/query-backend-config',
10404
+ '/api/knowledge/query/compare-backends',
10405
+ '/api/knowledge/runtime-capability-runbook/remediation-event',
10406
+ '/api/knowledge/runtime-capability-runbook/remediation-event/replay',
10407
+ '/api/knowledge/runtime-capability-runbook/remediation-event/replay-schedule',
10408
+ '/api/knowledge/runtime-capability-runbook/remediation-event/replay-schedule/tick',
10409
+ '/api/knowledge/session/action',
10410
+ '/api/knowledge/session/execute',
10411
+ '/api/knowledge/session/history',
10412
+ '/api/knowledge/session/orchestration/config',
10413
+ '/api/knowledge/session/plan',
10414
+ '/api/knowledge/session/plan/evaluate',
10415
+ '/api/knowledge/staleness/rebuild',
10416
+ '/api/knowledge/store/reload',
10417
+ '/api/knowledge/tutor/action',
10418
+ '/api/markdown/chunk',
10419
+ '/api/markdown/index',
10420
+ '/api/markdown/resolve-node',
10421
+ '/api/markdown/resolve-wiki',
10422
+ '/api/path-mode/settings',
10423
+ '/api/render/graphviz',
10424
+ '/api/render/math',
10425
+ '/api/render/mermaid',
10426
+ '/api/restore-cache',
10427
+ '/api/runtime-diagnostics',
10428
+ '/api/runtime-request-trace',
10429
+ ]);
10430
+ let intentionalInlineHits = 0;
10431
+ const inlineFallbacksByFamily = {};
10202
10432
  // Route migration tracking
10203
10433
  let routeRegistryHits = 0;
10204
10434
  let routeInlineFallbacks = 0;
@@ -10217,9 +10447,8 @@ const startServer = async (options = {}) => {
10217
10447
  }
10218
10448
  const routeMigrationStats = {
10219
10449
  totalModularRoutes: allRoutes.length,
10220
- totalInlineRoutes: 7, // terminal routes (meta/diagnostics/static-serve) — intentionally kept inline
10221
- // notemd inline block (~1,147 lines) is 100% registry-covered (30 registry routes > 16 inline).
10222
- // Pending safe deletion after integration test; set NOTE_CONNECTION_STRICT_REGISTRY=1 to skip inline.
10450
+ totalInlineRoutes: [...serverOwnedApiPaths].filter(pathname => !allRoutes.some(route => route.path === pathname)).length,
10451
+ dispatchMode: ROUTE_DISPATCH_MODE,
10223
10452
  registryHits: () => routeRegistryHits,
10224
10453
  inlineFallbacks: () => routeInlineFallbacks,
10225
10454
  registryHitRate: () => {
@@ -10227,9 +10456,9 @@ const startServer = async (options = {}) => {
10227
10456
  return total === 0 ? '0.0%' : (routeRegistryHits / total * 100).toFixed(1) + '%';
10228
10457
  },
10229
10458
  migrationProgress: () => {
10230
- const covered = allRoutes.length;
10231
- const total = covered + 7; // 7 terminal routes intentionally kept inline
10232
- return (covered / total * 100).toFixed(1) + '%';
10459
+ const registeredPaths = new Set(allRoutes.map(route => route.path));
10460
+ const supportedPaths = new Set([...registeredPaths, ...serverOwnedApiPaths]);
10461
+ return (registeredPaths.size / supportedPaths.size * 100).toFixed(1) + '%';
10233
10462
  },
10234
10463
  };
10235
10464
  const server = http.createServer(async (req, res) => {
@@ -10285,113 +10514,57 @@ const startServer = async (options = {}) => {
10285
10514
  res.end(JSON.stringify({ error: 'Unauthorized sidecar request.' }));
10286
10515
  return;
10287
10516
  }
10288
- // Route Registry Dispatch: try modular routes first, fall through to legacy inline chain
10289
- const methodMap = routeMap.get(req.method || 'GET');
10290
- if (methodMap) {
10291
- const exactRoute = methodMap.get(requestPath);
10292
- if (exactRoute) {
10293
- routeRegistryHits++;
10294
- await exactRoute.handler(req, res, routeContext);
10295
- return;
10517
+ const methodMap = routeMap.get(requestMethod);
10518
+ const registeredRoute = methodMap?.get(requestPath)
10519
+ || prefixRoutes.find(route => requestPath.startsWith(route.path) && route.method === requestMethod);
10520
+ if (registeredRoute && (USE_REGISTRY_DISPATCH || requestPath.startsWith('/api/notemd/'))) {
10521
+ routeRegistryHits++;
10522
+ try {
10523
+ await registeredRoute.handler(req, res, routeContext);
10296
10524
  }
10297
- for (const prefixRoute of prefixRoutes) {
10298
- if (requestPath.startsWith(prefixRoute.path) && prefixRoute.method === (req.method || 'GET')) {
10299
- routeRegistryHits++;
10300
- await prefixRoute.handler(req, res, routeContext);
10301
- return;
10302
- }
10525
+ catch (error) {
10526
+ writeApiErrorResponse(res, error, { context: requestMethod + ' ' + requestPath, requestId });
10303
10527
  }
10528
+ return;
10529
+ }
10530
+ if (requestPath.startsWith('/api/notemd/')) {
10531
+ res.writeHead(404, { 'Content-Type': 'application/json' });
10532
+ res.end(JSON.stringify({ success: false, error: 'NoteMD route not found' }));
10533
+ return;
10304
10534
  }
10305
- if (requestPath.startsWith('/api/')) {
10535
+ if (serverOwnedApiPaths.has(requestPath))
10536
+ intentionalInlineHits++;
10537
+ else if (requestPath.startsWith('/api/')) {
10306
10538
  routeInlineFallbacks++;
10539
+ const family = requestPath.split('/')[2] || 'unknown';
10540
+ inlineFallbacksByFamily[family] = (inlineFallbacksByFamily[family] || 0) + 1;
10307
10541
  }
10308
- // ── Inline Chain (Legacy) ──────────────────────────────────
10309
- // Route distribution:
10310
- // Registry-covered: ~80 routes (knowledge:36G+28P, notemd:2G+16P, + data/render)
10311
- // Terminal inline: ~7 routes intentionally kept inline:
10312
- // - runtime-diagnostics, runtime-request-trace (meta: report server state)
10313
- // - static file serving (requires FRONTEND_DIR + MIME resolution)
10314
- // - graph asset serving (requires cliOptions + generated asset paths)
10315
- // - generated asset resolution (requires CLI build output paths)
10316
- // These are terminal — they require deep server state that can't be
10317
- // cleanly exposed through ServerContext without circular dependencies.
10318
- // Total: ~105 route patterns
10319
- //
10320
- // The registry dispatch (above) intercepts covered routes first.
10321
- // Tag [REGISTRY_COVERED] marks routes already handled by registry.
10322
- // Migration metrics: GET /api/runtime-diagnostics → routeMigration
10542
+ // Server-owned operations retain their validation, state and native I/O boundaries.
10323
10543
  if (req.method === 'GET') {
10324
10544
  const getPathname = getRawRequestPathname(req.url);
10325
- // ── Notemd routes (covered by routes/notemd.ts) ──
10326
- // [REGISTRY_COVERED: routes/notemd.ts]
10327
- if (STRICT_REGISTRY && getPathname.startsWith('/api/notemd/')) {
10328
- res.writeHead(501, { 'Content-Type': 'application/json' });
10329
- res.end(JSON.stringify({ error: 'strict_registry: route must be handled by modular registry', path: getPathname }));
10330
- routeInlineFallbacks++;
10545
+ if (getPathname === '/api/knowledge/conversation/turn-cache/diagnostics') {
10546
+ res.writeHead(200, { 'Content-Type': 'application/json' });
10547
+ res.end(JSON.stringify({ success: true, result: getAgentConversationTurnCacheDiagnostics() }));
10331
10548
  return;
10332
10549
  }
10333
- if (getPathname === '/api/notemd/settings') {
10334
- try {
10335
- const settings = await loadNotemdSettings();
10336
- const activeOperationCount = Array.from(NOTEMD_ACTIVE_OPERATIONS.values()).filter((operation) => operation.status === 'running').length;
10337
- res.writeHead(200, { 'Content-Type': 'application/json' });
10338
- res.end(JSON.stringify({
10339
- success: true,
10340
- settings,
10341
- operationSummary: {
10342
- total: NOTEMD_ACTIVE_OPERATIONS.size,
10343
- running: activeOperationCount,
10344
- },
10345
- }));
10346
- }
10347
- catch (error) {
10348
- writeApiErrorResponse(res, error, {
10349
- context: 'API:GET /api/notemd/settings',
10350
- requestId,
10351
- });
10352
- }
10550
+ if (getPathname === '/api/knowledge/conversation/turn-cache/diagnostics/trend') {
10551
+ getAgentConversationTurnCacheDiagnostics();
10552
+ const request = normalizeAgentConversationTurnCacheAlertTrendRequestFromQuery(new URL(req.url, `http://${LOOPBACK_HOST}`).searchParams);
10553
+ const result = queryAgentConversationTurnCacheAlertTrend(request);
10554
+ appendRuntimeRunbookVerificationHistoryFromConversationTurnCacheAlertTrend(result);
10555
+ res.writeHead(200, { 'Content-Type': 'application/json' });
10556
+ res.end(JSON.stringify({ success: true, result }));
10353
10557
  return;
10354
10558
  }
10355
- if (getPathname === '/api/notemd/provider-templates') {
10356
- try {
10357
- const urlObj = new URL(req.url || '/', `http://${LOOPBACK_HOST}:${finalPort}`);
10358
- const persistTemplates = ['1', 'true', 'yes'].includes(String(urlObj.searchParams.get('persist') || '').trim().toLowerCase());
10359
- let persistence = { configPath: (0, AppConfigToml_1.resolveAppConfigPath)(), persisted: false };
10360
- if (persistTemplates) {
10361
- persistence = await ensureNotemdProviderTemplatesPersisted();
10362
- }
10363
- res.writeHead(200, { 'Content-Type': 'application/json' });
10364
- res.end(JSON.stringify({
10365
- success: true,
10366
- templates: providerTemplates_1.NOTEMD_PROVIDER_TEMPLATES,
10367
- configPath: persistence.configPath,
10368
- persisted: persistence.persisted,
10369
- }));
10370
- }
10371
- catch (error) {
10372
- writeApiErrorResponse(res, error, {
10373
- context: 'API:GET /api/notemd/provider-templates',
10374
- requestId,
10375
- });
10376
- }
10559
+ if (getPathname === '/api/knowledge/conversation/turn-cache/diagnostics/trend/index') {
10560
+ res.writeHead(200, { 'Content-Type': 'application/json' });
10561
+ res.end(JSON.stringify({ success: true, result: snapshotAgentConversationTurnCacheAlertHistoryStorageIndex() }));
10377
10562
  return;
10378
10563
  }
10379
- if (getPathname === '/api/notemd/workspace') {
10380
- try {
10381
- const settings = await loadNotemdSettings();
10382
- const workspace = extractNotemdWorkspaceState(settings);
10383
- res.writeHead(200, { 'Content-Type': 'application/json' });
10384
- res.end(JSON.stringify({
10385
- success: true,
10386
- workspace,
10387
- }));
10388
- }
10389
- catch (error) {
10390
- writeApiErrorResponse(res, error, {
10391
- context: 'API:GET /api/notemd/workspace',
10392
- requestId,
10393
- });
10394
- }
10564
+ if (getPathname === '/api/knowledge/conversation/turn-cache/diagnostics/trend/export') {
10565
+ const request = normalizeAgentConversationTurnCacheAlertTrendRequestFromQuery(new URL(req.url, `http://${LOOPBACK_HOST}`).searchParams);
10566
+ res.writeHead(200, { 'Content-Type': 'application/json' });
10567
+ res.end(JSON.stringify({ success: true, result: queryAgentConversationTurnCacheAlertTrendExport(request) }));
10395
10568
  return;
10396
10569
  }
10397
10570
  if (getPathname === '/api/path-mode/settings') {
@@ -10467,6 +10640,9 @@ const startServer = async (options = {}) => {
10467
10640
  res.writeHead(200, { 'Content-Type': 'application/json' });
10468
10641
  res.end(JSON.stringify({
10469
10642
  runtime: {
10643
+ nodeVersion: process.version,
10644
+ platform: process.platform,
10645
+ arch: process.arch,
10470
10646
  host: LOOPBACK_HOST,
10471
10647
  port: runtimePort,
10472
10648
  bridgePort: effectivePathBridgePort,
@@ -10515,10 +10691,13 @@ const startServer = async (options = {}) => {
10515
10691
  totalModularRoutes: routeMigrationStats.totalModularRoutes,
10516
10692
  registryHits: routeMigrationStats.registryHits(),
10517
10693
  inlineFallbacks: routeMigrationStats.inlineFallbacks(),
10694
+ intentionalInlineHits,
10695
+ inlineFallbacksByFamily: { ...inlineFallbacksByFamily },
10518
10696
  registryHitRate: routeMigrationStats.registryHitRate(),
10519
10697
  migrationProgress: routeMigrationStats.migrationProgress(),
10520
10698
  modularRoutes: routeMigrationStats.totalModularRoutes,
10521
10699
  inlineOnlyRoutes: routeMigrationStats.totalInlineRoutes,
10700
+ dispatchMode: routeMigrationStats.dispatchMode,
10522
10701
  },
10523
10702
  domains: {
10524
10703
  ingest: knowledgeIngestor.getDiagnostics(),
@@ -11006,24 +11185,12 @@ const startServer = async (options = {}) => {
11006
11185
  if (postPathname === '/api/knowledge/query-backend-config') {
11007
11186
  try {
11008
11187
  const payload = await readJsonBody(req);
11009
- const requestPayload = normalizeQueryBackendConfigRequestPayload(payload);
11010
- const result = await knowledgeLearningPlatform.updateQueryBackendConfig(requestPayload);
11011
- ACTIVE_KNOWLEDGE_QUERY_BACKEND = result.configuredBackend;
11012
- const baseDiagnostics = knowledgeLearningPlatform.getQueryBackendDiagnostics();
11013
- const graphvizRuntimeAvailability = await getGraphvizDotRuntimeAvailability();
11014
- const diagnostics = enrichQueryBackendDiagnosticsWithRendererRuntime(baseDiagnostics, graphvizRuntimeAvailability);
11015
- const rolloutProfile = buildKnowledgeRuntimeRolloutProfile();
11188
+ const responsePayload = await routeContext.executeQueryBackendConfigUpdate?.(payload);
11189
+ if (!responsePayload) {
11190
+ throw new Error('Query backend config operation is unavailable.');
11191
+ }
11016
11192
  res.writeHead(200, { 'Content-Type': 'application/json' });
11017
- res.end(JSON.stringify({
11018
- success: true,
11019
- result,
11020
- diagnostics,
11021
- configuredVectorAccelerationProvider: QUERY_VECTOR_ACCELERATION_PROVIDER,
11022
- configuredVectorAccelerationFailureMode: QUERY_VECTOR_ACCELERATION_FAILURE_MODE,
11023
- configuredVectorAccelerationRepresentationStrict: QUERY_VECTOR_ACCELERATION_REPRESENTATION_STRICT_ENABLED,
11024
- queryVectorAnnPrefilterEnabled: QUERY_VECTOR_ANN_PREFILTER_ENABLED,
11025
- rolloutProfile,
11026
- }));
11193
+ res.end(JSON.stringify({ success: true, ...responsePayload }));
11027
11194
  }
11028
11195
  catch (error) {
11029
11196
  writeApiErrorResponse(res, error, {
@@ -11069,13 +11236,15 @@ const startServer = async (options = {}) => {
11069
11236
  return;
11070
11237
  }
11071
11238
  if (postPathname === '/api/knowledge/conversation') {
11239
+ let releaseClient;
11072
11240
  try {
11073
11241
  const payload = await readJsonBody(req);
11074
- const requestPayload = normalizeAgentConversationRequestPayload(payload);
11242
+ const requestPayload = (0, requestNormalization_1.normalizeAgentConversationRequestPayload)(payload);
11075
11243
  const streamRequested = requestAcceptsEventStream(req);
11076
11244
  const turnId = resolveAgentConversationTurnId(req, requestId);
11077
11245
  const requestFingerprint = buildAgentConversationRequestFingerprint(requestPayload);
11078
11246
  const turnRecord = getOrCreateAgentConversationTurnCacheRecord(turnId, requestFingerprint);
11247
+ releaseClient = attachAgentConversationClient(turnRecord, req, res);
11079
11248
  if (!streamRequested) {
11080
11249
  const wasCompletedBeforeExecution = turnRecord.status === 'completed';
11081
11250
  await ensureAgentConversationTurnExecution(turnRecord, requestPayload);
@@ -11086,7 +11255,10 @@ const startServer = async (options = {}) => {
11086
11255
  res.setHeader('X-Agent-Conversation-Turn-Id', turnId);
11087
11256
  res.setHeader('X-Agent-Conversation-Replay', wasCompletedBeforeExecution ? 'hit' : 'miss');
11088
11257
  res.writeHead(200, { 'Content-Type': 'application/json' });
11089
- res.end(JSON.stringify({ success: true, result: turnRecord.result }));
11258
+ const responseResult = requestPayload.responseProfile === 'mobile_compact'
11259
+ ? buildMobileAgentConversationResponse(turnRecord.result)
11260
+ : turnRecord.result;
11261
+ res.end((0, agentConversationSerialization_1.serializeAgentConversationHttpResponse)(responseResult).json);
11090
11262
  return;
11091
11263
  }
11092
11264
  throwAgentConversationCachedFailure(turnRecord.failure);
@@ -11118,15 +11290,15 @@ const startServer = async (options = {}) => {
11118
11290
  await new Promise((resolve) => setImmediate(resolve));
11119
11291
  await ensureAgentConversationTurnExecution(turnRecord, requestPayload, {
11120
11292
  emitLiveEvent: (event) => {
11121
- writeSseEvent(res, event.type, event);
11293
+ writeSseEvent(res, event.type, projectAgentConversationTurnEvent(event, requestPayload));
11122
11294
  },
11123
11295
  });
11124
11296
  }
11125
11297
  else {
11126
11298
  await ensureAgentConversationTurnExecution(turnRecord, requestPayload);
11127
- replayAgentConversationTurnEvents(res, turnRecord);
11299
+ replayAgentConversationTurnEvents(res, turnRecord, requestPayload);
11128
11300
  }
11129
- res.end();
11301
+ await finishSseResponse(res);
11130
11302
  }
11131
11303
  catch (error) {
11132
11304
  writeApiErrorResponse(res, error, {
@@ -11135,6 +11307,9 @@ const startServer = async (options = {}) => {
11135
11307
  enableValidationStatus: true,
11136
11308
  });
11137
11309
  }
11310
+ finally {
11311
+ releaseClient?.();
11312
+ }
11138
11313
  return;
11139
11314
  }
11140
11315
  if (postPathname === '/api/knowledge/mastery/diagnose') {
@@ -11453,125 +11628,43 @@ const startServer = async (options = {}) => {
11453
11628
  }
11454
11629
  return;
11455
11630
  }
11456
- // ── Notemd POST routes (covered by routes/notemd.ts) ──
11457
- // [REGISTRY_COVERED: routes/notemd.ts]
11458
- if (STRICT_REGISTRY && postPathname.startsWith('/api/notemd/')) {
11459
- res.writeHead(501, { 'Content-Type': 'application/json' });
11460
- res.end(JSON.stringify({ error: 'strict_registry: route must be handled by modular registry', path: postPathname }));
11461
- routeInlineFallbacks++;
11462
- return;
11463
- }
11464
- if (postPathname === '/api/notemd/settings') {
11631
+ if (postPathname === '/api/path-mode/settings') {
11465
11632
  try {
11466
11633
  const payload = await readJsonBody(req);
11467
11634
  const settingsCandidate = isObjectRecord(payload) && payload.settings !== undefined
11468
11635
  ? payload.settings
11469
11636
  : payload;
11470
- const settings = await persistNotemdSettings(settingsCandidate);
11637
+ const settings = await persistPathModeSettings(settingsCandidate);
11471
11638
  res.writeHead(200, { 'Content-Type': 'application/json' });
11472
11639
  res.end(JSON.stringify({ success: true, settings }));
11473
11640
  }
11474
11641
  catch (error) {
11475
11642
  writeApiErrorResponse(res, error, {
11476
- context: 'API:POST /api/notemd/settings',
11643
+ context: 'API:POST /api/path-mode/settings',
11477
11644
  requestId,
11478
11645
  });
11479
11646
  }
11480
11647
  return;
11481
11648
  }
11482
- if (postPathname === '/api/notemd/provider-templates/apply') {
11649
+ if (postPathname === '/api/frontend/settings') {
11483
11650
  try {
11484
11651
  const payload = await readJsonBody(req);
11485
- const rawTemplateId = isObjectRecord(payload)
11486
- ? String(payload.templateId || payload.template_id || '').trim()
11487
- : '';
11488
- if (!rawTemplateId) {
11489
- res.writeHead(400, { 'Content-Type': 'application/json' });
11490
- res.end(JSON.stringify({ success: false, error: 'Missing templateId.' }));
11491
- return;
11492
- }
11493
- const template = (0, providerTemplates_1.getNotemdProviderTemplate)(rawTemplateId);
11494
- if (!template) {
11495
- res.writeHead(404, { 'Content-Type': 'application/json' });
11496
- res.end(JSON.stringify({ success: false, error: `Unknown provider template: ${rawTemplateId}` }));
11497
- return;
11498
- }
11499
- const settings = await loadNotemdSettings();
11500
- const updatedSettings = (0, providerTemplates_1.applyProviderTemplateToSettings)(settings, rawTemplateId);
11501
- const persistedSettings = await persistNotemdSettings(updatedSettings);
11502
- const persistence = await ensureNotemdProviderTemplatesPersisted();
11652
+ const settingsCandidate = isObjectRecord(payload) && payload.settings !== undefined
11653
+ ? payload.settings
11654
+ : payload;
11655
+ const settings = await persistFrontendSettings(settingsCandidate);
11503
11656
  res.writeHead(200, { 'Content-Type': 'application/json' });
11504
- res.end(JSON.stringify({
11505
- success: true,
11506
- template,
11507
- settings: persistedSettings,
11508
- configPath: persistence.configPath,
11509
- persistedTemplates: persistence.persisted,
11510
- }));
11657
+ res.end(JSON.stringify({ success: true, settings }));
11511
11658
  }
11512
11659
  catch (error) {
11513
11660
  writeApiErrorResponse(res, error, {
11514
- context: 'API:POST /api/notemd/provider-templates/apply',
11661
+ context: 'API:POST /api/frontend/settings',
11515
11662
  requestId,
11516
11663
  });
11517
11664
  }
11518
11665
  return;
11519
11666
  }
11520
- if (postPathname === '/api/notemd/workspace') {
11521
- try {
11522
- const payload = await readJsonBody(req);
11523
- const workspaceCandidate = isObjectRecord(payload) && payload.workspace !== undefined
11524
- ? payload.workspace
11525
- : payload;
11526
- const workspace = await persistNotemdWorkspacePatch(workspaceCandidate);
11527
- res.writeHead(200, { 'Content-Type': 'application/json' });
11528
- res.end(JSON.stringify({ success: true, workspace }));
11529
- }
11530
- catch (error) {
11531
- writeApiErrorResponse(res, error, {
11532
- context: 'API:POST /api/notemd/workspace',
11533
- requestId,
11534
- });
11535
- }
11536
- return;
11537
- }
11538
- if (postPathname === '/api/path-mode/settings') {
11539
- try {
11540
- const payload = await readJsonBody(req);
11541
- const settingsCandidate = isObjectRecord(payload) && payload.settings !== undefined
11542
- ? payload.settings
11543
- : payload;
11544
- const settings = await persistPathModeSettings(settingsCandidate);
11545
- res.writeHead(200, { 'Content-Type': 'application/json' });
11546
- res.end(JSON.stringify({ success: true, settings }));
11547
- }
11548
- catch (error) {
11549
- writeApiErrorResponse(res, error, {
11550
- context: 'API:POST /api/path-mode/settings',
11551
- requestId,
11552
- });
11553
- }
11554
- return;
11555
- }
11556
- if (postPathname === '/api/frontend/settings') {
11557
- try {
11558
- const payload = await readJsonBody(req);
11559
- const settingsCandidate = isObjectRecord(payload) && payload.settings !== undefined
11560
- ? payload.settings
11561
- : payload;
11562
- const settings = await persistFrontendSettings(settingsCandidate);
11563
- res.writeHead(200, { 'Content-Type': 'application/json' });
11564
- res.end(JSON.stringify({ success: true, settings }));
11565
- }
11566
- catch (error) {
11567
- writeApiErrorResponse(res, error, {
11568
- context: 'API:POST /api/frontend/settings',
11569
- requestId,
11570
- });
11571
- }
11572
- return;
11573
- }
11574
- if (postPathname === '/api/markdown/index') {
11667
+ if (postPathname === '/api/markdown/index') {
11575
11668
  try {
11576
11669
  const payload = await readJsonBody(req);
11577
11670
  const requestBody = isObjectRecord(payload) ? payload : {};
@@ -11789,645 +11882,6 @@ const startServer = async (options = {}) => {
11789
11882
  }
11790
11883
  return;
11791
11884
  }
11792
- if (postPathname === '/api/notemd/cancel') {
11793
- try {
11794
- const payload = await readJsonBody(req);
11795
- const operationId = String(payload.operationId || '').trim();
11796
- if (!operationId) {
11797
- res.writeHead(400, { 'Content-Type': 'application/json' });
11798
- res.end(JSON.stringify({ success: false, error: 'Missing operationId' }));
11799
- return;
11800
- }
11801
- const operation = NOTEMD_ACTIVE_OPERATIONS.get(operationId);
11802
- if (!operation) {
11803
- res.writeHead(404, { 'Content-Type': 'application/json' });
11804
- res.end(JSON.stringify({ success: false, error: 'Operation not found' }));
11805
- return;
11806
- }
11807
- if (operation.status !== 'running') {
11808
- res.writeHead(200, { 'Content-Type': 'application/json' });
11809
- res.end(JSON.stringify({
11810
- success: false,
11811
- operationId,
11812
- status: operation.status,
11813
- message: 'Operation is not running.',
11814
- }));
11815
- return;
11816
- }
11817
- operation.controller.abort();
11818
- finalizeNotemdOperation(operation, 'cancelled');
11819
- res.writeHead(200, { 'Content-Type': 'application/json' });
11820
- res.end(JSON.stringify({ success: true, operationId, status: 'cancelled' }));
11821
- }
11822
- catch (error) {
11823
- if (writeBodyParseErrorResponse(res, error)) {
11824
- return;
11825
- }
11826
- console.error(error);
11827
- CrashLogger_1.CrashLogger.log(error, 'API:POST /api/notemd/cancel');
11828
- res.writeHead(500, { 'Content-Type': 'application/json' });
11829
- res.end(JSON.stringify({ success: false, error: String(error) }));
11830
- }
11831
- return;
11832
- }
11833
- if (postPathname === '/api/notemd/test-llm') {
11834
- try {
11835
- const payload = await readJsonBody(req);
11836
- const settings = await loadNotemdSettings();
11837
- const providerName = String(payload.providerName || settings.activeProvider).trim();
11838
- const provider = settings.providers.find((item) => item.name === providerName);
11839
- if (!provider) {
11840
- res.writeHead(400, { 'Content-Type': 'application/json' });
11841
- res.end(JSON.stringify({ success: false, error: `Unknown provider: ${providerName}` }));
11842
- return;
11843
- }
11844
- const result = await notemdLlmClient.testConnection(provider);
11845
- const statusCode = result.success ? 200 : 400;
11846
- res.writeHead(statusCode, { 'Content-Type': 'application/json' });
11847
- res.end(JSON.stringify(result));
11848
- }
11849
- catch (error) {
11850
- if (writeBodyParseErrorResponse(res, error)) {
11851
- return;
11852
- }
11853
- console.error(error);
11854
- CrashLogger_1.CrashLogger.log(error, 'API:POST /api/notemd/test-llm');
11855
- res.writeHead(500, { 'Content-Type': 'application/json' });
11856
- res.end(JSON.stringify({ success: false, error: String(error) }));
11857
- }
11858
- return;
11859
- }
11860
- if (postPathname === '/api/notemd/process-file') {
11861
- const streamEnabled = shouldStreamNotemdResponse(req);
11862
- let operation = null;
11863
- try {
11864
- const payload = await readJsonBody(req);
11865
- const settings = await loadNotemdSettings();
11866
- operation = createNotemdOperation(payload.operationId);
11867
- const reporter = createNotemdReporter(operation, streamEnabled ? res : undefined);
11868
- if (streamEnabled) {
11869
- res.writeHead(200, {
11870
- 'Content-Type': 'text/event-stream',
11871
- 'Cache-Control': 'no-cache',
11872
- Connection: 'keep-alive',
11873
- });
11874
- writeSseEvent(res, 'operation', {
11875
- operationId: operation.id,
11876
- status: operation.status,
11877
- });
11878
- }
11879
- const resolvedFilePath = await resolvePathWithinKnowledgeBase(payload.filePath, {
11880
- expectedType: 'file',
11881
- });
11882
- const resolvedOutputPath = payload.outputPath
11883
- ? await resolvePathWithinKnowledgeBase(payload.outputPath, {
11884
- expectedType: 'any',
11885
- allowMissing: true,
11886
- })
11887
- : undefined;
11888
- const result = await notemdService.processFile({
11889
- filePath: resolvedFilePath,
11890
- outputPath: resolvedOutputPath,
11891
- createConceptNotes: payload.createConceptNotes === true,
11892
- dryRun: payload.dryRun === true,
11893
- }, settings, reporter, operation.controller.signal);
11894
- void persistNotemdWorkspacePatch({
11895
- filePath: resolvedFilePath,
11896
- folderPath: path.dirname(resolvedFilePath),
11897
- outputFilePath: resolvedOutputPath || result.outputPath || '',
11898
- outputFolderPath: path.dirname(resolvedOutputPath || result.outputPath || resolvedFilePath),
11899
- }).catch((workspaceError) => {
11900
- warnDiagnostic('[NoteMD] Failed to persist workspace state after process-file.', workspaceError);
11901
- });
11902
- finalizeNotemdOperation(operation, 'done');
11903
- if (streamEnabled) {
11904
- writeSseEvent(res, 'done', {
11905
- success: true,
11906
- operationId: operation.id,
11907
- result,
11908
- });
11909
- res.end();
11910
- }
11911
- else {
11912
- res.writeHead(200, { 'Content-Type': 'application/json' });
11913
- res.end(JSON.stringify({
11914
- success: true,
11915
- operationId: operation.id,
11916
- result,
11917
- logs: operation.logs,
11918
- }));
11919
- }
11920
- }
11921
- catch (error) {
11922
- if (operation) {
11923
- finalizeNotemdOperation(operation, operation.controller.signal.aborted ? 'cancelled' : 'error');
11924
- }
11925
- if (writeBodyParseErrorResponse(res, error)) {
11926
- return;
11927
- }
11928
- if (isAccessDeniedError(error)) {
11929
- const statusCode = operation?.controller.signal.aborted ? 499 : 403;
11930
- const payload = { success: false, error: String(error.message || 'Access denied') };
11931
- if (streamEnabled) {
11932
- writeSseEvent(res, 'error', payload);
11933
- res.end();
11934
- }
11935
- else {
11936
- res.writeHead(statusCode, { 'Content-Type': 'application/json' });
11937
- res.end(JSON.stringify(payload));
11938
- }
11939
- return;
11940
- }
11941
- console.error(error);
11942
- CrashLogger_1.CrashLogger.log(error, 'API:POST /api/notemd/process-file');
11943
- const payload = { success: false, error: String(error) };
11944
- if (streamEnabled) {
11945
- writeSseEvent(res, 'error', payload);
11946
- res.end();
11947
- }
11948
- else {
11949
- res.writeHead(500, { 'Content-Type': 'application/json' });
11950
- res.end(JSON.stringify(payload));
11951
- }
11952
- }
11953
- return;
11954
- }
11955
- if (postPathname === '/api/notemd/process-folder') {
11956
- const streamEnabled = shouldStreamNotemdResponse(req);
11957
- let operation = null;
11958
- try {
11959
- const payload = await readJsonBody(req);
11960
- const settings = await loadNotemdSettings();
11961
- operation = createNotemdOperation(payload.operationId);
11962
- const reporter = createNotemdReporter(operation, streamEnabled ? res : undefined);
11963
- if (streamEnabled) {
11964
- res.writeHead(200, {
11965
- 'Content-Type': 'text/event-stream',
11966
- 'Cache-Control': 'no-cache',
11967
- Connection: 'keep-alive',
11968
- });
11969
- writeSseEvent(res, 'operation', {
11970
- operationId: operation.id,
11971
- status: operation.status,
11972
- });
11973
- }
11974
- const resolvedFolderPath = await resolvePathWithinKnowledgeBase(payload.folderPath, {
11975
- expectedType: 'directory',
11976
- });
11977
- const resolvedOutputFolderPath = payload.outputFolderPath
11978
- ? await resolvePathWithinKnowledgeBase(payload.outputFolderPath, {
11979
- expectedType: 'any',
11980
- allowMissing: true,
11981
- })
11982
- : undefined;
11983
- const result = await notemdService.processFolder({
11984
- folderPath: resolvedFolderPath,
11985
- outputFolderPath: resolvedOutputFolderPath,
11986
- createConceptNotes: payload.createConceptNotes === true,
11987
- dryRun: payload.dryRun === true,
11988
- }, settings, reporter, operation.controller.signal);
11989
- void persistNotemdWorkspacePatch({
11990
- folderPath: resolvedFolderPath,
11991
- outputFolderPath: resolvedOutputFolderPath || '',
11992
- }).catch((workspaceError) => {
11993
- warnDiagnostic('[NoteMD] Failed to persist workspace state after process-folder.', workspaceError);
11994
- });
11995
- finalizeNotemdOperation(operation, 'done');
11996
- if (streamEnabled) {
11997
- writeSseEvent(res, 'done', {
11998
- success: true,
11999
- operationId: operation.id,
12000
- result,
12001
- });
12002
- res.end();
12003
- }
12004
- else {
12005
- res.writeHead(200, { 'Content-Type': 'application/json' });
12006
- res.end(JSON.stringify({
12007
- success: true,
12008
- operationId: operation.id,
12009
- result,
12010
- logs: operation.logs,
12011
- }));
12012
- }
12013
- }
12014
- catch (error) {
12015
- if (operation) {
12016
- finalizeNotemdOperation(operation, operation.controller.signal.aborted ? 'cancelled' : 'error');
12017
- }
12018
- if (writeBodyParseErrorResponse(res, error)) {
12019
- return;
12020
- }
12021
- if (isAccessDeniedError(error)) {
12022
- const statusCode = operation?.controller.signal.aborted ? 499 : 403;
12023
- const payload = { success: false, error: String(error.message || 'Access denied') };
12024
- if (streamEnabled) {
12025
- writeSseEvent(res, 'error', payload);
12026
- res.end();
12027
- }
12028
- else {
12029
- res.writeHead(statusCode, { 'Content-Type': 'application/json' });
12030
- res.end(JSON.stringify(payload));
12031
- }
12032
- return;
12033
- }
12034
- console.error(error);
12035
- CrashLogger_1.CrashLogger.log(error, 'API:POST /api/notemd/process-folder');
12036
- const payload = { success: false, error: String(error) };
12037
- if (streamEnabled) {
12038
- writeSseEvent(res, 'error', payload);
12039
- res.end();
12040
- }
12041
- else {
12042
- res.writeHead(500, { 'Content-Type': 'application/json' });
12043
- res.end(JSON.stringify(payload));
12044
- }
12045
- }
12046
- return;
12047
- }
12048
- if (postPathname === '/api/notemd/generate-content') {
12049
- try {
12050
- const payload = await readJsonBody(req);
12051
- const settings = await loadNotemdSettings();
12052
- let title = String(payload.title || '').trim();
12053
- const filePathCandidate = String(payload.filePath || '').trim();
12054
- if (!title && filePathCandidate) {
12055
- title = path.basename(filePathCandidate, path.extname(filePathCandidate));
12056
- }
12057
- if (!title) {
12058
- res.writeHead(400, { 'Content-Type': 'application/json' });
12059
- res.end(JSON.stringify({ success: false, error: 'Missing title or filePath' }));
12060
- return;
12061
- }
12062
- const content = await notemdService.generateContent(title, typeof payload.context === 'string' ? payload.context : undefined, settings);
12063
- let outputPath = null;
12064
- if (payload.outputPath) {
12065
- outputPath = await resolvePathWithinKnowledgeBase(payload.outputPath, {
12066
- expectedType: 'any',
12067
- allowMissing: true,
12068
- });
12069
- }
12070
- else if (filePathCandidate) {
12071
- outputPath = await resolvePathWithinKnowledgeBase(filePathCandidate, {
12072
- expectedType: 'any',
12073
- allowMissing: true,
12074
- });
12075
- }
12076
- if (outputPath) {
12077
- await fs.promises.mkdir(path.dirname(outputPath), { recursive: true });
12078
- await fs.promises.writeFile(outputPath, content, 'utf8');
12079
- }
12080
- res.writeHead(200, { 'Content-Type': 'application/json' });
12081
- res.end(JSON.stringify({
12082
- success: true,
12083
- title,
12084
- outputPath,
12085
- content,
12086
- }));
12087
- }
12088
- catch (error) {
12089
- if (writeBodyParseErrorResponse(res, error)) {
12090
- return;
12091
- }
12092
- if (isAccessDeniedError(error)) {
12093
- res.writeHead(403, { 'Content-Type': 'application/json' });
12094
- res.end(JSON.stringify({ success: false, error: String(error.message || 'Access denied') }));
12095
- return;
12096
- }
12097
- console.error(error);
12098
- CrashLogger_1.CrashLogger.log(error, 'API:POST /api/notemd/generate-content');
12099
- res.writeHead(500, { 'Content-Type': 'application/json' });
12100
- res.end(JSON.stringify({ success: false, error: String(error) }));
12101
- }
12102
- return;
12103
- }
12104
- if (postPathname === '/api/notemd/translate-file') {
12105
- try {
12106
- const payload = await readJsonBody(req);
12107
- const settings = await loadNotemdSettings();
12108
- const resolvedFilePath = await resolvePathWithinKnowledgeBase(payload.filePath, {
12109
- expectedType: 'file',
12110
- });
12111
- const resolvedOutputPath = payload.outputPath
12112
- ? await resolvePathWithinKnowledgeBase(payload.outputPath, {
12113
- expectedType: 'any',
12114
- allowMissing: true,
12115
- })
12116
- : undefined;
12117
- const targetLanguage = String(payload.targetLanguage || settings.translateLanguage || settings.language).trim();
12118
- if (!targetLanguage) {
12119
- res.writeHead(400, { 'Content-Type': 'application/json' });
12120
- res.end(JSON.stringify({ success: false, error: 'Missing targetLanguage' }));
12121
- return;
12122
- }
12123
- const result = await notemdService.translateFile({
12124
- filePath: resolvedFilePath,
12125
- outputPath: resolvedOutputPath,
12126
- targetLanguage,
12127
- }, settings);
12128
- res.writeHead(200, { 'Content-Type': 'application/json' });
12129
- res.end(JSON.stringify({ success: true, result }));
12130
- }
12131
- catch (error) {
12132
- if (writeBodyParseErrorResponse(res, error)) {
12133
- return;
12134
- }
12135
- if (isAccessDeniedError(error)) {
12136
- res.writeHead(403, { 'Content-Type': 'application/json' });
12137
- res.end(JSON.stringify({ success: false, error: String(error.message || 'Access denied') }));
12138
- return;
12139
- }
12140
- console.error(error);
12141
- CrashLogger_1.CrashLogger.log(error, 'API:POST /api/notemd/translate-file');
12142
- res.writeHead(500, { 'Content-Type': 'application/json' });
12143
- res.end(JSON.stringify({ success: false, error: String(error) }));
12144
- }
12145
- return;
12146
- }
12147
- if (postPathname === '/api/notemd/translate-folder') {
12148
- try {
12149
- const payload = await readJsonBody(req);
12150
- const settings = await loadNotemdSettings();
12151
- const resolvedFolderPath = await resolvePathWithinKnowledgeBase(payload.folderPath, {
12152
- expectedType: 'directory',
12153
- });
12154
- const targetLanguage = String(payload.targetLanguage || settings.translateLanguage || settings.language).trim();
12155
- if (!targetLanguage) {
12156
- res.writeHead(400, { 'Content-Type': 'application/json' });
12157
- res.end(JSON.stringify({ success: false, error: 'Missing targetLanguage' }));
12158
- return;
12159
- }
12160
- const result = await notemdService.translateFolder(resolvedFolderPath, targetLanguage, settings);
12161
- res.writeHead(200, { 'Content-Type': 'application/json' });
12162
- res.end(JSON.stringify({ success: true, result }));
12163
- }
12164
- catch (error) {
12165
- if (writeBodyParseErrorResponse(res, error)) {
12166
- return;
12167
- }
12168
- if (isAccessDeniedError(error)) {
12169
- res.writeHead(403, { 'Content-Type': 'application/json' });
12170
- res.end(JSON.stringify({ success: false, error: String(error.message || 'Access denied') }));
12171
- return;
12172
- }
12173
- console.error(error);
12174
- CrashLogger_1.CrashLogger.log(error, 'API:POST /api/notemd/translate-folder');
12175
- res.writeHead(500, { 'Content-Type': 'application/json' });
12176
- res.end(JSON.stringify({ success: false, error: String(error) }));
12177
- }
12178
- return;
12179
- }
12180
- if (postPathname === '/api/notemd/fix-mermaid') {
12181
- try {
12182
- const payload = await readJsonBody(req);
12183
- const resolvedFilePath = await resolvePathWithinKnowledgeBase(payload.filePath, {
12184
- expectedType: 'file',
12185
- });
12186
- const inPlace = payload.inPlace !== false;
12187
- const result = await notemdService.fixMermaid(resolvedFilePath, inPlace);
12188
- res.writeHead(200, { 'Content-Type': 'application/json' });
12189
- res.end(JSON.stringify({ success: true, result }));
12190
- }
12191
- catch (error) {
12192
- if (writeBodyParseErrorResponse(res, error)) {
12193
- return;
12194
- }
12195
- if (isAccessDeniedError(error)) {
12196
- res.writeHead(403, { 'Content-Type': 'application/json' });
12197
- res.end(JSON.stringify({ success: false, error: String(error.message || 'Access denied') }));
12198
- return;
12199
- }
12200
- console.error(error);
12201
- CrashLogger_1.CrashLogger.log(error, 'API:POST /api/notemd/fix-mermaid');
12202
- res.writeHead(500, { 'Content-Type': 'application/json' });
12203
- res.end(JSON.stringify({ success: false, error: String(error) }));
12204
- }
12205
- return;
12206
- }
12207
- if (postPathname === '/api/notemd/fix-formulas') {
12208
- try {
12209
- const payload = await readJsonBody(req);
12210
- const resolvedFilePath = await resolvePathWithinKnowledgeBase(payload.filePath, {
12211
- expectedType: 'file',
12212
- });
12213
- const inPlace = payload.inPlace !== false;
12214
- const result = await notemdService.fixFormulas(resolvedFilePath, inPlace);
12215
- res.writeHead(200, { 'Content-Type': 'application/json' });
12216
- res.end(JSON.stringify({ success: true, result }));
12217
- }
12218
- catch (error) {
12219
- if (writeBodyParseErrorResponse(res, error)) {
12220
- return;
12221
- }
12222
- if (isAccessDeniedError(error)) {
12223
- res.writeHead(403, { 'Content-Type': 'application/json' });
12224
- res.end(JSON.stringify({ success: false, error: String(error.message || 'Access denied') }));
12225
- return;
12226
- }
12227
- console.error(error);
12228
- CrashLogger_1.CrashLogger.log(error, 'API:POST /api/notemd/fix-formulas');
12229
- res.writeHead(500, { 'Content-Type': 'application/json' });
12230
- res.end(JSON.stringify({ success: false, error: String(error) }));
12231
- }
12232
- return;
12233
- }
12234
- if (postPathname === '/api/notemd/check-duplicates') {
12235
- try {
12236
- const payload = await readJsonBody(req);
12237
- const resolvedFilePath = await resolvePathWithinKnowledgeBase(payload.filePath, {
12238
- expectedType: 'file',
12239
- });
12240
- const result = await notemdService.checkDuplicates(resolvedFilePath);
12241
- res.writeHead(200, { 'Content-Type': 'application/json' });
12242
- res.end(JSON.stringify({ success: true, result }));
12243
- }
12244
- catch (error) {
12245
- if (writeBodyParseErrorResponse(res, error)) {
12246
- return;
12247
- }
12248
- if (isAccessDeniedError(error)) {
12249
- res.writeHead(403, { 'Content-Type': 'application/json' });
12250
- res.end(JSON.stringify({ success: false, error: String(error.message || 'Access denied') }));
12251
- return;
12252
- }
12253
- console.error(error);
12254
- CrashLogger_1.CrashLogger.log(error, 'API:POST /api/notemd/check-duplicates');
12255
- res.writeHead(500, { 'Content-Type': 'application/json' });
12256
- res.end(JSON.stringify({ success: false, error: String(error) }));
12257
- }
12258
- return;
12259
- }
12260
- if (postPathname === '/api/notemd/extract-concepts') {
12261
- const streamEnabled = shouldStreamNotemdResponse(req);
12262
- let operation = null;
12263
- try {
12264
- const payload = await readJsonBody(req);
12265
- const settings = await loadNotemdSettings();
12266
- operation = createNotemdOperation(payload.operationId);
12267
- const reporter = createNotemdReporter(operation, streamEnabled ? res : undefined);
12268
- if (streamEnabled) {
12269
- res.writeHead(200, {
12270
- 'Content-Type': 'text/event-stream',
12271
- 'Cache-Control': 'no-cache',
12272
- Connection: 'keep-alive',
12273
- });
12274
- writeSseEvent(res, 'operation', {
12275
- operationId: operation.id,
12276
- status: operation.status,
12277
- });
12278
- }
12279
- const resolvedFilePath = await resolvePathWithinKnowledgeBase(payload.filePath, {
12280
- expectedType: 'file',
12281
- });
12282
- const result = await notemdService.extractConcepts(resolvedFilePath, settings, reporter, operation.controller.signal);
12283
- void persistNotemdWorkspacePatch({
12284
- filePath: resolvedFilePath,
12285
- folderPath: path.dirname(resolvedFilePath),
12286
- }).catch((workspaceError) => {
12287
- warnDiagnostic('[NoteMD] Failed to persist workspace state after extract-concepts.', workspaceError);
12288
- });
12289
- finalizeNotemdOperation(operation, 'done');
12290
- if (streamEnabled) {
12291
- writeSseEvent(res, 'done', {
12292
- success: true,
12293
- operationId: operation.id,
12294
- result,
12295
- });
12296
- res.end();
12297
- }
12298
- else {
12299
- res.writeHead(200, { 'Content-Type': 'application/json' });
12300
- res.end(JSON.stringify({ success: true, operationId: operation.id, result, logs: operation.logs }));
12301
- }
12302
- }
12303
- catch (error) {
12304
- if (operation) {
12305
- finalizeNotemdOperation(operation, operation.controller.signal.aborted ? 'cancelled' : 'error');
12306
- }
12307
- if (writeBodyParseErrorResponse(res, error)) {
12308
- return;
12309
- }
12310
- if (isAccessDeniedError(error)) {
12311
- const statusCode = operation?.controller.signal.aborted ? 499 : 403;
12312
- const payload = { success: false, error: String(error.message || 'Access denied') };
12313
- if (streamEnabled) {
12314
- writeSseEvent(res, 'error', payload);
12315
- res.end();
12316
- }
12317
- else {
12318
- res.writeHead(statusCode, { 'Content-Type': 'application/json' });
12319
- res.end(JSON.stringify(payload));
12320
- }
12321
- return;
12322
- }
12323
- console.error(error);
12324
- CrashLogger_1.CrashLogger.log(error, 'API:POST /api/notemd/extract-concepts');
12325
- const payload = { success: false, error: String(error) };
12326
- if (streamEnabled) {
12327
- writeSseEvent(res, 'error', payload);
12328
- res.end();
12329
- }
12330
- else {
12331
- res.writeHead(500, { 'Content-Type': 'application/json' });
12332
- res.end(JSON.stringify(payload));
12333
- }
12334
- }
12335
- return;
12336
- }
12337
- if (postPathname === '/api/notemd/one-click-extract') {
12338
- let operation = null;
12339
- try {
12340
- const payload = await readJsonBody(req);
12341
- const settings = await loadNotemdSettings();
12342
- operation = createNotemdOperation(payload.operationId);
12343
- const reporter = createNotemdReporter(operation);
12344
- const resolvedFilePath = await resolvePathWithinKnowledgeBase(payload.filePath, {
12345
- expectedType: 'file',
12346
- });
12347
- const result = await notemdService.oneClickExtract(resolvedFilePath, settings, reporter, operation.controller.signal);
12348
- void persistNotemdWorkspacePatch({
12349
- filePath: resolvedFilePath,
12350
- folderPath: result.outputFolderPath,
12351
- outputFolderPath: result.outputFolderPath,
12352
- }).catch((workspaceError) => {
12353
- warnDiagnostic('[NoteMD] Failed to persist workspace state after one-click-extract.', workspaceError);
12354
- });
12355
- finalizeNotemdOperation(operation, 'done');
12356
- res.writeHead(200, { 'Content-Type': 'application/json' });
12357
- res.end(JSON.stringify({ success: true, operationId: operation.id, result, logs: operation.logs }));
12358
- }
12359
- catch (error) {
12360
- if (operation) {
12361
- finalizeNotemdOperation(operation, operation.controller.signal.aborted ? 'cancelled' : 'error');
12362
- }
12363
- if (writeBodyParseErrorResponse(res, error)) {
12364
- return;
12365
- }
12366
- if (isAccessDeniedError(error)) {
12367
- res.writeHead(403, { 'Content-Type': 'application/json' });
12368
- res.end(JSON.stringify({ success: false, error: String(error.message || 'Access denied') }));
12369
- return;
12370
- }
12371
- console.error(error);
12372
- CrashLogger_1.CrashLogger.log(error, 'API:POST /api/notemd/one-click-extract');
12373
- res.writeHead(500, { 'Content-Type': 'application/json' });
12374
- res.end(JSON.stringify({ success: false, error: String(error) }));
12375
- }
12376
- return;
12377
- }
12378
- if (postPathname === '/api/notemd/batch-fix-mermaid') {
12379
- try {
12380
- const payload = await readJsonBody(req);
12381
- const resolvedFolderPath = await resolvePathWithinKnowledgeBase(payload.folderPath, {
12382
- expectedType: 'directory',
12383
- });
12384
- const result = await notemdService.batchFixMermaid(resolvedFolderPath, payload.inPlace !== false);
12385
- res.writeHead(200, { 'Content-Type': 'application/json' });
12386
- res.end(JSON.stringify({ success: true, result }));
12387
- }
12388
- catch (error) {
12389
- if (writeBodyParseErrorResponse(res, error)) {
12390
- return;
12391
- }
12392
- if (isAccessDeniedError(error)) {
12393
- res.writeHead(403, { 'Content-Type': 'application/json' });
12394
- res.end(JSON.stringify({ success: false, error: String(error.message || 'Access denied') }));
12395
- return;
12396
- }
12397
- console.error(error);
12398
- CrashLogger_1.CrashLogger.log(error, 'API:POST /api/notemd/batch-fix-mermaid');
12399
- res.writeHead(500, { 'Content-Type': 'application/json' });
12400
- res.end(JSON.stringify({ success: false, error: String(error) }));
12401
- }
12402
- return;
12403
- }
12404
- if (postPathname === '/api/notemd/generate-folder-content') {
12405
- try {
12406
- const payload = await readJsonBody(req);
12407
- const settings = await loadNotemdSettings();
12408
- const resolvedFolderPath = await resolvePathWithinKnowledgeBase(payload.folderPath, {
12409
- expectedType: 'directory',
12410
- });
12411
- const result = await notemdService.generateFolderContent(resolvedFolderPath, settings);
12412
- res.writeHead(200, { 'Content-Type': 'application/json' });
12413
- res.end(JSON.stringify({ success: true, result }));
12414
- }
12415
- catch (error) {
12416
- if (writeBodyParseErrorResponse(res, error)) {
12417
- return;
12418
- }
12419
- if (isAccessDeniedError(error)) {
12420
- res.writeHead(403, { 'Content-Type': 'application/json' });
12421
- res.end(JSON.stringify({ success: false, error: String(error.message || 'Access denied') }));
12422
- return;
12423
- }
12424
- console.error(error);
12425
- CrashLogger_1.CrashLogger.log(error, 'API:POST /api/notemd/generate-folder-content');
12426
- res.writeHead(500, { 'Content-Type': 'application/json' });
12427
- res.end(JSON.stringify({ success: false, error: String(error) }));
12428
- }
12429
- return;
12430
- }
12431
11885
  if (req.url === '/api/render/math') {
12432
11886
  try {
12433
11887
  const payload = await readJsonBody(req);
@@ -12590,6 +12044,17 @@ const startServer = async (options = {}) => {
12590
12044
  try {
12591
12045
  const payload = await readJsonBody(req);
12592
12046
  const { target, maxWorkers, enableGPU, enableGPULayout, memorySavingMode, deepDebug } = payload;
12047
+ const requestedRelationRecomputeMode = payload?.relationRecomputeMode;
12048
+ const relationRecomputeMode = normalizeRelationRecomputeModeValue(requestedRelationRecomputeMode);
12049
+ if (requestedRelationRecomputeMode !== undefined
12050
+ && (!relationRecomputeMode || relationRecomputeMode === 'auto')) {
12051
+ res.writeHead(400, { 'Content-Type': 'application/json' });
12052
+ res.end(JSON.stringify({
12053
+ success: false,
12054
+ error: 'relationRecomputeMode must be one of: none, incremental, full',
12055
+ }));
12056
+ return;
12057
+ }
12593
12058
  logDiagnostic('Received build request for:', target, 'maxWorkers:', maxWorkers, 'enableGPU:', enableGPU, 'enableGPULayout:', enableGPULayout, 'memorySavingMode:', memorySavingMode, 'deepDebug:', deepDebug);
12594
12059
  const buildKey = JSON.stringify({
12595
12060
  target,
@@ -12597,7 +12062,8 @@ const startServer = async (options = {}) => {
12597
12062
  enableGPU,
12598
12063
  enableGPULayout,
12599
12064
  memorySavingMode,
12600
- deepDebug
12065
+ deepDebug,
12066
+ relationRecomputeMode: relationRecomputeMode || 'incremental',
12601
12067
  });
12602
12068
  // De-duplicate accidental double-submit from frontend.
12603
12069
  if (activeBuildPromise) {
@@ -12640,7 +12106,13 @@ const startServer = async (options = {}) => {
12640
12106
  deepDebug
12641
12107
  });
12642
12108
  ACTIVE_GRAPH_TARGET = normalizedRuntimeTarget;
12643
- return await syncLearningWorkspaceForTarget(normalizedRuntimeTarget, 'build_graph');
12109
+ const files = await collectMarkdownFilePaths(targetToBuild);
12110
+ const documents = await buildKnowledgeDocumentPayloadsFromPaths(files);
12111
+ const ingest = await knowledgeLearningPlatform.ingestKnowledge({
12112
+ incremental: true, documents, ingestedAt: new Date().toISOString(),
12113
+ relationRecomputeMode: relationRecomputeMode || 'incremental',
12114
+ });
12115
+ return { target: normalizedRuntimeTarget, documentCount: documents.length, summary: ingest.summary };
12644
12116
  })();
12645
12117
  activeBuildKey = buildKey;
12646
12118
  activeBuildPromise = buildPromise;
@@ -12707,6 +12179,10 @@ const startServer = async (options = {}) => {
12707
12179
  return;
12708
12180
  }
12709
12181
  }
12182
+ if (!res.writableEnded && !res.destroyed) {
12183
+ res.writeHead(404, { 'Content-Type': 'application/json' });
12184
+ res.end(JSON.stringify({ success: false, error: 'API route not found' }));
12185
+ }
12710
12186
  });
12711
12187
  server.on('close', () => {
12712
12188
  try {