@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.
Files changed (65) hide show
  1. package/openapi.yaml +10 -0
  2. package/package.json +1 -1
  3. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +22 -22
  4. package/src/__tests__/conversation-queue.test.ts +43 -0
  5. package/src/__tests__/job-handler-registry-guard.test.ts +104 -0
  6. package/src/__tests__/plugin-import-boundary-guard.test.ts +18 -0
  7. package/src/__tests__/schedule-retry.test.ts +51 -51
  8. package/src/__tests__/schedule-routes-workflow-validation.test.ts +45 -41
  9. package/src/__tests__/schedule-routes.test.ts +192 -182
  10. package/src/__tests__/schedule-store.test.ts +172 -170
  11. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  12. package/src/__tests__/scheduler-recurrence.test.ts +12 -12
  13. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -10
  14. package/src/__tests__/scheduler-wake.test.ts +6 -6
  15. package/src/__tests__/task-scheduler.test.ts +22 -22
  16. package/src/__tests__/tool-executor-redaction.test.ts +11 -0
  17. package/src/acp/__tests__/client-handler.test.ts +333 -0
  18. package/src/acp/__tests__/session-manager-persistence.test.ts +43 -0
  19. package/src/acp/client-handler.ts +65 -2
  20. package/src/acp/session-manager.ts +12 -1
  21. package/src/api/events/acp-session-update.test.ts +52 -0
  22. package/src/api/events/acp-session-update.ts +3 -0
  23. package/src/api/responses/conversation-message.ts +14 -0
  24. package/src/background-wake/wake-intent-hooks.test.ts +3 -3
  25. package/src/daemon/conversation-process.ts +13 -11
  26. package/src/daemon/external-plugins-bootstrap.ts +22 -0
  27. package/src/daemon/lifecycle.ts +1 -1
  28. package/src/daemon/message-types/acp.ts +4 -0
  29. package/src/daemon/shutdown-handlers.ts +148 -147
  30. package/src/jobs/register-job-handlers.ts +24 -167
  31. package/src/memory/memory-retrospective-job.ts +5 -5
  32. package/src/memory/memory-retrospective-state.ts +7 -7
  33. package/src/notifications/conversation-pairing.ts +23 -14
  34. package/src/persistence/conversation-bootstrap.ts +24 -15
  35. package/src/persistence/conversation-crud.ts +68 -59
  36. package/src/persistence/migrations/309-drop-redundant-indexes.ts +60 -0
  37. package/src/persistence/schema/conversations.ts +0 -1
  38. package/src/persistence/schema/infrastructure.ts +0 -1
  39. package/src/persistence/steps.ts +2 -0
  40. package/src/plugins/defaults/index.ts +28 -0
  41. package/src/plugins/defaults/memory/job-handlers.ts +203 -0
  42. package/src/plugins/job-handler-registry.ts +94 -0
  43. package/src/plugins/types.ts +26 -0
  44. package/src/runtime/background-job-runner.ts +9 -7
  45. package/src/runtime/routes/acp-routes-event-log.test.ts +51 -0
  46. package/src/runtime/routes/conversation-cli-routes.ts +5 -1
  47. package/src/runtime/routes/conversation-management-routes.ts +6 -6
  48. package/src/runtime/routes/conversation-routes.ts +12 -0
  49. package/src/runtime/routes/conversations-import-routes.ts +5 -1
  50. package/src/runtime/routes/defer-routes.ts +3 -3
  51. package/src/runtime/routes/home-feed-routes.ts +9 -4
  52. package/src/runtime/routes/schedule-routes.ts +31 -28
  53. package/src/runtime/routes/wipe-conversation-routes.ts +1 -1
  54. package/src/runtime/services/analyze-conversation.ts +18 -9
  55. package/src/schedule/retry-policy.ts +8 -8
  56. package/src/schedule/schedule-recovery.ts +8 -5
  57. package/src/schedule/schedule-store.ts +124 -109
  58. package/src/schedule/scheduler.ts +64 -52
  59. package/src/security/redaction.ts +18 -14
  60. package/src/subagent/manager.ts +1 -1
  61. package/src/tasks/task-runner.ts +1 -1
  62. package/src/tools/schedule/create.ts +2 -2
  63. package/src/tools/schedule/delete.ts +1 -1
  64. package/src/tools/schedule/update.ts +1 -1
  65. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.3-dev.202606292136.ce8bf33",
3
+ "version": "0.10.3-dev.202606292252.adea010",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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",