@vellumai/assistant 0.10.3-dev.202606290949.0cf8e08 → 0.10.3-dev.202606291342.c7a83ea
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 +54 -0
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +0 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -0
- package/src/__tests__/btw-routes.test.ts +0 -1
- package/src/__tests__/config-watcher-skill-reseed.test.ts +37 -110
- package/src/__tests__/config-watcher.test.ts +53 -30
- package/src/__tests__/secret-routes-platform-proxy.test.ts +8 -6
- package/src/__tests__/tool-preview-lifecycle.test.ts +66 -0
- package/src/__tests__/tool-start-timestamp.test.ts +63 -0
- package/src/api/events/tool-use-preview-start.ts +11 -0
- package/src/api/events/tool-use-start.ts +14 -0
- package/src/api/responses/conversation-message.ts +10 -1
- package/src/config/feature-flag-registry.json +8 -0
- package/src/contacts/__tests__/guardian-contact-reader.test.ts +6 -10
- package/src/contacts/__tests__/guardian-delivery-reader.test.ts +3 -14
- package/src/contacts/contact-store.ts +6 -6
- package/src/contacts/guardian-contact-reader.ts +4 -6
- package/src/contacts/guardian-delivery-reader.ts +21 -23
- package/src/contacts/notify-contacts-changed.ts +17 -0
- package/src/daemon/__tests__/meet-host-supervisor.test.ts +0 -3
- package/src/daemon/app-source-watcher.ts +71 -10
- package/src/daemon/config-watcher.ts +69 -45
- package/src/daemon/conversation-agent-loop-handlers.ts +77 -0
- package/src/daemon/conversation-store.ts +56 -25
- package/src/daemon/handlers/__tests__/config-channels.test.ts +17 -11
- package/src/daemon/handlers/config-channels.ts +2 -2
- package/src/daemon/handlers/shared.ts +2 -0
- package/src/daemon/lifecycle.ts +28 -12
- package/src/daemon/meet-host-supervisor.ts +9 -36
- package/src/daemon/server.ts +1 -240
- package/src/daemon/shutdown-handlers.ts +13 -1
- package/src/ipc/assistant-server.ts +47 -0
- package/src/ipc/skill-server.ts +42 -6
- package/src/memory/__tests__/conversation-row-batch-delete.test.ts +199 -0
- package/src/memory/__tests__/memory-retrospective-job.test.ts +10 -0
- package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +5 -0
- package/src/memory/memory-retrospective-job.ts +12 -7
- package/src/memory/memory-retrospective-state.ts +43 -29
- package/src/persistence/conversation-crud.ts +136 -74
- package/src/persistence/conversation-row-batch-delete.ts +205 -0
- package/src/persistence/db-connection.ts +5 -1
- package/src/runtime/__tests__/channel-verification-service.test.ts +9 -5
- package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +3 -3
- package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +17 -23
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/migration-routes.ts +135 -0
- package/src/runtime/routes/secret-routes.ts +7 -6
- package/src/schedule/schedule-store.ts +216 -123
- package/src/util/sqlite-retry.ts +123 -0
- package/src/contacts/contact-events.ts +0 -21
- package/src/runtime/routes/secrets-deps.ts +0 -21
package/openapi.yaml
CHANGED
|
@@ -16667,6 +16667,8 @@ paths:
|
|
|
16667
16667
|
type: string
|
|
16668
16668
|
startedAt:
|
|
16669
16669
|
type: number
|
|
16670
|
+
previewStartedAt:
|
|
16671
|
+
type: number
|
|
16670
16672
|
completedAt:
|
|
16671
16673
|
type: number
|
|
16672
16674
|
confirmationDecision:
|
|
@@ -17097,6 +17099,8 @@ paths:
|
|
|
17097
17099
|
type: string
|
|
17098
17100
|
startedAt:
|
|
17099
17101
|
type: number
|
|
17102
|
+
previewStartedAt:
|
|
17103
|
+
type: number
|
|
17100
17104
|
completedAt:
|
|
17101
17105
|
type: number
|
|
17102
17106
|
confirmationDecision:
|
|
@@ -18665,6 +18669,56 @@ paths:
|
|
|
18665
18669
|
type: object
|
|
18666
18670
|
"404":
|
|
18667
18671
|
description: No job matches the given id.
|
|
18672
|
+
/v1/migrations/preflight-from-gcs:
|
|
18673
|
+
post:
|
|
18674
|
+
operationId: migrations_preflightfromgcs_post
|
|
18675
|
+
summary: Dry-run import analysis from a signed GCS URL
|
|
18676
|
+
description:
|
|
18677
|
+
Fetch a .vbundle archive from a signed GCS download URL and return a preflight report — what would change
|
|
18678
|
+
if the bundle were imported — without writing anything to disk. Enables `vellum teleport --dry-run` against
|
|
18679
|
+
local and docker targets.
|
|
18680
|
+
tags:
|
|
18681
|
+
- migrations
|
|
18682
|
+
requestBody:
|
|
18683
|
+
required: true
|
|
18684
|
+
content:
|
|
18685
|
+
application/json:
|
|
18686
|
+
schema:
|
|
18687
|
+
type: object
|
|
18688
|
+
properties:
|
|
18689
|
+
bundle_url:
|
|
18690
|
+
type: string
|
|
18691
|
+
format: uri
|
|
18692
|
+
description: Signed GCS GET URL for the bundle.
|
|
18693
|
+
required:
|
|
18694
|
+
- bundle_url
|
|
18695
|
+
responses:
|
|
18696
|
+
"200":
|
|
18697
|
+
description: Successful response
|
|
18698
|
+
content:
|
|
18699
|
+
application/json:
|
|
18700
|
+
schema:
|
|
18701
|
+
type: object
|
|
18702
|
+
properties:
|
|
18703
|
+
can_import:
|
|
18704
|
+
type: boolean
|
|
18705
|
+
summary:
|
|
18706
|
+
type: object
|
|
18707
|
+
properties: {}
|
|
18708
|
+
additionalProperties: {}
|
|
18709
|
+
files:
|
|
18710
|
+
type: array
|
|
18711
|
+
items: {}
|
|
18712
|
+
conflicts:
|
|
18713
|
+
type: array
|
|
18714
|
+
items: {}
|
|
18715
|
+
validation:
|
|
18716
|
+
type: object
|
|
18717
|
+
properties: {}
|
|
18718
|
+
additionalProperties: {}
|
|
18719
|
+
required:
|
|
18720
|
+
- can_import
|
|
18721
|
+
additionalProperties: false
|
|
18668
18722
|
/v1/migrations/validate:
|
|
18669
18723
|
post:
|
|
18670
18724
|
operationId: migrations_validate_post
|
package/package.json
CHANGED
|
@@ -37,7 +37,6 @@ mock.module("../contacts/guardian-delivery-reader.js", () => ({
|
|
|
37
37
|
fakeGuardianDelivery ? [fakeGuardianDelivery] : null,
|
|
38
38
|
guardianForChannel: (list: { principalId?: string | null }[]) => list[0],
|
|
39
39
|
invalidateGuardianDeliveryCache: () => {},
|
|
40
|
-
onContactChange: () => {},
|
|
41
40
|
}));
|
|
42
41
|
|
|
43
42
|
import { getDb } from "../persistence/db-connection.js";
|
|
@@ -116,6 +116,10 @@ mock.module("../persistence/conversation-crud.js", () => ({
|
|
|
116
116
|
deleteMessageById: mock(() => {}),
|
|
117
117
|
clearAll: mock(async () => ({ conversations: 0, messages: 0 })),
|
|
118
118
|
deleteConversation: mock(() => ({ memoryIds: [] })),
|
|
119
|
+
deleteConversationGently: mock(async () => ({
|
|
120
|
+
segmentIds: [],
|
|
121
|
+
deletedSummaryIds: [],
|
|
122
|
+
})),
|
|
119
123
|
deleteLastExchange: mock(() => 0),
|
|
120
124
|
findAnalysisConversationFor: mock(() => null),
|
|
121
125
|
findMostRecentRetrospectiveFor: mock(() => null),
|
|
@@ -103,7 +103,6 @@ mock.module("../daemon/conversation-store.js", () => ({
|
|
|
103
103
|
conversationEntries: () => [][Symbol.iterator](),
|
|
104
104
|
conversationIds: () => [][Symbol.iterator](),
|
|
105
105
|
getConversationMap: () => new Map(),
|
|
106
|
-
initConversationLifecycle: () => {},
|
|
107
106
|
registerConversationFactory: () => {},
|
|
108
107
|
getOrCreateActiveConversation: mockGetOrCreateConversation,
|
|
109
108
|
getConversationOptions: () => undefined,
|
|
@@ -34,6 +34,11 @@ interface CapturedWatcher {
|
|
|
34
34
|
const capturedWatchers: CapturedWatcher[] = [];
|
|
35
35
|
let recursiveWatchAvailable = false;
|
|
36
36
|
|
|
37
|
+
// The skills watcher reacts by calling these directly; count the calls so tests
|
|
38
|
+
// can assert dispatch without driving real eviction or skill-memory reseeding.
|
|
39
|
+
let evictCalls = 0;
|
|
40
|
+
let skillsChangedCalls = 0;
|
|
41
|
+
|
|
37
42
|
mock.module("node:fs", () => {
|
|
38
43
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
44
|
const actual = require("node:fs");
|
|
@@ -114,6 +119,29 @@ mock.module("../signals/user-message.js", () => ({
|
|
|
114
119
|
handleUserMessageSignal: () => {},
|
|
115
120
|
}));
|
|
116
121
|
|
|
122
|
+
mock.module("../daemon/conversation-store.js", () => ({
|
|
123
|
+
evictConversationsForReload: () => {
|
|
124
|
+
evictCalls++;
|
|
125
|
+
},
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
mock.module("../daemon/skill-memory-refresh.js", () => ({
|
|
129
|
+
refreshSkillCapabilityMemories: () => {
|
|
130
|
+
skillsChangedCalls++;
|
|
131
|
+
},
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
mock.module("../runtime/sync/resource-sync-events.js", () => ({
|
|
135
|
+
publishIdentityChanged: () => {},
|
|
136
|
+
publishConfigChanged: () => {},
|
|
137
|
+
publishSoundsConfigUpdated: () => {},
|
|
138
|
+
publishAvatarChanged: () => {},
|
|
139
|
+
}));
|
|
140
|
+
|
|
141
|
+
mock.module("../platform/sync-identity.js", () => ({
|
|
142
|
+
syncIdentityNameToPlatform: () => {},
|
|
143
|
+
}));
|
|
144
|
+
|
|
117
145
|
const { ConfigWatcher } = await import("../daemon/config-watcher.js");
|
|
118
146
|
|
|
119
147
|
function findWatcher(dir: string): CapturedWatcher | undefined {
|
|
@@ -130,8 +158,6 @@ function capturedWatcherCount(dir: string): number {
|
|
|
130
158
|
|
|
131
159
|
describe("ConfigWatcher skills watcher reseeding", () => {
|
|
132
160
|
let watcher: InstanceType<typeof ConfigWatcher>;
|
|
133
|
-
let evictCalls: number;
|
|
134
|
-
let skillsChangedCalls: number;
|
|
135
161
|
|
|
136
162
|
beforeEach(() => {
|
|
137
163
|
capturedWatchers.length = 0;
|
|
@@ -152,18 +178,7 @@ describe("ConfigWatcher skills watcher reseeding", () => {
|
|
|
152
178
|
});
|
|
153
179
|
|
|
154
180
|
test("watched skill file changes evict conversations and refresh skill memories", async () => {
|
|
155
|
-
watcher.start(
|
|
156
|
-
() => {
|
|
157
|
-
evictCalls++;
|
|
158
|
-
},
|
|
159
|
-
undefined,
|
|
160
|
-
undefined,
|
|
161
|
-
undefined,
|
|
162
|
-
undefined,
|
|
163
|
-
() => {
|
|
164
|
-
skillsChangedCalls++;
|
|
165
|
-
},
|
|
166
|
-
);
|
|
181
|
+
watcher.start();
|
|
167
182
|
|
|
168
183
|
const skillsWatcher = findWatcher(SKILLS_DIR);
|
|
169
184
|
expect(skillsWatcher).toBeDefined();
|
|
@@ -178,18 +193,7 @@ describe("ConfigWatcher skills watcher reseeding", () => {
|
|
|
178
193
|
test("recursive watcher ignores skipped dependency and staging paths", async () => {
|
|
179
194
|
recursiveWatchAvailable = true;
|
|
180
195
|
|
|
181
|
-
watcher.start(
|
|
182
|
-
() => {
|
|
183
|
-
evictCalls++;
|
|
184
|
-
},
|
|
185
|
-
undefined,
|
|
186
|
-
undefined,
|
|
187
|
-
undefined,
|
|
188
|
-
undefined,
|
|
189
|
-
() => {
|
|
190
|
-
skillsChangedCalls++;
|
|
191
|
-
},
|
|
192
|
-
);
|
|
196
|
+
watcher.start();
|
|
193
197
|
|
|
194
198
|
const skillsWatcher = findWatcher(SKILLS_DIR);
|
|
195
199
|
expect(skillsWatcher).toBeDefined();
|
|
@@ -218,18 +222,7 @@ describe("ConfigWatcher skills watcher reseeding", () => {
|
|
|
218
222
|
test("recursive watcher reloads skill memories for build output changes", async () => {
|
|
219
223
|
recursiveWatchAvailable = true;
|
|
220
224
|
|
|
221
|
-
watcher.start(
|
|
222
|
-
() => {
|
|
223
|
-
evictCalls++;
|
|
224
|
-
},
|
|
225
|
-
undefined,
|
|
226
|
-
undefined,
|
|
227
|
-
undefined,
|
|
228
|
-
undefined,
|
|
229
|
-
() => {
|
|
230
|
-
skillsChangedCalls++;
|
|
231
|
-
},
|
|
232
|
-
);
|
|
225
|
+
watcher.start();
|
|
233
226
|
|
|
234
227
|
const skillsWatcher = findWatcher(SKILLS_DIR);
|
|
235
228
|
expect(skillsWatcher).toBeDefined();
|
|
@@ -242,18 +235,7 @@ describe("ConfigWatcher skills watcher reseeding", () => {
|
|
|
242
235
|
});
|
|
243
236
|
|
|
244
237
|
test("coalesces multiple skill file changes into one catalog refresh", async () => {
|
|
245
|
-
watcher.start(
|
|
246
|
-
() => {
|
|
247
|
-
evictCalls++;
|
|
248
|
-
},
|
|
249
|
-
undefined,
|
|
250
|
-
undefined,
|
|
251
|
-
undefined,
|
|
252
|
-
undefined,
|
|
253
|
-
() => {
|
|
254
|
-
skillsChangedCalls++;
|
|
255
|
-
},
|
|
256
|
-
);
|
|
238
|
+
watcher.start();
|
|
257
239
|
|
|
258
240
|
const skillsWatcher = findWatcher(SKILLS_DIR);
|
|
259
241
|
expect(skillsWatcher).toBeDefined();
|
|
@@ -271,18 +253,7 @@ describe("ConfigWatcher skills watcher reseeding", () => {
|
|
|
271
253
|
const referencesDir = join(SKILLS_DIR, "example-skill", "references");
|
|
272
254
|
mkdirSync(referencesDir, { recursive: true });
|
|
273
255
|
|
|
274
|
-
watcher.start(
|
|
275
|
-
() => {
|
|
276
|
-
evictCalls++;
|
|
277
|
-
},
|
|
278
|
-
undefined,
|
|
279
|
-
undefined,
|
|
280
|
-
undefined,
|
|
281
|
-
undefined,
|
|
282
|
-
() => {
|
|
283
|
-
skillsChangedCalls++;
|
|
284
|
-
},
|
|
285
|
-
);
|
|
256
|
+
watcher.start();
|
|
286
257
|
|
|
287
258
|
const referencesWatcher = findWatcher(referencesDir);
|
|
288
259
|
expect(referencesWatcher).toBeDefined();
|
|
@@ -305,18 +276,7 @@ describe("ConfigWatcher skills watcher reseeding", () => {
|
|
|
305
276
|
mkdirSync(stagedSkillDir, { recursive: true });
|
|
306
277
|
mkdirSync(referencesDir, { recursive: true });
|
|
307
278
|
|
|
308
|
-
watcher.start(
|
|
309
|
-
() => {
|
|
310
|
-
evictCalls++;
|
|
311
|
-
},
|
|
312
|
-
undefined,
|
|
313
|
-
undefined,
|
|
314
|
-
undefined,
|
|
315
|
-
undefined,
|
|
316
|
-
() => {
|
|
317
|
-
skillsChangedCalls++;
|
|
318
|
-
},
|
|
319
|
-
);
|
|
279
|
+
watcher.start();
|
|
320
280
|
|
|
321
281
|
expect(findWatcher(nodeModulesDir)).toBeUndefined();
|
|
322
282
|
expect(findWatcher(dependencyDir)).toBeUndefined();
|
|
@@ -353,18 +313,7 @@ describe("ConfigWatcher skills watcher reseeding", () => {
|
|
|
353
313
|
mkdirSync(distDir, { recursive: true });
|
|
354
314
|
mkdirSync(buildDir, { recursive: true });
|
|
355
315
|
|
|
356
|
-
watcher.start(
|
|
357
|
-
() => {
|
|
358
|
-
evictCalls++;
|
|
359
|
-
},
|
|
360
|
-
undefined,
|
|
361
|
-
undefined,
|
|
362
|
-
undefined,
|
|
363
|
-
undefined,
|
|
364
|
-
() => {
|
|
365
|
-
skillsChangedCalls++;
|
|
366
|
-
},
|
|
367
|
-
);
|
|
316
|
+
watcher.start();
|
|
368
317
|
|
|
369
318
|
const distWatcher = findWatcher(distDir);
|
|
370
319
|
const buildWatcher = findWatcher(buildDir);
|
|
@@ -392,18 +341,7 @@ describe("ConfigWatcher skills watcher reseeding", () => {
|
|
|
392
341
|
const generatedDir = join(toolsDir, "generated");
|
|
393
342
|
mkdirSync(skillDir, { recursive: true });
|
|
394
343
|
|
|
395
|
-
watcher.start(
|
|
396
|
-
() => {
|
|
397
|
-
evictCalls++;
|
|
398
|
-
},
|
|
399
|
-
undefined,
|
|
400
|
-
undefined,
|
|
401
|
-
undefined,
|
|
402
|
-
undefined,
|
|
403
|
-
() => {
|
|
404
|
-
skillsChangedCalls++;
|
|
405
|
-
},
|
|
406
|
-
);
|
|
344
|
+
watcher.start();
|
|
407
345
|
|
|
408
346
|
const skillWatcher = findWatcher(skillDir);
|
|
409
347
|
expect(skillWatcher).toBeDefined();
|
|
@@ -424,18 +362,7 @@ describe("ConfigWatcher skills watcher reseeding", () => {
|
|
|
424
362
|
const deepDir = join(referencesDir, "deep");
|
|
425
363
|
mkdirSync(deepDir, { recursive: true });
|
|
426
364
|
|
|
427
|
-
watcher.start(
|
|
428
|
-
() => {
|
|
429
|
-
evictCalls++;
|
|
430
|
-
},
|
|
431
|
-
undefined,
|
|
432
|
-
undefined,
|
|
433
|
-
undefined,
|
|
434
|
-
undefined,
|
|
435
|
-
() => {
|
|
436
|
-
skillsChangedCalls++;
|
|
437
|
-
},
|
|
438
|
-
);
|
|
365
|
+
watcher.start();
|
|
439
366
|
|
|
440
367
|
expect(findWatcher(referencesDir)).toBeDefined();
|
|
441
368
|
expect(findWatcher(deepDir)).toBeDefined();
|
|
@@ -142,6 +142,35 @@ mock.module("../signals/cancel.js", () => ({
|
|
|
142
142
|
handleCancelSignal: () => {},
|
|
143
143
|
}));
|
|
144
144
|
|
|
145
|
+
// The watcher reacts to file changes by calling these directly; capture the
|
|
146
|
+
// reaction counts so tests can assert dispatch without driving real eviction
|
|
147
|
+
// or client broadcasts.
|
|
148
|
+
let evictCallCount = 0;
|
|
149
|
+
let identityCallCount = 0;
|
|
150
|
+
|
|
151
|
+
mock.module("../daemon/conversation-store.js", () => ({
|
|
152
|
+
evictConversationsForReload: () => {
|
|
153
|
+
evictCallCount++;
|
|
154
|
+
},
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
mock.module("../runtime/sync/resource-sync-events.js", () => ({
|
|
158
|
+
publishIdentityChanged: () => {
|
|
159
|
+
identityCallCount++;
|
|
160
|
+
},
|
|
161
|
+
publishConfigChanged: () => {},
|
|
162
|
+
publishSoundsConfigUpdated: () => {},
|
|
163
|
+
publishAvatarChanged: () => {},
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
mock.module("../daemon/skill-memory-refresh.js", () => ({
|
|
167
|
+
refreshSkillCapabilityMemories: () => {},
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
mock.module("../platform/sync-identity.js", () => ({
|
|
171
|
+
syncIdentityNameToPlatform: () => {},
|
|
172
|
+
}));
|
|
173
|
+
|
|
145
174
|
// Import after mocks are set up
|
|
146
175
|
const { ConfigWatcher } = await import("../daemon/config-watcher.js");
|
|
147
176
|
|
|
@@ -206,16 +235,13 @@ beforeAll(() => {
|
|
|
206
235
|
});
|
|
207
236
|
|
|
208
237
|
let watcher: InstanceType<typeof ConfigWatcher>;
|
|
209
|
-
let evictCallCount: number;
|
|
210
|
-
const onConversationEvict = () => {
|
|
211
|
-
evictCallCount++;
|
|
212
|
-
};
|
|
213
238
|
|
|
214
239
|
beforeEach(() => {
|
|
215
240
|
capturedWatchers.length = 0;
|
|
216
241
|
capturedFileWatches.length = 0;
|
|
217
242
|
inoMap.clear();
|
|
218
243
|
evictCallCount = 0;
|
|
244
|
+
identityCallCount = 0;
|
|
219
245
|
watcher = new ConfigWatcher(undefined, TEST_DEBOUNCE_MS);
|
|
220
246
|
});
|
|
221
247
|
|
|
@@ -224,32 +250,29 @@ afterEach(() => {
|
|
|
224
250
|
});
|
|
225
251
|
|
|
226
252
|
describe("ConfigWatcher workspace file handlers", () => {
|
|
227
|
-
test("SOUL.md change
|
|
228
|
-
watcher.start(
|
|
253
|
+
test("SOUL.md change evicts conversations", async () => {
|
|
254
|
+
watcher.start();
|
|
229
255
|
simulateFileChange(WORKSPACE_DIR, "SOUL.md");
|
|
230
256
|
await new Promise((r) => setTimeout(r, WAIT_MS));
|
|
231
257
|
expect(evictCallCount).toBe(1);
|
|
232
258
|
});
|
|
233
259
|
|
|
234
|
-
test("IDENTITY.md change
|
|
235
|
-
watcher.start(
|
|
260
|
+
test("IDENTITY.md change evicts conversations", async () => {
|
|
261
|
+
watcher.start();
|
|
236
262
|
simulateFileChange(WORKSPACE_DIR, "IDENTITY.md");
|
|
237
263
|
await new Promise((r) => setTimeout(r, WAIT_MS));
|
|
238
264
|
expect(evictCallCount).toBe(1);
|
|
239
265
|
});
|
|
240
266
|
|
|
241
|
-
test("IDENTITY.md change
|
|
242
|
-
|
|
243
|
-
watcher.start(onConversationEvict, () => {
|
|
244
|
-
identityCallCount += 1;
|
|
245
|
-
});
|
|
267
|
+
test("IDENTITY.md change broadcasts the identity update", async () => {
|
|
268
|
+
watcher.start();
|
|
246
269
|
simulateFileChange(WORKSPACE_DIR, "IDENTITY.md");
|
|
247
270
|
await new Promise((r) => setTimeout(r, WAIT_MS));
|
|
248
271
|
expect(identityCallCount).toBe(1);
|
|
249
272
|
});
|
|
250
273
|
|
|
251
274
|
test("unregistered workspace files are not subscribed (only the registered handler set is)", () => {
|
|
252
|
-
watcher.start(
|
|
275
|
+
watcher.start();
|
|
253
276
|
// Per-file watching only registers config.json, SOUL.md, IDENTITY.md.
|
|
254
277
|
// The whole workspace dir must not be watched either — that was the
|
|
255
278
|
// ENXIO-on-Unix-sockets bug.
|
|
@@ -263,16 +286,16 @@ describe("ConfigWatcher workspace file handlers", () => {
|
|
|
263
286
|
refreshCalled = true;
|
|
264
287
|
return false;
|
|
265
288
|
};
|
|
266
|
-
watcher.start(
|
|
289
|
+
watcher.start();
|
|
267
290
|
simulateFileChange(WORKSPACE_DIR, "config.json");
|
|
268
291
|
await new Promise((r) => setTimeout(r, WAIT_MS));
|
|
269
292
|
expect(refreshCalled).toBe(true);
|
|
270
293
|
expect(evictCallCount).toBe(0);
|
|
271
294
|
});
|
|
272
295
|
|
|
273
|
-
test("config.json change
|
|
296
|
+
test("config.json change evicts conversations when config actually changed", async () => {
|
|
274
297
|
watcher.refreshConfigFromSources = async () => true;
|
|
275
|
-
watcher.start(
|
|
298
|
+
watcher.start();
|
|
276
299
|
simulateFileChange(WORKSPACE_DIR, "config.json");
|
|
277
300
|
await new Promise((r) => setTimeout(r, WAIT_MS));
|
|
278
301
|
expect(evictCallCount).toBe(1);
|
|
@@ -285,7 +308,7 @@ describe("ConfigWatcher workspace file handlers", () => {
|
|
|
285
308
|
return true;
|
|
286
309
|
};
|
|
287
310
|
watcher.suppressConfigReload = true;
|
|
288
|
-
watcher.start(
|
|
311
|
+
watcher.start();
|
|
289
312
|
simulateFileChange(WORKSPACE_DIR, "config.json");
|
|
290
313
|
await new Promise((r) => setTimeout(r, WAIT_MS));
|
|
291
314
|
expect(refreshCalled).toBe(false);
|
|
@@ -295,7 +318,7 @@ describe("ConfigWatcher workspace file handlers", () => {
|
|
|
295
318
|
|
|
296
319
|
describe("ConfigWatcher watcher lifecycle", () => {
|
|
297
320
|
test("start does NOT subscribe to /workspace as a directory (regression: ENXIO on Unix sockets)", () => {
|
|
298
|
-
watcher.start(
|
|
321
|
+
watcher.start();
|
|
299
322
|
expect(findWatcher(WORKSPACE_DIR)).toBeUndefined();
|
|
300
323
|
// The per-file watchFile subscriptions are tracked separately from
|
|
301
324
|
// capturedWatchers; assert the expected ones are present.
|
|
@@ -305,7 +328,7 @@ describe("ConfigWatcher watcher lifecycle", () => {
|
|
|
305
328
|
});
|
|
306
329
|
|
|
307
330
|
test("stop cancels pending debounce work, no eviction fires after", async () => {
|
|
308
|
-
watcher.start(
|
|
331
|
+
watcher.start();
|
|
309
332
|
simulateFileChange(WORKSPACE_DIR, "SOUL.md");
|
|
310
333
|
watcher.stop();
|
|
311
334
|
await new Promise((r) => setTimeout(r, WAIT_MS));
|
|
@@ -313,7 +336,7 @@ describe("ConfigWatcher watcher lifecycle", () => {
|
|
|
313
336
|
});
|
|
314
337
|
|
|
315
338
|
test("multiple rapid changes to the same workspace file are coalesced to one eviction", async () => {
|
|
316
|
-
watcher.start(
|
|
339
|
+
watcher.start();
|
|
317
340
|
simulateFileChange(WORKSPACE_DIR, "SOUL.md");
|
|
318
341
|
simulateFileChange(WORKSPACE_DIR, "SOUL.md");
|
|
319
342
|
simulateFileChange(WORKSPACE_DIR, "SOUL.md");
|
|
@@ -322,7 +345,7 @@ describe("ConfigWatcher watcher lifecycle", () => {
|
|
|
322
345
|
});
|
|
323
346
|
|
|
324
347
|
test("changes to different files each trigger their own handler", async () => {
|
|
325
|
-
watcher.start(
|
|
348
|
+
watcher.start();
|
|
326
349
|
simulateFileChange(WORKSPACE_DIR, "SOUL.md");
|
|
327
350
|
simulateFileChange(WORKSPACE_DIR, "IDENTITY.md");
|
|
328
351
|
await new Promise((r) => setTimeout(r, WAIT_MS));
|
|
@@ -335,7 +358,7 @@ describe("ConfigWatcher per-file polling listener", () => {
|
|
|
335
358
|
// Simulates `writeFile(tmp) + rename(tmp, target)`: the inode at the
|
|
336
359
|
// path is replaced. The listener should fire once per debounced window.
|
|
337
360
|
watcher.refreshConfigFromSources = async () => true;
|
|
338
|
-
watcher.start(
|
|
361
|
+
watcher.start();
|
|
339
362
|
const fw = findFileWatch(join(WORKSPACE_DIR, "config.json"));
|
|
340
363
|
expect(fw).toBeDefined();
|
|
341
364
|
fw!.listener({ ino: 2, mtimeMs: 1_001 }, { ino: 1, mtimeMs: 1_000 });
|
|
@@ -345,7 +368,7 @@ describe("ConfigWatcher per-file polling listener", () => {
|
|
|
345
368
|
|
|
346
369
|
test("mtime change is treated as a file change (in-place edit)", async () => {
|
|
347
370
|
watcher.refreshConfigFromSources = async () => true;
|
|
348
|
-
watcher.start(
|
|
371
|
+
watcher.start();
|
|
349
372
|
const fw = findFileWatch(join(WORKSPACE_DIR, "config.json"));
|
|
350
373
|
expect(fw).toBeDefined();
|
|
351
374
|
// Same inode, different mtime — what an `echo >> file` produces.
|
|
@@ -358,7 +381,7 @@ describe("ConfigWatcher per-file polling listener", () => {
|
|
|
358
381
|
// fs.watchFile sometimes invokes the listener with curr === prev
|
|
359
382
|
// (e.g. on initial subscription); the watcher must not re-fire in that case.
|
|
360
383
|
watcher.refreshConfigFromSources = async () => true;
|
|
361
|
-
watcher.start(
|
|
384
|
+
watcher.start();
|
|
362
385
|
const fw = findFileWatch(join(WORKSPACE_DIR, "config.json"));
|
|
363
386
|
expect(fw).toBeDefined();
|
|
364
387
|
fw!.listener({ ino: 1, mtimeMs: 1_000 }, { ino: 1, mtimeMs: 1_000 });
|
|
@@ -370,8 +393,8 @@ describe("ConfigWatcher per-file polling listener", () => {
|
|
|
370
393
|
describe("ConfigWatcher users directory watcher", () => {
|
|
371
394
|
const USERS_DIR = join(WORKSPACE_DIR, "users");
|
|
372
395
|
|
|
373
|
-
test("editing users/<slug>.md
|
|
374
|
-
watcher.start(
|
|
396
|
+
test("editing users/<slug>.md evicts conversations", async () => {
|
|
397
|
+
watcher.start();
|
|
375
398
|
simulateFileChange(USERS_DIR, "alice.md");
|
|
376
399
|
|
|
377
400
|
await new Promise((r) => setTimeout(r, WAIT_MS));
|
|
@@ -379,7 +402,7 @@ describe("ConfigWatcher users directory watcher", () => {
|
|
|
379
402
|
});
|
|
380
403
|
|
|
381
404
|
test("non-.md files in users/ do NOT trigger eviction", async () => {
|
|
382
|
-
watcher.start(
|
|
405
|
+
watcher.start();
|
|
383
406
|
simulateFileChange(USERS_DIR, "alice.json");
|
|
384
407
|
simulateFileChange(USERS_DIR, "notes.txt");
|
|
385
408
|
simulateFileChange(USERS_DIR, "README");
|
|
@@ -389,7 +412,7 @@ describe("ConfigWatcher users directory watcher", () => {
|
|
|
389
412
|
});
|
|
390
413
|
|
|
391
414
|
test("null filename in users/ does not trigger eviction", async () => {
|
|
392
|
-
watcher.start(
|
|
415
|
+
watcher.start();
|
|
393
416
|
const usersWatcher = findWatcher(USERS_DIR);
|
|
394
417
|
expect(usersWatcher).toBeDefined();
|
|
395
418
|
usersWatcher!.callback("change", null);
|
|
@@ -399,7 +422,7 @@ describe("ConfigWatcher users directory watcher", () => {
|
|
|
399
422
|
});
|
|
400
423
|
|
|
401
424
|
test("multiple rapid changes to the same persona file are debounced", async () => {
|
|
402
|
-
watcher.start(
|
|
425
|
+
watcher.start();
|
|
403
426
|
simulateFileChange(USERS_DIR, "bob.md");
|
|
404
427
|
simulateFileChange(USERS_DIR, "bob.md");
|
|
405
428
|
simulateFileChange(USERS_DIR, "bob.md");
|
|
@@ -129,13 +129,20 @@ mock.module("../daemon/memory-v2-startup.js", () => ({
|
|
|
129
129
|
maybeReseedCapabilitiesAfterManagedCredential: async () => {},
|
|
130
130
|
}));
|
|
131
131
|
|
|
132
|
+
// secret-routes evicts conversations after a credential change so the next turn
|
|
133
|
+
// rebuilds against the new providers; count the calls to assert that happens.
|
|
134
|
+
mock.module("../daemon/conversation-store.js", () => ({
|
|
135
|
+
evictConversationsForReload: () => {
|
|
136
|
+
providerRefreshCalls++;
|
|
137
|
+
},
|
|
138
|
+
}));
|
|
139
|
+
|
|
132
140
|
import {
|
|
133
141
|
getProviderRoutingSource,
|
|
134
142
|
initializeProviders,
|
|
135
143
|
listProviders,
|
|
136
144
|
} from "../providers/registry.js";
|
|
137
145
|
import { ROUTES } from "../runtime/routes/secret-routes.js";
|
|
138
|
-
import { registerSecretsDeps } from "../runtime/routes/secrets-deps.js";
|
|
139
146
|
|
|
140
147
|
const addRoute = ROUTES.find(
|
|
141
148
|
(r) => r.method === "POST" && r.endpoint === "secrets",
|
|
@@ -177,11 +184,6 @@ describe("secret routes managed proxy registry sync", () => {
|
|
|
177
184
|
lastGeminiConstructorOpts = null;
|
|
178
185
|
platformBaseUrlOverride = undefined;
|
|
179
186
|
providerRefreshCalls = 0;
|
|
180
|
-
registerSecretsDeps({
|
|
181
|
-
onProviderCredentialsChanged: () => {
|
|
182
|
-
providerRefreshCalls++;
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
187
|
await initializeProviders(mockConfig);
|
|
186
188
|
});
|
|
187
189
|
|
|
@@ -256,6 +256,72 @@ describe("tool preview lifecycle", () => {
|
|
|
256
256
|
expect((emitted as any).conversationId).toBe("test-session-id");
|
|
257
257
|
});
|
|
258
258
|
|
|
259
|
+
test("stamps previewStartedAt on the event and stores it in state", () => {
|
|
260
|
+
const collector = createEventCollector();
|
|
261
|
+
const deps = createMockDeps({
|
|
262
|
+
onEvent: collector.onEvent,
|
|
263
|
+
ctx: {
|
|
264
|
+
...createMockDeps().ctx,
|
|
265
|
+
emitActivityState: collector.emitActivityState,
|
|
266
|
+
} as unknown as EventHandlerDeps["ctx"],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const before = Date.now();
|
|
270
|
+
handleToolUsePreviewStart(state, deps, {
|
|
271
|
+
type: "tool_use_preview_start",
|
|
272
|
+
toolUseId: "toolu_preview_ts",
|
|
273
|
+
toolName: "bash",
|
|
274
|
+
});
|
|
275
|
+
const after = Date.now();
|
|
276
|
+
|
|
277
|
+
const emitted = collector.events[0] as { previewStartedAt?: number };
|
|
278
|
+
expect(typeof emitted.previewStartedAt).toBe("number");
|
|
279
|
+
expect(emitted.previewStartedAt!).toBeGreaterThanOrEqual(before);
|
|
280
|
+
expect(emitted.previewStartedAt!).toBeLessThanOrEqual(after);
|
|
281
|
+
// The same first-byte timestamp is retained in state so tool_use_start
|
|
282
|
+
// can carry it through.
|
|
283
|
+
expect(state.toolPreviewStartedAt.get("toolu_preview_ts")).toBe(
|
|
284
|
+
emitted.previewStartedAt,
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("handleToolUse carries the stored previewStartedAt onto tool_use_start", () => {
|
|
289
|
+
const collector = createEventCollector();
|
|
290
|
+
const deps = createMockDeps({
|
|
291
|
+
onEvent: collector.onEvent,
|
|
292
|
+
ctx: {
|
|
293
|
+
...createMockDeps().ctx,
|
|
294
|
+
emitActivityState: collector.emitActivityState,
|
|
295
|
+
} as unknown as EventHandlerDeps["ctx"],
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// GIVEN a preview was recognized first
|
|
299
|
+
handleToolUsePreviewStart(state, deps, {
|
|
300
|
+
type: "tool_use_preview_start",
|
|
301
|
+
toolUseId: "toolu_carry",
|
|
302
|
+
toolName: "bash",
|
|
303
|
+
});
|
|
304
|
+
const previewStartedAt = state.toolPreviewStartedAt.get("toolu_carry");
|
|
305
|
+
|
|
306
|
+
// WHEN the tool actually begins executing
|
|
307
|
+
handleToolUse(state, deps, {
|
|
308
|
+
type: "tool_use",
|
|
309
|
+
id: "toolu_carry",
|
|
310
|
+
name: "bash",
|
|
311
|
+
input: { command: "ls" },
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// THEN the tool_use_start event carries the first-byte anchor alongside
|
|
315
|
+
// its own (later) execution startedAt
|
|
316
|
+
const toolUseStart = collector.events.find(
|
|
317
|
+
(e) => e.type === "tool_use_start",
|
|
318
|
+
) as { previewStartedAt?: number; startedAt?: number };
|
|
319
|
+
expect(toolUseStart).toBeDefined();
|
|
320
|
+
expect(toolUseStart.previewStartedAt).toBe(previewStartedAt);
|
|
321
|
+
expect(typeof toolUseStart.startedAt).toBe("number");
|
|
322
|
+
expect(toolUseStart.startedAt!).toBeGreaterThanOrEqual(previewStartedAt!);
|
|
323
|
+
});
|
|
324
|
+
|
|
259
325
|
test("emits activity state with tool_running phase and preview_start reason", () => {
|
|
260
326
|
const collector = createEventCollector();
|
|
261
327
|
const deps = createMockDeps({
|