@vellumai/assistant 0.3.16 → 0.3.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/ARCHITECTURE.md +70 -13
  2. package/README.md +6 -0
  3. package/docs/architecture/http-token-refresh.md +23 -1
  4. package/package.json +1 -1
  5. package/src/__tests__/access-request-decision.test.ts +4 -7
  6. package/src/__tests__/channel-guardian.test.ts +3 -1
  7. package/src/__tests__/checker.test.ts +79 -48
  8. package/src/__tests__/config-watcher.test.ts +11 -13
  9. package/src/__tests__/conversation-pairing.test.ts +103 -3
  10. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -1
  11. package/src/__tests__/guardian-action-followup-executor.test.ts +1 -1
  12. package/src/__tests__/guardian-action-late-reply.test.ts +131 -0
  13. package/src/__tests__/guardian-action-store.test.ts +182 -0
  14. package/src/__tests__/guardian-dispatch.test.ts +120 -0
  15. package/src/__tests__/ipc-snapshot.test.ts +21 -0
  16. package/src/__tests__/non-member-access-request.test.ts +1 -2
  17. package/src/__tests__/notification-broadcaster.test.ts +115 -4
  18. package/src/__tests__/notification-decision-strategy.test.ts +2 -1
  19. package/src/__tests__/notification-deep-link.test.ts +44 -1
  20. package/src/__tests__/notification-guardian-path.test.ts +157 -0
  21. package/src/__tests__/notification-thread-candidate-validation.test.ts +215 -0
  22. package/src/__tests__/slack-channel-config.test.ts +3 -3
  23. package/src/__tests__/trust-store.test.ts +21 -21
  24. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +5 -7
  25. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -6
  26. package/src/__tests__/trusted-contact-verification.test.ts +9 -9
  27. package/src/__tests__/update-bulletin-state.test.ts +1 -1
  28. package/src/__tests__/update-bulletin.test.ts +66 -3
  29. package/src/__tests__/update-template-contract.test.ts +6 -11
  30. package/src/__tests__/voice-session-bridge.test.ts +109 -9
  31. package/src/calls/call-controller.ts +129 -8
  32. package/src/calls/guardian-action-sweep.ts +1 -1
  33. package/src/calls/guardian-dispatch.ts +8 -0
  34. package/src/calls/voice-session-bridge.ts +4 -2
  35. package/src/cli/core-commands.ts +41 -1
  36. package/src/config/templates/UPDATES.md +5 -6
  37. package/src/config/update-bulletin-format.ts +2 -0
  38. package/src/config/update-bulletin-state.ts +1 -1
  39. package/src/config/update-bulletin-template-path.ts +6 -0
  40. package/src/config/update-bulletin.ts +21 -6
  41. package/src/daemon/config-watcher.ts +3 -2
  42. package/src/daemon/daemon-control.ts +64 -10
  43. package/src/daemon/handlers/config-slack-channel.ts +1 -1
  44. package/src/daemon/handlers/identity.ts +45 -25
  45. package/src/daemon/handlers/sessions.ts +1 -1
  46. package/src/daemon/ipc-contract/sessions.ts +1 -1
  47. package/src/daemon/ipc-contract/workspace.ts +12 -1
  48. package/src/daemon/ipc-contract-inventory.json +1 -0
  49. package/src/daemon/lifecycle.ts +8 -0
  50. package/src/daemon/server.ts +25 -3
  51. package/src/daemon/session-process.ts +438 -184
  52. package/src/daemon/tls-certs.ts +17 -12
  53. package/src/daemon/tool-side-effects.ts +1 -1
  54. package/src/memory/channel-delivery-store.ts +18 -20
  55. package/src/memory/channel-guardian-store.ts +39 -42
  56. package/src/memory/conversation-crud.ts +2 -2
  57. package/src/memory/conversation-queries.ts +2 -2
  58. package/src/memory/conversation-store.ts +24 -25
  59. package/src/memory/db-init.ts +9 -1
  60. package/src/memory/fts-reconciler.ts +41 -26
  61. package/src/memory/guardian-action-store.ts +57 -7
  62. package/src/memory/guardian-verification.ts +1 -0
  63. package/src/memory/jobs-worker.ts +2 -2
  64. package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +15 -0
  65. package/src/memory/migrations/032-notification-delivery-thread-decision.ts +20 -0
  66. package/src/memory/migrations/index.ts +4 -2
  67. package/src/memory/schema-migration.ts +1 -0
  68. package/src/memory/schema.ts +6 -1
  69. package/src/memory/search/semantic.ts +3 -3
  70. package/src/notifications/README.md +158 -17
  71. package/src/notifications/broadcaster.ts +68 -50
  72. package/src/notifications/conversation-pairing.ts +96 -18
  73. package/src/notifications/decision-engine.ts +6 -3
  74. package/src/notifications/deliveries-store.ts +12 -0
  75. package/src/notifications/emit-signal.ts +1 -0
  76. package/src/notifications/thread-candidates.ts +60 -25
  77. package/src/notifications/types.ts +2 -1
  78. package/src/permissions/checker.ts +1 -16
  79. package/src/permissions/defaults.ts +14 -4
  80. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  81. package/src/runtime/http-server.ts +11 -11
  82. package/src/runtime/routes/access-request-decision.ts +1 -1
  83. package/src/runtime/routes/debug-routes.ts +4 -4
  84. package/src/runtime/routes/guardian-approval-interception.ts +4 -4
  85. package/src/runtime/routes/inbound-message-handler.ts +6 -6
  86. package/src/runtime/routes/integration-routes.ts +2 -2
  87. package/src/tools/permission-checker.ts +1 -2
  88. package/src/tools/secret-detection-handler.ts +1 -1
  89. package/src/tools/system/voice-config.ts +1 -1
  90. package/src/version.ts +29 -2
@@ -19,7 +19,7 @@ import { getLogger } from '../util/logger.js';
19
19
  import { createDecision } from './decisions-store.js';
20
20
  import { getPreferenceSummary } from './preference-summary.js';
21
21
  import type { NotificationSignal, RoutingIntent } from './signal.js';
22
- import { type ThreadCandidateSet, buildThreadCandidates, serializeCandidatesForPrompt } from './thread-candidates.js';
22
+ import { buildThreadCandidates, serializeCandidatesForPrompt,type ThreadCandidateSet } from './thread-candidates.js';
23
23
  import type { NotificationChannel, NotificationDecision, RenderedChannelCopy, ThreadAction } from './types.js';
24
24
 
25
25
  const log = getLogger('notification-decision-engine');
@@ -385,13 +385,16 @@ export function validateThreadActions(
385
385
  if (action.action === 'start_new') {
386
386
  result[channel] = { action: 'start_new' };
387
387
  } else if (action.action === 'reuse_existing') {
388
- const conversationId = action.conversationId;
389
- if (typeof conversationId !== 'string' || !conversationId.trim()) {
388
+ const rawConversationId = action.conversationId;
389
+ if (typeof rawConversationId !== 'string' || !rawConversationId.trim()) {
390
390
  log.warn({ channel }, 'LLM returned reuse_existing without conversationId — downgrading to start_new');
391
391
  result[channel] = { action: 'start_new' };
392
392
  continue;
393
393
  }
394
394
 
395
+ // Normalize: the LLM may return a valid ID with leading/trailing whitespace
396
+ const conversationId = rawConversationId.trim();
397
+
395
398
  // Strict validation: the conversationId must exist in the candidate set
396
399
  const candidateIds = validCandidateIds.get(channel);
397
400
  if (!candidateIds || !candidateIds.has(conversationId)) {
@@ -28,6 +28,9 @@ export interface NotificationDeliveryRow {
28
28
  conversationId: string | null;
29
29
  messageId: string | null;
30
30
  conversationStrategy: string | null;
31
+ threadAction: string | null;
32
+ threadTargetConversationId: string | null;
33
+ threadDecisionFallbackUsed: number | null;
31
34
  clientDeliveryStatus: string | null;
32
35
  clientDeliveryError: string | null;
33
36
  clientDeliveryAt: number | null;
@@ -52,6 +55,9 @@ function rowToDelivery(row: typeof notificationDeliveries.$inferSelect): Notific
52
55
  conversationId: row.conversationId,
53
56
  messageId: row.messageId,
54
57
  conversationStrategy: row.conversationStrategy,
58
+ threadAction: row.threadAction,
59
+ threadTargetConversationId: row.threadTargetConversationId,
60
+ threadDecisionFallbackUsed: row.threadDecisionFallbackUsed,
55
61
  clientDeliveryStatus: row.clientDeliveryStatus,
56
62
  clientDeliveryError: row.clientDeliveryError,
57
63
  clientDeliveryAt: row.clientDeliveryAt,
@@ -76,6 +82,9 @@ export interface CreateDeliveryParams {
76
82
  conversationId?: string;
77
83
  messageId?: string;
78
84
  conversationStrategy?: string;
85
+ threadAction?: string;
86
+ threadTargetConversationId?: string;
87
+ threadDecisionFallbackUsed?: boolean;
79
88
  }
80
89
 
81
90
  /** Create a new delivery audit record. */
@@ -99,6 +108,9 @@ export function createDelivery(params: CreateDeliveryParams): NotificationDelive
99
108
  conversationId: params.conversationId ?? null,
100
109
  messageId: params.messageId ?? null,
101
110
  conversationStrategy: params.conversationStrategy ?? null,
111
+ threadAction: params.threadAction ?? null,
112
+ threadTargetConversationId: params.threadTargetConversationId ?? null,
113
+ threadDecisionFallbackUsed: params.threadDecisionFallbackUsed != null ? (params.threadDecisionFallbackUsed ? 1 : 0) : null,
102
114
  clientDeliveryStatus: null,
103
115
  clientDeliveryError: null,
104
116
  clientDeliveryAt: null,
@@ -205,6 +205,7 @@ export async function emitNotificationSignal(params: EmitSignalParams): Promise<
205
205
 
206
206
  // Step 2: Evaluate the signal through the decision engine
207
207
  const connectedChannels = getConnectedChannels(assistantId);
208
+
208
209
  let decision = await evaluateSignal(signal, connectedChannels);
209
210
 
210
211
  // Step 2.5: Enforce routing intent policy (fire-time guard)
@@ -10,11 +10,10 @@
10
10
  * needs for a routing decision, not full conversation contents.
11
11
  */
12
12
 
13
- import { and, desc, eq, isNotNull } from 'drizzle-orm';
13
+ import { and, count, desc, eq, inArray, isNotNull } from 'drizzle-orm';
14
14
 
15
15
  import { getDb } from '../memory/db.js';
16
- import { countPendingByConversation } from '../memory/guardian-approvals.js';
17
- import { conversations, notificationDeliveries, notificationDecisions, notificationEvents } from '../memory/schema.js';
16
+ import { channelGuardianApprovalRequests, conversations, notificationDecisions, notificationDeliveries, notificationEvents } from '../memory/schema.js';
18
17
  import { getLogger } from '../util/logger.js';
19
18
  import type { NotificationChannel } from './types.js';
20
19
 
@@ -148,49 +147,78 @@ function buildCandidatesForChannel(
148
147
 
149
148
  seen.add(row.conversationId);
150
149
 
151
- const candidate: ThreadCandidate = {
150
+ candidates.push({
152
151
  conversationId: row.conversationId,
153
152
  title: row.convTitle,
154
153
  updatedAt: row.convUpdatedAt,
155
154
  latestSourceEventName: row.sourceEventName ?? null,
156
155
  channel: channel,
157
- };
158
-
159
- // Enrich with guardian context
160
- const guardianContext = buildGuardianContext(row.conversationId, assistantId);
161
- if (guardianContext) {
162
- candidate.guardianContext = guardianContext;
163
- }
164
-
165
- candidates.push(candidate);
156
+ });
166
157
 
167
158
  if (candidates.length >= MAX_CANDIDATES_PER_CHANNEL) break;
168
159
  }
169
160
 
161
+ // Batch-enrich all candidates with guardian context in a single query
162
+ if (candidates.length > 0) {
163
+ const pendingCounts = batchCountPendingByConversation(
164
+ candidates.map((c) => c.conversationId),
165
+ assistantId,
166
+ );
167
+ for (const candidate of candidates) {
168
+ const pendingCount = pendingCounts.get(candidate.conversationId) ?? 0;
169
+ if (pendingCount > 0) {
170
+ candidate.guardianContext = { pendingUnresolvedRequestCount: pendingCount };
171
+ }
172
+ }
173
+ }
174
+
170
175
  return candidates;
171
176
  }
172
177
 
173
178
  // -- Guardian context enrichment ----------------------------------------------
174
179
 
175
180
  /**
176
- * Build guardian-specific context for a candidate conversation.
177
- * Returns null when there is no guardian-relevant data.
181
+ * Batch-count pending guardian approval requests for multiple conversations
182
+ * in a single query. Returns a map from conversationId to pending count
183
+ * (only entries with count > 0 are included).
178
184
  */
179
- function buildGuardianContext(
180
- conversationId: string,
185
+ function batchCountPendingByConversation(
186
+ conversationIds: string[],
181
187
  assistantId: string,
182
- ): GuardianCandidateContext | null {
188
+ ): Map<string, number> {
189
+ const result = new Map<string, number>();
190
+ if (conversationIds.length === 0) return result;
191
+
183
192
  try {
184
- const pendingCount = countPendingByConversation(conversationId, assistantId);
185
- if (pendingCount > 0) {
186
- return { pendingUnresolvedRequestCount: pendingCount };
193
+ const db = getDb();
194
+
195
+ const rows = db
196
+ .select({
197
+ conversationId: channelGuardianApprovalRequests.conversationId,
198
+ count: count(),
199
+ })
200
+ .from(channelGuardianApprovalRequests)
201
+ .where(
202
+ and(
203
+ inArray(channelGuardianApprovalRequests.conversationId, conversationIds),
204
+ eq(channelGuardianApprovalRequests.status, 'pending'),
205
+ eq(channelGuardianApprovalRequests.assistantId, assistantId),
206
+ ),
207
+ )
208
+ .groupBy(channelGuardianApprovalRequests.conversationId)
209
+ .all();
210
+
211
+ for (const row of rows) {
212
+ if (row.count > 0) {
213
+ result.set(row.conversationId, row.count);
214
+ }
187
215
  }
188
216
  } catch (err) {
189
217
  const errMsg = err instanceof Error ? err.message : String(err);
190
- log.warn({ err: errMsg, conversationId }, 'Failed to query guardian context for candidate');
218
+ log.warn({ err: errMsg }, 'Failed to batch-query guardian context for candidates');
191
219
  }
192
220
 
193
- return null;
221
+ return result;
194
222
  }
195
223
 
196
224
  // -- Prompt serialization -----------------------------------------------------
@@ -213,13 +241,20 @@ export function serializeCandidatesForPrompt(candidateSet: ThreadCandidateSet):
213
241
 
214
242
  const lines: string[] = [`Channel: ${channel}`];
215
243
  for (const c of candidates) {
244
+ // Escape title to prevent format corruption from quotes or newlines in
245
+ // user/model-provided text. JSON.stringify produces a safe single-line
246
+ // quoted string; we strip the outer quotes since we wrap in our own.
247
+ const safeTitle = c.title
248
+ ? JSON.stringify(c.title).slice(1, -1)
249
+ : '(untitled)';
216
250
  const parts: string[] = [
217
251
  ` - id=${c.conversationId}`,
218
- `title="${c.title ?? '(untitled)'}"`,
252
+ `title="${safeTitle}"`,
219
253
  `updated=${new Date(c.updatedAt).toISOString()}`,
220
254
  ];
221
255
  if (c.latestSourceEventName) {
222
- parts.push(`lastEvent="${c.latestSourceEventName}"`);
256
+ const safeEventName = JSON.stringify(c.latestSourceEventName).slice(1, -1);
257
+ parts.push(`lastEvent="${safeEventName}"`);
223
258
  }
224
259
  if (c.guardianContext) {
225
260
  parts.push(`pendingRequests=${c.guardianContext.pendingUnresolvedRequestCount}`);
@@ -95,13 +95,14 @@ export interface ThreadActionReuseExisting {
95
95
  /** Per-channel thread action — either start a new thread or reuse an existing one. */
96
96
  export type ThreadAction = ThreadActionStartNew | ThreadActionReuseExisting;
97
97
 
98
+
98
99
  /** Output produced by the notification decision engine for a given signal. */
99
100
  export interface NotificationDecision {
100
101
  shouldNotify: boolean;
101
102
  selectedChannels: NotificationChannel[];
102
103
  reasoningSummary: string;
103
104
  renderedCopy: Partial<Record<NotificationChannel, RenderedChannelCopy>>;
104
- /** Per-channel thread action. When absent for a channel, defaults to start_new. */
105
+ /** Per-channel thread actions decided by the model. Absent channels default to start_new. */
105
106
  threadActions?: Partial<Record<NotificationChannel, ThreadAction>>;
106
107
  deepLinkTarget?: Record<string, unknown>;
107
108
  dedupeKey: string;
@@ -123,19 +123,6 @@ function getWrappedProgram(seg: { program: string; args: string[] }): string | u
123
123
  return undefined;
124
124
  }
125
125
 
126
- function isHighRiskRm(args: string[]): boolean {
127
- // rm with -r, -rf, -fr, or targeting root/home
128
- for (const arg of args) {
129
- if (arg.startsWith('-') && (arg.includes('r') || arg.includes('f'))) {
130
- return true;
131
- }
132
- if (arg === '/' || arg === '~' || arg === '$HOME') {
133
- return true;
134
- }
135
- }
136
- return false;
137
- }
138
-
139
126
  function getStringField(input: Record<string, unknown>, ...keys: string[]): string {
140
127
  for (const key of keys) {
141
128
  const value = input[key];
@@ -398,9 +385,7 @@ async function classifyRiskUncached(toolName: string, input: Record<string, unkn
398
385
  if (HIGH_RISK_PROGRAMS.has(prog)) return RiskLevel.High;
399
386
 
400
387
  if (prog === 'rm') {
401
- if (isHighRiskRm(seg.args)) return RiskLevel.High;
402
- maxRisk = RiskLevel.Medium;
403
- continue;
388
+ return RiskLevel.High;
404
389
  }
405
390
 
406
391
  if (prog === 'chmod' || prog === 'chown' || prog === 'chgrp'
@@ -37,6 +37,13 @@ const COMPUTER_USE_TOOLS = [
37
37
  * Computed at runtime so paths reflect the configured root directory.
38
38
  */
39
39
  export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
40
+ // Some test suites mock getConfig() with partial objects; treat missing
41
+ // branches as defaults so rule generation remains deterministic.
42
+ const config = getConfig() as {
43
+ sandbox?: { enabled?: boolean };
44
+ skills?: { load?: { extraDirs?: unknown } };
45
+ };
46
+
40
47
  const hostFileRules = HOST_FILE_TOOLS.map((tool) => ({
41
48
  id: `default:ask-${tool}-global`,
42
49
  tool,
@@ -50,11 +57,11 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
50
57
  // global default ask rule uses "**" (globstar) instead of a "tool:*" prefix
51
58
  // because commands often contain "/" (e.g. "cat /etc/hosts").
52
59
  const hostShellRule: DefaultRuleTemplate = {
53
- id: 'default:ask-host_bash-global',
60
+ id: 'default:allow-host_bash-global',
54
61
  tool: 'host_bash',
55
62
  pattern: '**',
56
63
  scope: 'everywhere',
57
- decision: 'ask',
64
+ decision: 'allow',
58
65
  priority: 50,
59
66
  };
60
67
 
@@ -62,7 +69,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
62
69
  // them (including high-risk) so the user is never prompted for sandbox work.
63
70
  // Only emit this rule when the sandbox is actually enabled; otherwise bash
64
71
  // commands execute on the host and must go through normal permission checks.
65
- const sandboxEnabled = getConfig().sandbox.enabled;
72
+ const sandboxEnabled = config.sandbox?.enabled !== false;
66
73
  const sandboxShellRule: DefaultRuleTemplate | null = sandboxEnabled
67
74
  ? {
68
75
  id: 'default:allow-bash-global',
@@ -149,7 +156,10 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
149
156
 
150
157
  // Append any user-configured extra skill directories so they get the
151
158
  // same default ask rules as managed and bundled dirs.
152
- const extraDirs = getConfig().skills.load.extraDirs;
159
+ const rawExtraDirs = config.skills?.load?.extraDirs;
160
+ const extraDirs = Array.isArray(rawExtraDirs)
161
+ ? rawExtraDirs.filter((dir): dir is string => typeof dir === 'string')
162
+ : [];
153
163
  for (let i = 0; i < extraDirs.length; i++) {
154
164
  skillDirs.push({ dir: extraDirs[i].replaceAll('\\', '/'), label: `extra-${i}` });
155
165
  }
@@ -24,8 +24,8 @@ import { getGatewayInternalBaseUrl } from '../config/env.js';
24
24
  import { getOrCreateConversation } from '../memory/conversation-key-store.js';
25
25
  import {
26
26
  finalizeFollowup,
27
- getGuardianActionRequest,
28
27
  type FollowupAction,
28
+ getGuardianActionRequest,
29
29
  type GuardianActionRequest,
30
30
  } from '../memory/guardian-action-store.js';
31
31
  import { getLogger } from '../util/logger.js';
@@ -85,7 +85,6 @@ import {
85
85
  handleGetAttachmentContent,
86
86
  handleUploadAttachment,
87
87
  } from './routes/attachment-routes.js';
88
- import { handleDebug } from './routes/debug-routes.js';
89
88
  import {
90
89
  handleAnswerCall,
91
90
  handleCancelCall,
@@ -116,8 +115,19 @@ import {
116
115
  handleSearchConversations,
117
116
  handleSendMessage,
118
117
  } from './routes/conversation-routes.js';
118
+ import { handleDebug } from './routes/debug-routes.js';
119
119
  import { handleSubscribeAssistantEvents } from './routes/events-routes.js';
120
120
  import { handleGetIdentity,handleHealth } from './routes/identity-routes.js';
121
+ import {
122
+ handleBlockMember,
123
+ handleCreateInvite,
124
+ handleListInvites,
125
+ handleListMembers,
126
+ handleRedeemInvite,
127
+ handleRevokeInvite,
128
+ handleRevokeMember,
129
+ handleUpsertMember,
130
+ } from './routes/ingress-routes.js';
121
131
  import {
122
132
  handleCancelOutbound,
123
133
  handleClearSlackChannelConfig,
@@ -140,16 +150,6 @@ import {
140
150
  handlePairingRequest,
141
151
  handlePairingStatus,
142
152
  } from './routes/pairing-routes.js';
143
- import {
144
- handleBlockMember,
145
- handleCreateInvite,
146
- handleListInvites,
147
- handleListMembers,
148
- handleRedeemInvite,
149
- handleRevokeInvite,
150
- handleRevokeMember,
151
- handleUpsertMember,
152
- } from './routes/ingress-routes.js';
153
153
  import { handleAddSecret } from './routes/secret-routes.js';
154
154
 
155
155
  // Re-export for consumers
@@ -6,8 +6,8 @@
6
6
  * instead of resuming an agent loop.
7
7
  */
8
8
  import {
9
- resolveApprovalRequest,
10
9
  type GuardianApprovalRequest,
10
+ resolveApprovalRequest,
11
11
  } from '../../memory/channel-guardian-store.js';
12
12
  import { getLogger } from '../../util/logger.js';
13
13
  import { createOutboundSession } from '../channel-guardian-service.js';
@@ -4,13 +4,13 @@
4
4
 
5
5
  import { statSync } from 'node:fs';
6
6
 
7
- import { getDbPath } from '../../util/platform.js';
7
+ import { getConfig } from '../../config/loader.js';
8
8
  import { countConversations } from '../../memory/conversation-store.js';
9
- import { getMemoryJobCounts } from '../../memory/jobs-store.js';
10
- import { countSchedules } from '../../schedule/schedule-store.js';
11
9
  import { rawAll } from '../../memory/db.js';
12
- import { getConfig } from '../../config/loader.js';
10
+ import { getMemoryJobCounts } from '../../memory/jobs-store.js';
13
11
  import { getProviderDebugStatus } from '../../providers/registry.js';
12
+ import { countSchedules } from '../../schedule/schedule-store.js';
13
+ import { getDbPath } from '../../util/platform.js';
14
14
 
15
15
  /** Process start time — used to calculate uptime. */
16
16
  const startedAt = Date.now();
@@ -8,8 +8,8 @@ import {
8
8
  getPendingApprovalByRequestAndGuardianChat,
9
9
  getPendingApprovalForRequest,
10
10
  getUnresolvedApprovalForRequest,
11
- updateApprovalDecision,
12
11
  type GuardianApprovalRequest,
12
+ updateApprovalDecision,
13
13
  } from '../../memory/channel-guardian-store.js';
14
14
  import { emitNotificationSignal } from '../../notifications/emit-signal.js';
15
15
  import { getLogger } from '../../util/logger.js';
@@ -31,12 +31,12 @@ import type {
31
31
  ApprovalCopyGenerator,
32
32
  } from '../http-types.js';
33
33
  import {
34
- handleAccessRequestDecision,
35
34
  deliverVerificationCodeToGuardian,
35
+ type DeliveryResult,
36
+ handleAccessRequestDecision,
36
37
  notifyRequesterOfApproval,
37
- notifyRequesterOfDenial,
38
38
  notifyRequesterOfDeliveryFailure,
39
- type DeliveryResult,
39
+ notifyRequesterOfDenial,
40
40
  } from './access-request-decision.js';
41
41
  import {
42
42
  buildGuardianDenyContext,
@@ -50,12 +50,16 @@ import {
50
50
  validateAndConsumeChallenge,
51
51
  } from '../channel-guardian-service.js';
52
52
  import { deliverChannelReply } from '../gateway-client.js';
53
+ import { processGuardianFollowUpTurn } from '../guardian-action-conversation-turn.js';
54
+ import { executeFollowupAction } from '../guardian-action-followup-executor.js';
55
+ import { composeGuardianActionMessageGenerative } from '../guardian-action-message-composer.js';
53
56
  import { resolveGuardianContext } from '../guardian-context-resolver.js';
54
57
  import {
55
58
  composeChannelVerifyReply,
56
59
  composeVerificationTelegram,
57
60
  GUARDIAN_VERIFY_TEMPLATE_KEYS,
58
61
  } from '../guardian-verification-templates.js';
62
+ import { httpError } from '../http-errors.js';
59
63
  import type {
60
64
  ApprovalConversationGenerator,
61
65
  ApprovalCopyGenerator,
@@ -63,10 +67,6 @@ import type {
63
67
  GuardianFollowUpConversationGenerator,
64
68
  MessageProcessor,
65
69
  } from '../http-types.js';
66
- import { processGuardianFollowUpTurn } from '../guardian-action-conversation-turn.js';
67
- import { composeGuardianActionMessageGenerative } from '../guardian-action-message-composer.js';
68
- import { executeFollowupAction } from '../guardian-action-followup-executor.js';
69
- import { httpError } from '../http-errors.js';
70
70
  import { deliverReplyViaCallback } from './channel-delivery-routes.js';
71
71
  import {
72
72
  canonicalChannelAssistantId,
@@ -1120,9 +1120,9 @@ export async function handleChannelInbound(
1120
1120
  // that the request was already resolved and skip action execution.
1121
1121
  let stateApplied = true;
1122
1122
  if (turnResult.disposition === 'call_back' || turnResult.disposition === 'message_back') {
1123
- stateApplied = progressFollowupState(followupRequest.id, 'dispatching', turnResult.disposition) !== null;
1123
+ stateApplied = progressFollowupState(followupRequest.id, 'dispatching', turnResult.disposition) !== undefined;
1124
1124
  } else if (turnResult.disposition === 'decline') {
1125
- stateApplied = finalizeFollowup(followupRequest.id, 'declined') !== null;
1125
+ stateApplied = finalizeFollowup(followupRequest.id, 'declined') !== undefined;
1126
1126
  }
1127
1127
  // keep_pending: no state change — guardian can reply again
1128
1128
 
@@ -26,7 +26,6 @@ import {
26
26
  createGuardianChallenge,
27
27
  getGuardianStatus,
28
28
  } from '../../daemon/handlers/config-channels.js';
29
- import { httpError } from '../http-errors.js';
30
29
  import {
31
30
  clearSlackChannelConfig,
32
31
  getSlackChannelConfig,
@@ -39,14 +38,15 @@ import {
39
38
  setTelegramConfig,
40
39
  setupTelegram,
41
40
  } from '../../daemon/handlers/config-telegram.js';
41
+ import { normalizePhoneNumber } from '../../util/phone.js';
42
42
  import {
43
43
  cancelOutbound,
44
44
  normalizeTelegramDestination,
45
45
  resendOutbound,
46
46
  startOutbound,
47
47
  } from '../guardian-outbound-actions.js';
48
+ import { httpError } from '../http-errors.js';
48
49
  import { guardianVerificationLimiter } from '../verification-rate-limiter.js';
49
- import { normalizePhoneNumber } from '../../util/phone.js';
50
50
 
51
51
  /**
52
52
  * GET /v1/integrations/telegram/config
@@ -3,12 +3,11 @@ import { getHookManager } from '../hooks/manager.js';
3
3
  import { check, classifyRisk, generateAllowlistOptions, generateScopeOptions } from '../permissions/checker.js';
4
4
  import type { PermissionPrompter } from '../permissions/prompter.js';
5
5
  import { addRule } from '../permissions/trust-store.js';
6
- import { RiskLevel } from '../permissions/types.js';
7
6
  import { getLogger } from '../util/logger.js';
8
- import type { ExecutionTarget } from './types.js';
9
7
  import { buildPolicyContext } from './policy-context.js';
10
8
  import { isSideEffectTool } from './side-effects.js';
11
9
  import { wrapCommand } from './terminal/sandbox.js';
10
+ import type { ExecutionTarget } from './types.js';
12
11
  import type { Tool, ToolContext, ToolLifecycleEvent } from './types.js';
13
12
 
14
13
  const log = getLogger('permission-checker');
@@ -2,8 +2,8 @@ import { getConfig } from '../config/loader.js';
2
2
  import { getHookManager } from '../hooks/manager.js';
3
3
  import { PermissionPrompter } from '../permissions/prompter.js';
4
4
  import { RiskLevel } from '../permissions/types.js';
5
- import { compileCustomPatterns, redactSecrets, scanText } from '../security/secret-scanner.js';
6
5
  import type { SecretPattern } from '../security/secret-scanner.js';
6
+ import { compileCustomPatterns, redactSecrets, scanText } from '../security/secret-scanner.js';
7
7
  import type { ExecutionTarget, ToolContext, ToolExecutionResult, ToolLifecycleEvent } from './types.js';
8
8
 
9
9
  /**
@@ -32,7 +32,7 @@ export const voiceConfigUpdateTool: Tool = {
32
32
 
33
33
  async execute(
34
34
  input: Record<string, unknown>,
35
- context: ToolContext,
35
+ _context: ToolContext,
36
36
  ): Promise<ToolExecutionResult> {
37
37
  const rawKey = input.activation_key;
38
38
  if (typeof rawKey !== 'string' || rawKey.trim() === '') {
package/src/version.ts CHANGED
@@ -1,3 +1,30 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ function resolveVersion(): string {
5
+ const envVersion = process.env.APP_VERSION;
6
+
7
+ // When APP_VERSION is not set, we're in local development — return the dev
8
+ // sentinel so Sentry (and similar) classify the session as "development".
9
+ if (!envVersion) return '0.0.0-dev';
10
+
11
+ // CI sets APP_VERSION to the dev placeholder during builds; resolve it to
12
+ // the package.json release version so Sentry gets a meaningful release tag.
13
+ if (envVersion === '0.0.0-dev') {
14
+ try {
15
+ const pkgPath = join(import.meta.dirname ?? __dirname, '..', 'package.json');
16
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
17
+ if (pkg.version && typeof pkg.version === 'string') return pkg.version;
18
+ } catch {
19
+ // package.json missing or unreadable
20
+ }
21
+ return '0.0.0-dev';
22
+ }
23
+
24
+ return envVersion;
25
+ }
26
+
1
27
  // Version is embedded at compile time via --define in CI.
2
- // Falls back to "0.0.0-dev" for local development.
3
- export const APP_VERSION: string = process.env.APP_VERSION ?? "0.0.0-dev";
28
+ // Falls back to "0.0.0-dev" for local development, or resolves the dev
29
+ // placeholder to package.json version when explicitly set in CI.
30
+ export const APP_VERSION: string = resolveVersion();