@vellumai/assistant 0.4.49 → 0.4.50

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 (239) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/memory.md +180 -119
  4. package/package.json +2 -2
  5. package/src/__tests__/agent-loop.test.ts +3 -1
  6. package/src/__tests__/anthropic-provider.test.ts +114 -23
  7. package/src/__tests__/approval-cascade.test.ts +1 -15
  8. package/src/__tests__/approval-routes-http.test.ts +2 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  10. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  11. package/src/__tests__/checker.test.ts +13 -0
  12. package/src/__tests__/config-schema.test.ts +1 -68
  13. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  14. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  15. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  16. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  17. package/src/__tests__/credential-vault-unit.test.ts +4 -0
  18. package/src/__tests__/credential-vault.test.ts +13 -1
  19. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  20. package/src/__tests__/date-context.test.ts +93 -77
  21. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  22. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  23. package/src/__tests__/history-repair.test.ts +245 -0
  24. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  25. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  26. package/src/__tests__/invite-redemption-service.test.ts +65 -1
  27. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  28. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  29. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  30. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  31. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  32. package/src/__tests__/memory-regressions.test.ts +477 -2841
  33. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  34. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  35. package/src/__tests__/mime-builder.test.ts +28 -0
  36. package/src/__tests__/native-web-search.test.ts +1 -0
  37. package/src/__tests__/oauth-cli.test.ts +572 -5
  38. package/src/__tests__/oauth-store.test.ts +120 -6
  39. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  40. package/src/__tests__/registry.test.ts +0 -1
  41. package/src/__tests__/relay-server.test.ts +46 -1
  42. package/src/__tests__/schedule-tools.test.ts +32 -0
  43. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  44. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  45. package/src/__tests__/secure-keys.test.ts +7 -2
  46. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  47. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  48. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  49. package/src/__tests__/session-agent-loop.test.ts +19 -15
  50. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  51. package/src/__tests__/session-error.test.ts +124 -2
  52. package/src/__tests__/session-history-web-search.test.ts +918 -0
  53. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  54. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  55. package/src/__tests__/session-queue.test.ts +37 -27
  56. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  57. package/src/__tests__/session-slash-known.test.ts +1 -15
  58. package/src/__tests__/session-slash-queue.test.ts +1 -15
  59. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  60. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  61. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  62. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  63. package/src/__tests__/skills-install-extract.test.ts +93 -0
  64. package/src/__tests__/skillssh-registry.test.ts +451 -0
  65. package/src/__tests__/trust-store.test.ts +15 -0
  66. package/src/__tests__/voice-invite-redemption.test.ts +32 -1
  67. package/src/agent/ax-tree-compaction.test.ts +51 -0
  68. package/src/agent/loop.ts +39 -12
  69. package/src/approvals/AGENTS.md +1 -1
  70. package/src/approvals/guardian-request-resolvers.ts +14 -2
  71. package/src/bundler/compiler-tools.ts +66 -2
  72. package/src/calls/call-domain.ts +132 -0
  73. package/src/calls/call-store.ts +6 -0
  74. package/src/calls/relay-server.ts +43 -5
  75. package/src/calls/relay-setup-router.ts +17 -1
  76. package/src/calls/twilio-config.ts +1 -1
  77. package/src/calls/types.ts +3 -1
  78. package/src/cli/commands/doctor.ts +4 -3
  79. package/src/cli/commands/mcp.ts +46 -59
  80. package/src/cli/commands/memory.ts +16 -165
  81. package/src/cli/commands/oauth/apps.ts +31 -2
  82. package/src/cli/commands/oauth/connections.ts +431 -97
  83. package/src/cli/commands/oauth/providers.ts +15 -1
  84. package/src/cli/commands/sessions.ts +5 -2
  85. package/src/cli/commands/skills.ts +173 -1
  86. package/src/cli/http-client.ts +0 -20
  87. package/src/cli/main-screen.tsx +2 -2
  88. package/src/cli/program.ts +5 -6
  89. package/src/cli.ts +4 -10
  90. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  91. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  92. package/src/config/bundled-tool-registry.ts +2 -5
  93. package/src/config/schema.ts +1 -12
  94. package/src/config/schemas/memory-lifecycle.ts +0 -9
  95. package/src/config/schemas/memory-processing.ts +0 -180
  96. package/src/config/schemas/memory-retrieval.ts +32 -104
  97. package/src/config/schemas/memory.ts +0 -10
  98. package/src/config/types.ts +0 -4
  99. package/src/context/window-manager.ts +4 -1
  100. package/src/daemon/config-watcher.ts +61 -3
  101. package/src/daemon/daemon-control.ts +1 -1
  102. package/src/daemon/date-context.ts +114 -31
  103. package/src/daemon/handlers/sessions.ts +18 -13
  104. package/src/daemon/handlers/skills.ts +20 -1
  105. package/src/daemon/history-repair.ts +72 -8
  106. package/src/daemon/host-cu-proxy.ts +55 -26
  107. package/src/daemon/lifecycle.ts +31 -3
  108. package/src/daemon/mcp-reload-service.ts +2 -2
  109. package/src/daemon/message-types/computer-use.ts +1 -12
  110. package/src/daemon/message-types/memory.ts +4 -16
  111. package/src/daemon/message-types/messages.ts +1 -0
  112. package/src/daemon/message-types/sessions.ts +4 -0
  113. package/src/daemon/server.ts +12 -1
  114. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  115. package/src/daemon/session-agent-loop.ts +334 -48
  116. package/src/daemon/session-error.ts +89 -6
  117. package/src/daemon/session-history.ts +17 -7
  118. package/src/daemon/session-media-retry.ts +6 -2
  119. package/src/daemon/session-memory.ts +69 -149
  120. package/src/daemon/session-process.ts +10 -1
  121. package/src/daemon/session-runtime-assembly.ts +49 -19
  122. package/src/daemon/session-surfaces.ts +4 -1
  123. package/src/daemon/session-tool-setup.ts +7 -1
  124. package/src/daemon/session.ts +12 -2
  125. package/src/instrument.ts +61 -1
  126. package/src/memory/admin.ts +2 -191
  127. package/src/memory/canonical-guardian-store.ts +38 -2
  128. package/src/memory/conversation-crud.ts +0 -33
  129. package/src/memory/conversation-queries.ts +22 -3
  130. package/src/memory/db-init.ts +28 -0
  131. package/src/memory/embedding-backend.ts +84 -8
  132. package/src/memory/embedding-types.ts +9 -1
  133. package/src/memory/indexer.ts +7 -46
  134. package/src/memory/items-extractor.ts +274 -76
  135. package/src/memory/job-handlers/backfill.ts +2 -127
  136. package/src/memory/job-handlers/cleanup.ts +2 -16
  137. package/src/memory/job-handlers/extraction.ts +2 -138
  138. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  139. package/src/memory/job-handlers/summarization.ts +3 -148
  140. package/src/memory/job-utils.ts +21 -59
  141. package/src/memory/jobs-store.ts +1 -159
  142. package/src/memory/jobs-worker.ts +9 -52
  143. package/src/memory/migrations/104-core-indexes.ts +3 -3
  144. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  145. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  146. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  147. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  148. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  149. package/src/memory/migrations/154-drop-fts.ts +20 -0
  150. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  151. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  152. package/src/memory/migrations/index.ts +7 -0
  153. package/src/memory/qdrant-client.ts +148 -51
  154. package/src/memory/raw-query.ts +1 -1
  155. package/src/memory/retriever.test.ts +294 -273
  156. package/src/memory/retriever.ts +421 -645
  157. package/src/memory/schema/calls.ts +2 -0
  158. package/src/memory/schema/memory-core.ts +3 -48
  159. package/src/memory/schema/oauth.ts +2 -0
  160. package/src/memory/search/formatting.ts +263 -176
  161. package/src/memory/search/lexical.ts +1 -254
  162. package/src/memory/search/ranking.ts +0 -455
  163. package/src/memory/search/semantic.ts +100 -14
  164. package/src/memory/search/staleness.ts +47 -0
  165. package/src/memory/search/tier-classifier.ts +21 -0
  166. package/src/memory/search/types.ts +15 -77
  167. package/src/memory/task-memory-cleanup.ts +4 -6
  168. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  169. package/src/oauth/byo-connection.test.ts +8 -1
  170. package/src/oauth/oauth-store.ts +113 -27
  171. package/src/oauth/seed-providers.ts +6 -0
  172. package/src/oauth/token-persistence.ts +11 -3
  173. package/src/permissions/defaults.ts +1 -0
  174. package/src/permissions/trust-store.ts +23 -1
  175. package/src/playbooks/playbook-compiler.ts +1 -1
  176. package/src/prompts/system-prompt.ts +18 -2
  177. package/src/providers/anthropic/client.ts +56 -126
  178. package/src/providers/types.ts +7 -1
  179. package/src/runtime/AGENTS.md +9 -0
  180. package/src/runtime/auth/route-policy.ts +6 -3
  181. package/src/runtime/guardian-reply-router.ts +24 -22
  182. package/src/runtime/http-server.ts +2 -2
  183. package/src/runtime/invite-redemption-service.ts +19 -1
  184. package/src/runtime/invite-service.ts +25 -0
  185. package/src/runtime/pending-interactions.ts +2 -2
  186. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  187. package/src/runtime/routes/conversation-routes.ts +9 -1
  188. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  189. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  190. package/src/runtime/routes/memory-item-routes.ts +503 -0
  191. package/src/runtime/routes/session-management-routes.ts +3 -3
  192. package/src/runtime/routes/settings-routes.ts +2 -2
  193. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  194. package/src/runtime/routes/workspace-routes.ts +2 -1
  195. package/src/security/keychain-broker-client.ts +17 -4
  196. package/src/security/secure-keys.ts +25 -3
  197. package/src/security/token-manager.ts +36 -36
  198. package/src/skills/catalog-install.ts +74 -18
  199. package/src/skills/skillssh-registry.ts +503 -0
  200. package/src/tools/assets/search.ts +5 -1
  201. package/src/tools/computer-use/definitions.ts +0 -10
  202. package/src/tools/computer-use/registry.ts +1 -1
  203. package/src/tools/credentials/vault.ts +1 -3
  204. package/src/tools/memory/definitions.ts +4 -13
  205. package/src/tools/memory/handlers.test.ts +83 -103
  206. package/src/tools/memory/handlers.ts +50 -85
  207. package/src/tools/schedule/create.ts +8 -1
  208. package/src/tools/schedule/update.ts +8 -1
  209. package/src/tools/skills/load.ts +25 -2
  210. package/src/__tests__/clarification-resolver.test.ts +0 -193
  211. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  212. package/src/__tests__/conflict-policy.test.ts +0 -269
  213. package/src/__tests__/conflict-store.test.ts +0 -372
  214. package/src/__tests__/contradiction-checker.test.ts +0 -361
  215. package/src/__tests__/entity-extractor.test.ts +0 -211
  216. package/src/__tests__/entity-search.test.ts +0 -1117
  217. package/src/__tests__/profile-compiler.test.ts +0 -392
  218. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  219. package/src/__tests__/session-profile-injection.test.ts +0 -557
  220. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  221. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  222. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  223. package/src/daemon/session-conflict-gate.ts +0 -167
  224. package/src/daemon/session-dynamic-profile.ts +0 -77
  225. package/src/memory/clarification-resolver.ts +0 -417
  226. package/src/memory/conflict-intent.ts +0 -205
  227. package/src/memory/conflict-policy.ts +0 -127
  228. package/src/memory/conflict-store.ts +0 -410
  229. package/src/memory/contradiction-checker.ts +0 -508
  230. package/src/memory/entity-extractor.ts +0 -535
  231. package/src/memory/format-recall.ts +0 -47
  232. package/src/memory/fts-reconciler.ts +0 -165
  233. package/src/memory/job-handlers/conflict.ts +0 -200
  234. package/src/memory/profile-compiler.ts +0 -195
  235. package/src/memory/recall-cache.ts +0 -117
  236. package/src/memory/search/entity.ts +0 -535
  237. package/src/memory/search/query-expansion.test.ts +0 -70
  238. package/src/memory/search/query-expansion.ts +0 -118
  239. package/src/runtime/routes/mcp-routes.ts +0 -20
@@ -1,127 +0,0 @@
1
- /**
2
- * Pure, deterministic policy helpers for memory conflict eligibility.
3
- * Used by contradiction checker, session conflict gate, and background resolver.
4
- */
5
-
6
- export interface ConflictPolicyConfig {
7
- conflictableKinds: readonly string[];
8
- [key: string]: unknown;
9
- }
10
-
11
- /**
12
- * Returns true when the given memory item kind is eligible to participate
13
- * in conflict detection according to the current policy.
14
- */
15
- export function isConflictKindEligible(
16
- kind: string,
17
- config: ConflictPolicyConfig,
18
- ): boolean {
19
- return config.conflictableKinds.includes(kind);
20
- }
21
-
22
- /**
23
- * Returns true when both sides of a potential conflict pair are kind-eligible.
24
- */
25
- export function isConflictKindPairEligible(
26
- existingKind: string,
27
- candidateKind: string,
28
- config: ConflictPolicyConfig,
29
- ): boolean {
30
- return (
31
- isConflictKindEligible(existingKind, config) &&
32
- isConflictKindEligible(candidateKind, config)
33
- );
34
- }
35
-
36
- // ── Transient statement classification ─────────────────────────────────
37
-
38
- const PR_URL_PATTERN = /github\.com\/[^/]+\/[^/]+\/pull\/\d+/i;
39
- const ISSUE_TICKET_PATTERN = /\b(?:issue|pr|ticket|pull request)\s*#?\d+/i;
40
- const TRACKING_LANGUAGE_PATTERN =
41
- /\b(?:this pr|that issue|while we wait|currently tracking)\b/i;
42
-
43
- // Statements about needing clarification are transient conversational artifacts
44
- // extracted from previous conflict-gate interactions — not durable facts.
45
- // Allowing them into the conflict pipeline creates self-reinforcing loops.
46
- // Patterns are kept narrow to avoid filtering legitimate durable instructions.
47
- const META_CLARIFICATION_PATTERN =
48
- /\b(?:needs? clarification\b|unclear which (?:version|value|setting)\b|user should (?:specify|clarify)\b|conflicting (?:notes|instructions)(?:\s*[:."]|\s+(?:about|regarding|for)\b))/i;
49
-
50
- /**
51
- * Returns true when a statement looks like a transient tracking note
52
- * (PR URLs, issue references, short-lived progress notes) rather than
53
- * a durable user preference or instruction.
54
- */
55
- export function isTransientTrackingStatement(statement: string): boolean {
56
- if (PR_URL_PATTERN.test(statement)) return true;
57
- if (ISSUE_TICKET_PATTERN.test(statement)) return true;
58
- if (TRACKING_LANGUAGE_PATTERN.test(statement)) return true;
59
- if (META_CLARIFICATION_PATTERN.test(statement)) return true;
60
- return false;
61
- }
62
-
63
- const DURABLE_INSTRUCTION_CUES =
64
- /\b(?:always|never|default|every time|by default|style|format|tone|convention|standard)\b/i;
65
-
66
- /**
67
- * Returns true when a statement contains strong durable instruction cues,
68
- * suggesting it represents a persistent user preference or style rule.
69
- */
70
- export function isDurableInstructionStatement(statement: string): boolean {
71
- return DURABLE_INSTRUCTION_CUES.test(statement);
72
- }
73
-
74
- // ── Verification-state provenance ──────────────────────────────────────
75
-
76
- // States indicating user involvement — either the user directly stated
77
- // the information, explicitly confirmed it, or it was bulk-imported from
78
- // a trusted external source the user chose to connect.
79
- const USER_EVIDENCED_STATES = new Set([
80
- "user_reported",
81
- "user_confirmed",
82
- "legacy_import",
83
- ]);
84
-
85
- /**
86
- * Returns true when the verification state indicates user provenance
87
- * (as opposed to purely assistant-inferred).
88
- */
89
- export function isUserEvidencedVerificationState(state: string): boolean {
90
- return USER_EVIDENCED_STATES.has(state);
91
- }
92
-
93
- /**
94
- * Returns true when at least one side of a conflict pair has user-evidenced
95
- * provenance. Assistant-inferred-only conflicts should not escalate into
96
- * user-facing behavior.
97
- */
98
- export function isConflictUserEvidenced(
99
- existingState: string,
100
- candidateState: string,
101
- ): boolean {
102
- return (
103
- isUserEvidencedVerificationState(existingState) ||
104
- isUserEvidencedVerificationState(candidateState)
105
- );
106
- }
107
-
108
- /**
109
- * Returns true when a statement of the given kind is eligible to participate
110
- * in conflict detection at the statement level. This combines kind eligibility
111
- * with statement-level durability heuristics.
112
- *
113
- * For instruction/style kinds: requires positive durable cues and no transient cues.
114
- * For other eligible kinds: rejects if transient tracking cues dominate.
115
- */
116
- export function isStatementConflictEligible(
117
- kind: string,
118
- statement: string,
119
- config?: ConflictPolicyConfig,
120
- ): boolean {
121
- if (config && !isConflictKindEligible(kind, config)) return false;
122
- if (isTransientTrackingStatement(statement)) return false;
123
- if (kind === "instruction" || kind === "style") {
124
- return isDurableInstructionStatement(statement);
125
- }
126
- return true;
127
- }
@@ -1,410 +0,0 @@
1
- import { and, asc, eq } from "drizzle-orm";
2
- import { v4 as uuid } from "uuid";
3
-
4
- import { getDb, getSqlite, rawAll } from "./db.js";
5
- import { enqueueMemoryJob } from "./jobs-store.js";
6
- import { memoryItemConflicts, memoryItems } from "./schema.js";
7
- import { clampUnitInterval } from "./validation.js";
8
-
9
- export type MemoryConflictRelationship =
10
- | "contradiction"
11
- | "ambiguous_contradiction"
12
- | "update"
13
- | "complement";
14
-
15
- export type MemoryConflictStatus =
16
- | "pending_clarification"
17
- | "resolved_keep_existing"
18
- | "resolved_keep_candidate"
19
- | "resolved_merge"
20
- | "dismissed";
21
-
22
- export type ResolvedMemoryConflictStatus = Exclude<
23
- MemoryConflictStatus,
24
- "pending_clarification"
25
- >;
26
-
27
- export interface MemoryItemConflict {
28
- id: string;
29
- scopeId: string;
30
- existingItemId: string;
31
- candidateItemId: string;
32
- relationship: string;
33
- status: MemoryConflictStatus;
34
- clarificationQuestion: string | null;
35
- resolutionNote: string | null;
36
- lastAskedAt: number | null;
37
- resolvedAt: number | null;
38
- createdAt: number;
39
- updatedAt: number;
40
- }
41
-
42
- export interface CreatePendingConflictInput {
43
- scopeId: string;
44
- existingItemId: string;
45
- candidateItemId: string;
46
- relationship: string;
47
- clarificationQuestion?: string | null;
48
- }
49
-
50
- export interface ResolveConflictInput {
51
- status: ResolvedMemoryConflictStatus;
52
- resolutionNote?: string | null;
53
- }
54
-
55
- export interface PendingConflictDetail extends MemoryItemConflict {
56
- existingStatement: string;
57
- candidateStatement: string;
58
- existingKind: string;
59
- candidateKind: string;
60
- existingVerificationState: string;
61
- candidateVerificationState: string;
62
- }
63
-
64
- export type ConflictResolutionAction =
65
- | "keep_existing"
66
- | "keep_candidate"
67
- | "merge";
68
-
69
- export interface ApplyConflictResolutionInput {
70
- conflictId: string;
71
- resolution: ConflictResolutionAction;
72
- mergedStatement?: string | null;
73
- resolutionNote?: string | null;
74
- }
75
-
76
- export function createOrUpdatePendingConflict(
77
- input: CreatePendingConflictInput,
78
- ): MemoryItemConflict {
79
- // Wrap in BEGIN IMMEDIATE so the SELECT-then-INSERT is atomic against concurrent
80
- // writers. Without this, two parallel memory workers could both observe no
81
- // existing conflict and both attempt to INSERT the same pair, resulting in a
82
- // duplicate or an unexpected constraint violation.
83
- return getSqlite()
84
- .transaction((): MemoryItemConflict => {
85
- const db = getDb();
86
- const now = Date.now();
87
- const scopeId = input.scopeId;
88
- const existing = getPendingConflictByPair(
89
- scopeId,
90
- input.existingItemId,
91
- input.candidateItemId,
92
- );
93
-
94
- if (existing) {
95
- db.update(memoryItemConflicts)
96
- .set({
97
- relationship: input.relationship,
98
- clarificationQuestion:
99
- input.clarificationQuestion !== undefined
100
- ? input.clarificationQuestion
101
- : existing.clarificationQuestion,
102
- updatedAt: now,
103
- })
104
- .where(eq(memoryItemConflicts.id, existing.id))
105
- .run();
106
- const updated = getConflictById(existing.id);
107
- if (!updated) {
108
- throw new Error(`Failed to reload updated conflict: ${existing.id}`);
109
- }
110
- return updated;
111
- }
112
-
113
- const id = uuid();
114
- db.insert(memoryItemConflicts)
115
- .values({
116
- id,
117
- scopeId,
118
- existingItemId: input.existingItemId,
119
- candidateItemId: input.candidateItemId,
120
- relationship: input.relationship,
121
- status: "pending_clarification",
122
- clarificationQuestion: input.clarificationQuestion ?? null,
123
- resolutionNote: null,
124
- lastAskedAt: null,
125
- resolvedAt: null,
126
- createdAt: now,
127
- updatedAt: now,
128
- })
129
- .run();
130
-
131
- const created = getConflictById(id);
132
- if (!created) {
133
- throw new Error(`Failed to load created conflict: ${id}`);
134
- }
135
- return created;
136
- })
137
- .immediate();
138
- }
139
-
140
- export function getConflictById(conflictId: string): MemoryItemConflict | null {
141
- const db = getDb();
142
- const row = db
143
- .select()
144
- .from(memoryItemConflicts)
145
- .where(eq(memoryItemConflicts.id, conflictId))
146
- .get();
147
- return row ? toConflict(row) : null;
148
- }
149
-
150
- export function getPendingConflictByPair(
151
- scopeId: string,
152
- existingItemId: string,
153
- candidateItemId: string,
154
- ): MemoryItemConflict | null {
155
- const db = getDb();
156
- const row = db
157
- .select()
158
- .from(memoryItemConflicts)
159
- .where(
160
- and(
161
- eq(memoryItemConflicts.scopeId, scopeId),
162
- eq(memoryItemConflicts.existingItemId, existingItemId),
163
- eq(memoryItemConflicts.candidateItemId, candidateItemId),
164
- eq(memoryItemConflicts.status, "pending_clarification"),
165
- ),
166
- )
167
- .get();
168
- return row ? toConflict(row) : null;
169
- }
170
-
171
- export function listPendingConflicts(
172
- scopeId: string,
173
- limit = 100,
174
- ): MemoryItemConflict[] {
175
- if (limit <= 0) return [];
176
- const db = getDb();
177
- const rows = db
178
- .select()
179
- .from(memoryItemConflicts)
180
- .where(
181
- and(
182
- eq(memoryItemConflicts.scopeId, scopeId),
183
- eq(memoryItemConflicts.status, "pending_clarification"),
184
- ),
185
- )
186
- .orderBy(asc(memoryItemConflicts.createdAt))
187
- .limit(limit)
188
- .all();
189
- return rows.map(toConflict);
190
- }
191
-
192
- export function listPendingConflictDetails(
193
- scopeId: string,
194
- limit = 100,
195
- cursor?: { createdAt: number; id: string },
196
- ): PendingConflictDetail[] {
197
- if (limit <= 0) return [];
198
- interface ConflictDetailRow {
199
- id: string;
200
- scope_id: string;
201
- existing_item_id: string;
202
- candidate_item_id: string;
203
- relationship: string;
204
- status: MemoryConflictStatus;
205
- clarification_question: string | null;
206
- resolution_note: string | null;
207
- last_asked_at: number | null;
208
- resolved_at: number | null;
209
- created_at: number;
210
- updated_at: number;
211
- existing_statement: string;
212
- candidate_statement: string;
213
- existing_kind: string;
214
- candidate_kind: string;
215
- existing_verification_state: string;
216
- candidate_verification_state: string;
217
- }
218
- const cursorClause = cursor
219
- ? `AND (c.created_at > ? OR (c.created_at = ? AND c.id > ?))`
220
- : "";
221
- const params: (string | number)[] = cursor
222
- ? [scopeId, cursor.createdAt, cursor.createdAt, cursor.id, limit]
223
- : [scopeId, limit];
224
- const rows = rawAll<ConflictDetailRow>(
225
- `
226
- SELECT
227
- c.id,
228
- c.scope_id,
229
- c.existing_item_id,
230
- c.candidate_item_id,
231
- c.relationship,
232
- c.status,
233
- c.clarification_question,
234
- c.resolution_note,
235
- c.last_asked_at,
236
- c.resolved_at,
237
- c.created_at,
238
- c.updated_at,
239
- existing_item.statement AS existing_statement,
240
- candidate_item.statement AS candidate_statement,
241
- existing_item.kind AS existing_kind,
242
- candidate_item.kind AS candidate_kind,
243
- existing_item.verification_state AS existing_verification_state,
244
- candidate_item.verification_state AS candidate_verification_state
245
- FROM memory_item_conflicts c
246
- INNER JOIN memory_items existing_item ON existing_item.id = c.existing_item_id
247
- INNER JOIN memory_items candidate_item ON candidate_item.id = c.candidate_item_id
248
- WHERE c.scope_id = ?
249
- AND c.status = 'pending_clarification'
250
- ${cursorClause}
251
- ORDER BY c.created_at ASC, c.id ASC
252
- LIMIT ?
253
- `,
254
- ...params,
255
- );
256
-
257
- return rows.map((row) => ({
258
- id: row.id,
259
- scopeId: row.scope_id,
260
- existingItemId: row.existing_item_id,
261
- candidateItemId: row.candidate_item_id,
262
- relationship: row.relationship,
263
- status: row.status,
264
- clarificationQuestion: row.clarification_question,
265
- resolutionNote: row.resolution_note,
266
- lastAskedAt: row.last_asked_at,
267
- resolvedAt: row.resolved_at,
268
- createdAt: row.created_at,
269
- updatedAt: row.updated_at,
270
- existingStatement: row.existing_statement,
271
- candidateStatement: row.candidate_statement,
272
- existingKind: row.existing_kind,
273
- candidateKind: row.candidate_kind,
274
- existingVerificationState: row.existing_verification_state,
275
- candidateVerificationState: row.candidate_verification_state,
276
- }));
277
- }
278
-
279
- export function resolveConflict(
280
- conflictId: string,
281
- input: ResolveConflictInput,
282
- ): MemoryItemConflict | null {
283
- const existing = getConflictById(conflictId);
284
- if (!existing) return null;
285
-
286
- const db = getDb();
287
- const now = Date.now();
288
- db.update(memoryItemConflicts)
289
- .set({
290
- status: input.status,
291
- resolutionNote:
292
- input.resolutionNote !== undefined
293
- ? input.resolutionNote
294
- : existing.resolutionNote,
295
- resolvedAt: now,
296
- updatedAt: now,
297
- })
298
- .where(eq(memoryItemConflicts.id, conflictId))
299
- .run();
300
-
301
- return getConflictById(conflictId);
302
- }
303
-
304
- export function applyConflictResolution(
305
- input: ApplyConflictResolutionInput,
306
- ): boolean {
307
- const conflict = getConflictById(input.conflictId);
308
- if (!conflict || conflict.status !== "pending_clarification") return false;
309
-
310
- const db = getDb();
311
- const now = Date.now();
312
- const existingItem = db
313
- .select()
314
- .from(memoryItems)
315
- .where(eq(memoryItems.id, conflict.existingItemId))
316
- .get();
317
- const candidateItem = db
318
- .select()
319
- .from(memoryItems)
320
- .where(eq(memoryItems.id, conflict.candidateItemId))
321
- .get();
322
-
323
- if (!existingItem || !candidateItem) {
324
- resolveConflict(conflict.id, {
325
- status: "dismissed",
326
- resolutionNote:
327
- input.resolutionNote ?? "Conflict items missing at resolution time.",
328
- });
329
- return false;
330
- }
331
-
332
- switch (input.resolution) {
333
- case "keep_existing": {
334
- db.update(memoryItems)
335
- .set({ status: "superseded", invalidAt: now })
336
- .where(eq(memoryItems.id, candidateItem.id))
337
- .run();
338
- resolveConflict(conflict.id, {
339
- status: "resolved_keep_existing",
340
- resolutionNote: input.resolutionNote ?? null,
341
- });
342
- return true;
343
- }
344
- case "keep_candidate": {
345
- db.update(memoryItems)
346
- .set({ status: "superseded", invalidAt: now })
347
- .where(eq(memoryItems.id, existingItem.id))
348
- .run();
349
- db.update(memoryItems)
350
- .set({ status: "active", validFrom: now })
351
- .where(eq(memoryItems.id, candidateItem.id))
352
- .run();
353
- resolveConflict(conflict.id, {
354
- status: "resolved_keep_candidate",
355
- resolutionNote: input.resolutionNote ?? null,
356
- });
357
- return true;
358
- }
359
- case "merge": {
360
- const mergedStatement = (input.mergedStatement ?? "").trim();
361
- const nextStatement =
362
- mergedStatement.length > 0 ? mergedStatement : candidateItem.statement;
363
- db.update(memoryItems)
364
- .set({
365
- statement: nextStatement,
366
- status: "active",
367
- invalidAt: null,
368
- lastSeenAt: Math.max(
369
- existingItem.lastSeenAt,
370
- candidateItem.lastSeenAt,
371
- now,
372
- ),
373
- confidence: clampUnitInterval(
374
- Math.max(existingItem.confidence, candidateItem.confidence),
375
- ),
376
- })
377
- .where(eq(memoryItems.id, existingItem.id))
378
- .run();
379
- db.update(memoryItems)
380
- .set({ status: "superseded", invalidAt: now })
381
- .where(eq(memoryItems.id, candidateItem.id))
382
- .run();
383
- enqueueMemoryJob("embed_item", { itemId: existingItem.id });
384
- resolveConflict(conflict.id, {
385
- status: "resolved_merge",
386
- resolutionNote: input.resolutionNote ?? null,
387
- });
388
- return true;
389
- }
390
- }
391
- }
392
-
393
- function toConflict(
394
- row: typeof memoryItemConflicts.$inferSelect,
395
- ): MemoryItemConflict {
396
- return {
397
- id: row.id,
398
- scopeId: row.scopeId,
399
- existingItemId: row.existingItemId,
400
- candidateItemId: row.candidateItemId,
401
- relationship: row.relationship,
402
- status: row.status as MemoryConflictStatus,
403
- clarificationQuestion: row.clarificationQuestion,
404
- resolutionNote: row.resolutionNote,
405
- lastAskedAt: row.lastAskedAt,
406
- resolvedAt: row.resolvedAt,
407
- createdAt: row.createdAt,
408
- updatedAt: row.updatedAt,
409
- };
410
- }