@vellumai/assistant 0.10.3-dev.202606292136.ce8bf33 → 0.10.3-dev.202606292252.adea010
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/openapi.yaml +10 -0
- package/package.json +1 -1
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +22 -22
- package/src/__tests__/conversation-queue.test.ts +43 -0
- package/src/__tests__/job-handler-registry-guard.test.ts +104 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +18 -0
- package/src/__tests__/schedule-retry.test.ts +51 -51
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +45 -41
- package/src/__tests__/schedule-routes.test.ts +192 -182
- package/src/__tests__/schedule-store.test.ts +172 -170
- package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
- package/src/__tests__/scheduler-recurrence.test.ts +12 -12
- package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -10
- package/src/__tests__/scheduler-wake.test.ts +6 -6
- package/src/__tests__/task-scheduler.test.ts +22 -22
- package/src/__tests__/tool-executor-redaction.test.ts +11 -0
- package/src/acp/__tests__/client-handler.test.ts +333 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +43 -0
- package/src/acp/client-handler.ts +65 -2
- package/src/acp/session-manager.ts +12 -1
- package/src/api/events/acp-session-update.test.ts +52 -0
- package/src/api/events/acp-session-update.ts +3 -0
- package/src/api/responses/conversation-message.ts +14 -0
- package/src/background-wake/wake-intent-hooks.test.ts +3 -3
- package/src/daemon/conversation-process.ts +13 -11
- package/src/daemon/external-plugins-bootstrap.ts +22 -0
- package/src/daemon/lifecycle.ts +1 -1
- package/src/daemon/message-types/acp.ts +4 -0
- package/src/daemon/shutdown-handlers.ts +148 -147
- package/src/jobs/register-job-handlers.ts +24 -167
- package/src/memory/memory-retrospective-job.ts +5 -5
- package/src/memory/memory-retrospective-state.ts +7 -7
- package/src/notifications/conversation-pairing.ts +23 -14
- package/src/persistence/conversation-bootstrap.ts +24 -15
- package/src/persistence/conversation-crud.ts +68 -59
- package/src/persistence/migrations/309-drop-redundant-indexes.ts +60 -0
- package/src/persistence/schema/conversations.ts +0 -1
- package/src/persistence/schema/infrastructure.ts +0 -1
- package/src/persistence/steps.ts +2 -0
- package/src/plugins/defaults/index.ts +28 -0
- package/src/plugins/defaults/memory/job-handlers.ts +203 -0
- package/src/plugins/job-handler-registry.ts +94 -0
- package/src/plugins/types.ts +26 -0
- package/src/runtime/background-job-runner.ts +9 -7
- package/src/runtime/routes/acp-routes-event-log.test.ts +51 -0
- package/src/runtime/routes/conversation-cli-routes.ts +5 -1
- package/src/runtime/routes/conversation-management-routes.ts +6 -6
- package/src/runtime/routes/conversation-routes.ts +12 -0
- package/src/runtime/routes/conversations-import-routes.ts +5 -1
- package/src/runtime/routes/defer-routes.ts +3 -3
- package/src/runtime/routes/home-feed-routes.ts +9 -4
- package/src/runtime/routes/schedule-routes.ts +31 -28
- package/src/runtime/routes/wipe-conversation-routes.ts +1 -1
- package/src/runtime/services/analyze-conversation.ts +18 -9
- package/src/schedule/retry-policy.ts +8 -8
- package/src/schedule/schedule-recovery.ts +8 -5
- package/src/schedule/schedule-store.ts +124 -109
- package/src/schedule/scheduler.ts +64 -52
- package/src/security/redaction.ts +18 -14
- package/src/subagent/manager.ts +1 -1
- package/src/tasks/task-runner.ts +1 -1
- package/src/tools/schedule/create.ts +2 -2
- package/src/tools/schedule/delete.ts +1 -1
- package/src/tools/schedule/update.ts +1 -1
- package/src/util/sqlite-retry.ts +8 -41
package/openapi.yaml
CHANGED
|
@@ -17599,6 +17599,16 @@ paths:
|
|
|
17599
17599
|
- label
|
|
17600
17600
|
- status
|
|
17601
17601
|
additionalProperties: false
|
|
17602
|
+
acpNotification:
|
|
17603
|
+
type: object
|
|
17604
|
+
properties:
|
|
17605
|
+
acpSessionId:
|
|
17606
|
+
type: string
|
|
17607
|
+
agent:
|
|
17608
|
+
type: string
|
|
17609
|
+
required:
|
|
17610
|
+
- acpSessionId
|
|
17611
|
+
additionalProperties: false
|
|
17602
17612
|
slackMessage:
|
|
17603
17613
|
type: object
|
|
17604
17614
|
properties:
|
package/package.json
CHANGED
|
@@ -69,8 +69,8 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
|
|
|
69
69
|
getRawDb().run("DELETE FROM conversations");
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
test("deleting a conversation with a scheduleJobId removes the schedule", () => {
|
|
73
|
-
const schedule = createSchedule({
|
|
72
|
+
test("deleting a conversation with a scheduleJobId removes the schedule", async () => {
|
|
73
|
+
const schedule = await createSchedule({
|
|
74
74
|
name: "Daily standup",
|
|
75
75
|
expression: "0 9 * * 1-5",
|
|
76
76
|
message: "Time for standup!",
|
|
@@ -83,7 +83,7 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
|
|
|
83
83
|
|
|
84
84
|
expect(getSchedule(schedule.id)).not.toBeNull();
|
|
85
85
|
|
|
86
|
-
deleteRoute.handler({
|
|
86
|
+
await deleteRoute.handler({
|
|
87
87
|
pathParams: { id: conv.id },
|
|
88
88
|
body: {},
|
|
89
89
|
headers: {},
|
|
@@ -93,8 +93,8 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
|
|
|
93
93
|
expect(getConversation(conv.id)).toBeNull();
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
test("deleting a conversation without a scheduleJobId does not affect schedules", () => {
|
|
97
|
-
const schedule = createSchedule({
|
|
96
|
+
test("deleting a conversation without a scheduleJobId does not affect schedules", async () => {
|
|
97
|
+
const schedule = await createSchedule({
|
|
98
98
|
name: "Unrelated schedule",
|
|
99
99
|
expression: "0 12 * * *",
|
|
100
100
|
message: "Noon check",
|
|
@@ -102,7 +102,7 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
|
|
|
102
102
|
|
|
103
103
|
const conv = createConversation("no-schedule-conv");
|
|
104
104
|
|
|
105
|
-
deleteRoute.handler({
|
|
105
|
+
await deleteRoute.handler({
|
|
106
106
|
pathParams: { id: conv.id },
|
|
107
107
|
body: {},
|
|
108
108
|
headers: {},
|
|
@@ -112,8 +112,8 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
|
|
|
112
112
|
expect(getConversation(conv.id)).toBeNull();
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
-
test("deleting a conversation with a schedule also removes its cron_runs", () => {
|
|
116
|
-
const schedule = createSchedule({
|
|
115
|
+
test("deleting a conversation with a schedule also removes its cron_runs", async () => {
|
|
116
|
+
const schedule = await createSchedule({
|
|
117
117
|
name: "Recurring job",
|
|
118
118
|
expression: "0 9 * * *",
|
|
119
119
|
message: "Daily task",
|
|
@@ -137,7 +137,7 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
|
|
|
137
137
|
.get();
|
|
138
138
|
expect(runBefore).not.toBeNull();
|
|
139
139
|
|
|
140
|
-
deleteRoute.handler({
|
|
140
|
+
await deleteRoute.handler({
|
|
141
141
|
pathParams: { id: conv.id },
|
|
142
142
|
body: {},
|
|
143
143
|
headers: {},
|
|
@@ -150,8 +150,8 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
|
|
|
150
150
|
expect(runAfter).toBeNull();
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
-
test("deleting one of multiple conversations sharing a schedule preserves the schedule", () => {
|
|
154
|
-
const schedule = createSchedule({
|
|
153
|
+
test("deleting one of multiple conversations sharing a schedule preserves the schedule", async () => {
|
|
154
|
+
const schedule = await createSchedule({
|
|
155
155
|
name: "Recurring daily",
|
|
156
156
|
expression: "0 9 * * *",
|
|
157
157
|
message: "Daily task",
|
|
@@ -166,7 +166,7 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
|
|
|
166
166
|
scheduleJobId: schedule.id,
|
|
167
167
|
});
|
|
168
168
|
|
|
169
|
-
deleteRoute.handler({
|
|
169
|
+
await deleteRoute.handler({
|
|
170
170
|
pathParams: { id: conv1.id },
|
|
171
171
|
body: {},
|
|
172
172
|
headers: {},
|
|
@@ -175,13 +175,13 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
|
|
|
175
175
|
expect(getSchedule(schedule.id)).not.toBeNull();
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
-
test("deleting one scheduled conversation does not affect other schedules", () => {
|
|
179
|
-
const scheduleA = createSchedule({
|
|
178
|
+
test("deleting one scheduled conversation does not affect other schedules", async () => {
|
|
179
|
+
const scheduleA = await createSchedule({
|
|
180
180
|
name: "Schedule A",
|
|
181
181
|
expression: "0 9 * * *",
|
|
182
182
|
message: "Task A",
|
|
183
183
|
});
|
|
184
|
-
const scheduleB = createSchedule({
|
|
184
|
+
const scheduleB = await createSchedule({
|
|
185
185
|
name: "Schedule B",
|
|
186
186
|
expression: "0 17 * * *",
|
|
187
187
|
message: "Task B",
|
|
@@ -196,7 +196,7 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
|
|
|
196
196
|
scheduleJobId: scheduleB.id,
|
|
197
197
|
});
|
|
198
198
|
|
|
199
|
-
deleteRoute.handler({
|
|
199
|
+
await deleteRoute.handler({
|
|
200
200
|
pathParams: { id: convA.id },
|
|
201
201
|
body: {},
|
|
202
202
|
headers: {},
|
|
@@ -222,8 +222,8 @@ describe("POST /conversations/:id/wipe — schedule cleanup", () => {
|
|
|
222
222
|
getRawDb().run("DELETE FROM conversations");
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
-
test("wiping a conversation with a scheduleJobId removes the schedule", () => {
|
|
226
|
-
const schedule = createSchedule({
|
|
225
|
+
test("wiping a conversation with a scheduleJobId removes the schedule", async () => {
|
|
226
|
+
const schedule = await createSchedule({
|
|
227
227
|
name: "Wipe-test schedule",
|
|
228
228
|
expression: "0 9 * * 1-5",
|
|
229
229
|
message: "Time for standup!",
|
|
@@ -236,7 +236,7 @@ describe("POST /conversations/:id/wipe — schedule cleanup", () => {
|
|
|
236
236
|
|
|
237
237
|
expect(getSchedule(schedule.id)).not.toBeNull();
|
|
238
238
|
|
|
239
|
-
wipeRoute.handler({
|
|
239
|
+
await wipeRoute.handler({
|
|
240
240
|
pathParams: { id: conv.id },
|
|
241
241
|
body: {},
|
|
242
242
|
headers: {},
|
|
@@ -245,8 +245,8 @@ describe("POST /conversations/:id/wipe — schedule cleanup", () => {
|
|
|
245
245
|
expect(getSchedule(schedule.id)).toBeNull();
|
|
246
246
|
});
|
|
247
247
|
|
|
248
|
-
test("wiping a conversation without a scheduleJobId does not affect schedules", () => {
|
|
249
|
-
const schedule = createSchedule({
|
|
248
|
+
test("wiping a conversation without a scheduleJobId does not affect schedules", async () => {
|
|
249
|
+
const schedule = await createSchedule({
|
|
250
250
|
name: "Unrelated schedule",
|
|
251
251
|
expression: "0 12 * * *",
|
|
252
252
|
message: "Noon check",
|
|
@@ -254,7 +254,7 @@ describe("POST /conversations/:id/wipe — schedule cleanup", () => {
|
|
|
254
254
|
|
|
255
255
|
const conv = createConversation("no-schedule-wipe");
|
|
256
256
|
|
|
257
|
-
wipeRoute.handler({
|
|
257
|
+
await wipeRoute.handler({
|
|
258
258
|
pathParams: { id: conv.id },
|
|
259
259
|
body: {},
|
|
260
260
|
headers: {},
|
|
@@ -3004,4 +3004,47 @@ describe("subagent notification user_message_echo suppression", () => {
|
|
|
3004
3004
|
await resolveRun(1);
|
|
3005
3005
|
await new Promise((r) => setTimeout(r, 10));
|
|
3006
3006
|
});
|
|
3007
|
+
|
|
3008
|
+
test("drained acp-notification message persists and wakes the agent but emits no user_message_echo", async () => {
|
|
3009
|
+
const conversation = makeConversation();
|
|
3010
|
+
await conversation.loadFromDb();
|
|
3011
|
+
|
|
3012
|
+
const events1: ServerMessage[] = [];
|
|
3013
|
+
const eventsNotif: ServerMessage[] = [];
|
|
3014
|
+
|
|
3015
|
+
// Occupy the conversation so the injected notification queues.
|
|
3016
|
+
const p1 = conversation.processMessage({
|
|
3017
|
+
content: "msg-1",
|
|
3018
|
+
attachments: [],
|
|
3019
|
+
onEvent: (e) => events1.push(e),
|
|
3020
|
+
requestId: "req-1",
|
|
3021
|
+
});
|
|
3022
|
+
await waitForPendingRun(1);
|
|
3023
|
+
|
|
3024
|
+
// A daemon-injected ACP completion notification carries `acpNotification`.
|
|
3025
|
+
conversation.enqueueMessage({
|
|
3026
|
+
content: '[ACP agent "claude" completed]',
|
|
3027
|
+
onEvent: (e) => eventsNotif.push(e),
|
|
3028
|
+
requestId: "req-acp-notif",
|
|
3029
|
+
metadata: {
|
|
3030
|
+
acpNotification: { acpSessionId: "acp-1", agent: "claude" },
|
|
3031
|
+
},
|
|
3032
|
+
});
|
|
3033
|
+
|
|
3034
|
+
await resolveRun(0);
|
|
3035
|
+
await p1;
|
|
3036
|
+
await waitForPendingRun(2);
|
|
3037
|
+
|
|
3038
|
+
// Still persisted (so the orchestrator LLM sees it) and still wakes the
|
|
3039
|
+
// agent...
|
|
3040
|
+
expect(
|
|
3041
|
+
capturedAddMessages.some((m) => m.content.includes("ACP agent")),
|
|
3042
|
+
).toBe(true);
|
|
3043
|
+
expect(pendingRuns.length).toBe(2);
|
|
3044
|
+
// ...but no user_message_echo, so the client never renders a live bubble.
|
|
3045
|
+
expect(eventsNotif.some((e) => e.type === "user_message_echo")).toBe(false);
|
|
3046
|
+
|
|
3047
|
+
await resolveRun(1);
|
|
3048
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
3049
|
+
});
|
|
3007
3050
|
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { registerMemoryJobHandlers } from "../jobs/register-job-handlers.js";
|
|
4
|
+
import * as jobsWorker from "../persistence/jobs-worker.js";
|
|
5
|
+
import { registerDefaultPluginJobHandlers } from "../plugins/defaults/index.js";
|
|
6
|
+
import {
|
|
7
|
+
clearJobHandlerRegistry,
|
|
8
|
+
getRegisteredJobHandlers,
|
|
9
|
+
registerPluginJobHandlers,
|
|
10
|
+
} from "../plugins/job-handler-registry.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The exact job types the `default-memory` plugin contributes via its
|
|
14
|
+
* `jobHandlers` field. Locks the contribution against an accidental add/drop —
|
|
15
|
+
* the job-handler analog of the injector order guard.
|
|
16
|
+
*/
|
|
17
|
+
const MEMORY_JOB_TYPES = [
|
|
18
|
+
"embed_segment",
|
|
19
|
+
"embed_summary",
|
|
20
|
+
"backfill",
|
|
21
|
+
"rebuild_index",
|
|
22
|
+
"delete_qdrant_vectors",
|
|
23
|
+
"embed_media",
|
|
24
|
+
"embed_attachment",
|
|
25
|
+
"embed_graph_node",
|
|
26
|
+
"embed_pkb_file",
|
|
27
|
+
"graph_trigger_embed",
|
|
28
|
+
"graph_extract",
|
|
29
|
+
"graph_decay",
|
|
30
|
+
"graph_consolidate",
|
|
31
|
+
"graph_pattern_scan",
|
|
32
|
+
"graph_narrative_refine",
|
|
33
|
+
"graph_bootstrap",
|
|
34
|
+
"embed_concept_page",
|
|
35
|
+
"memory_v2_sweep",
|
|
36
|
+
"memory_v2_consolidate",
|
|
37
|
+
"memory_v2_migrate",
|
|
38
|
+
"memory_v2_reembed",
|
|
39
|
+
"memory_v2_activation_recompute",
|
|
40
|
+
"memory_v3_maintain",
|
|
41
|
+
"memory_retrospective",
|
|
42
|
+
].sort();
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Job types wired directly by `registerMemoryJobHandlers` for domains that are
|
|
46
|
+
* not plugins (persistence cleanup, conversations, media, home, runtime).
|
|
47
|
+
*/
|
|
48
|
+
const NON_PLUGIN_JOB_TYPES = [
|
|
49
|
+
"prune_old_conversations",
|
|
50
|
+
"prune_old_llm_request_logs",
|
|
51
|
+
"prune_old_trace_events",
|
|
52
|
+
"build_conversation_summary",
|
|
53
|
+
"media_processing",
|
|
54
|
+
"conversation_analyze",
|
|
55
|
+
"generate_conversation_starters",
|
|
56
|
+
].sort();
|
|
57
|
+
|
|
58
|
+
describe("job-handler registry — memory plugin contribution", () => {
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
clearJobHandlerRegistry();
|
|
61
|
+
});
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
clearJobHandlerRegistry();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("default-memory contributes exactly the memory job types", () => {
|
|
67
|
+
registerDefaultPluginJobHandlers();
|
|
68
|
+
const types = getRegisteredJobHandlers()
|
|
69
|
+
.map((e) => e.type)
|
|
70
|
+
.sort();
|
|
71
|
+
expect(types).toEqual(MEMORY_JOB_TYPES);
|
|
72
|
+
// Globally unique types (the registry would have thrown on a dupe).
|
|
73
|
+
expect(new Set(types).size).toBe(types.length);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("registerMemoryJobHandlers wires the full job-type set into the worker", () => {
|
|
77
|
+
const captured: string[] = [];
|
|
78
|
+
const spy = spyOn(jobsWorker, "registerJobHandler").mockImplementation(
|
|
79
|
+
(type: string) => {
|
|
80
|
+
captured.push(type);
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
try {
|
|
84
|
+
registerMemoryJobHandlers();
|
|
85
|
+
} finally {
|
|
86
|
+
spy.mockRestore();
|
|
87
|
+
}
|
|
88
|
+
const expected = [...MEMORY_JOB_TYPES, ...NON_PLUGIN_JOB_TYPES].sort();
|
|
89
|
+
expect(captured.slice().sort()).toEqual(expected);
|
|
90
|
+
// Each type registered exactly once — no plugin/non-plugin overlap, no drop.
|
|
91
|
+
expect(new Set(captured).size).toBe(captured.length);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("rejects a duplicate job-handler type across plugins", () => {
|
|
95
|
+
registerPluginJobHandlers("plugin-a", [
|
|
96
|
+
{ type: "graph_extract", handler: () => undefined },
|
|
97
|
+
]);
|
|
98
|
+
expect(() =>
|
|
99
|
+
registerPluginJobHandlers("plugin-b", [
|
|
100
|
+
{ type: "graph_extract", handler: () => undefined },
|
|
101
|
+
]),
|
|
102
|
+
).toThrow(/already registered by plugin "plugin-a"/);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -125,16 +125,34 @@ const BASELINE: Record<string, readonly string[]> = {
|
|
|
125
125
|
"../../../../util/strip-comment-lines.js",
|
|
126
126
|
"../../../../util/truncate.js",
|
|
127
127
|
"../../../config/loader.js",
|
|
128
|
+
"../../../config/types.js",
|
|
128
129
|
"../../../context/strip-injections.js",
|
|
129
130
|
"../../../daemon/pkb-context-tracker.js",
|
|
130
131
|
"../../../daemon/pkb-reminder-builder.js",
|
|
131
132
|
"../../../daemon/trust-context.js",
|
|
133
|
+
"../../../memory/graph/bootstrap.js",
|
|
134
|
+
"../../../memory/graph/consolidation.js",
|
|
132
135
|
"../../../memory/graph/conversation-graph-memory.js",
|
|
136
|
+
"../../../memory/graph/decay.js",
|
|
137
|
+
"../../../memory/graph/extraction-job.js",
|
|
138
|
+
"../../../memory/graph/graph-search.js",
|
|
139
|
+
"../../../memory/graph/narrative.js",
|
|
140
|
+
"../../../memory/graph/pattern-scan.js",
|
|
141
|
+
"../../../memory/job-handlers/backfill.js",
|
|
142
|
+
"../../../memory/job-handlers/embedding.js",
|
|
143
|
+
"../../../memory/job-handlers/index-maintenance.js",
|
|
144
|
+
"../../../memory/jobs/embed-concept-page.js",
|
|
145
|
+
"../../../memory/jobs/embed-pkb-file.js",
|
|
146
|
+
"../../../memory/memory-retrospective-job.js",
|
|
133
147
|
"../../../memory/pkb/autoinject.js",
|
|
134
148
|
"../../../memory/pkb/context.js",
|
|
135
149
|
"../../../memory/pkb/pkb-search.js",
|
|
136
150
|
"../../../memory/pkb/types.js",
|
|
151
|
+
"../../../memory/v2/backfill-jobs.js",
|
|
152
|
+
"../../../memory/v2/consolidation-job.js",
|
|
137
153
|
"../../../memory/v2/static-context.js",
|
|
154
|
+
"../../../memory/v2/sweep-job.js",
|
|
155
|
+
"../../../persistence/jobs-store.js",
|
|
138
156
|
"../../../types.js",
|
|
139
157
|
"../../../util/logger.js",
|
|
140
158
|
"../../../util/platform.js",
|