@devflow-tools/mcp-server 0.16.34 → 0.17.1

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" },
@@ -242,8 +248,19 @@ export function getCoreTools(knowledgeScopes = []) {
242
248
  status: { type: "string", enum: ["current", "temporary", "historical"] },
243
249
  },
244
250
  },
251
+ sourceEventIds: {
252
+ type: "array",
253
+ items: { type: "string" },
254
+ description: "Session event IDs that directly support this memory. Omit only when committing the current single turn event.",
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
+ },
245
263
  },
246
- required: ["type", "title"],
247
264
  },
248
265
  },
249
266
  },
@@ -284,6 +301,7 @@ export function getCoreTools(knowledgeScopes = []) {
284
301
  concepts: { type: "array", items: { type: "string" } },
285
302
  importance: { type: "number", minimum: 1, maximum: 10 },
286
303
  confidence: { type: "number", minimum: 0, maximum: 1 },
304
+ sourceEventIds: { type: "array", items: { type: "string" } },
287
305
  },
288
306
  required: ["type", "title"],
289
307
  },
@@ -314,6 +332,12 @@ export function getCoreTools(knowledgeScopes = []) {
314
332
  concepts: { type: "array", items: { type: "string" } },
315
333
  importance: { type: "number" },
316
334
  confidence: { type: "number" },
335
+ subject: { type: "string" },
336
+ predicate: { type: "string" },
337
+ value: { type: "string" },
338
+ polarity: { type: "string", enum: ["positive", "negative", "neutral"] },
339
+ entities: { type: "array", items: { type: "string" } },
340
+ sourceEventIds: { type: "array", items: { type: "string" } },
317
341
  },
318
342
  required: ["type", "title"],
319
343
  },
@@ -399,7 +423,7 @@ function replayMemoryDecision(turn) {
399
423
  replayed: true,
400
424
  };
401
425
  }
402
- function resolveMemoryDecisionTurn(database, projectRoot, sessionId, requestedTurnId) {
426
+ function resolveMemoryDecisionTurn(database, projectRoot, sessionId, requestedTurnId, options = {}) {
403
427
  if (typeof requestedTurnId === 'string' && requestedTurnId.trim()) {
404
428
  let normalized;
405
429
  try {
@@ -425,6 +449,7 @@ function resolveMemoryDecisionTurn(database, projectRoot, sessionId, requestedTu
425
449
  };
426
450
  }
427
451
  return requested.status === 'pending'
452
+ || (options.allowCommittedExplicitRefinement && requested.status === 'committed' && requested.source === 'explicit_intent')
428
453
  ? { turn: requested }
429
454
  : { replay: replayMemoryDecision(requested) };
430
455
  }
@@ -535,6 +560,92 @@ function findHostAction(actions, stepId) {
535
560
  const matches = actions.filter(action => action.stepId === stepId);
536
561
  return matches.length === 1 ? matches[0] : null;
537
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
+ }
538
649
  export function createToolHandlers(engines, boundProjectRoot) {
539
650
  const hasBoundProjectRoot = typeof boundProjectRoot === 'string' && boundProjectRoot.trim().length > 0;
540
651
  const canonicalProjectRoot = canonicalizeProjectRoot(String(boundProjectRoot ?? process.cwd()));
@@ -548,23 +659,37 @@ export function createToolHandlers(engines, boundProjectRoot) {
548
659
  }
549
660
  }
550
661
  const handlers = {
551
- 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, {
552
- query: query,
553
- mode: mode ?? "fast",
554
- expandedTerms: typeof expandedTerms === "string" ? expandedTerms : undefined,
555
- retrievalSessionId: typeof retrievalSessionId === 'string' ? retrievalSessionId : undefined,
556
- baselineReceipt: typeof baselineReceipt === 'string' ? baselineReceipt : undefined,
557
- missingContext: Array.isArray(missingContext) ? missingContext : undefined,
558
- sessionId: typeof _devflow_session_id === 'string' ? _devflow_session_id : process.env.DEVFLOW_SESSION_ID ?? undefined,
559
- executionId: typeof _devflow_execution_id === 'string' ? _devflow_execution_id : undefined,
560
- requestId: typeof _devflow_request_id === 'string' ? _devflow_request_id : undefined,
561
- activeSkill: typeof _devflow_active_skill === 'string'
562
- ? _devflow_active_skill
563
- : typeof activeSkill === 'string' ? activeSkill : undefined,
564
- projectRoot: boundProjectRoot,
565
- host: 'claude-code',
566
- command: 'devflow:context',
567
- }, 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
+ },
568
693
  search_symbol: async ({ symbol }) => {
569
694
  const result = await engines.indexer.findSymbol(symbol);
570
695
  return {
@@ -975,18 +1100,22 @@ export function createToolHandlers(engines, boundProjectRoot) {
975
1100
  }
976
1101
  const database = openGlobalDevFlowDatabase();
977
1102
  try {
978
- 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
+ });
979
1106
  if (resolution.replay)
980
1107
  return resolution.replay;
981
1108
  if (resolution.error || !resolution.turn)
982
1109
  return { ...resolution.error, committed: false };
983
1110
  const pending = resolution.turn;
1111
+ const explicitRefinement = pending.status === 'committed' && pending.source === 'explicit_intent';
984
1112
  const recovery = getMemoryTurnRecoveryPayload(database, pending);
985
1113
  const memory = engines.memory;
986
- if (recovery?.operation === 'commit_explicit' && recovery.explicitContent?.trim()) {
1114
+ if (pending.status === 'pending' && recovery?.operation === 'commit_explicit' && recovery.explicitContent?.trim()) {
987
1115
  await ensureMemoryTurnEvent(memory, pending, recovery);
988
1116
  const saved = await memory.saveExplicitMemoryIntent(recovery.explicitContent.trim(), sessionId, pending.eventId);
989
- 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');
990
1119
  if (memory.finalizeTurnDecision && receipt.receiptId) {
991
1120
  try {
992
1121
  await memory.finalizeTurnDecision({
@@ -995,7 +1124,7 @@ export function createToolHandlers(engines, boundProjectRoot) {
995
1124
  turnId: pending.turnId,
996
1125
  receiptId: receipt.receiptId,
997
1126
  status: 'committed',
998
- memoryIds: [saved.id],
1127
+ memoryIds: recoveredMemoryIds,
999
1128
  reason: 'explicit_user_request_mcp_recovery',
1000
1129
  });
1001
1130
  }
@@ -1017,15 +1146,17 @@ export function createToolHandlers(engines, boundProjectRoot) {
1017
1146
  });
1018
1147
  }
1019
1148
  }
1020
- database.enqueueWork({
1021
- idempotencyKey: `vector:${String(projectRoot)}:${saved.id}`,
1022
- kind: 'memory.vector_backfill',
1023
- projectRoot: String(projectRoot),
1024
- sessionId,
1025
- turnId: pending.turnId,
1026
- payload: { observationId: saved.id },
1027
- maxAttempts: 20,
1028
- });
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
+ }
1029
1160
  return acceptedMemoryReceipt(receipt, {
1030
1161
  acceptedCandidates: [],
1031
1162
  rejectedCandidates: [],
@@ -1035,6 +1166,28 @@ export function createToolHandlers(engines, boundProjectRoot) {
1035
1166
  }
1036
1167
  const validation = normalizeMemoryCandidates(candidates);
1037
1168
  if (validation.accepted.length === 0) {
1169
+ database.insertEvent({
1170
+ kind: 'memory_semantic_validation',
1171
+ timestamp: Date.now(),
1172
+ success: false,
1173
+ metadata: {
1174
+ projectRoot: String(projectRoot), sessionId, turnId: pending.turnId,
1175
+ accepted: 0, rejected: validation.rejected,
1176
+ },
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
+ }
1038
1191
  return {
1039
1192
  ...rejectedToolResult('INVALID_MEMORY_CANDIDATES', 'No valid durable memory candidates were supplied.', false, validation.rejected.flatMap(candidate => candidate.fieldErrors)),
1040
1193
  committed: false,
@@ -1055,9 +1208,142 @@ export function createToolHandlers(engines, boundProjectRoot) {
1055
1208
  polarity: candidate.polarity,
1056
1209
  entities: candidate.entities,
1057
1210
  temporal: candidate.temporal,
1211
+ sourceEventIds: candidate.sourceEventIds,
1212
+ evidenceSpans: candidate.evidenceSpans,
1213
+ disposition: candidate.disposition,
1058
1214
  }));
1059
1215
  await ensureMemoryTurnEvent(memory, pending, recovery);
1060
- const saved = await memory.saveTurnObservations(pending.eventId, sessionId, parsed);
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
+ }
1327
+ let saved;
1328
+ try {
1329
+ saved = await memory.saveTurnObservations(pending.eventId, sessionId, durableParsed);
1330
+ }
1331
+ catch (error) {
1332
+ const reason = error.message;
1333
+ database.insertEvent({
1334
+ kind: 'memory_semantic_validation',
1335
+ timestamp: Date.now(),
1336
+ success: false,
1337
+ metadata: {
1338
+ projectRoot: String(projectRoot), sessionId, turnId: pending.turnId,
1339
+ accepted: 0, rejected: validation.accepted.length, reason,
1340
+ },
1341
+ });
1342
+ return {
1343
+ ...rejectedToolResult('MEMORY_SEMANTIC_VALIDATION_FAILED', reason, false),
1344
+ committed: false,
1345
+ };
1346
+ }
1061
1347
  const memoryIds = saved.map(item => item.id);
1062
1348
  const receipt = commitMemoryReceipt(database, pending, memoryIds, 'host_turn', 'host_semantic_commit');
1063
1349
  if (memory.finalizeTurnDecision && receipt.receiptId) {
@@ -1085,6 +1371,16 @@ export function createToolHandlers(engines, boundProjectRoot) {
1085
1371
  maxAttempts: 20,
1086
1372
  });
1087
1373
  }
1374
+ database.insertEvent({
1375
+ kind: 'memory_semantic_validation',
1376
+ timestamp: Date.now(),
1377
+ success: true,
1378
+ metadata: {
1379
+ projectRoot: String(projectRoot), sessionId, turnId: pending.turnId,
1380
+ receiptId: receipt.receiptId, accepted: memoryIds.length,
1381
+ rejected: validation.rejected.length, memoryIds,
1382
+ },
1383
+ });
1088
1384
  return acceptedMemoryReceipt(receipt, {
1089
1385
  acceptedCandidates: validation.accepted.map(candidate => ({ index: candidate.index, type: candidate.type, normalizedFrom: candidate.normalizedFrom })),
1090
1386
  rejectedCandidates: validation.rejected,
@@ -1121,6 +1417,23 @@ export function createToolHandlers(engines, boundProjectRoot) {
1121
1417
  receiptId,
1122
1418
  reason: skipReason,
1123
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
+ });
1124
1437
  if (memory.finalizeTurnDecision && receipt.receiptId) {
1125
1438
  try {
1126
1439
  await memory.finalizeTurnDecision({
@@ -1165,6 +1478,12 @@ export function createToolHandlers(engines, boundProjectRoot) {
1165
1478
  concepts: Array.isArray(o.concepts) ? o.concepts.map(String) : undefined,
1166
1479
  importance: typeof o.importance === 'number' ? o.importance : undefined,
1167
1480
  confidence: typeof o.confidence === 'number' ? o.confidence : undefined,
1481
+ subject: typeof o.subject === 'string' ? o.subject : undefined,
1482
+ predicate: typeof o.predicate === 'string' ? o.predicate : undefined,
1483
+ value: typeof o.value === 'string' ? o.value : undefined,
1484
+ polarity: o.polarity === 'positive' || o.polarity === 'negative' || o.polarity === 'neutral' ? o.polarity : undefined,
1485
+ entities: Array.isArray(o.entities) ? o.entities.map(String) : undefined,
1486
+ sourceEventIds: Array.isArray(o.sourceEventIds) ? o.sourceEventIds.map(String) : undefined,
1168
1487
  }));
1169
1488
  if (typeof batchId !== "string" || batchId.trim().length === 0) {
1170
1489
  return {
@@ -1197,7 +1516,12 @@ export function createToolHandlers(engines, boundProjectRoot) {
1197
1516
  vectorQueueStatus = 'queued';
1198
1517
  }
1199
1518
  catch (error) {
1200
- 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
+ });
1201
1525
  }
1202
1526
  finally {
1203
1527
  database.close();