@devflow-tools/mcp-server 0.17.0 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tools.js CHANGED
@@ -6,6 +6,7 @@ import { normalizeTurnId, openGlobalDevFlowDatabase, } from "@devflow-tools/data
6
6
  import { createHash } from "node:crypto";
7
7
  import { isAbsolute, normalize, relative } from "node:path";
8
8
  import { normalizeMemoryCandidates, OBSERVATION_TYPE_SCHEMA, rejectedToolResult, TOOL_RESULT_OUTPUT_SCHEMA, } from "./tool-contracts.js";
9
+ import { emitSemanticControlLog } from '@devflow-tools/telemetry';
9
10
  export const GUARDED_CORE_TOOL_NAMES = new Set([
10
11
  "get_project_context",
11
12
  "search_symbol",
@@ -205,7 +206,7 @@ export function getCoreTools(knowledgeScopes = []) {
205
206
  },
206
207
  {
207
208
  name: "memory_commit_turn",
208
- description: "提交当前用户轮次并返回 DevFlow canonical receipt。显式记忆 turn 会从 durable prompt 自动恢复并提交;普通 turn 必须提供宿主语义判断得到的 candidates。",
209
+ description: "提交当前用户轮次并返回 DevFlow canonical receipt。显式记忆先可靠直存,再用带逐字 evidenceSpans 的宿主 candidates 幂等精炼;普通 turn 必须提供宿主语义判断得到的 candidates。",
209
210
  inputSchema: {
210
211
  type: "object",
211
212
  properties: {
@@ -213,7 +214,7 @@ export function getCoreTools(knowledgeScopes = []) {
213
214
  candidates: {
214
215
  type: "array",
215
216
  minItems: 1,
216
- description: "普通 turn 必填;显式记忆 turn 可省略,服务会使用已持久化的显式内容。",
217
+ description: "普通 turn 必填;显式记忆恢复时可省略,已有 canonical receipt 的显式 turn 则用它执行证据绑定语义精炼。",
217
218
  items: {
218
219
  type: "object",
219
220
  properties: {
@@ -224,6 +225,11 @@ export function getCoreTools(knowledgeScopes = []) {
224
225
  concepts: { type: "array", items: { type: "string" } },
225
226
  importance: { type: "number", minimum: 1, maximum: 10 },
226
227
  confidence: { type: "number", minimum: 0, maximum: 1 },
228
+ disposition: {
229
+ type: "string",
230
+ enum: ["durable_memory", "turn_constraint"],
231
+ description: "Classify reusable project truth separately from instructions that apply only to the current turn. Defaults to durable_memory for compatibility.",
232
+ },
227
233
  subject: { type: "string", minLength: 1, description: "Concrete project facet or entity the durable fact concerns" },
228
234
  predicate: { type: "string", minLength: 1, description: "Stable relation such as use, require, prefer, version, or state" },
229
235
  value: { type: "string", minLength: 1, description: "Exact selected value; preserve package names, flags, versions, and identifiers" },
@@ -247,8 +253,14 @@ export function getCoreTools(knowledgeScopes = []) {
247
253
  items: { type: "string" },
248
254
  description: "Session event IDs that directly support this memory. Omit only when committing the current single turn event.",
249
255
  },
256
+ evidenceSpans: {
257
+ type: "array",
258
+ minItems: 1,
259
+ maxItems: 16,
260
+ items: { type: "string", minLength: 1 },
261
+ description: "Exact, verbatim substrings copied from the canonical user prompt that support this candidate. Required when refining an already committed explicit-memory turn.",
262
+ },
250
263
  },
251
- required: ["type", "title"],
252
264
  },
253
265
  },
254
266
  },
@@ -411,7 +423,7 @@ function replayMemoryDecision(turn) {
411
423
  replayed: true,
412
424
  };
413
425
  }
414
- function resolveMemoryDecisionTurn(database, projectRoot, sessionId, requestedTurnId) {
426
+ function resolveMemoryDecisionTurn(database, projectRoot, sessionId, requestedTurnId, options = {}) {
415
427
  if (typeof requestedTurnId === 'string' && requestedTurnId.trim()) {
416
428
  let normalized;
417
429
  try {
@@ -437,6 +449,7 @@ function resolveMemoryDecisionTurn(database, projectRoot, sessionId, requestedTu
437
449
  };
438
450
  }
439
451
  return requested.status === 'pending'
452
+ || (options.allowCommittedExplicitRefinement && requested.status === 'committed' && requested.source === 'explicit_intent')
440
453
  ? { turn: requested }
441
454
  : { replay: replayMemoryDecision(requested) };
442
455
  }
@@ -547,6 +560,92 @@ function findHostAction(actions, stepId) {
547
560
  const matches = actions.filter(action => action.stepId === stepId);
548
561
  return matches.length === 1 ? matches[0] : null;
549
562
  }
563
+ function resolveBoundContextTask(input) {
564
+ if (!input.intentArtifactHash) {
565
+ return {
566
+ query: input.agentQuery,
567
+ expandedTerms: input.expandedTerms,
568
+ requestId: input.requestId,
569
+ turnId: input.turnId,
570
+ };
571
+ }
572
+ if (!input.sessionId || !input.turnId || !input.requestId) {
573
+ throw new Error('TASK_IDENTITY_INCOMPLETE:intent artifact binding requires session, turn, and request');
574
+ }
575
+ const database = openGlobalDevFlowDatabase(undefined, { busyTimeoutMs: 250 });
576
+ try {
577
+ const record = database.getTaskIntentArtifact(input.intentArtifactHash);
578
+ if (!record)
579
+ throw new Error(`TASK_INTENT_NOT_FOUND:${input.intentArtifactHash}`);
580
+ if (record.projectRoot !== input.projectRoot || record.sessionId !== input.sessionId
581
+ || record.turnId !== input.turnId || record.requestId !== input.requestId) {
582
+ throw new Error('TASK_IDENTITY_MISMATCH:intent artifact does not belong to this MCP request');
583
+ }
584
+ const planRecord = database.getChannelQueryPlanForIntent({
585
+ projectRoot: record.projectRoot,
586
+ sessionId: record.sessionId,
587
+ turnId: record.turnId,
588
+ sourceIntentHash: record.artifactHash,
589
+ });
590
+ if (!planRecord)
591
+ throw new Error(`QUERY_PLAN_NOT_FOUND:${record.artifactHash}`);
592
+ const identity = {
593
+ projectRoot: record.projectRoot,
594
+ projectId: record.projectId,
595
+ hostId: record.hostId,
596
+ sessionId: record.sessionId,
597
+ turnId: record.turnId,
598
+ requestId: record.requestId,
599
+ ...(record.executionId ? { executionId: record.executionId } : {}),
600
+ };
601
+ const artifact = {
602
+ identity,
603
+ version: record.version,
604
+ ...(record.supersedesHash ? { supersedesHash: record.supersedesHash } : {}),
605
+ rawPrompt: record.rawPrompt,
606
+ normalizedPrompt: record.normalizedPrompt,
607
+ ...(record.command ? { command: record.command } : {}),
608
+ slashArgs: record.slashArgs,
609
+ ...(record.activeSkill ? { activeSkill: record.activeSkill } : {}),
610
+ intent: record.intent,
611
+ action: record.action,
612
+ entities: record.entities,
613
+ targetAnchors: record.targetAnchors,
614
+ policyConstraints: record.policyConstraints,
615
+ classificationEvidence: record.classificationEvidence,
616
+ sourceEventIds: record.sourceEventIds,
617
+ sourceHash: record.sourceHash,
618
+ artifactHash: record.artifactHash,
619
+ createdAt: record.createdAt,
620
+ };
621
+ const queryPlan = {
622
+ identity,
623
+ sourceIntentHash: planRecord.sourceIntentHash,
624
+ planHash: planRecord.planHash,
625
+ code: planRecord.code,
626
+ memory: planRecord.memory,
627
+ knowledge: planRecord.knowledge,
628
+ generatedBy: planRecord.generatedBy,
629
+ ...(planRecord.degradation ? { degradation: planRecord.degradation } : {}),
630
+ createdAt: planRecord.createdAt,
631
+ };
632
+ const query = stripArtifactCommand(artifact.normalizedPrompt, artifact.command);
633
+ const expandedTerms = [input.agentQuery, input.expandedTerms]
634
+ .filter((value) => typeof value === 'string' && value.trim().length > 0)
635
+ .join(' ')
636
+ .slice(0, 4_000) || undefined;
637
+ return { query, expandedTerms, requestId: record.requestId, turnId: record.turnId, intentArtifact: artifact, queryPlan };
638
+ }
639
+ finally {
640
+ database.close();
641
+ }
642
+ }
643
+ function stripArtifactCommand(prompt, command) {
644
+ if (!command)
645
+ return prompt;
646
+ const escaped = command.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
647
+ return prompt.replace(new RegExp(`^\\/${escaped}\\b`, 'iu'), '').trim() || prompt;
648
+ }
550
649
  export function createToolHandlers(engines, boundProjectRoot) {
551
650
  const hasBoundProjectRoot = typeof boundProjectRoot === 'string' && boundProjectRoot.trim().length > 0;
552
651
  const canonicalProjectRoot = canonicalizeProjectRoot(String(boundProjectRoot ?? process.cwd()));
@@ -560,23 +659,37 @@ export function createToolHandlers(engines, boundProjectRoot) {
560
659
  }
561
660
  }
562
661
  const handlers = {
563
- get_project_context: async ({ query, mode, expandedTerms, retrievalSessionId, baselineReceipt, missingContext, _devflow_session_id, _devflow_execution_id, _devflow_request_id, _devflow_active_skill, activeSkill }) => getProjectContextResult(engines.context, {
564
- query: query,
565
- mode: mode ?? "fast",
566
- expandedTerms: typeof expandedTerms === "string" ? expandedTerms : undefined,
567
- retrievalSessionId: typeof retrievalSessionId === 'string' ? retrievalSessionId : undefined,
568
- baselineReceipt: typeof baselineReceipt === 'string' ? baselineReceipt : undefined,
569
- missingContext: Array.isArray(missingContext) ? missingContext : undefined,
570
- sessionId: typeof _devflow_session_id === 'string' ? _devflow_session_id : process.env.DEVFLOW_SESSION_ID ?? undefined,
571
- executionId: typeof _devflow_execution_id === 'string' ? _devflow_execution_id : undefined,
572
- requestId: typeof _devflow_request_id === 'string' ? _devflow_request_id : undefined,
573
- activeSkill: typeof _devflow_active_skill === 'string'
574
- ? _devflow_active_skill
575
- : typeof activeSkill === 'string' ? activeSkill : undefined,
576
- projectRoot: boundProjectRoot,
577
- host: 'claude-code',
578
- command: 'devflow:context',
579
- }, getFreshnessMs()),
662
+ get_project_context: async ({ query, mode, expandedTerms, retrievalSessionId, baselineReceipt, missingContext, _devflow_session_id, _devflow_execution_id, _devflow_request_id, _devflow_turn_id, _devflow_intent_artifact_hash, _devflow_active_skill, activeSkill }) => {
663
+ const boundTask = resolveBoundContextTask({
664
+ projectRoot: canonicalProjectRoot,
665
+ sessionId: typeof _devflow_session_id === 'string' ? _devflow_session_id : undefined,
666
+ requestId: typeof _devflow_request_id === 'string' ? _devflow_request_id : undefined,
667
+ turnId: typeof _devflow_turn_id === 'string' ? _devflow_turn_id : undefined,
668
+ intentArtifactHash: typeof _devflow_intent_artifact_hash === 'string' ? _devflow_intent_artifact_hash : undefined,
669
+ agentQuery: query,
670
+ expandedTerms: typeof expandedTerms === 'string' ? expandedTerms : undefined,
671
+ });
672
+ return getProjectContextResult(engines.context, {
673
+ query: boundTask.query,
674
+ mode: mode ?? "fast",
675
+ expandedTerms: boundTask.expandedTerms,
676
+ retrievalSessionId: typeof retrievalSessionId === 'string' ? retrievalSessionId : undefined,
677
+ baselineReceipt: typeof baselineReceipt === 'string' ? baselineReceipt : undefined,
678
+ missingContext: Array.isArray(missingContext) ? missingContext : undefined,
679
+ sessionId: typeof _devflow_session_id === 'string' ? _devflow_session_id : process.env.DEVFLOW_SESSION_ID ?? undefined,
680
+ executionId: typeof _devflow_execution_id === 'string' ? _devflow_execution_id : undefined,
681
+ requestId: boundTask.requestId,
682
+ turnId: boundTask.turnId,
683
+ intentArtifact: boundTask.intentArtifact,
684
+ queryPlan: boundTask.queryPlan,
685
+ activeSkill: boundTask.intentArtifact?.activeSkill ?? (typeof _devflow_active_skill === 'string'
686
+ ? _devflow_active_skill
687
+ : typeof activeSkill === 'string' ? activeSkill : undefined),
688
+ projectRoot: boundProjectRoot,
689
+ host: 'claude-code',
690
+ command: boundTask.intentArtifact?.command ?? 'devflow:context',
691
+ }, getFreshnessMs());
692
+ },
580
693
  search_symbol: async ({ symbol }) => {
581
694
  const result = await engines.indexer.findSymbol(symbol);
582
695
  return {
@@ -987,18 +1100,22 @@ export function createToolHandlers(engines, boundProjectRoot) {
987
1100
  }
988
1101
  const database = openGlobalDevFlowDatabase();
989
1102
  try {
990
- const resolution = resolveMemoryDecisionTurn(database, String(projectRoot), sessionId, turnId);
1103
+ const resolution = resolveMemoryDecisionTurn(database, String(projectRoot), sessionId, turnId, {
1104
+ allowCommittedExplicitRefinement: Array.isArray(candidates) && candidates.length > 0,
1105
+ });
991
1106
  if (resolution.replay)
992
1107
  return resolution.replay;
993
1108
  if (resolution.error || !resolution.turn)
994
1109
  return { ...resolution.error, committed: false };
995
1110
  const pending = resolution.turn;
1111
+ const explicitRefinement = pending.status === 'committed' && pending.source === 'explicit_intent';
996
1112
  const recovery = getMemoryTurnRecoveryPayload(database, pending);
997
1113
  const memory = engines.memory;
998
- if (recovery?.operation === 'commit_explicit' && recovery.explicitContent?.trim()) {
1114
+ if (pending.status === 'pending' && recovery?.operation === 'commit_explicit' && recovery.explicitContent?.trim()) {
999
1115
  await ensureMemoryTurnEvent(memory, pending, recovery);
1000
1116
  const saved = await memory.saveExplicitMemoryIntent(recovery.explicitContent.trim(), sessionId, pending.eventId);
1001
- const receipt = commitMemoryReceipt(database, pending, [saved.id], 'explicit_intent', 'explicit_user_request_mcp_recovery');
1117
+ const recoveredMemoryIds = [...new Set([saved.id, ...(saved.relatedMemoryIds ?? [])])];
1118
+ const receipt = commitMemoryReceipt(database, pending, recoveredMemoryIds, 'explicit_intent', 'explicit_user_request_mcp_recovery');
1002
1119
  if (memory.finalizeTurnDecision && receipt.receiptId) {
1003
1120
  try {
1004
1121
  await memory.finalizeTurnDecision({
@@ -1007,7 +1124,7 @@ export function createToolHandlers(engines, boundProjectRoot) {
1007
1124
  turnId: pending.turnId,
1008
1125
  receiptId: receipt.receiptId,
1009
1126
  status: 'committed',
1010
- memoryIds: [saved.id],
1127
+ memoryIds: recoveredMemoryIds,
1011
1128
  reason: 'explicit_user_request_mcp_recovery',
1012
1129
  });
1013
1130
  }
@@ -1029,15 +1146,17 @@ export function createToolHandlers(engines, boundProjectRoot) {
1029
1146
  });
1030
1147
  }
1031
1148
  }
1032
- database.enqueueWork({
1033
- idempotencyKey: `vector:${String(projectRoot)}:${saved.id}`,
1034
- kind: 'memory.vector_backfill',
1035
- projectRoot: String(projectRoot),
1036
- sessionId,
1037
- turnId: pending.turnId,
1038
- payload: { observationId: saved.id },
1039
- maxAttempts: 20,
1040
- });
1149
+ for (const memoryId of recoveredMemoryIds) {
1150
+ database.enqueueWork({
1151
+ idempotencyKey: `vector:${String(projectRoot)}:${memoryId}`,
1152
+ kind: 'memory.vector_backfill',
1153
+ projectRoot: String(projectRoot),
1154
+ sessionId,
1155
+ turnId: pending.turnId,
1156
+ payload: { observationId: memoryId },
1157
+ maxAttempts: 20,
1158
+ });
1159
+ }
1041
1160
  return acceptedMemoryReceipt(receipt, {
1042
1161
  acceptedCandidates: [],
1043
1162
  rejectedCandidates: [],
@@ -1056,6 +1175,19 @@ export function createToolHandlers(engines, boundProjectRoot) {
1056
1175
  accepted: 0, rejected: validation.rejected,
1057
1176
  },
1058
1177
  });
1178
+ if (explicitRefinement) {
1179
+ return {
1180
+ ...replayMemoryDecision(pending),
1181
+ status: 'degraded',
1182
+ replayed: false,
1183
+ refined: false,
1184
+ degradation: {
1185
+ reason: 'invalid_host_refinement_candidates',
1186
+ fallback: 'deterministic_explicit_memory',
1187
+ },
1188
+ rejectedCandidates: validation.rejected,
1189
+ };
1190
+ }
1059
1191
  return {
1060
1192
  ...rejectedToolResult('INVALID_MEMORY_CANDIDATES', 'No valid durable memory candidates were supplied.', false, validation.rejected.flatMap(candidate => candidate.fieldErrors)),
1061
1193
  committed: false,
@@ -1077,11 +1209,124 @@ export function createToolHandlers(engines, boundProjectRoot) {
1077
1209
  entities: candidate.entities,
1078
1210
  temporal: candidate.temporal,
1079
1211
  sourceEventIds: candidate.sourceEventIds,
1212
+ evidenceSpans: candidate.evidenceSpans,
1213
+ disposition: candidate.disposition,
1080
1214
  }));
1081
1215
  await ensureMemoryTurnEvent(memory, pending, recovery);
1216
+ if (explicitRefinement) {
1217
+ if (!memory.refineExplicitTurnObservations) {
1218
+ return {
1219
+ ...replayMemoryDecision(pending),
1220
+ status: 'degraded',
1221
+ replayed: false,
1222
+ degradation: {
1223
+ reason: 'explicit_refinement_unavailable',
1224
+ fallback: 'deterministic_explicit_memory',
1225
+ },
1226
+ };
1227
+ }
1228
+ try {
1229
+ const refined = await memory.refineExplicitTurnObservations(pending.eventId, sessionId, parsed);
1230
+ const refinedMemoryIds = refined.memories.map(item => item.id);
1231
+ const refinementReceiptId = `memory-receipt:${createHash('sha256')
1232
+ .update(`${pending.turnId}\0host_refinement\0${[...refinedMemoryIds, ...refined.retiredMemoryIds].sort().join('\0')}`)
1233
+ .digest('hex').slice(0, 24)}`;
1234
+ if (memory.finalizeTurnDecision) {
1235
+ await memory.finalizeTurnDecision({
1236
+ turnEventId: pending.eventId,
1237
+ sessionId,
1238
+ turnId: pending.turnId,
1239
+ receiptId: refinementReceiptId,
1240
+ status: 'committed',
1241
+ memoryIds: refinedMemoryIds,
1242
+ reason: 'host_explicit_semantic_refinement',
1243
+ });
1244
+ }
1245
+ for (const memoryId of refinedMemoryIds) {
1246
+ database.enqueueWork({
1247
+ idempotencyKey: `vector:${String(projectRoot)}:${memoryId}`,
1248
+ kind: 'memory.vector_backfill',
1249
+ projectRoot: String(projectRoot),
1250
+ sessionId,
1251
+ turnId: pending.turnId,
1252
+ payload: { observationId: memoryId },
1253
+ maxAttempts: 20,
1254
+ });
1255
+ }
1256
+ database.insertEvent({
1257
+ kind: 'memory_semantic_refinement',
1258
+ timestamp: Date.now(),
1259
+ success: true,
1260
+ metadata: {
1261
+ projectRoot: String(projectRoot), sessionId, turnId: pending.turnId,
1262
+ receiptId: pending.receiptId, refinementReceiptId,
1263
+ accepted: refinedMemoryIds.length,
1264
+ retired: refined.retiredMemoryIds.length,
1265
+ operations: refined.operations,
1266
+ },
1267
+ });
1268
+ return {
1269
+ ...replayMemoryDecision(pending),
1270
+ replayed: false,
1271
+ refined: true,
1272
+ refinementReceiptId,
1273
+ data: {
1274
+ acceptedCandidates: validation.accepted.map(candidate => ({
1275
+ index: candidate.index,
1276
+ type: candidate.type,
1277
+ disposition: candidate.disposition,
1278
+ normalizedFrom: candidate.normalizedFrom,
1279
+ })),
1280
+ rejectedCandidates: validation.rejected,
1281
+ refinedMemoryIds,
1282
+ retiredMemoryIds: refined.retiredMemoryIds,
1283
+ operations: refined.operations,
1284
+ fallbackReceiptPreserved: true,
1285
+ },
1286
+ };
1287
+ }
1288
+ catch (error) {
1289
+ const reason = error.message;
1290
+ database.insertEvent({
1291
+ kind: 'memory_semantic_refinement',
1292
+ timestamp: Date.now(),
1293
+ success: false,
1294
+ metadata: {
1295
+ projectRoot: String(projectRoot), sessionId, turnId: pending.turnId,
1296
+ receiptId: pending.receiptId, reason,
1297
+ fallback: 'deterministic_explicit_memory',
1298
+ },
1299
+ });
1300
+ emitSemanticControlLog({
1301
+ event: 'memory.write.degraded',
1302
+ identity: { projectRoot: String(projectRoot), sessionId, turnId: pending.turnId },
1303
+ level: 'warn',
1304
+ timestamp: Date.now(),
1305
+ data: { stage: 'explicit_host_refinement', reason, fallback: 'deterministic_explicit_memory' },
1306
+ });
1307
+ return {
1308
+ ...replayMemoryDecision(pending),
1309
+ status: 'degraded',
1310
+ replayed: false,
1311
+ refined: false,
1312
+ degradation: { reason, fallback: 'deterministic_explicit_memory' },
1313
+ };
1314
+ }
1315
+ }
1316
+ const durableParsed = parsed.filter(candidate => candidate.disposition !== 'turn_constraint');
1317
+ if (durableParsed.length === 0) {
1318
+ return {
1319
+ ...rejectedToolResult('NO_DURABLE_MEMORY_CANDIDATES', 'All candidates were classified as turn constraints. Call memory_skip_turn for this turn.', false),
1320
+ committed: false,
1321
+ acceptedCandidates: validation.accepted.map(candidate => ({
1322
+ index: candidate.index,
1323
+ disposition: candidate.disposition,
1324
+ })),
1325
+ };
1326
+ }
1082
1327
  let saved;
1083
1328
  try {
1084
- saved = await memory.saveTurnObservations(pending.eventId, sessionId, parsed);
1329
+ saved = await memory.saveTurnObservations(pending.eventId, sessionId, durableParsed);
1085
1330
  }
1086
1331
  catch (error) {
1087
1332
  const reason = error.message;
@@ -1172,6 +1417,23 @@ export function createToolHandlers(engines, boundProjectRoot) {
1172
1417
  receiptId,
1173
1418
  reason: skipReason,
1174
1419
  });
1420
+ emitSemanticControlLog({
1421
+ event: 'memory.semantic.decided',
1422
+ identity: {
1423
+ projectRoot: String(projectRoot),
1424
+ sessionId,
1425
+ turnId: pending.turnId,
1426
+ actionReceipt: receipt.receiptId,
1427
+ },
1428
+ level: 'info',
1429
+ timestamp: Date.now(),
1430
+ data: {
1431
+ status: 'skipped',
1432
+ operations: [],
1433
+ memoryIds: [],
1434
+ reason: skipReason.slice(0, 500),
1435
+ },
1436
+ });
1175
1437
  if (memory.finalizeTurnDecision && receipt.receiptId) {
1176
1438
  try {
1177
1439
  await memory.finalizeTurnDecision({
@@ -1254,7 +1516,12 @@ export function createToolHandlers(engines, boundProjectRoot) {
1254
1516
  vectorQueueStatus = 'queued';
1255
1517
  }
1256
1518
  catch (error) {
1257
- console.error('[devflow] Unable to enqueue distilled memory vector backfill:', error.message);
1519
+ emitSemanticControlLog({
1520
+ event: 'memory.write.degraded',
1521
+ identity: { projectRoot: canonicalProjectRoot, sessionId: typeof _devflow_session_id === 'string' ? _devflow_session_id : undefined },
1522
+ level: 'warn', timestamp: Date.now(),
1523
+ data: { stage: 'distill_vector_enqueue', batchId, reason: error.message, fallback: 'lexical_memory_ready' },
1524
+ });
1258
1525
  }
1259
1526
  finally {
1260
1527
  database.close();