@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.
- package/ARCHITECTURE.md +24 -33
- package/README.md +3 -3
- package/docs/architecture/memory.md +180 -119
- package/package.json +2 -2
- package/src/__tests__/agent-loop.test.ts +3 -1
- package/src/__tests__/anthropic-provider.test.ts +114 -23
- package/src/__tests__/approval-cascade.test.ts +1 -15
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
- package/src/__tests__/canonical-guardian-store.test.ts +95 -0
- package/src/__tests__/checker.test.ts +13 -0
- package/src/__tests__/config-schema.test.ts +1 -68
- package/src/__tests__/context-memory-e2e.test.ts +11 -100
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -0
- package/src/__tests__/credential-vault.test.ts +13 -1
- package/src/__tests__/cu-unified-flow.test.ts +532 -0
- package/src/__tests__/date-context.test.ts +93 -77
- package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
- package/src/__tests__/history-repair.test.ts +245 -0
- package/src/__tests__/host-cu-proxy.test.ts +165 -3
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/invite-redemption-service.test.ts +65 -1
- package/src/__tests__/keychain-broker-client.test.ts +4 -4
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
- package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
- package/src/__tests__/memory-recall-quality.test.ts +244 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
- package/src/__tests__/memory-regressions.test.ts +477 -2841
- package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
- package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
- package/src/__tests__/mime-builder.test.ts +28 -0
- package/src/__tests__/native-web-search.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +572 -5
- package/src/__tests__/oauth-store.test.ts +120 -6
- package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
- package/src/__tests__/registry.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +46 -1
- package/src/__tests__/schedule-tools.test.ts +32 -0
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -0
- package/src/__tests__/secure-keys.test.ts +7 -2
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/session-abort-tool-results.test.ts +1 -14
- package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
- package/src/__tests__/session-agent-loop.test.ts +19 -15
- package/src/__tests__/session-confirmation-signals.test.ts +1 -15
- package/src/__tests__/session-error.test.ts +124 -2
- package/src/__tests__/session-history-web-search.test.ts +918 -0
- package/src/__tests__/session-pre-run-repair.test.ts +1 -14
- package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
- package/src/__tests__/session-queue.test.ts +37 -27
- package/src/__tests__/session-runtime-assembly.test.ts +54 -0
- package/src/__tests__/session-slash-known.test.ts +1 -15
- package/src/__tests__/session-slash-queue.test.ts +1 -15
- package/src/__tests__/session-slash-unknown.test.ts +1 -15
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
- package/src/__tests__/session-workspace-injection.test.ts +3 -37
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
- package/src/__tests__/skills-install-extract.test.ts +93 -0
- package/src/__tests__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/trust-store.test.ts +15 -0
- package/src/__tests__/voice-invite-redemption.test.ts +32 -1
- package/src/agent/ax-tree-compaction.test.ts +51 -0
- package/src/agent/loop.ts +39 -12
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-request-resolvers.ts +14 -2
- package/src/bundler/compiler-tools.ts +66 -2
- package/src/calls/call-domain.ts +132 -0
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +43 -5
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +1 -1
- package/src/calls/types.ts +3 -1
- package/src/cli/commands/doctor.ts +4 -3
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +31 -2
- package/src/cli/commands/oauth/connections.ts +431 -97
- package/src/cli/commands/oauth/providers.ts +15 -1
- package/src/cli/commands/sessions.ts +5 -2
- package/src/cli/commands/skills.ts +173 -1
- package/src/cli/http-client.ts +0 -20
- package/src/cli/main-screen.tsx +2 -2
- package/src/cli/program.ts +5 -6
- package/src/cli.ts +4 -10
- package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
- package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/schema.ts +1 -12
- package/src/config/schemas/memory-lifecycle.ts +0 -9
- package/src/config/schemas/memory-processing.ts +0 -180
- package/src/config/schemas/memory-retrieval.ts +32 -104
- package/src/config/schemas/memory.ts +0 -10
- package/src/config/types.ts +0 -4
- package/src/context/window-manager.ts +4 -1
- package/src/daemon/config-watcher.ts +61 -3
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/sessions.ts +18 -13
- package/src/daemon/handlers/skills.ts +20 -1
- package/src/daemon/history-repair.ts +72 -8
- package/src/daemon/host-cu-proxy.ts +55 -26
- package/src/daemon/lifecycle.ts +31 -3
- package/src/daemon/mcp-reload-service.ts +2 -2
- package/src/daemon/message-types/computer-use.ts +1 -12
- package/src/daemon/message-types/memory.ts +4 -16
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/sessions.ts +4 -0
- package/src/daemon/server.ts +12 -1
- package/src/daemon/session-agent-loop-handlers.ts +38 -0
- package/src/daemon/session-agent-loop.ts +334 -48
- package/src/daemon/session-error.ts +89 -6
- package/src/daemon/session-history.ts +17 -7
- package/src/daemon/session-media-retry.ts +6 -2
- package/src/daemon/session-memory.ts +69 -149
- package/src/daemon/session-process.ts +10 -1
- package/src/daemon/session-runtime-assembly.ts +49 -19
- package/src/daemon/session-surfaces.ts +4 -1
- package/src/daemon/session-tool-setup.ts +7 -1
- package/src/daemon/session.ts +12 -2
- package/src/instrument.ts +61 -1
- package/src/memory/admin.ts +2 -191
- package/src/memory/canonical-guardian-store.ts +38 -2
- package/src/memory/conversation-crud.ts +0 -33
- package/src/memory/conversation-queries.ts +22 -3
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +84 -8
- package/src/memory/embedding-types.ts +9 -1
- package/src/memory/indexer.ts +7 -46
- package/src/memory/items-extractor.ts +274 -76
- package/src/memory/job-handlers/backfill.ts +2 -127
- package/src/memory/job-handlers/cleanup.ts +2 -16
- package/src/memory/job-handlers/extraction.ts +2 -138
- package/src/memory/job-handlers/index-maintenance.ts +1 -6
- package/src/memory/job-handlers/summarization.ts +3 -148
- package/src/memory/job-utils.ts +21 -59
- package/src/memory/jobs-store.ts +1 -159
- package/src/memory/jobs-worker.ts +9 -52
- package/src/memory/migrations/104-core-indexes.ts +3 -3
- package/src/memory/migrations/149-oauth-tables.ts +2 -0
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
- package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
- package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
- package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
- package/src/memory/migrations/154-drop-fts.ts +20 -0
- package/src/memory/migrations/155-drop-conflicts.ts +7 -0
- package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/qdrant-client.ts +148 -51
- package/src/memory/raw-query.ts +1 -1
- package/src/memory/retriever.test.ts +294 -273
- package/src/memory/retriever.ts +421 -645
- package/src/memory/schema/calls.ts +2 -0
- package/src/memory/schema/memory-core.ts +3 -48
- package/src/memory/schema/oauth.ts +2 -0
- package/src/memory/search/formatting.ts +263 -176
- package/src/memory/search/lexical.ts +1 -254
- package/src/memory/search/ranking.ts +0 -455
- package/src/memory/search/semantic.ts +100 -14
- package/src/memory/search/staleness.ts +47 -0
- package/src/memory/search/tier-classifier.ts +21 -0
- package/src/memory/search/types.ts +15 -77
- package/src/memory/task-memory-cleanup.ts +4 -6
- package/src/messaging/providers/gmail/mime-builder.ts +17 -7
- package/src/oauth/byo-connection.test.ts +8 -1
- package/src/oauth/oauth-store.ts +113 -27
- package/src/oauth/seed-providers.ts +6 -0
- package/src/oauth/token-persistence.ts +11 -3
- package/src/permissions/defaults.ts +1 -0
- package/src/permissions/trust-store.ts +23 -1
- package/src/playbooks/playbook-compiler.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -2
- package/src/providers/anthropic/client.ts +56 -126
- package/src/providers/types.ts +7 -1
- package/src/runtime/AGENTS.md +9 -0
- package/src/runtime/auth/route-policy.ts +6 -3
- package/src/runtime/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/invite-redemption-service.ts +19 -1
- package/src/runtime/invite-service.ts +25 -0
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/conversation-routes.ts +9 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/memory-item-routes.test.ts +754 -0
- package/src/runtime/routes/memory-item-routes.ts +503 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +2 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/workspace-routes.ts +2 -1
- package/src/security/keychain-broker-client.ts +17 -4
- package/src/security/secure-keys.ts +25 -3
- package/src/security/token-manager.ts +36 -36
- package/src/skills/catalog-install.ts +74 -18
- package/src/skills/skillssh-registry.ts +503 -0
- package/src/tools/assets/search.ts +5 -1
- package/src/tools/computer-use/definitions.ts +0 -10
- package/src/tools/computer-use/registry.ts +1 -1
- package/src/tools/credentials/vault.ts +1 -3
- package/src/tools/memory/definitions.ts +4 -13
- package/src/tools/memory/handlers.test.ts +83 -103
- package/src/tools/memory/handlers.ts +50 -85
- package/src/tools/schedule/create.ts +8 -1
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +25 -2
- package/src/__tests__/clarification-resolver.test.ts +0 -193
- package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
- package/src/__tests__/conflict-policy.test.ts +0 -269
- package/src/__tests__/conflict-store.test.ts +0 -372
- package/src/__tests__/contradiction-checker.test.ts +0 -361
- package/src/__tests__/entity-extractor.test.ts +0 -211
- package/src/__tests__/entity-search.test.ts +0 -1117
- package/src/__tests__/profile-compiler.test.ts +0 -392
- package/src/__tests__/session-conflict-gate.test.ts +0 -1228
- package/src/__tests__/session-profile-injection.test.ts +0 -557
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
- package/src/daemon/session-conflict-gate.ts +0 -167
- package/src/daemon/session-dynamic-profile.ts +0 -77
- package/src/memory/clarification-resolver.ts +0 -417
- package/src/memory/conflict-intent.ts +0 -205
- package/src/memory/conflict-policy.ts +0 -127
- package/src/memory/conflict-store.ts +0 -410
- package/src/memory/contradiction-checker.ts +0 -508
- package/src/memory/entity-extractor.ts +0 -535
- package/src/memory/format-recall.ts +0 -47
- package/src/memory/fts-reconciler.ts +0 -165
- package/src/memory/job-handlers/conflict.ts +0 -200
- package/src/memory/profile-compiler.ts +0 -195
- package/src/memory/recall-cache.ts +0 -117
- package/src/memory/search/entity.ts +0 -535
- package/src/memory/search/query-expansion.test.ts +0 -70
- package/src/memory/search/query-expansion.ts +0 -118
- package/src/runtime/routes/mcp-routes.ts +0 -20
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route handlers for memory item CRUD endpoints.
|
|
3
|
+
*
|
|
4
|
+
* GET /v1/memory-items — list memory items (with filtering, search, sort, pagination)
|
|
5
|
+
* GET /v1/memory-items/:id — get a single memory item
|
|
6
|
+
* POST /v1/memory-items — create a new memory item
|
|
7
|
+
* PATCH /v1/memory-items/:id — update an existing memory item
|
|
8
|
+
* DELETE /v1/memory-items/:id — delete a memory item and its embeddings
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { and, asc, count, desc, eq, like, ne, or } from "drizzle-orm";
|
|
12
|
+
import { v4 as uuid } from "uuid";
|
|
13
|
+
|
|
14
|
+
import { getDb } from "../../memory/db.js";
|
|
15
|
+
import { computeMemoryFingerprint } from "../../memory/fingerprint.js";
|
|
16
|
+
import { enqueueMemoryJob } from "../../memory/jobs-store.js";
|
|
17
|
+
import { memoryEmbeddings, memoryItems } from "../../memory/schema.js";
|
|
18
|
+
import { truncate } from "../../util/truncate.js";
|
|
19
|
+
import { httpError } from "../http-errors.js";
|
|
20
|
+
import type { RouteContext, RouteDefinition } from "../http-router.js";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Constants
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const VALID_KINDS = [
|
|
27
|
+
"identity",
|
|
28
|
+
"preference",
|
|
29
|
+
"project",
|
|
30
|
+
"decision",
|
|
31
|
+
"constraint",
|
|
32
|
+
"event",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
type MemoryItemKind = (typeof VALID_KINDS)[number];
|
|
36
|
+
|
|
37
|
+
const VALID_SORT_FIELDS = [
|
|
38
|
+
"lastSeenAt",
|
|
39
|
+
"importance",
|
|
40
|
+
"kind",
|
|
41
|
+
"firstSeenAt",
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
type SortField = (typeof VALID_SORT_FIELDS)[number];
|
|
45
|
+
|
|
46
|
+
const SORT_COLUMN_MAP = {
|
|
47
|
+
lastSeenAt: memoryItems.lastSeenAt,
|
|
48
|
+
importance: memoryItems.importance,
|
|
49
|
+
kind: memoryItems.kind,
|
|
50
|
+
firstSeenAt: memoryItems.firstSeenAt,
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Helpers
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
function isValidKind(value: string): value is MemoryItemKind {
|
|
58
|
+
return (VALID_KINDS as readonly string[]).includes(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isValidSortField(value: string): value is SortField {
|
|
62
|
+
return (VALID_SORT_FIELDS as readonly string[]).includes(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// GET /v1/memory-items
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
export function handleListMemoryItems(url: URL): Response {
|
|
70
|
+
const kindParam = url.searchParams.get("kind");
|
|
71
|
+
const statusParam = url.searchParams.get("status") ?? "active";
|
|
72
|
+
const searchParam = url.searchParams.get("search");
|
|
73
|
+
const sortParam = url.searchParams.get("sort") ?? "lastSeenAt";
|
|
74
|
+
const orderParam = url.searchParams.get("order") ?? "desc";
|
|
75
|
+
const limitParam = Number(url.searchParams.get("limit") ?? 100);
|
|
76
|
+
const offsetParam = Number(url.searchParams.get("offset") ?? 0);
|
|
77
|
+
|
|
78
|
+
if (kindParam && !isValidKind(kindParam)) {
|
|
79
|
+
return httpError(
|
|
80
|
+
"BAD_REQUEST",
|
|
81
|
+
`Invalid kind "${kindParam}". Must be one of: ${VALID_KINDS.join(", ")}`,
|
|
82
|
+
400,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!isValidSortField(sortParam)) {
|
|
87
|
+
return httpError(
|
|
88
|
+
"BAD_REQUEST",
|
|
89
|
+
`Invalid sort "${sortParam}". Must be one of: ${VALID_SORT_FIELDS.join(", ")}`,
|
|
90
|
+
400,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (orderParam !== "asc" && orderParam !== "desc") {
|
|
95
|
+
return httpError(
|
|
96
|
+
"BAD_REQUEST",
|
|
97
|
+
`Invalid order "${orderParam}". Must be "asc" or "desc"`,
|
|
98
|
+
400,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const db = getDb();
|
|
103
|
+
|
|
104
|
+
// Build WHERE conditions
|
|
105
|
+
const conditions = [];
|
|
106
|
+
if (statusParam && statusParam !== "all") {
|
|
107
|
+
conditions.push(eq(memoryItems.status, statusParam));
|
|
108
|
+
}
|
|
109
|
+
if (kindParam) {
|
|
110
|
+
conditions.push(eq(memoryItems.kind, kindParam));
|
|
111
|
+
}
|
|
112
|
+
if (searchParam) {
|
|
113
|
+
conditions.push(
|
|
114
|
+
or(
|
|
115
|
+
like(memoryItems.subject, `%${searchParam}%`),
|
|
116
|
+
like(memoryItems.statement, `%${searchParam}%`),
|
|
117
|
+
)!,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
|
122
|
+
|
|
123
|
+
// Count query
|
|
124
|
+
const countResult = db
|
|
125
|
+
.select({ count: count() })
|
|
126
|
+
.from(memoryItems)
|
|
127
|
+
.where(whereClause)
|
|
128
|
+
.get();
|
|
129
|
+
const total = countResult?.count ?? 0;
|
|
130
|
+
|
|
131
|
+
// Data query
|
|
132
|
+
const sortColumn = SORT_COLUMN_MAP[sortParam];
|
|
133
|
+
const orderFn = orderParam === "asc" ? asc : desc;
|
|
134
|
+
|
|
135
|
+
const items = db
|
|
136
|
+
.select()
|
|
137
|
+
.from(memoryItems)
|
|
138
|
+
.where(whereClause)
|
|
139
|
+
.orderBy(orderFn(sortColumn))
|
|
140
|
+
.limit(limitParam)
|
|
141
|
+
.offset(offsetParam)
|
|
142
|
+
.all();
|
|
143
|
+
|
|
144
|
+
return Response.json({ items, total });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// GET /v1/memory-items/:id
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
export function handleGetMemoryItem(ctx: RouteContext): Response {
|
|
152
|
+
const { id } = ctx.params;
|
|
153
|
+
const db = getDb();
|
|
154
|
+
|
|
155
|
+
const item = db
|
|
156
|
+
.select()
|
|
157
|
+
.from(memoryItems)
|
|
158
|
+
.where(eq(memoryItems.id, id))
|
|
159
|
+
.get();
|
|
160
|
+
|
|
161
|
+
if (!item) {
|
|
162
|
+
return httpError("NOT_FOUND", "Memory item not found", 404);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let supersedesSubject: string | undefined;
|
|
166
|
+
let supersededBySubject: string | undefined;
|
|
167
|
+
|
|
168
|
+
if (item.supersedes) {
|
|
169
|
+
const superseded = db
|
|
170
|
+
.select({ subject: memoryItems.subject })
|
|
171
|
+
.from(memoryItems)
|
|
172
|
+
.where(eq(memoryItems.id, item.supersedes))
|
|
173
|
+
.get();
|
|
174
|
+
supersedesSubject = superseded?.subject;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (item.supersededBy) {
|
|
178
|
+
const superseding = db
|
|
179
|
+
.select({ subject: memoryItems.subject })
|
|
180
|
+
.from(memoryItems)
|
|
181
|
+
.where(eq(memoryItems.id, item.supersededBy))
|
|
182
|
+
.get();
|
|
183
|
+
supersededBySubject = superseding?.subject;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return Response.json({
|
|
187
|
+
item: {
|
|
188
|
+
...item,
|
|
189
|
+
...(supersedesSubject !== undefined ? { supersedesSubject } : {}),
|
|
190
|
+
...(supersededBySubject !== undefined ? { supersededBySubject } : {}),
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// POST /v1/memory-items
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
export async function handleCreateMemoryItem(
|
|
200
|
+
ctx: RouteContext,
|
|
201
|
+
): Promise<Response> {
|
|
202
|
+
const body = (await ctx.req.json()) as {
|
|
203
|
+
kind?: string;
|
|
204
|
+
subject?: string;
|
|
205
|
+
statement?: string;
|
|
206
|
+
importance?: number;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const { kind, subject, statement, importance } = body;
|
|
210
|
+
|
|
211
|
+
// Validate kind
|
|
212
|
+
if (typeof kind !== "string" || !isValidKind(kind)) {
|
|
213
|
+
return httpError(
|
|
214
|
+
"BAD_REQUEST",
|
|
215
|
+
`kind is required and must be one of: ${VALID_KINDS.join(", ")}`,
|
|
216
|
+
400,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Validate subject
|
|
221
|
+
if (typeof subject !== "string" || subject.trim().length === 0) {
|
|
222
|
+
return httpError(
|
|
223
|
+
"BAD_REQUEST",
|
|
224
|
+
"subject is required and must be a non-empty string",
|
|
225
|
+
400,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Validate statement
|
|
230
|
+
if (typeof statement !== "string" || statement.trim().length === 0) {
|
|
231
|
+
return httpError(
|
|
232
|
+
"BAD_REQUEST",
|
|
233
|
+
"statement is required and must be a non-empty string",
|
|
234
|
+
400,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const trimmedSubject = truncate(subject.trim(), 80, "");
|
|
239
|
+
const trimmedStatement = truncate(statement.trim(), 500, "");
|
|
240
|
+
|
|
241
|
+
const scopeId = "default";
|
|
242
|
+
const fingerprint = computeMemoryFingerprint(
|
|
243
|
+
scopeId,
|
|
244
|
+
kind,
|
|
245
|
+
trimmedSubject,
|
|
246
|
+
trimmedStatement,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const db = getDb();
|
|
250
|
+
|
|
251
|
+
// Check for existing item with same fingerprint + scopeId
|
|
252
|
+
const existing = db
|
|
253
|
+
.select()
|
|
254
|
+
.from(memoryItems)
|
|
255
|
+
.where(
|
|
256
|
+
and(
|
|
257
|
+
eq(memoryItems.fingerprint, fingerprint),
|
|
258
|
+
eq(memoryItems.scopeId, scopeId),
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
.get();
|
|
262
|
+
|
|
263
|
+
if (existing) {
|
|
264
|
+
return httpError(
|
|
265
|
+
"CONFLICT",
|
|
266
|
+
"A memory with this content already exists",
|
|
267
|
+
409,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const id = uuid();
|
|
272
|
+
const now = Date.now();
|
|
273
|
+
|
|
274
|
+
db.insert(memoryItems)
|
|
275
|
+
.values({
|
|
276
|
+
id,
|
|
277
|
+
kind,
|
|
278
|
+
subject: trimmedSubject,
|
|
279
|
+
statement: trimmedStatement,
|
|
280
|
+
status: "active",
|
|
281
|
+
confidence: 0.95,
|
|
282
|
+
importance: importance ?? 0.8,
|
|
283
|
+
fingerprint,
|
|
284
|
+
verificationState: "user_confirmed",
|
|
285
|
+
scopeId,
|
|
286
|
+
firstSeenAt: now,
|
|
287
|
+
lastSeenAt: now,
|
|
288
|
+
lastUsedAt: null,
|
|
289
|
+
overrideConfidence: "explicit",
|
|
290
|
+
})
|
|
291
|
+
.run();
|
|
292
|
+
|
|
293
|
+
enqueueMemoryJob("embed_item", { itemId: id });
|
|
294
|
+
|
|
295
|
+
// Fetch the inserted row to return it
|
|
296
|
+
const insertedRow = db
|
|
297
|
+
.select()
|
|
298
|
+
.from(memoryItems)
|
|
299
|
+
.where(eq(memoryItems.id, id))
|
|
300
|
+
.get();
|
|
301
|
+
|
|
302
|
+
return Response.json({ item: insertedRow }, { status: 201 });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// PATCH /v1/memory-items/:id
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
export async function handleUpdateMemoryItem(
|
|
310
|
+
ctx: RouteContext,
|
|
311
|
+
): Promise<Response> {
|
|
312
|
+
const { id } = ctx.params;
|
|
313
|
+
const body = (await ctx.req.json()) as {
|
|
314
|
+
subject?: string;
|
|
315
|
+
statement?: string;
|
|
316
|
+
kind?: string;
|
|
317
|
+
status?: string;
|
|
318
|
+
importance?: number;
|
|
319
|
+
verificationState?: string;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const db = getDb();
|
|
323
|
+
|
|
324
|
+
const existing = db
|
|
325
|
+
.select()
|
|
326
|
+
.from(memoryItems)
|
|
327
|
+
.where(eq(memoryItems.id, id))
|
|
328
|
+
.get();
|
|
329
|
+
|
|
330
|
+
if (!existing) {
|
|
331
|
+
return httpError("NOT_FOUND", "Memory item not found", 404);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Build the update set with only provided fields
|
|
335
|
+
const set: Record<string, unknown> = {
|
|
336
|
+
lastSeenAt: Date.now(),
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
if (body.subject !== undefined) {
|
|
340
|
+
if (typeof body.subject !== "string") {
|
|
341
|
+
return httpError("BAD_REQUEST", "subject must be a string", 400);
|
|
342
|
+
}
|
|
343
|
+
set.subject = truncate(body.subject.trim(), 80, "");
|
|
344
|
+
}
|
|
345
|
+
if (body.statement !== undefined) {
|
|
346
|
+
if (typeof body.statement !== "string") {
|
|
347
|
+
return httpError("BAD_REQUEST", "statement must be a string", 400);
|
|
348
|
+
}
|
|
349
|
+
set.statement = truncate(body.statement.trim(), 500, "");
|
|
350
|
+
}
|
|
351
|
+
if (body.kind !== undefined) {
|
|
352
|
+
if (!isValidKind(body.kind)) {
|
|
353
|
+
return httpError(
|
|
354
|
+
"BAD_REQUEST",
|
|
355
|
+
`Invalid kind "${body.kind}". Must be one of: ${VALID_KINDS.join(", ")}`,
|
|
356
|
+
400,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
set.kind = body.kind;
|
|
360
|
+
}
|
|
361
|
+
if (body.status !== undefined) {
|
|
362
|
+
set.status = body.status;
|
|
363
|
+
}
|
|
364
|
+
if (body.importance !== undefined) {
|
|
365
|
+
set.importance = body.importance;
|
|
366
|
+
}
|
|
367
|
+
if (body.verificationState !== undefined) {
|
|
368
|
+
set.verificationState = body.verificationState;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// If subject, statement, or kind changed, recompute fingerprint
|
|
372
|
+
const contentChanged =
|
|
373
|
+
body.subject !== undefined ||
|
|
374
|
+
body.statement !== undefined ||
|
|
375
|
+
body.kind !== undefined;
|
|
376
|
+
|
|
377
|
+
if (contentChanged) {
|
|
378
|
+
const newSubject = (set.subject as string | undefined) ?? existing.subject;
|
|
379
|
+
const newStatement =
|
|
380
|
+
(set.statement as string | undefined) ?? existing.statement;
|
|
381
|
+
const newKind = (set.kind as string | undefined) ?? existing.kind;
|
|
382
|
+
const scopeId = existing.scopeId;
|
|
383
|
+
|
|
384
|
+
const fingerprint = computeMemoryFingerprint(
|
|
385
|
+
scopeId,
|
|
386
|
+
newKind,
|
|
387
|
+
newSubject,
|
|
388
|
+
newStatement,
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// Check for collision (exclude self)
|
|
392
|
+
const collision = db
|
|
393
|
+
.select({ id: memoryItems.id })
|
|
394
|
+
.from(memoryItems)
|
|
395
|
+
.where(
|
|
396
|
+
and(
|
|
397
|
+
eq(memoryItems.fingerprint, fingerprint),
|
|
398
|
+
eq(memoryItems.scopeId, scopeId),
|
|
399
|
+
ne(memoryItems.id, id),
|
|
400
|
+
),
|
|
401
|
+
)
|
|
402
|
+
.get();
|
|
403
|
+
|
|
404
|
+
if (collision) {
|
|
405
|
+
return httpError(
|
|
406
|
+
"CONFLICT",
|
|
407
|
+
"Another memory item with this content already exists",
|
|
408
|
+
409,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
set.fingerprint = fingerprint;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
db.update(memoryItems).set(set).where(eq(memoryItems.id, id)).run();
|
|
416
|
+
|
|
417
|
+
// If statement changed, enqueue embed job
|
|
418
|
+
if (body.statement !== undefined) {
|
|
419
|
+
enqueueMemoryJob("embed_item", { itemId: id });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Fetch and return the updated row
|
|
423
|
+
const updatedRow = db
|
|
424
|
+
.select()
|
|
425
|
+
.from(memoryItems)
|
|
426
|
+
.where(eq(memoryItems.id, id))
|
|
427
|
+
.get();
|
|
428
|
+
|
|
429
|
+
return Response.json({ item: updatedRow });
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
// DELETE /v1/memory-items/:id
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
export async function handleDeleteMemoryItem(
|
|
437
|
+
ctx: RouteContext,
|
|
438
|
+
): Promise<Response> {
|
|
439
|
+
const { id } = ctx.params;
|
|
440
|
+
const db = getDb();
|
|
441
|
+
|
|
442
|
+
const existing = db
|
|
443
|
+
.select()
|
|
444
|
+
.from(memoryItems)
|
|
445
|
+
.where(eq(memoryItems.id, id))
|
|
446
|
+
.get();
|
|
447
|
+
|
|
448
|
+
if (!existing) {
|
|
449
|
+
return httpError("NOT_FOUND", "Memory item not found", 404);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Delete embeddings for this item
|
|
453
|
+
db.delete(memoryEmbeddings)
|
|
454
|
+
.where(
|
|
455
|
+
and(
|
|
456
|
+
eq(memoryEmbeddings.targetType, "item"),
|
|
457
|
+
eq(memoryEmbeddings.targetId, id),
|
|
458
|
+
),
|
|
459
|
+
)
|
|
460
|
+
.run();
|
|
461
|
+
|
|
462
|
+
// Delete the item (cascades memoryItemSources)
|
|
463
|
+
db.delete(memoryItems).where(eq(memoryItems.id, id)).run();
|
|
464
|
+
|
|
465
|
+
return new Response(null, { status: 204 });
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
// Route definitions
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
export function memoryItemRouteDefinitions(): RouteDefinition[] {
|
|
473
|
+
return [
|
|
474
|
+
{
|
|
475
|
+
endpoint: "memory-items",
|
|
476
|
+
method: "GET",
|
|
477
|
+
handler: (ctx) => handleListMemoryItems(ctx.url),
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
endpoint: "memory-items/:id",
|
|
481
|
+
method: "GET",
|
|
482
|
+
policyKey: "memory-items",
|
|
483
|
+
handler: (ctx) => handleGetMemoryItem(ctx),
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
endpoint: "memory-items",
|
|
487
|
+
method: "POST",
|
|
488
|
+
handler: (ctx) => handleCreateMemoryItem(ctx),
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
endpoint: "memory-items/:id",
|
|
492
|
+
method: "PATCH",
|
|
493
|
+
policyKey: "memory-items",
|
|
494
|
+
handler: (ctx) => handleUpdateMemoryItem(ctx),
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
endpoint: "memory-items/:id",
|
|
498
|
+
method: "DELETE",
|
|
499
|
+
policyKey: "memory-items",
|
|
500
|
+
handler: (ctx) => handleDeleteMemoryItem(ctx),
|
|
501
|
+
},
|
|
502
|
+
];
|
|
503
|
+
}
|
|
@@ -29,7 +29,7 @@ export interface SessionManagementDeps {
|
|
|
29
29
|
renameSession: (sessionId: string, name: string) => boolean;
|
|
30
30
|
clearAllSessions: () => number;
|
|
31
31
|
cancelGeneration: (sessionId: string) => boolean;
|
|
32
|
-
undoLastMessage: (sessionId: string) => { removedCount: number } | null
|
|
32
|
+
undoLastMessage: (sessionId: string) => Promise<{ removedCount: number } | null>;
|
|
33
33
|
regenerateResponse: (
|
|
34
34
|
sessionId: string,
|
|
35
35
|
) => Promise<{ requestId: string } | null>;
|
|
@@ -115,8 +115,8 @@ export function sessionManagementRouteDefinitions(
|
|
|
115
115
|
endpoint: "conversations/:id/undo",
|
|
116
116
|
method: "POST",
|
|
117
117
|
policyKey: "conversations/undo",
|
|
118
|
-
handler: ({ params }) => {
|
|
119
|
-
const result = deps.undoLastMessage(params.id);
|
|
118
|
+
handler: async ({ params }) => {
|
|
119
|
+
const result = await deps.undoLastMessage(params.id);
|
|
120
120
|
if (!result) {
|
|
121
121
|
return httpError(
|
|
122
122
|
"NOT_FOUND",
|
|
@@ -160,7 +160,7 @@ async function handleOAuthConnectStart(body: {
|
|
|
160
160
|
const app = getApp(conn.oauthAppId);
|
|
161
161
|
if (app) {
|
|
162
162
|
clientId = app.clientId;
|
|
163
|
-
clientSecret = getSecureKey(
|
|
163
|
+
clientSecret = getSecureKey(app.clientSecretCredentialPath);
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
@@ -170,7 +170,7 @@ async function handleOAuthConnectStart(body: {
|
|
|
170
170
|
if (dbApp) {
|
|
171
171
|
clientId = dbApp.clientId;
|
|
172
172
|
if (!clientSecret) {
|
|
173
|
-
clientSecret = getSecureKey(
|
|
173
|
+
clientSecret = getSecureKey(dbApp.clientSecretCredentialPath);
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
}
|
|
@@ -48,6 +48,13 @@ export async function handleAddTrustRuleManage(
|
|
|
48
48
|
if (!toolName || typeof toolName !== "string") {
|
|
49
49
|
return httpError("BAD_REQUEST", "toolName is required", 400);
|
|
50
50
|
}
|
|
51
|
+
if (toolName.startsWith("__internal:")) {
|
|
52
|
+
return httpError(
|
|
53
|
+
"BAD_REQUEST",
|
|
54
|
+
"toolName must not start with __internal:",
|
|
55
|
+
400,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
51
58
|
if (!pattern || typeof pattern !== "string") {
|
|
52
59
|
return httpError("BAD_REQUEST", "pattern is required", 400);
|
|
53
60
|
}
|
|
@@ -124,6 +131,13 @@ export async function handleUpdateTrustRuleManage(
|
|
|
124
131
|
priority?: number;
|
|
125
132
|
};
|
|
126
133
|
|
|
134
|
+
if (typeof body.tool === "string" && body.tool.startsWith("__internal:")) {
|
|
135
|
+
return httpError(
|
|
136
|
+
"BAD_REQUEST",
|
|
137
|
+
"tool must not start with __internal:",
|
|
138
|
+
400,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
127
141
|
if (body.decision !== undefined) {
|
|
128
142
|
const validDecisions = ["allow", "deny", "ask"] as const;
|
|
129
143
|
if (
|
|
@@ -33,6 +33,7 @@ interface TreeEntry {
|
|
|
33
33
|
|
|
34
34
|
function handleWorkspaceTree(ctx: RouteContext): Response {
|
|
35
35
|
const requestedPath = ctx.url.searchParams.get("path") ?? "";
|
|
36
|
+
const showHidden = ctx.url.searchParams.get("showHidden") === "true";
|
|
36
37
|
const resolved = resolveWorkspacePath(requestedPath);
|
|
37
38
|
if (resolved === undefined) {
|
|
38
39
|
return httpError("BAD_REQUEST", "Invalid path", 400);
|
|
@@ -45,7 +46,7 @@ function handleWorkspaceTree(ctx: RouteContext): Response {
|
|
|
45
46
|
const entries: TreeEntry[] = [];
|
|
46
47
|
for (const entry of dirents) {
|
|
47
48
|
// Filter out dotfiles/directories (.env, .git, .private, etc.)
|
|
48
|
-
if (entry.name.startsWith(".")) continue;
|
|
49
|
+
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
49
50
|
|
|
50
51
|
const fullPath = join(resolved, entry.name);
|
|
51
52
|
|
|
@@ -33,11 +33,18 @@ const REQUEST_TIMEOUT_MS = 5_000;
|
|
|
33
33
|
* back); `{ found: false }` means the key doesn't exist in the keychain. */
|
|
34
34
|
export type BrokerGetResult = { found: boolean; value?: string } | null;
|
|
35
35
|
|
|
36
|
+
/** Result of a `set()` call — distinguishes broker-unreachable from an active
|
|
37
|
+
* rejection so callers can log meaningful diagnostics. */
|
|
38
|
+
export type BrokerSetResult =
|
|
39
|
+
| { status: "ok" }
|
|
40
|
+
| { status: "unreachable" }
|
|
41
|
+
| { status: "rejected"; code: string; message: string };
|
|
42
|
+
|
|
36
43
|
export interface KeychainBrokerClient {
|
|
37
44
|
isAvailable(): boolean;
|
|
38
45
|
ping(): Promise<{ pong: boolean } | null>;
|
|
39
46
|
get(account: string): Promise<BrokerGetResult>;
|
|
40
|
-
set(account: string, value: string): Promise<
|
|
47
|
+
set(account: string, value: string): Promise<BrokerSetResult>;
|
|
41
48
|
del(account: string): Promise<boolean>;
|
|
42
49
|
list(): Promise<string[]>;
|
|
43
50
|
}
|
|
@@ -360,12 +367,18 @@ export function createBrokerClient(): KeychainBrokerClient {
|
|
|
360
367
|
}
|
|
361
368
|
},
|
|
362
369
|
|
|
363
|
-
async set(account: string, value: string): Promise<
|
|
370
|
+
async set(account: string, value: string): Promise<BrokerSetResult> {
|
|
364
371
|
try {
|
|
365
372
|
const response = await doRequest("key.set", { account, value });
|
|
366
|
-
|
|
373
|
+
if (!response) return { status: "unreachable" };
|
|
374
|
+
if (response.ok) return { status: "ok" };
|
|
375
|
+
return {
|
|
376
|
+
status: "rejected",
|
|
377
|
+
code: response.error?.code ?? "UNKNOWN",
|
|
378
|
+
message: response.error?.message ?? "unknown error",
|
|
379
|
+
};
|
|
367
380
|
} catch {
|
|
368
|
-
return
|
|
381
|
+
return { status: "unreachable" };
|
|
369
382
|
}
|
|
370
383
|
},
|
|
371
384
|
|
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
* encrypted store (startup code paths cannot do async I/O).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { getLogger } from "../util/logger.js";
|
|
10
11
|
import * as encryptedStore from "./encrypted-store.js";
|
|
11
12
|
import type { KeychainBrokerClient } from "./keychain-broker-client.js";
|
|
12
13
|
import { createBrokerClient } from "./keychain-broker-client.js";
|
|
13
14
|
|
|
15
|
+
const log = getLogger("secure-keys");
|
|
16
|
+
|
|
14
17
|
let _broker: KeychainBrokerClient | undefined;
|
|
15
18
|
|
|
16
19
|
function getBroker(): KeychainBrokerClient {
|
|
@@ -120,13 +123,32 @@ export async function setSecureKeyAsync(
|
|
|
120
123
|
): Promise<boolean> {
|
|
121
124
|
const broker = getBroker();
|
|
122
125
|
if (broker.isAvailable()) {
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
126
|
+
const result = await broker.set(account, value);
|
|
127
|
+
if (result.status !== "ok") {
|
|
128
|
+
log.warn(
|
|
129
|
+
{
|
|
130
|
+
account,
|
|
131
|
+
brokerStatus: result.status,
|
|
132
|
+
...(result.status === "rejected"
|
|
133
|
+
? { brokerCode: result.code, brokerMessage: result.message }
|
|
134
|
+
: {}),
|
|
135
|
+
},
|
|
136
|
+
"Broker set failed for secure key",
|
|
137
|
+
);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
125
140
|
// Broker succeeded — also persist to encrypted store for sync callers.
|
|
126
141
|
const encOk = encryptedStore.setKey(account, value);
|
|
142
|
+
if (!encOk) {
|
|
143
|
+
log.warn({ account }, "Encrypted store set failed after broker success");
|
|
144
|
+
}
|
|
127
145
|
return encOk;
|
|
128
146
|
}
|
|
129
|
-
|
|
147
|
+
const encOk = encryptedStore.setKey(account, value);
|
|
148
|
+
if (!encOk) {
|
|
149
|
+
log.warn({ account }, "Encrypted store set failed (broker unavailable)");
|
|
150
|
+
}
|
|
151
|
+
return encOk;
|
|
130
152
|
}
|
|
131
153
|
|
|
132
154
|
/**
|