@vellumai/assistant 0.3.19 → 0.3.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +151 -15
- package/Dockerfile +1 -0
- package/README.md +40 -4
- package/bun.lock +139 -2
- package/docs/architecture/integrations.md +7 -11
- package/package.json +2 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
- package/src/__tests__/approval-primitive.test.ts +540 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
- package/src/__tests__/call-controller.test.ts +439 -108
- package/src/__tests__/channel-invite-transport.test.ts +264 -0
- package/src/__tests__/cli.test.ts +42 -1
- package/src/__tests__/config-schema.test.ts +11 -127
- package/src/__tests__/config-watcher.test.ts +0 -8
- package/src/__tests__/daemon-lifecycle.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +8 -2
- package/src/__tests__/diff.test.ts +22 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
- package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
- package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
- package/src/__tests__/guardian-dispatch.test.ts +124 -0
- package/src/__tests__/guardian-grant-minting.test.ts +6 -17
- package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
- package/src/__tests__/invite-redemption-service.test.ts +306 -0
- package/src/__tests__/ipc-snapshot.test.ts +57 -0
- package/src/__tests__/notification-decision-fallback.test.ts +88 -0
- package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
- package/src/__tests__/sandbox-host-parity.test.ts +6 -13
- package/src/__tests__/scoped-approval-grants.test.ts +6 -6
- package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
- package/src/__tests__/session-load-history-repair.test.ts +169 -2
- package/src/__tests__/session-runtime-assembly.test.ts +33 -5
- package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
- package/src/__tests__/skill-feature-flags.test.ts +188 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
- package/src/__tests__/skill-mirror-parity.test.ts +1 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/terminal-sandbox.test.ts +142 -9
- package/src/__tests__/terminal-tools.test.ts +2 -93
- package/src/__tests__/thread-seed-composer.test.ts +18 -0
- package/src/__tests__/tool-approval-handler.test.ts +350 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
- package/src/agent/loop.ts +36 -1
- package/src/approvals/approval-primitive.ts +381 -0
- package/src/approvals/guardian-decision-primitive.ts +191 -0
- package/src/calls/call-controller.ts +252 -209
- package/src/calls/call-domain.ts +44 -6
- package/src/calls/guardian-dispatch.ts +48 -0
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +46 -30
- package/src/cli/core-commands.ts +0 -4
- package/src/cli/mcp.ts +58 -0
- package/src/cli.ts +76 -34
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
- package/src/config/assistant-feature-flags.ts +162 -0
- package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
- package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/reminder/SKILL.md +49 -2
- package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
- package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
- package/src/config/core-schema.ts +1 -1
- package/src/config/env-registry.ts +10 -0
- package/src/config/feature-flag-registry.json +61 -0
- package/src/config/loader.ts +22 -1
- package/src/config/mcp-schema.ts +46 -0
- package/src/config/sandbox-schema.ts +0 -39
- package/src/config/schema.ts +18 -2
- package/src/config/skill-state.ts +34 -0
- package/src/config/skills-schema.ts +0 -1
- package/src/config/skills.ts +9 -0
- package/src/config/system-prompt.ts +110 -46
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +19 -1
- package/src/config/vellum-skills/catalog.json +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -5
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +105 -3
- package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
- package/src/daemon/config-watcher.ts +0 -1
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/guardian-invite-intent.ts +124 -0
- package/src/daemon/handlers/avatar.ts +68 -0
- package/src/daemon/handlers/browser.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +120 -0
- package/src/daemon/handlers/index.ts +4 -0
- package/src/daemon/handlers/sessions.ts +19 -0
- package/src/daemon/handlers/shared.ts +3 -1
- package/src/daemon/install-cli-launchers.ts +58 -13
- package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -2
- package/src/daemon/ipc-contract/settings.ts +25 -2
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +4 -0
- package/src/daemon/lifecycle.ts +14 -2
- package/src/daemon/main.ts +1 -0
- package/src/daemon/providers-setup.ts +26 -1
- package/src/daemon/server.ts +1 -0
- package/src/daemon/session-lifecycle.ts +52 -7
- package/src/daemon/session-memory.ts +45 -0
- package/src/daemon/session-process.ts +258 -432
- package/src/daemon/session-runtime-assembly.ts +12 -0
- package/src/daemon/session-skill-tools.ts +14 -1
- package/src/daemon/session-tool-setup.ts +5 -0
- package/src/daemon/session.ts +11 -0
- package/src/daemon/shutdown-handlers.ts +11 -0
- package/src/daemon/tool-side-effects.ts +35 -9
- package/src/index.ts +2 -2
- package/src/mcp/client.ts +152 -0
- package/src/mcp/manager.ts +139 -0
- package/src/memory/conversation-display-order-migration.ts +44 -0
- package/src/memory/conversation-queries.ts +2 -0
- package/src/memory/conversation-store.ts +91 -0
- package/src/memory/db-init.ts +5 -1
- package/src/memory/embedding-local.ts +13 -8
- package/src/memory/guardian-action-store.ts +125 -2
- package/src/memory/ingress-invite-store.ts +95 -1
- package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
- package/src/memory/migrations/index.ts +2 -1
- package/src/memory/schema.ts +5 -1
- package/src/memory/scoped-approval-grants.ts +14 -5
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/notifications/decision-engine.ts +49 -12
- package/src/notifications/emit-signal.ts +7 -0
- package/src/notifications/signal.ts +7 -0
- package/src/notifications/thread-seed-composer.ts +2 -1
- package/src/runtime/channel-approval-types.ts +16 -6
- package/src/runtime/channel-approvals.ts +19 -15
- package/src/runtime/channel-invite-transport.ts +85 -0
- package/src/runtime/channel-invite-transports/telegram.ts +105 -0
- package/src/runtime/guardian-action-grant-minter.ts +92 -35
- package/src/runtime/guardian-action-message-composer.ts +30 -0
- package/src/runtime/guardian-decision-types.ts +91 -0
- package/src/runtime/http-server.ts +23 -1
- package/src/runtime/ingress-service.ts +22 -0
- package/src/runtime/invite-redemption-service.ts +181 -0
- package/src/runtime/invite-redemption-templates.ts +39 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/guardian-action-routes.ts +206 -0
- package/src/runtime/routes/guardian-approval-interception.ts +66 -190
- package/src/runtime/routes/identity-routes.ts +73 -0
- package/src/runtime/routes/inbound-message-handler.ts +486 -394
- package/src/runtime/routes/pairing-routes.ts +4 -0
- package/src/security/encrypted-store.ts +31 -17
- package/src/security/keychain.ts +176 -2
- package/src/security/secure-keys.ts +97 -0
- package/src/security/tool-approval-digest.ts +1 -1
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/browser-manager.ts +46 -32
- package/src/tools/browser/browser-screencast.ts +2 -2
- package/src/tools/calls/call-start.ts +1 -1
- package/src/tools/executor.ts +22 -17
- package/src/tools/mcp/mcp-tool-factory.ts +100 -0
- package/src/tools/network/script-proxy/session-manager.ts +1 -5
- package/src/tools/registry.ts +64 -1
- package/src/tools/skills/load.ts +22 -8
- package/src/tools/system/avatar-generator.ts +119 -0
- package/src/tools/system/navigate-settings.ts +65 -0
- package/src/tools/system/open-system-settings.ts +75 -0
- package/src/tools/system/voice-config.ts +121 -32
- package/src/tools/terminal/backends/native.ts +40 -19
- package/src/tools/terminal/backends/types.ts +3 -3
- package/src/tools/terminal/parser.ts +1 -1
- package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
- package/src/tools/terminal/sandbox.ts +1 -12
- package/src/tools/terminal/shell.ts +3 -31
- package/src/tools/tool-approval-handler.ts +141 -3
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +10 -2
- package/src/util/diff.ts +36 -13
- package/Dockerfile.sandbox +0 -5
- package/src/__tests__/doordash-client.test.ts +0 -187
- package/src/__tests__/doordash-session.test.ts +0 -154
- package/src/__tests__/signup-e2e.test.ts +0 -354
- package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
- package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
- package/src/cli/doordash.ts +0 -1057
- package/src/config/bundled-skills/doordash/SKILL.md +0 -163
- package/src/config/templates/LOOKS.md +0 -25
- package/src/doordash/cart-queries.ts +0 -787
- package/src/doordash/client.ts +0 -1016
- package/src/doordash/order-queries.ts +0 -85
- package/src/doordash/queries.ts +0 -13
- package/src/doordash/query-extractor.ts +0 -94
- package/src/doordash/search-queries.ts +0 -203
- package/src/doordash/session.ts +0 -84
- package/src/doordash/store-queries.ts +0 -246
- package/src/doordash/types.ts +0 -367
- package/src/tools/terminal/backends/docker.ts +0 -379
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Re-export all conversation store functionality from focused sub-modules.
|
|
2
2
|
// Existing imports from this file continue to work without changes.
|
|
3
3
|
|
|
4
|
+
import { ensureDisplayOrderMigration } from './conversation-display-order-migration.js';
|
|
5
|
+
import { rawExec, rawGet, rawRun } from './db.js';
|
|
6
|
+
|
|
4
7
|
export {
|
|
5
8
|
addMessage,
|
|
6
9
|
clearAll,
|
|
@@ -42,3 +45,91 @@ export {
|
|
|
42
45
|
type PaginatedMessagesResult,
|
|
43
46
|
searchConversations,
|
|
44
47
|
} from './conversation-queries.js';
|
|
48
|
+
|
|
49
|
+
// Re-export for backward compat — callers that imported ensureColumns from here
|
|
50
|
+
export { ensureDisplayOrderMigration as ensureColumns } from './conversation-display-order-migration.js';
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// CRUD functions for display_order and is_pinned
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
export function getDisplayOrder(conversationId: string): number | null {
|
|
57
|
+
ensureDisplayOrderMigration();
|
|
58
|
+
const row = rawGet<{ display_order: number | null }>(
|
|
59
|
+
'SELECT display_order FROM conversations WHERE id = ?',
|
|
60
|
+
conversationId,
|
|
61
|
+
);
|
|
62
|
+
return row?.display_order ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function setDisplayOrder(conversationId: string, order: number | null): void {
|
|
66
|
+
ensureDisplayOrderMigration();
|
|
67
|
+
rawRun(
|
|
68
|
+
'UPDATE conversations SET display_order = ? WHERE id = ?',
|
|
69
|
+
order,
|
|
70
|
+
conversationId,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function batchSetDisplayOrders(
|
|
75
|
+
updates: Array<{ id: string; displayOrder: number | null; isPinned: boolean }>,
|
|
76
|
+
): void {
|
|
77
|
+
ensureDisplayOrderMigration();
|
|
78
|
+
rawExec('BEGIN');
|
|
79
|
+
try {
|
|
80
|
+
for (const update of updates) {
|
|
81
|
+
rawRun(
|
|
82
|
+
'UPDATE conversations SET display_order = ?, is_pinned = ? WHERE id = ?',
|
|
83
|
+
update.displayOrder,
|
|
84
|
+
update.isPinned ? 1 : 0,
|
|
85
|
+
update.id,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
rawExec('COMMIT');
|
|
89
|
+
} catch (err) {
|
|
90
|
+
rawExec('ROLLBACK');
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function setConversationPinned(conversationId: string, isPinned: boolean): void {
|
|
96
|
+
ensureDisplayOrderMigration();
|
|
97
|
+
rawRun(
|
|
98
|
+
'UPDATE conversations SET is_pinned = ? WHERE id = ?',
|
|
99
|
+
isPinned ? 1 : 0,
|
|
100
|
+
conversationId,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getConversationDisplayMeta(
|
|
105
|
+
conversationId: string,
|
|
106
|
+
): { displayOrder: number | null; isPinned: boolean } {
|
|
107
|
+
ensureDisplayOrderMigration();
|
|
108
|
+
const row = rawGet<{ display_order: number | null; is_pinned: number | null }>(
|
|
109
|
+
'SELECT display_order, is_pinned FROM conversations WHERE id = ?',
|
|
110
|
+
conversationId,
|
|
111
|
+
);
|
|
112
|
+
return {
|
|
113
|
+
displayOrder: row?.display_order ?? null,
|
|
114
|
+
isPinned: (row?.is_pinned ?? 0) === 1,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getDisplayMetaForConversations(
|
|
119
|
+
conversationIds: string[],
|
|
120
|
+
): Map<string, { displayOrder: number | null; isPinned: boolean }> {
|
|
121
|
+
ensureDisplayOrderMigration();
|
|
122
|
+
const result = new Map<string, { displayOrder: number | null; isPinned: boolean }>();
|
|
123
|
+
if (conversationIds.length === 0) return result;
|
|
124
|
+
for (const id of conversationIds) {
|
|
125
|
+
const row = rawGet<{ display_order: number | null; is_pinned: number | null }>(
|
|
126
|
+
'SELECT display_order, is_pinned FROM conversations WHERE id = ?',
|
|
127
|
+
id,
|
|
128
|
+
);
|
|
129
|
+
result.set(id, {
|
|
130
|
+
displayOrder: row?.display_order ?? null,
|
|
131
|
+
isPinned: (row?.is_pinned ?? 0) === 1,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
package/src/memory/db-init.ts
CHANGED
|
@@ -8,12 +8,12 @@ import {
|
|
|
8
8
|
createConversationAttentionTables,
|
|
9
9
|
createCoreIndexes,
|
|
10
10
|
createCoreTables,
|
|
11
|
-
createScopedApprovalGrantsTable,
|
|
12
11
|
createExternalConversationBindingsTables,
|
|
13
12
|
createFollowupsTables,
|
|
14
13
|
createMediaAssetsTables,
|
|
15
14
|
createMessagesFts,
|
|
16
15
|
createNotificationTables,
|
|
16
|
+
createScopedApprovalGrantsTable,
|
|
17
17
|
createSequenceTables,
|
|
18
18
|
createTasksAndWorkItemsTables,
|
|
19
19
|
createWatchersAndLogsTables,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
migrateConversationsThreadTypeIndex,
|
|
23
23
|
migrateFkCascadeRebuilds,
|
|
24
24
|
migrateGuardianActionFollowup,
|
|
25
|
+
migrateGuardianActionSupersession,
|
|
25
26
|
migrateGuardianActionToolMetadata,
|
|
26
27
|
migrateGuardianBootstrapToken,
|
|
27
28
|
migrateGuardianDeliveryConversationIndex,
|
|
@@ -107,6 +108,9 @@ export function initializeDb(): void {
|
|
|
107
108
|
// 14c2. Guardian action tool-approval metadata columns (tool_name, input_digest)
|
|
108
109
|
migrateGuardianActionToolMetadata(database);
|
|
109
110
|
|
|
111
|
+
// 14c3. Guardian action supersession metadata (superseded_by_request_id, superseded_at) + session lookup index
|
|
112
|
+
migrateGuardianActionSupersession(database);
|
|
113
|
+
|
|
110
114
|
// 14d. Index on conversations.thread_type for frequent WHERE filters
|
|
111
115
|
migrateConversationsThreadTypeIndex(database);
|
|
112
116
|
|
|
@@ -58,24 +58,29 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
|
|
|
58
58
|
|
|
59
59
|
private async initialize(): Promise<void> {
|
|
60
60
|
log.info({ model: this.model }, 'Loading local embedding model (first load downloads the model)');
|
|
61
|
+
|
|
62
|
+
// In compiled Bun binaries, bare specifier resolution for packages with
|
|
63
|
+
// subdirectory entry points (like onnxruntime-common's dist/esm/index.js)
|
|
64
|
+
// fails. Additionally, CJS/ESM dual-instance issues cause onnxruntime-node's
|
|
65
|
+
// backend registration to be invisible to transformers. To solve both, the
|
|
66
|
+
// build step pre-bundles all JS deps into a single file placed inside
|
|
67
|
+
// onnxruntime-node/dist/ so native .node binary relative paths resolve.
|
|
68
|
+
const execDir = dirname(process.execPath);
|
|
69
|
+
const bundlePath = join(execDir, 'node_modules', 'onnxruntime-node', 'dist', 'transformers-bundle.mjs');
|
|
61
70
|
let transformers: typeof import('@huggingface/transformers');
|
|
62
71
|
try {
|
|
63
|
-
transformers = await import(
|
|
72
|
+
transformers = await import(bundlePath);
|
|
64
73
|
} catch {
|
|
65
|
-
//
|
|
66
|
-
// virtual /$bunfs/root/ filesystem and can't find externalized packages.
|
|
67
|
-
// Fall back to resolving from the executable's real disk location where
|
|
68
|
-
// node_modules/ is co-located.
|
|
74
|
+
// Fall back to bare specifier for dev mode (running via `bun run`, not compiled)
|
|
69
75
|
try {
|
|
70
|
-
|
|
71
|
-
const modulePath = join(execDir, 'node_modules', '@huggingface', 'transformers');
|
|
72
|
-
transformers = await import(modulePath);
|
|
76
|
+
transformers = await import('@huggingface/transformers');
|
|
73
77
|
} catch (err) {
|
|
74
78
|
throw new Error(
|
|
75
79
|
`Local embedding backend unavailable: failed to load @huggingface/transformers (${err instanceof Error ? err.message : String(err)})`,
|
|
76
80
|
);
|
|
77
81
|
}
|
|
78
82
|
}
|
|
83
|
+
|
|
79
84
|
this.extractor = await transformers.pipeline('feature-extraction', this.model, {
|
|
80
85
|
dtype: 'fp32',
|
|
81
86
|
}) as unknown as FeatureExtractionPipeline;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* answer resolves the request and all other deliveries are marked answered.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { and, count, desc, eq, inArray, lt } from 'drizzle-orm';
|
|
10
|
+
import { and, count, desc, eq, inArray, isNotNull, lt } from 'drizzle-orm';
|
|
11
11
|
import { v4 as uuid } from 'uuid';
|
|
12
12
|
|
|
13
13
|
import { getLogger } from '../util/logger.js';
|
|
@@ -25,7 +25,7 @@ const log = getLogger('guardian-action-store');
|
|
|
25
25
|
|
|
26
26
|
export type GuardianActionRequestStatus = 'pending' | 'answered' | 'expired' | 'cancelled';
|
|
27
27
|
export type GuardianActionDeliveryStatus = 'pending' | 'sent' | 'failed' | 'answered' | 'expired' | 'cancelled';
|
|
28
|
-
export type ExpiredReason = 'call_timeout' | 'sweep_timeout' | 'cancelled';
|
|
28
|
+
export type ExpiredReason = 'call_timeout' | 'sweep_timeout' | 'cancelled' | 'superseded';
|
|
29
29
|
export type FollowupState = 'none' | 'awaiting_guardian_choice' | 'dispatching' | 'completed' | 'declined' | 'failed';
|
|
30
30
|
export type FollowupAction = 'call_back' | 'message_back' | 'decline';
|
|
31
31
|
|
|
@@ -53,6 +53,8 @@ export interface GuardianActionRequest {
|
|
|
53
53
|
followupCompletedAt: number | null;
|
|
54
54
|
toolName: string | null;
|
|
55
55
|
inputDigest: string | null;
|
|
56
|
+
supersededByRequestId: string | null;
|
|
57
|
+
supersededAt: number | null;
|
|
56
58
|
createdAt: number;
|
|
57
59
|
updatedAt: number;
|
|
58
60
|
}
|
|
@@ -101,6 +103,8 @@ function rowToRequest(row: typeof guardianActionRequests.$inferSelect): Guardian
|
|
|
101
103
|
followupCompletedAt: row.followupCompletedAt ?? null,
|
|
102
104
|
toolName: row.toolName ?? null,
|
|
103
105
|
inputDigest: row.inputDigest ?? null,
|
|
106
|
+
supersededByRequestId: row.supersededByRequestId ?? null,
|
|
107
|
+
supersededAt: row.supersededAt ?? null,
|
|
104
108
|
createdAt: row.createdAt,
|
|
105
109
|
updatedAt: row.updatedAt,
|
|
106
110
|
};
|
|
@@ -172,6 +176,8 @@ export function createGuardianActionRequest(params: {
|
|
|
172
176
|
followupCompletedAt: null,
|
|
173
177
|
toolName: params.toolName ?? null,
|
|
174
178
|
inputDigest: params.inputDigest ?? null,
|
|
179
|
+
supersededByRequestId: null,
|
|
180
|
+
supersededAt: null,
|
|
175
181
|
createdAt: now,
|
|
176
182
|
updatedAt: now,
|
|
177
183
|
};
|
|
@@ -240,6 +246,45 @@ export function countPendingRequestsByCallSessionId(callSessionId: string): numb
|
|
|
240
246
|
return row?.count ?? 0;
|
|
241
247
|
}
|
|
242
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Look up the vellum conversation ID used for the first guardian question
|
|
251
|
+
* delivery in a given call session. Returns the conversation ID when one
|
|
252
|
+
* exists, or null if no vellum delivery has been recorded yet.
|
|
253
|
+
*
|
|
254
|
+
* Used by guardian-dispatch to enforce deterministic thread affinity:
|
|
255
|
+
* all guardian questions within the same call session should route to
|
|
256
|
+
* the same vellum conversation.
|
|
257
|
+
*/
|
|
258
|
+
export function getGuardianConversationIdForCallSession(callSessionId: string): string | null {
|
|
259
|
+
try {
|
|
260
|
+
const db = getDb();
|
|
261
|
+
const row = db
|
|
262
|
+
.select({ conversationId: guardianActionDeliveries.destinationConversationId })
|
|
263
|
+
.from(guardianActionDeliveries)
|
|
264
|
+
.innerJoin(
|
|
265
|
+
guardianActionRequests,
|
|
266
|
+
eq(guardianActionDeliveries.requestId, guardianActionRequests.id),
|
|
267
|
+
)
|
|
268
|
+
.where(
|
|
269
|
+
and(
|
|
270
|
+
eq(guardianActionRequests.callSessionId, callSessionId),
|
|
271
|
+
eq(guardianActionDeliveries.destinationChannel, 'vellum'),
|
|
272
|
+
isNotNull(guardianActionDeliveries.destinationConversationId),
|
|
273
|
+
),
|
|
274
|
+
)
|
|
275
|
+
.orderBy(guardianActionDeliveries.createdAt)
|
|
276
|
+
.limit(1)
|
|
277
|
+
.get();
|
|
278
|
+
return row?.conversationId ?? null;
|
|
279
|
+
} catch (err) {
|
|
280
|
+
if (err instanceof Error && err.message.includes('no such table')) {
|
|
281
|
+
log.warn({ err }, 'guardian tables not yet created');
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
throw err;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
243
288
|
/**
|
|
244
289
|
* First-response-wins resolution. Checks that the request is still
|
|
245
290
|
* 'pending' before updating; returns the updated request on success
|
|
@@ -313,6 +358,84 @@ export function expireGuardianActionRequest(id: string, reason?: ExpiredReason):
|
|
|
313
358
|
.run();
|
|
314
359
|
}
|
|
315
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Supersede a pending guardian action request: mark it expired with
|
|
363
|
+
* reason='superseded', record the replacement request ID and timestamp,
|
|
364
|
+
* and expire its active deliveries.
|
|
365
|
+
*
|
|
366
|
+
* Returns the updated request on success, or null if the request was
|
|
367
|
+
* not in 'pending' status (first-writer-wins).
|
|
368
|
+
*/
|
|
369
|
+
export function supersedeGuardianActionRequest(
|
|
370
|
+
id: string,
|
|
371
|
+
supersededByRequestId: string,
|
|
372
|
+
): GuardianActionRequest | null {
|
|
373
|
+
const db = getDb();
|
|
374
|
+
const now = Date.now();
|
|
375
|
+
|
|
376
|
+
db.update(guardianActionRequests)
|
|
377
|
+
.set({
|
|
378
|
+
status: 'expired',
|
|
379
|
+
expiredReason: 'superseded',
|
|
380
|
+
supersededByRequestId,
|
|
381
|
+
supersededAt: now,
|
|
382
|
+
updatedAt: now,
|
|
383
|
+
})
|
|
384
|
+
.where(
|
|
385
|
+
and(
|
|
386
|
+
eq(guardianActionRequests.id, id),
|
|
387
|
+
eq(guardianActionRequests.status, 'pending'),
|
|
388
|
+
),
|
|
389
|
+
)
|
|
390
|
+
.run();
|
|
391
|
+
|
|
392
|
+
if (rawChanges() === 0) return null;
|
|
393
|
+
|
|
394
|
+
// Also expire active deliveries
|
|
395
|
+
db.update(guardianActionDeliveries)
|
|
396
|
+
.set({ status: 'expired', updatedAt: now })
|
|
397
|
+
.where(
|
|
398
|
+
and(
|
|
399
|
+
eq(guardianActionDeliveries.requestId, id),
|
|
400
|
+
inArray(guardianActionDeliveries.status, ['pending', 'sent']),
|
|
401
|
+
),
|
|
402
|
+
)
|
|
403
|
+
.run();
|
|
404
|
+
|
|
405
|
+
return getGuardianActionRequest(id);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Backfill supersession metadata on an already-expired request.
|
|
410
|
+
* Used when the superseding request ID is not known at the time the
|
|
411
|
+
* original request is expired (e.g., the new request is created
|
|
412
|
+
* asynchronously via dispatchGuardianQuestion).
|
|
413
|
+
*
|
|
414
|
+
* Only updates requests that are already in 'expired' status with
|
|
415
|
+
* expired_reason='superseded'.
|
|
416
|
+
*/
|
|
417
|
+
export function backfillSupersessionMetadata(
|
|
418
|
+
id: string,
|
|
419
|
+
supersededByRequestId: string,
|
|
420
|
+
): void {
|
|
421
|
+
const db = getDb();
|
|
422
|
+
const now = Date.now();
|
|
423
|
+
|
|
424
|
+
db.update(guardianActionRequests)
|
|
425
|
+
.set({
|
|
426
|
+
supersededByRequestId,
|
|
427
|
+
supersededAt: now,
|
|
428
|
+
updatedAt: now,
|
|
429
|
+
})
|
|
430
|
+
.where(
|
|
431
|
+
and(
|
|
432
|
+
eq(guardianActionRequests.id, id),
|
|
433
|
+
eq(guardianActionRequests.status, 'expired'),
|
|
434
|
+
),
|
|
435
|
+
)
|
|
436
|
+
.run();
|
|
437
|
+
}
|
|
438
|
+
|
|
316
439
|
/**
|
|
317
440
|
* Get all pending guardian action requests that have expired.
|
|
318
441
|
*/
|
|
@@ -66,7 +66,7 @@ const DEFAULT_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
|
66
66
|
// Helpers
|
|
67
67
|
// ---------------------------------------------------------------------------
|
|
68
68
|
|
|
69
|
-
function hashToken(rawToken: string): string {
|
|
69
|
+
export function hashToken(rawToken: string): string {
|
|
70
70
|
return createHash('sha256').update(rawToken).digest('hex');
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -268,6 +268,12 @@ export function redeemInvite(params: {
|
|
|
268
268
|
return { error: 'invite_max_uses_reached' };
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
// Enforce channel-scoped redemption: when the caller specifies a channel, it
|
|
272
|
+
// must match the channel the invite was created for.
|
|
273
|
+
if (params.sourceChannel && params.sourceChannel !== invite.sourceChannel) {
|
|
274
|
+
return { error: 'invite_channel_mismatch' };
|
|
275
|
+
}
|
|
276
|
+
|
|
271
277
|
const newUseCount = invite.useCount + 1;
|
|
272
278
|
const newStatus = newUseCount >= invite.maxUses ? 'redeemed' : 'active';
|
|
273
279
|
|
|
@@ -323,6 +329,94 @@ export function redeemInvite(params: {
|
|
|
323
329
|
return { invite: updatedInvite, member: rowToMember(memberRow) };
|
|
324
330
|
}
|
|
325
331
|
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
// recordInviteUse — consume one use without creating a member row
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Increment an invite's use count and record redemption metadata without
|
|
338
|
+
* inserting a new member row. Used when reactivating an existing inactive
|
|
339
|
+
* member via invite — the member row already exists and just needs an
|
|
340
|
+
* update, so the transactional INSERT in `redeemInvite` would hit a
|
|
341
|
+
* unique-key constraint.
|
|
342
|
+
*
|
|
343
|
+
* Returns `true` if the use was recorded, or `false` if the invite was
|
|
344
|
+
* concurrently revoked/expired (the WHERE clause constrains to
|
|
345
|
+
* `status = 'active'` so a stale write is impossible).
|
|
346
|
+
*/
|
|
347
|
+
export function recordInviteUse(params: {
|
|
348
|
+
inviteId: string;
|
|
349
|
+
externalUserId?: string;
|
|
350
|
+
externalChatId?: string;
|
|
351
|
+
}): boolean {
|
|
352
|
+
const db = getDb();
|
|
353
|
+
const now = Date.now();
|
|
354
|
+
|
|
355
|
+
const invite = db
|
|
356
|
+
.select()
|
|
357
|
+
.from(assistantIngressInvites)
|
|
358
|
+
.where(eq(assistantIngressInvites.id, params.inviteId))
|
|
359
|
+
.get();
|
|
360
|
+
|
|
361
|
+
if (!invite) return false;
|
|
362
|
+
|
|
363
|
+
const newUseCount = invite.useCount + 1;
|
|
364
|
+
const newStatus = newUseCount >= invite.maxUses ? 'redeemed' : 'active';
|
|
365
|
+
|
|
366
|
+
// Constrain the update to active invites so a concurrent revoke/expire
|
|
367
|
+
// prevents this write rather than silently overwriting the new status.
|
|
368
|
+
db.update(assistantIngressInvites)
|
|
369
|
+
.set({
|
|
370
|
+
useCount: newUseCount,
|
|
371
|
+
status: newStatus,
|
|
372
|
+
redeemedByExternalUserId: params.externalUserId ?? null,
|
|
373
|
+
redeemedByExternalChatId: params.externalChatId ?? null,
|
|
374
|
+
redeemedAt: now,
|
|
375
|
+
updatedAt: now,
|
|
376
|
+
})
|
|
377
|
+
.where(
|
|
378
|
+
and(
|
|
379
|
+
eq(assistantIngressInvites.id, invite.id),
|
|
380
|
+
eq(assistantIngressInvites.status, 'active'),
|
|
381
|
+
),
|
|
382
|
+
)
|
|
383
|
+
.run();
|
|
384
|
+
|
|
385
|
+
// Re-read to confirm the update took effect (the WHERE clause constrains
|
|
386
|
+
// to status = 'active', so a concurrent revoke/expire would prevent it).
|
|
387
|
+
const updated = db
|
|
388
|
+
.select({ useCount: assistantIngressInvites.useCount })
|
|
389
|
+
.from(assistantIngressInvites)
|
|
390
|
+
.where(eq(assistantIngressInvites.id, invite.id))
|
|
391
|
+
.get();
|
|
392
|
+
|
|
393
|
+
return !!updated && updated.useCount === newUseCount;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
// markInviteExpired
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Transition an invite's status to 'expired' in storage. This is safe to call
|
|
402
|
+
* even if the invite is already expired — the WHERE clause scopes the update
|
|
403
|
+
* to 'active' rows so it becomes a no-op in that case.
|
|
404
|
+
*/
|
|
405
|
+
export function markInviteExpired(inviteId: string): void {
|
|
406
|
+
const db = getDb();
|
|
407
|
+
const now = Date.now();
|
|
408
|
+
|
|
409
|
+
db.update(assistantIngressInvites)
|
|
410
|
+
.set({ status: 'expired', updatedAt: now })
|
|
411
|
+
.where(
|
|
412
|
+
and(
|
|
413
|
+
eq(assistantIngressInvites.id, inviteId),
|
|
414
|
+
eq(assistantIngressInvites.status, 'active'),
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
.run();
|
|
418
|
+
}
|
|
419
|
+
|
|
326
420
|
// ---------------------------------------------------------------------------
|
|
327
421
|
// findByTokenHash
|
|
328
422
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { DrizzleDb } from '../db-connection.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add supersession metadata columns to guardian_action_requests and
|
|
5
|
+
* create an index for efficient pending-request lookups by call session.
|
|
6
|
+
*
|
|
7
|
+
* - superseded_by_request_id: links to the request that replaced this one
|
|
8
|
+
* - superseded_at: timestamp when supersession occurred
|
|
9
|
+
* - Index (call_session_id, status, created_at DESC) for fast lookups of
|
|
10
|
+
* the most recent pending request per call session
|
|
11
|
+
*
|
|
12
|
+
* The existing expired_reason column already supports 'superseded' as a
|
|
13
|
+
* value — this migration adds the structural metadata to track the
|
|
14
|
+
* supersession chain.
|
|
15
|
+
*/
|
|
16
|
+
export function migrateGuardianActionSupersession(database: DrizzleDb): void {
|
|
17
|
+
try { database.run(/*sql*/ `ALTER TABLE guardian_action_requests ADD COLUMN superseded_by_request_id TEXT`); } catch { /* already exists */ }
|
|
18
|
+
try { database.run(/*sql*/ `ALTER TABLE guardian_action_requests ADD COLUMN superseded_at INTEGER`); } catch { /* already exists */ }
|
|
19
|
+
|
|
20
|
+
database.run(
|
|
21
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_guardian_action_requests_session_status_created ON guardian_action_requests(call_session_id, status, created_at DESC)`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -33,9 +33,10 @@ export { migrateGuardianActionFollowup } from './030-guardian-action-followup.js
|
|
|
33
33
|
export { migrateGuardianVerificationPurpose } from './030-guardian-verification-purpose.js';
|
|
34
34
|
export { migrateConversationsThreadTypeIndex } from './031-conversations-thread-type-index.js';
|
|
35
35
|
export { migrateGuardianDeliveryConversationIndex } from './032-guardian-delivery-conversation-index.js';
|
|
36
|
+
export { migrateNotificationDeliveryThreadDecision } from './032-notification-delivery-thread-decision.js';
|
|
36
37
|
export { createScopedApprovalGrantsTable } from './033-scoped-approval-grants.js';
|
|
37
38
|
export { migrateGuardianActionToolMetadata } from './034-guardian-action-tool-metadata.js';
|
|
38
|
-
export {
|
|
39
|
+
export { migrateGuardianActionSupersession } from './035-guardian-action-supersession.js';
|
|
39
40
|
export { createCoreTables } from './100-core-tables.js';
|
|
40
41
|
export { createWatchersAndLogsTables } from './101-watchers-and-logs.js';
|
|
41
42
|
export { addCoreColumns } from './102-alter-table-columns.js';
|
package/src/memory/schema.ts
CHANGED
|
@@ -845,9 +845,13 @@ export const guardianActionRequests = sqliteTable('guardian_action_requests', {
|
|
|
845
845
|
followupCompletedAt: integer('followup_completed_at'),
|
|
846
846
|
toolName: text('tool_name'), // tool identity for tool-approval requests
|
|
847
847
|
inputDigest: text('input_digest'), // canonical SHA-256 digest of tool input
|
|
848
|
+
supersededByRequestId: text('superseded_by_request_id'), // links to the request that replaced this one
|
|
849
|
+
supersededAt: integer('superseded_at'), // epoch ms when supersession occurred
|
|
848
850
|
createdAt: integer('created_at').notNull(),
|
|
849
851
|
updatedAt: integer('updated_at').notNull(),
|
|
850
|
-
})
|
|
852
|
+
}, (table) => [
|
|
853
|
+
index('idx_guardian_action_requests_session_status_created').on(table.callSessionId, table.status, table.createdAt),
|
|
854
|
+
]);
|
|
851
855
|
|
|
852
856
|
// ── Guardian Action Deliveries (per-channel delivery tracking) ───────
|
|
853
857
|
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
* - Expired and revoked grants cannot be consumed.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { and, eq,
|
|
14
|
+
import { and, eq, sql } from 'drizzle-orm';
|
|
15
15
|
import { v4 as uuid } from 'uuid';
|
|
16
16
|
|
|
17
|
+
import { getLogger } from '../util/logger.js';
|
|
17
18
|
import { getDb, rawChanges } from './db.js';
|
|
18
19
|
import { scopedApprovalGrants } from './schema.js';
|
|
19
|
-
import { getLogger } from '../util/logger.js';
|
|
20
20
|
|
|
21
21
|
const log = getLogger('scoped-approval-grants');
|
|
22
22
|
|
|
@@ -104,7 +104,7 @@ export interface CreateScopedApprovalGrantParams {
|
|
|
104
104
|
expiresAt: string;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
function createScopedApprovalGrant(params: CreateScopedApprovalGrantParams): ScopedApprovalGrant {
|
|
108
108
|
const db = getDb();
|
|
109
109
|
const now = new Date().toISOString();
|
|
110
110
|
const id = uuid();
|
|
@@ -167,7 +167,7 @@ export interface ConsumeByRequestIdResult {
|
|
|
167
167
|
* given `requestId` and `assistantId`. Uses compare-and-swap on the
|
|
168
168
|
* `status` column so concurrent consumers race safely — at most one wins.
|
|
169
169
|
*/
|
|
170
|
-
|
|
170
|
+
function consumeScopedApprovalGrantByRequestId(
|
|
171
171
|
requestId: string,
|
|
172
172
|
consumingRequestId: string,
|
|
173
173
|
assistantId: string,
|
|
@@ -280,7 +280,7 @@ export interface ConsumeByToolSignatureResult {
|
|
|
280
280
|
* times before giving up. This prevents false denials when multiple matching
|
|
281
281
|
* grants exist but a concurrent consumer steals the first pick.
|
|
282
282
|
*/
|
|
283
|
-
|
|
283
|
+
function consumeScopedApprovalGrantByToolSignature(
|
|
284
284
|
params: ConsumeByToolSignatureParams,
|
|
285
285
|
): ConsumeByToolSignatureResult {
|
|
286
286
|
const db = getDb();
|
|
@@ -507,3 +507,12 @@ export function revokeScopedApprovalGrantsForContext(params: RevokeContextParams
|
|
|
507
507
|
|
|
508
508
|
return count;
|
|
509
509
|
}
|
|
510
|
+
|
|
511
|
+
// @internal — exposed for tests and the approval-primitive wrapper only.
|
|
512
|
+
// Do not import these from production code outside this package; use the
|
|
513
|
+
// approval-primitive API instead.
|
|
514
|
+
export const _internal = {
|
|
515
|
+
createScopedApprovalGrant,
|
|
516
|
+
consumeScopedApprovalGrantByRequestId,
|
|
517
|
+
consumeScopedApprovalGrantByToolSignature,
|
|
518
|
+
};
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type {
|
|
9
9
|
SlackApiResponse,
|
|
10
10
|
SlackAuthTestResponse,
|
|
11
|
+
SlackChatDeleteResponse,
|
|
11
12
|
SlackConversationHistoryResponse,
|
|
12
13
|
SlackConversationLeaveResponse,
|
|
13
14
|
SlackConversationMarkResponse,
|
|
@@ -188,6 +189,17 @@ export async function addReaction(
|
|
|
188
189
|
});
|
|
189
190
|
}
|
|
190
191
|
|
|
192
|
+
export async function deleteMessage(
|
|
193
|
+
token: string,
|
|
194
|
+
channel: string,
|
|
195
|
+
ts: string,
|
|
196
|
+
): Promise<SlackChatDeleteResponse> {
|
|
197
|
+
return request<SlackChatDeleteResponse>(token, 'chat.delete', undefined, {
|
|
198
|
+
channel,
|
|
199
|
+
ts,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
191
203
|
export async function leaveConversation(
|
|
192
204
|
token: string,
|
|
193
205
|
channel: string,
|
|
@@ -116,4 +116,9 @@ export type SlackReactionAddResponse = SlackApiResponse;
|
|
|
116
116
|
|
|
117
117
|
export type SlackConversationLeaveResponse = SlackApiResponse;
|
|
118
118
|
|
|
119
|
+
export interface SlackChatDeleteResponse extends SlackApiResponse {
|
|
120
|
+
channel: string;
|
|
121
|
+
ts: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
119
124
|
export type SlackConversationMarkResponse = SlackApiResponse;
|