@vellumai/assistant 0.4.30 → 0.4.31
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/Dockerfile +14 -8
- package/README.md +2 -2
- package/docs/architecture/memory.md +28 -29
- package/docs/runbook-trusted-contacts.md +1 -4
- package/package.json +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +0 -4
- package/src/__tests__/config-schema.test.ts +0 -9
- package/src/__tests__/conflict-policy.test.ts +76 -0
- package/src/__tests__/conflict-store.test.ts +14 -20
- package/src/__tests__/contacts-tools.test.ts +8 -61
- package/src/__tests__/contradiction-checker.test.ts +5 -1
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
- package/src/__tests__/guardian-routing-invariants.test.ts +6 -4
- package/src/__tests__/memory-lifecycle-e2e.test.ts +11 -10
- package/src/__tests__/registry.test.ts +0 -10
- package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
- package/src/__tests__/session-agent-loop.test.ts +0 -2
- package/src/__tests__/session-conflict-gate.test.ts +243 -388
- package/src/__tests__/session-profile-injection.test.ts +0 -2
- package/src/__tests__/session-runtime-assembly.test.ts +2 -3
- package/src/__tests__/session-skill-tools.test.ts +0 -49
- package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
- package/src/__tests__/session-workspace-injection.test.ts +0 -1
- package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
- package/src/approvals/guardian-decision-primitive.ts +11 -7
- package/src/approvals/guardian-request-resolvers.ts +5 -3
- package/src/calls/relay-server.ts +5 -0
- package/src/config/bundled-skills/contacts/SKILL.md +7 -18
- package/src/config/bundled-skills/contacts/TOOLS.json +4 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +2 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +6 -12
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +3 -24
- package/src/config/bundled-tool-registry.ts +0 -5
- package/src/config/memory-schema.ts +0 -10
- package/src/config/system-prompt.ts +6 -0
- package/src/contacts/contact-store.ts +36 -62
- package/src/contacts/contacts-write.ts +14 -3
- package/src/contacts/types.ts +9 -4
- package/src/daemon/handlers/config-heartbeat.ts +1 -2
- package/src/daemon/handlers/contacts.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +1 -1
- package/src/daemon/handlers/sessions.ts +2 -1
- package/src/daemon/ipc-contract/contacts.ts +2 -2
- package/src/daemon/session-agent-loop.ts +1 -45
- package/src/daemon/session-conflict-gate.ts +21 -82
- package/src/daemon/session-memory.ts +7 -52
- package/src/daemon/session-process.ts +3 -1
- package/src/daemon/session-runtime-assembly.ts +18 -35
- package/src/heartbeat/heartbeat-service.ts +5 -1
- package/src/memory/conflict-intent.ts +3 -6
- package/src/memory/conflict-policy.ts +34 -0
- package/src/memory/conflict-store.ts +10 -18
- package/src/memory/contradiction-checker.ts +2 -2
- package/src/memory/db-init.ts +4 -0
- package/src/memory/job-handlers/conflict.ts +0 -7
- package/src/memory/migrations/134-contacts-notes-column.ts +51 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema.ts +1 -18
- package/src/messaging/index.ts +0 -1
- package/src/messaging/types.ts +0 -38
- package/src/runtime/guardian-action-service.ts +3 -2
- package/src/runtime/guardian-outbound-actions.ts +3 -3
- package/src/runtime/guardian-reply-router.ts +4 -4
- package/src/runtime/http-server.ts +12 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
- package/src/runtime/routes/contact-routes.ts +308 -29
- package/src/runtime/routes/conversation-routes.ts +2 -1
- package/src/runtime/routes/global-search-routes.ts +2 -2
- package/src/runtime/routes/guardian-action-routes.ts +1 -1
- package/src/runtime/routes/guardian-approval-interception.ts +2 -1
- package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +5 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
- package/src/runtime/routes/migration-routes.ts +17 -17
- package/src/workspace/git-service.ts +6 -4
- package/src/__tests__/get-weather.test.ts +0 -393
- package/src/__tests__/weather-skill-regression.test.ts +0 -276
- package/src/autonomy/autonomy-resolver.ts +0 -62
- package/src/autonomy/autonomy-store.ts +0 -138
- package/src/autonomy/disposition-mapper.ts +0 -31
- package/src/autonomy/index.ts +0 -11
- package/src/autonomy/types.ts +0 -43
- package/src/config/bundled-skills/weather/SKILL.md +0 -38
- package/src/config/bundled-skills/weather/TOOLS.json +0 -36
- package/src/config/bundled-skills/weather/icon.svg +0 -24
- package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
- package/src/messaging/triage-engine.ts +0 -344
- package/src/tools/weather/service.ts +0 -712
|
@@ -33,10 +33,7 @@ function parseContact(row: typeof contacts.$inferSelect): Contact {
|
|
|
33
33
|
return {
|
|
34
34
|
id: row.id,
|
|
35
35
|
displayName: row.displayName,
|
|
36
|
-
|
|
37
|
-
importance: row.importance,
|
|
38
|
-
responseExpectation: row.responseExpectation,
|
|
39
|
-
preferredTone: row.preferredTone,
|
|
36
|
+
notes: row.notes,
|
|
40
37
|
lastInteraction: row.lastInteraction,
|
|
41
38
|
interactionCount: row.interactionCount,
|
|
42
39
|
createdAt: row.createdAt,
|
|
@@ -153,10 +150,7 @@ export function getChannelById(channelId: string): ContactChannel | null {
|
|
|
153
150
|
export function upsertContact(params: {
|
|
154
151
|
id?: string;
|
|
155
152
|
displayName: string;
|
|
156
|
-
|
|
157
|
-
importance?: number;
|
|
158
|
-
responseExpectation?: string | null;
|
|
159
|
-
preferredTone?: string | null;
|
|
153
|
+
notes?: string | null;
|
|
160
154
|
role?: ContactRole;
|
|
161
155
|
contactType?: ContactType;
|
|
162
156
|
principalId?: string | null;
|
|
@@ -178,24 +172,9 @@ export function upsertContact(params: {
|
|
|
178
172
|
if (existing) {
|
|
179
173
|
const updateSet: Record<string, unknown> = {
|
|
180
174
|
displayName: params.displayName,
|
|
181
|
-
relationship:
|
|
182
|
-
params.relationship !== undefined
|
|
183
|
-
? params.relationship
|
|
184
|
-
: existing.relationship,
|
|
185
|
-
importance:
|
|
186
|
-
params.importance !== undefined
|
|
187
|
-
? params.importance
|
|
188
|
-
: existing.importance,
|
|
189
|
-
responseExpectation:
|
|
190
|
-
params.responseExpectation !== undefined
|
|
191
|
-
? params.responseExpectation
|
|
192
|
-
: existing.responseExpectation,
|
|
193
|
-
preferredTone:
|
|
194
|
-
params.preferredTone !== undefined
|
|
195
|
-
? params.preferredTone
|
|
196
|
-
: existing.preferredTone,
|
|
197
175
|
updatedAt: now,
|
|
198
176
|
};
|
|
177
|
+
if (params.notes !== undefined) updateSet.notes = params.notes;
|
|
199
178
|
if (params.role !== undefined) updateSet.role = params.role;
|
|
200
179
|
if (params.contactType !== undefined)
|
|
201
180
|
updateSet.contactType = params.contactType;
|
|
@@ -213,6 +192,7 @@ export function upsertContact(params: {
|
|
|
213
192
|
syncChannels(contactId, params.channels, now);
|
|
214
193
|
}
|
|
215
194
|
|
|
195
|
+
emitContactChange();
|
|
216
196
|
return { ...getContactInternal(contactId)!, created: false };
|
|
217
197
|
}
|
|
218
198
|
}
|
|
@@ -259,14 +239,7 @@ export function upsertContact(params: {
|
|
|
259
239
|
displayName: params.displayName,
|
|
260
240
|
updatedAt: now,
|
|
261
241
|
};
|
|
262
|
-
if (params.
|
|
263
|
-
updateSet.relationship = params.relationship;
|
|
264
|
-
if (params.importance !== undefined)
|
|
265
|
-
updateSet.importance = params.importance;
|
|
266
|
-
if (params.responseExpectation !== undefined)
|
|
267
|
-
updateSet.responseExpectation = params.responseExpectation;
|
|
268
|
-
if (params.preferredTone !== undefined)
|
|
269
|
-
updateSet.preferredTone = params.preferredTone;
|
|
242
|
+
if (params.notes !== undefined) updateSet.notes = params.notes;
|
|
270
243
|
if (params.role !== undefined) updateSet.role = params.role;
|
|
271
244
|
if (params.contactType !== undefined)
|
|
272
245
|
updateSet.contactType = params.contactType;
|
|
@@ -281,6 +254,7 @@ export function upsertContact(params: {
|
|
|
281
254
|
.run();
|
|
282
255
|
|
|
283
256
|
syncChannels(contactId, params.channels, now);
|
|
257
|
+
emitContactChange();
|
|
284
258
|
return { ...getContactInternal(contactId)!, created: false };
|
|
285
259
|
}
|
|
286
260
|
}
|
|
@@ -292,10 +266,7 @@ export function upsertContact(params: {
|
|
|
292
266
|
.values({
|
|
293
267
|
id: contactId,
|
|
294
268
|
displayName: params.displayName,
|
|
295
|
-
|
|
296
|
-
importance: params.importance ?? 0.5,
|
|
297
|
-
responseExpectation: params.responseExpectation ?? null,
|
|
298
|
-
preferredTone: params.preferredTone ?? null,
|
|
269
|
+
notes: params.notes ?? null,
|
|
299
270
|
lastInteraction: null,
|
|
300
271
|
interactionCount: 0,
|
|
301
272
|
role: params.role ?? "contact",
|
|
@@ -311,6 +282,7 @@ export function upsertContact(params: {
|
|
|
311
282
|
syncChannels(contactId, params.channels, now);
|
|
312
283
|
}
|
|
313
284
|
|
|
285
|
+
emitContactChange();
|
|
314
286
|
return { ...getContactInternal(contactId)!, created: true };
|
|
315
287
|
}
|
|
316
288
|
|
|
@@ -436,7 +408,6 @@ export function searchContacts(params: {
|
|
|
436
408
|
query?: string;
|
|
437
409
|
channelAddress?: string;
|
|
438
410
|
channelType?: string;
|
|
439
|
-
relationship?: string;
|
|
440
411
|
role?: ContactRole;
|
|
441
412
|
contactType?: ContactType;
|
|
442
413
|
limit?: number;
|
|
@@ -491,7 +462,7 @@ export function searchContacts(params: {
|
|
|
491
462
|
}
|
|
492
463
|
|
|
493
464
|
// Search by channel type alone (no address)
|
|
494
|
-
if (params.channelType && !params.query
|
|
465
|
+
if (params.channelType && !params.query) {
|
|
495
466
|
const channelRows = db
|
|
496
467
|
.select({ contactId: contactChannels.contactId })
|
|
497
468
|
.from(contactChannels)
|
|
@@ -525,7 +496,7 @@ export function searchContacts(params: {
|
|
|
525
496
|
return results;
|
|
526
497
|
}
|
|
527
498
|
|
|
528
|
-
// Search by display name
|
|
499
|
+
// Search by display name, optionally filtered by channelType
|
|
529
500
|
const conditions = [
|
|
530
501
|
or(
|
|
531
502
|
eq(contacts.assistantId, params.assistantId),
|
|
@@ -534,20 +505,11 @@ export function searchContacts(params: {
|
|
|
534
505
|
];
|
|
535
506
|
if (params.query) {
|
|
536
507
|
const sanitized = escapeLike(params.query);
|
|
537
|
-
if (
|
|
538
|
-
!sanitized &&
|
|
539
|
-
!params.relationship &&
|
|
540
|
-
!params.role &&
|
|
541
|
-
!params.contactType
|
|
542
|
-
)
|
|
543
|
-
return [];
|
|
508
|
+
if (!sanitized && !params.role && !params.contactType) return [];
|
|
544
509
|
if (sanitized) {
|
|
545
510
|
conditions.push(like(contacts.displayName, `%${sanitized}%`));
|
|
546
511
|
}
|
|
547
512
|
}
|
|
548
|
-
if (params.relationship) {
|
|
549
|
-
conditions.push(eq(contacts.relationship, params.relationship));
|
|
550
|
-
}
|
|
551
513
|
if (params.role) {
|
|
552
514
|
conditions.push(eq(contacts.role, params.role));
|
|
553
515
|
}
|
|
@@ -569,7 +531,7 @@ export function searchContacts(params: {
|
|
|
569
531
|
.from(contacts)
|
|
570
532
|
.innerJoin(contactChannels, eq(contacts.id, contactChannels.contactId))
|
|
571
533
|
.where(whereClause)
|
|
572
|
-
.orderBy(desc(contacts.
|
|
534
|
+
.orderBy(desc(contacts.updatedAt), desc(contacts.lastInteraction))
|
|
573
535
|
.all();
|
|
574
536
|
|
|
575
537
|
const contactIds = [...new Set(rows.map((r) => r.contactId))];
|
|
@@ -590,7 +552,7 @@ export function searchContacts(params: {
|
|
|
590
552
|
.select()
|
|
591
553
|
.from(contacts)
|
|
592
554
|
.where(whereClause)
|
|
593
|
-
.orderBy(desc(contacts.
|
|
555
|
+
.orderBy(desc(contacts.updatedAt), desc(contacts.lastInteraction))
|
|
594
556
|
.limit(limit)
|
|
595
557
|
.all();
|
|
596
558
|
|
|
@@ -617,7 +579,7 @@ export function listContacts(
|
|
|
617
579
|
.where(conditions.length === 1 ? conditions[0] : and(...conditions))
|
|
618
580
|
.orderBy(
|
|
619
581
|
sql`${contacts.role} = 'guardian' DESC`,
|
|
620
|
-
desc(contacts.
|
|
582
|
+
desc(contacts.updatedAt),
|
|
621
583
|
desc(contacts.lastInteraction),
|
|
622
584
|
)
|
|
623
585
|
.limit(effectiveLimit)
|
|
@@ -626,9 +588,9 @@ export function listContacts(
|
|
|
626
588
|
}
|
|
627
589
|
|
|
628
590
|
/**
|
|
629
|
-
* Merge two contacts into one. The surviving contact keeps the
|
|
630
|
-
* more recent interaction timestamp, and all channels
|
|
631
|
-
* The donor contact is deleted after merging.
|
|
591
|
+
* Merge two contacts into one. The surviving contact keeps the
|
|
592
|
+
* more recent interaction timestamp, concatenated notes, and all channels
|
|
593
|
+
* from both contacts. The donor contact is deleted after merging.
|
|
632
594
|
*/
|
|
633
595
|
export function mergeContacts(
|
|
634
596
|
keepId: string,
|
|
@@ -673,7 +635,6 @@ export function mergeContacts(
|
|
|
673
635
|
if (!merge) throw new Error(`Contact "${mergeId}" not found`);
|
|
674
636
|
|
|
675
637
|
// Resolve merged field values — pick the better/more recent value
|
|
676
|
-
const mergedImportance = Math.max(keep.importance, merge.importance);
|
|
677
638
|
const mergedInteractionCount =
|
|
678
639
|
keep.interactionCount + merge.interactionCount;
|
|
679
640
|
const mergedLastInteraction =
|
|
@@ -681,14 +642,9 @@ export function mergeContacts(
|
|
|
681
642
|
|
|
682
643
|
tx.update(contacts)
|
|
683
644
|
.set({
|
|
684
|
-
importance: mergedImportance,
|
|
685
645
|
interactionCount: mergedInteractionCount,
|
|
686
646
|
lastInteraction: mergedLastInteraction,
|
|
687
|
-
|
|
688
|
-
relationship: keep.relationship ?? merge.relationship,
|
|
689
|
-
responseExpectation:
|
|
690
|
-
keep.responseExpectation ?? merge.responseExpectation,
|
|
691
|
-
preferredTone: keep.preferredTone ?? merge.preferredTone,
|
|
647
|
+
notes: [keep.notes, merge.notes].filter(Boolean).join("\n") || null,
|
|
692
648
|
updatedAt: now,
|
|
693
649
|
// Rebind legacy null-scoped contacts to prevent cross-assistant leakage
|
|
694
650
|
...(keep.assistantId == null ? { assistantId } : {}),
|
|
@@ -728,6 +684,7 @@ export function mergeContacts(
|
|
|
728
684
|
tx.delete(contacts).where(eq(contacts.id, mergeId)).run();
|
|
729
685
|
});
|
|
730
686
|
|
|
687
|
+
emitContactChange();
|
|
731
688
|
return getContactInternal(keepId)!;
|
|
732
689
|
}
|
|
733
690
|
|
|
@@ -1026,6 +983,23 @@ export function updateChannelLastSeenById(channelId: string): void {
|
|
|
1026
983
|
.run();
|
|
1027
984
|
}
|
|
1028
985
|
|
|
986
|
+
/**
|
|
987
|
+
* Atomically increment interactionCount and set lastInteraction on a contact.
|
|
988
|
+
* Optimized for the hot path — single UPDATE with no prior SELECT.
|
|
989
|
+
*/
|
|
990
|
+
export function updateContactInteraction(contactId: string): void {
|
|
991
|
+
const db = getDb();
|
|
992
|
+
const now = Date.now();
|
|
993
|
+
db.update(contacts)
|
|
994
|
+
.set({
|
|
995
|
+
lastInteraction: now,
|
|
996
|
+
interactionCount: sql`${contacts.interactionCount} + 1`,
|
|
997
|
+
updatedAt: now,
|
|
998
|
+
})
|
|
999
|
+
.where(eq(contacts.id, contactId))
|
|
1000
|
+
.run();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1029
1003
|
// ── Assistant Contact Metadata ──────────────────────────────────────
|
|
1030
1004
|
|
|
1031
1005
|
function parseAssistantMetadata(
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
getContactInternal,
|
|
20
20
|
updateChannelLastSeenById,
|
|
21
21
|
updateChannelStatus,
|
|
22
|
+
updateContactInteraction,
|
|
22
23
|
upsertContact,
|
|
23
24
|
} from "./contact-store.js";
|
|
24
25
|
import type {
|
|
@@ -78,6 +79,7 @@ export function createGuardianBinding(params: {
|
|
|
78
79
|
upsertContact({
|
|
79
80
|
displayName,
|
|
80
81
|
role: "guardian",
|
|
82
|
+
notes: "guardian",
|
|
81
83
|
principalId: params.guardianPrincipalId,
|
|
82
84
|
assistantId: params.assistantId,
|
|
83
85
|
channels: [
|
|
@@ -109,7 +111,6 @@ export function createGuardianBinding(params: {
|
|
|
109
111
|
updatedAt: now,
|
|
110
112
|
};
|
|
111
113
|
|
|
112
|
-
emitContactChange();
|
|
113
114
|
return result;
|
|
114
115
|
}
|
|
115
116
|
|
|
@@ -204,8 +205,6 @@ export function upsertMember(params: {
|
|
|
204
205
|
externalChatId: params.externalChatId,
|
|
205
206
|
});
|
|
206
207
|
|
|
207
|
-
emitContactChange();
|
|
208
|
-
|
|
209
208
|
if (contactResult) {
|
|
210
209
|
return { contact: contactResult.contact, channel: contactResult.channel };
|
|
211
210
|
}
|
|
@@ -285,3 +284,15 @@ export function touchChannelLastSeen(channelId: string): void {
|
|
|
285
284
|
log.warn({ err }, "Failed to update channel lastSeenAt");
|
|
286
285
|
}
|
|
287
286
|
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Increment the interaction count and update lastInteraction on a contact.
|
|
290
|
+
* Expects a plain contact UUID (Contact.id).
|
|
291
|
+
*/
|
|
292
|
+
export function touchContactInteraction(contactId: string): void {
|
|
293
|
+
try {
|
|
294
|
+
updateContactInteraction(contactId);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
log.warn({ err }, "Failed to update contact interaction stats");
|
|
297
|
+
}
|
|
298
|
+
}
|
package/src/contacts/types.ts
CHANGED
|
@@ -28,16 +28,21 @@ export type AssistantContactMetadata =
|
|
|
28
28
|
export interface Contact {
|
|
29
29
|
id: string;
|
|
30
30
|
displayName: string;
|
|
31
|
-
relationship
|
|
32
|
-
|
|
33
|
-
responseExpectation: string | null;
|
|
34
|
-
preferredTone: string | null;
|
|
31
|
+
/** Free-text notes about this contact (e.g. relationship, communication preferences). */
|
|
32
|
+
notes: string | null;
|
|
35
33
|
lastInteraction: number | null;
|
|
36
34
|
interactionCount: number;
|
|
37
35
|
createdAt: number;
|
|
38
36
|
updatedAt: number;
|
|
39
37
|
role: ContactRole;
|
|
40
38
|
contactType: ContactType;
|
|
39
|
+
/**
|
|
40
|
+
* Internal auth identity (e.g. "vellum-principal-<uuid>"). Only meaningful
|
|
41
|
+
* for guardian contacts — it ties the contact record to the auth layer so
|
|
42
|
+
* the system can verify "this API caller IS this guardian" via JWT
|
|
43
|
+
* actorPrincipalId. Always null for non-guardian contacts, which are
|
|
44
|
+
* identified by channel address instead.
|
|
45
|
+
*/
|
|
41
46
|
principalId: string | null;
|
|
42
47
|
assistantId: string | null;
|
|
43
48
|
}
|
|
@@ -218,8 +218,7 @@ export function handleHeartbeatRunNow(
|
|
|
218
218
|
ctx.send(socket, {
|
|
219
219
|
type: "heartbeat_run_now_response",
|
|
220
220
|
success: false,
|
|
221
|
-
error:
|
|
222
|
-
"Heartbeat skipped (a previous run is still active)",
|
|
221
|
+
error: "Heartbeat skipped (a previous run is still active)",
|
|
223
222
|
});
|
|
224
223
|
}
|
|
225
224
|
})
|
|
@@ -38,8 +38,8 @@ function toContactPayload(contact: ContactWithChannels): ContactPayload {
|
|
|
38
38
|
id: contact.id,
|
|
39
39
|
displayName: contact.displayName,
|
|
40
40
|
role: contact.role,
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
notes: contact.notes ?? undefined,
|
|
42
|
+
contactType: contact.contactType ?? undefined,
|
|
43
43
|
lastInteraction: contact.lastInteraction ?? undefined,
|
|
44
44
|
interactionCount: contact.interactionCount,
|
|
45
45
|
channels: contact.channels.map(toChannelPayload),
|
|
@@ -38,7 +38,7 @@ export const guardianActionsHandlers = defineHandlers({
|
|
|
38
38
|
conversationId: msg.conversationId,
|
|
39
39
|
channel: "vellum",
|
|
40
40
|
actorContext: {
|
|
41
|
-
|
|
41
|
+
actorPrincipalId: localTrustCtx.guardianExternalUserId,
|
|
42
42
|
guardianPrincipalId: localTrustCtx.guardianPrincipalId ?? undefined,
|
|
43
43
|
},
|
|
44
44
|
});
|
|
@@ -895,7 +895,8 @@ export async function handleUserMessage(
|
|
|
895
895
|
messageText: messageText.trim(),
|
|
896
896
|
channel: ipcChannel,
|
|
897
897
|
actor: {
|
|
898
|
-
|
|
898
|
+
actorPrincipalId: localCtx.guardianPrincipalId ?? undefined,
|
|
899
|
+
actorExternalUserId: localCtx.guardianExternalUserId,
|
|
899
900
|
channel: ipcChannel,
|
|
900
901
|
guardianPrincipalId: localCtx.guardianPrincipalId ?? undefined,
|
|
901
902
|
},
|
|
@@ -40,8 +40,8 @@ export interface ContactPayload {
|
|
|
40
40
|
id: string;
|
|
41
41
|
displayName: string;
|
|
42
42
|
role: "guardian" | "contact";
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
notes?: string;
|
|
44
|
+
contactType?: string;
|
|
45
45
|
lastInteraction?: number;
|
|
46
46
|
interactionCount: number;
|
|
47
47
|
channels: ContactChannelPayload[];
|
|
@@ -453,50 +453,7 @@ export async function runAgentLoopImpl(
|
|
|
453
453
|
onEvent,
|
|
454
454
|
);
|
|
455
455
|
|
|
456
|
-
|
|
457
|
-
const loopChannelMeta = {
|
|
458
|
-
...provenanceFromTrustContext(ctx.trustContext),
|
|
459
|
-
userMessageChannel: capturedTurnChannelContext.userMessageChannel,
|
|
460
|
-
assistantMessageChannel:
|
|
461
|
-
capturedTurnChannelContext.assistantMessageChannel,
|
|
462
|
-
userMessageInterface: capturedTurnInterfaceContext.userMessageInterface,
|
|
463
|
-
assistantMessageInterface:
|
|
464
|
-
capturedTurnInterfaceContext.assistantMessageInterface,
|
|
465
|
-
};
|
|
466
|
-
const assistantMessage = createAssistantMessage(
|
|
467
|
-
memoryResult.conflictClarification,
|
|
468
|
-
);
|
|
469
|
-
await conversationStore.addMessage(
|
|
470
|
-
ctx.conversationId,
|
|
471
|
-
"assistant",
|
|
472
|
-
JSON.stringify(assistantMessage.content),
|
|
473
|
-
loopChannelMeta,
|
|
474
|
-
);
|
|
475
|
-
ctx.messages.push(assistantMessage);
|
|
476
|
-
onEvent({
|
|
477
|
-
type: "assistant_text_delta",
|
|
478
|
-
text: memoryResult.conflictClarification,
|
|
479
|
-
sessionId: ctx.conversationId,
|
|
480
|
-
});
|
|
481
|
-
ctx.traceEmitter.emit(
|
|
482
|
-
"message_complete",
|
|
483
|
-
"Conflict clarification requested (relevant)",
|
|
484
|
-
{
|
|
485
|
-
requestId: reqId,
|
|
486
|
-
status: "info",
|
|
487
|
-
attributes: { conflictGate: "relevant" },
|
|
488
|
-
},
|
|
489
|
-
);
|
|
490
|
-
onEvent({ type: "message_complete", sessionId: ctx.conversationId });
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const {
|
|
495
|
-
recall,
|
|
496
|
-
dynamicProfile,
|
|
497
|
-
softConflictInstruction,
|
|
498
|
-
recallInjectionStrategy,
|
|
499
|
-
} = memoryResult;
|
|
456
|
+
const { recall, dynamicProfile, recallInjectionStrategy } = memoryResult;
|
|
500
457
|
runMessages = memoryResult.runMessages;
|
|
501
458
|
|
|
502
459
|
// Build active surface context
|
|
@@ -585,7 +542,6 @@ export async function runAgentLoopImpl(
|
|
|
585
542
|
|
|
586
543
|
// Shared injection options — reused whenever we need to re-inject after reduction.
|
|
587
544
|
const injectionOpts = {
|
|
588
|
-
softConflictInstruction,
|
|
589
545
|
activeSurface,
|
|
590
546
|
workspaceTopLevelContext: ctx.workspaceTopLevelContext,
|
|
591
547
|
channelCapabilities: ctx.channelCapabilities ?? null,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Conflict-gate logic extracted from Session.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Handles pending memory conflicts internally: dismisses non-user-evidenced
|
|
5
|
+
* and non-actionable conflicts, and attempts resolution when the user's reply
|
|
6
|
+
* looks like an explicit clarification with topical relevance. Never produces
|
|
7
|
+
* user-facing clarification text.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { resolveConflictClarification } from "../memory/clarification-resolver.js";
|
|
@@ -14,47 +16,34 @@ import {
|
|
|
14
16
|
} from "../memory/conflict-intent.js";
|
|
15
17
|
import {
|
|
16
18
|
isConflictKindPairEligible,
|
|
19
|
+
isConflictUserEvidenced,
|
|
17
20
|
isStatementConflictEligible,
|
|
18
21
|
} from "../memory/conflict-policy.js";
|
|
19
22
|
import type { PendingConflictDetail } from "../memory/conflict-store.js";
|
|
20
23
|
import {
|
|
21
24
|
applyConflictResolution,
|
|
22
25
|
listPendingConflictDetails,
|
|
23
|
-
markConflictAsked,
|
|
24
26
|
resolveConflict,
|
|
25
27
|
} from "../memory/conflict-store.js";
|
|
26
28
|
|
|
27
|
-
export interface ConflictGateDecision {
|
|
28
|
-
question: string;
|
|
29
|
-
relevant: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
29
|
export class ConflictGate {
|
|
33
|
-
private turnCounter = 0;
|
|
34
|
-
private lastAskedTurn = new Map<string, number>();
|
|
35
|
-
|
|
36
30
|
async evaluate(
|
|
37
31
|
userMessage: string,
|
|
38
32
|
conflictConfig: {
|
|
39
33
|
enabled: boolean;
|
|
40
34
|
gateMode: string;
|
|
41
35
|
relevanceThreshold: number;
|
|
42
|
-
reaskCooldownTurns: number;
|
|
43
36
|
resolverLlmTimeoutMs: number;
|
|
44
|
-
askOnIrrelevantTurns: boolean;
|
|
45
37
|
conflictableKinds: readonly string[];
|
|
46
38
|
},
|
|
47
39
|
scopeId = "default",
|
|
48
|
-
): Promise<
|
|
49
|
-
if (!conflictConfig.enabled || conflictConfig.gateMode !== "soft")
|
|
50
|
-
return null;
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
if (!conflictConfig.enabled || conflictConfig.gateMode !== "soft") return;
|
|
51
42
|
|
|
52
|
-
this.turnCounter += 1;
|
|
53
|
-
const threshold = conflictConfig.relevanceThreshold;
|
|
54
|
-
const cooldownTurns = Math.max(1, conflictConfig.reaskCooldownTurns);
|
|
55
43
|
const pendingBeforeResolve = listPendingConflictDetails(scopeId, 50);
|
|
56
44
|
|
|
57
|
-
// Dismiss non-actionable conflicts (kind/statement policy
|
|
45
|
+
// Dismiss non-actionable conflicts (kind/statement policy, incoherent pair,
|
|
46
|
+
// or assistant-inferred-only provenance with no user evidence)
|
|
58
47
|
const dismissedIds = new Set<string>();
|
|
59
48
|
for (const conflict of pendingBeforeResolve) {
|
|
60
49
|
const dismissReason = this.getDismissReason(
|
|
@@ -73,13 +62,15 @@ export class ConflictGate {
|
|
|
73
62
|
const actionablePending = pendingBeforeResolve.filter(
|
|
74
63
|
(c) => !dismissedIds.has(c.id),
|
|
75
64
|
);
|
|
65
|
+
|
|
66
|
+
// Attempt resolution only for explicit clarification-like replies with
|
|
67
|
+
// topical relevance to the conflict statements
|
|
76
68
|
const clarificationReply = looksLikeClarificationReply(userMessage);
|
|
77
69
|
const candidatesBeforeResolve = actionablePending.filter((conflict) => {
|
|
78
70
|
const relevance = computeConflictRelevance(userMessage, conflict);
|
|
79
71
|
return shouldAttemptConflictResolution({
|
|
80
72
|
clarificationReply,
|
|
81
73
|
relevance,
|
|
82
|
-
wasRecentlyAsked: this.wasRecentlyAsked(conflict.id, cooldownTurns),
|
|
83
74
|
});
|
|
84
75
|
});
|
|
85
76
|
await this.resolvePendingConflicts(
|
|
@@ -87,45 +78,6 @@ export class ConflictGate {
|
|
|
87
78
|
conflictConfig.resolverLlmTimeoutMs,
|
|
88
79
|
candidatesBeforeResolve,
|
|
89
80
|
);
|
|
90
|
-
|
|
91
|
-
const pending = listPendingConflictDetails(scopeId, 50);
|
|
92
|
-
if (pending.length === 0) return null;
|
|
93
|
-
|
|
94
|
-
const scored = pending.map((conflict) => ({
|
|
95
|
-
conflict,
|
|
96
|
-
relevance: computeConflictRelevance(userMessage, conflict),
|
|
97
|
-
}));
|
|
98
|
-
// Try relevant conflicts first
|
|
99
|
-
const askable = scored
|
|
100
|
-
.filter((entry) => entry.relevance >= threshold)
|
|
101
|
-
.find((entry) => this.shouldAsk(entry.conflict.id, cooldownTurns));
|
|
102
|
-
|
|
103
|
-
// If no relevant conflict to ask and askOnIrrelevantTurns is enabled, try ones
|
|
104
|
-
// below the threshold (including zero-relevance). Zero-relevance conflicts are
|
|
105
|
-
// surfaced but not tracked as asked, preventing wasRecentlyAsked from triggering
|
|
106
|
-
// heuristic resolution on subsequent unrelated turns.
|
|
107
|
-
const candidateToAsk =
|
|
108
|
-
askable ??
|
|
109
|
-
(conflictConfig.askOnIrrelevantTurns
|
|
110
|
-
? scored.find(
|
|
111
|
-
(entry) =>
|
|
112
|
-
entry.relevance < threshold &&
|
|
113
|
-
this.shouldAsk(entry.conflict.id, cooldownTurns),
|
|
114
|
-
)
|
|
115
|
-
: undefined);
|
|
116
|
-
|
|
117
|
-
if (!candidateToAsk) return null;
|
|
118
|
-
|
|
119
|
-
if (askable || candidateToAsk.relevance > 0) {
|
|
120
|
-
this.lastAskedTurn.set(candidateToAsk.conflict.id, this.turnCounter);
|
|
121
|
-
markConflictAsked(candidateToAsk.conflict.id);
|
|
122
|
-
}
|
|
123
|
-
return {
|
|
124
|
-
question:
|
|
125
|
-
candidateToAsk.conflict.clarificationQuestion ??
|
|
126
|
-
buildFallbackConflictQuestion(candidateToAsk.conflict),
|
|
127
|
-
relevant: candidateToAsk.relevance >= threshold,
|
|
128
|
-
};
|
|
129
81
|
}
|
|
130
82
|
|
|
131
83
|
private async resolvePendingConflicts(
|
|
@@ -156,18 +108,6 @@ export class ConflictGate {
|
|
|
156
108
|
}
|
|
157
109
|
}
|
|
158
110
|
|
|
159
|
-
private shouldAsk(conflictId: string, cooldownTurns: number): boolean {
|
|
160
|
-
const lastAsked = this.lastAskedTurn.get(conflictId);
|
|
161
|
-
if (lastAsked === undefined) return true;
|
|
162
|
-
return this.turnCounter - lastAsked >= cooldownTurns;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
private wasRecentlyAsked(conflictId: string, cooldownTurns: number): boolean {
|
|
166
|
-
const lastAsked = this.lastAskedTurn.get(conflictId);
|
|
167
|
-
if (lastAsked === undefined) return false;
|
|
168
|
-
return this.turnCounter - lastAsked <= cooldownTurns;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
111
|
/**
|
|
172
112
|
* Returns a dismissal reason if the conflict should be dismissed, or null if actionable.
|
|
173
113
|
*/
|
|
@@ -211,18 +151,17 @@ export class ConflictGate {
|
|
|
211
151
|
) {
|
|
212
152
|
return "Dismissed by conflict policy (incoherent — zero statement overlap).";
|
|
213
153
|
}
|
|
154
|
+
// Dismiss conflicts where neither side has user-evidenced provenance
|
|
155
|
+
if (
|
|
156
|
+
!isConflictUserEvidenced(
|
|
157
|
+
conflict.existingVerificationState,
|
|
158
|
+
conflict.candidateVerificationState,
|
|
159
|
+
)
|
|
160
|
+
) {
|
|
161
|
+
return "Dismissed by conflict policy (no user-evidenced provenance).";
|
|
162
|
+
}
|
|
214
163
|
return null;
|
|
215
164
|
}
|
|
216
165
|
}
|
|
217
166
|
|
|
218
|
-
export function buildFallbackConflictQuestion(
|
|
219
|
-
conflict: PendingConflictDetail,
|
|
220
|
-
): string {
|
|
221
|
-
return [
|
|
222
|
-
"I have two conflicting notes and need your confirmation.",
|
|
223
|
-
`A) ${conflict.existingStatement}`,
|
|
224
|
-
`B) ${conflict.candidateStatement}`,
|
|
225
|
-
"Which one should I keep?",
|
|
226
|
-
].join("\n");
|
|
227
|
-
}
|
|
228
167
|
export { computeConflictRelevance, looksLikeClarificationReply };
|