@vellumai/assistant 0.3.7 → 0.3.9
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/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +20 -0
- package/src/__tests__/approval-routes-http.test.ts +704 -0
- package/src/__tests__/call-controller.test.ts +835 -0
- package/src/__tests__/call-state.test.ts +24 -24
- package/src/__tests__/ipc-snapshot.test.ts +14 -0
- package/src/__tests__/relay-server.test.ts +9 -9
- package/src/__tests__/run-orchestrator.test.ts +399 -3
- package/src/__tests__/runtime-runs.test.ts +12 -4
- package/src/__tests__/send-endpoint-busy.test.ts +284 -0
- package/src/__tests__/session-init.benchmark.test.ts +3 -3
- package/src/__tests__/subagent-manager-notify.test.ts +3 -3
- package/src/__tests__/voice-session-bridge.test.ts +869 -0
- package/src/calls/{call-orchestrator.ts → call-controller.ts} +156 -257
- package/src/calls/call-domain.ts +21 -21
- package/src/calls/call-state.ts +12 -12
- package/src/calls/guardian-dispatch.ts +43 -3
- package/src/calls/relay-server.ts +34 -39
- package/src/calls/twilio-routes.ts +3 -3
- package/src/calls/voice-session-bridge.ts +244 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +81 -14
- package/src/config/bundled-skills/media-processing/TOOLS.json +3 -3
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +3 -3
- package/src/config/defaults.ts +5 -0
- package/src/config/notifications-schema.ts +15 -0
- package/src/config/schema.ts +13 -0
- package/src/config/types.ts +1 -0
- package/src/daemon/daemon-control.ts +13 -12
- package/src/daemon/handlers/subagents.ts +10 -3
- package/src/daemon/ipc-contract/notifications.ts +9 -0
- package/src/daemon/ipc-contract-inventory.json +2 -0
- package/src/daemon/ipc-contract.ts +4 -1
- package/src/daemon/lifecycle.ts +100 -1
- package/src/daemon/server.ts +8 -0
- package/src/daemon/session-agent-loop.ts +4 -0
- package/src/daemon/session-process.ts +51 -0
- package/src/daemon/session-runtime-assembly.ts +32 -0
- package/src/daemon/session.ts +5 -0
- package/src/memory/db-init.ts +80 -0
- package/src/memory/guardian-action-store.ts +2 -2
- package/src/memory/migrations/016-memory-segments-indexes.ts +1 -0
- package/src/memory/migrations/019-notification-tables-schema-migration.ts +70 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/schema.ts +59 -1
- package/src/notifications/README.md +134 -0
- package/src/notifications/adapters/macos.ts +55 -0
- package/src/notifications/adapters/telegram.ts +65 -0
- package/src/notifications/broadcaster.ts +175 -0
- package/src/notifications/copy-composer.ts +118 -0
- package/src/notifications/decision-engine.ts +391 -0
- package/src/notifications/decisions-store.ts +158 -0
- package/src/notifications/deliveries-store.ts +130 -0
- package/src/notifications/destination-resolver.ts +54 -0
- package/src/notifications/deterministic-checks.ts +187 -0
- package/src/notifications/emit-signal.ts +191 -0
- package/src/notifications/events-store.ts +145 -0
- package/src/notifications/preference-extractor.ts +223 -0
- package/src/notifications/preference-summary.ts +110 -0
- package/src/notifications/preferences-store.ts +142 -0
- package/src/notifications/runtime-dispatch.ts +100 -0
- package/src/notifications/signal.ts +24 -0
- package/src/notifications/types.ts +75 -0
- package/src/runtime/http-server.ts +15 -0
- package/src/runtime/http-types.ts +22 -0
- package/src/runtime/pending-interactions.ts +73 -0
- package/src/runtime/routes/approval-routes.ts +179 -0
- package/src/runtime/routes/channel-inbound-routes.ts +39 -4
- package/src/runtime/routes/conversation-routes.ts +107 -1
- package/src/runtime/routes/run-routes.ts +1 -1
- package/src/runtime/run-orchestrator.ts +157 -2
- package/src/subagent/manager.ts +6 -6
- package/src/tools/browser/browser-manager.ts +1 -1
- package/src/tools/subagent/message.ts +9 -2
- package/src/__tests__/call-orchestrator.test.ts +0 -1496
|
@@ -34,11 +34,11 @@ Preprocess a video asset: detect dead time via mpdecimate, segment the video int
|
|
|
34
34
|
|
|
35
35
|
Parameters:
|
|
36
36
|
- `asset_id` (required) — ID of the media asset.
|
|
37
|
-
- `interval_seconds` — Interval between keyframes (default:
|
|
38
|
-
- `segment_duration` — Duration of each segment window (default:
|
|
37
|
+
- `interval_seconds` — Interval between keyframes (default: 1s). Use 0.5s for sports/action content where frame density matters.
|
|
38
|
+
- `segment_duration` — Duration of each segment window (default: 15s).
|
|
39
39
|
- `dead_time_threshold` — Sensitivity for dead-time detection (default: 0.02).
|
|
40
40
|
- `section_config` — Path to a JSON file with manual section boundaries.
|
|
41
|
-
- `skip_dead_time` — Whether to detect and skip dead time (default:
|
|
41
|
+
- `skip_dead_time` — Whether to detect and skip dead time (default: false). Dead-time detection can be too aggressive for continuous action video like sports — it may incorrectly skip live play. Enable only for content with clear idle periods (e.g., lectures, surveillance footage).
|
|
42
42
|
- `short_edge` — Short edge resolution for downscaled frames in pixels (default: 480).
|
|
43
43
|
|
|
44
44
|
### analyze_keyframes
|
|
@@ -74,7 +74,7 @@ Get a diagnostic report for a media asset. Returns:
|
|
|
74
74
|
- **Processing stats**: total keyframes extracted.
|
|
75
75
|
- **Per-stage status and timing**: which stages (preprocess, map, reduce) have run, how long each took, current progress.
|
|
76
76
|
- **Failure reasons**: last error from any failed stage.
|
|
77
|
-
- **Cost estimation**: based on segment count and Gemini
|
|
77
|
+
- **Cost estimation**: based on segment count and current Gemini pricing.
|
|
78
78
|
|
|
79
79
|
## Services
|
|
80
80
|
|
|
@@ -110,6 +110,82 @@ Limits concurrent API calls during the Map phase to avoid rate limiting.
|
|
|
110
110
|
|
|
111
111
|
Tracks estimated API costs during pipeline execution.
|
|
112
112
|
|
|
113
|
+
## Best Practices
|
|
114
|
+
|
|
115
|
+
### Map Prompt Strategy: Go Broad, Not Targeted
|
|
116
|
+
|
|
117
|
+
The single most important insight: **always use a broad, descriptive map prompt** instead of a targeted one.
|
|
118
|
+
|
|
119
|
+
A targeted prompt like "find turnovers" locks you into one topic. If the user later wants to ask about defense, formations, or specific players, you'd need to reprocess the entire video. Instead, run a general-purpose descriptive prompt that captures everything visible, creating a rich, reusable dataset. Then all follow-up questions can be handled via `query_media` with no reprocessing.
|
|
120
|
+
|
|
121
|
+
**One map run, many queries.**
|
|
122
|
+
|
|
123
|
+
The map output will be larger (more tokens per segment), but Gemini Flash is cheap enough that this is a good tradeoff. Only use a targeted prompt if the user explicitly asks for something narrow.
|
|
124
|
+
|
|
125
|
+
#### Sample General-Purpose Map Prompt
|
|
126
|
+
|
|
127
|
+
Use this as a starting point for the `system_prompt` parameter in `analyze_keyframes`:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
You are analyzing keyframes from a video. For each segment, describe everything you can observe:
|
|
131
|
+
|
|
132
|
+
- People visible: count, positions, identifying features (jersey numbers, clothing, names if visible)
|
|
133
|
+
- Actions and movements: what people are doing, direction of movement, interactions
|
|
134
|
+
- Objects of interest: ball location, equipment, vehicles, on-screen graphics
|
|
135
|
+
- Environment: setting, lighting, weather if outdoors
|
|
136
|
+
- Text on screen: scores, captions, titles, signs, timestamps
|
|
137
|
+
- Scene composition: camera angle, zoom level, any transitions between shots
|
|
138
|
+
- Any stoppages, pauses, or changes in activity
|
|
139
|
+
|
|
140
|
+
Be specific and factual. Describe what you see, not what you infer happened between frames.
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### Sample Output Schema
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"type": "object",
|
|
148
|
+
"properties": {
|
|
149
|
+
"scene_description": { "type": "string" },
|
|
150
|
+
"people": {
|
|
151
|
+
"type": "array",
|
|
152
|
+
"items": {
|
|
153
|
+
"type": "object",
|
|
154
|
+
"properties": {
|
|
155
|
+
"description": { "type": "string" },
|
|
156
|
+
"position": { "type": "string" },
|
|
157
|
+
"action": { "type": "string" }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"objects_of_interest": { "type": "array", "items": { "type": "string" } },
|
|
162
|
+
"on_screen_text": { "type": "array", "items": { "type": "string" } },
|
|
163
|
+
"camera": { "type": "string" },
|
|
164
|
+
"notable_events": { "type": "array", "items": { "type": "string" } }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Clip Delivery
|
|
170
|
+
|
|
171
|
+
The `generate_clip` tool outputs clips as temporary files. These may not deliver reliably via sandbox attachments. For reliable delivery, use `host_bash` + ffmpeg to save clips to a user-specified location as a fallback.
|
|
172
|
+
|
|
173
|
+
## Known Limitations — Vision Analysis
|
|
174
|
+
|
|
175
|
+
Gemini performs well at **spatial/descriptive analysis** from static keyframes:
|
|
176
|
+
- Player positions, formations, and spacing
|
|
177
|
+
- Jersey numbers and identifying features
|
|
178
|
+
- Ball location and which team has possession
|
|
179
|
+
- Score and on-screen text
|
|
180
|
+
- Camera angles and scene composition
|
|
181
|
+
|
|
182
|
+
Gemini **hallucinates when asked to detect fast temporal events** from static frames, regardless of frame density:
|
|
183
|
+
- Turnovers, steals, fouls, and specific plays
|
|
184
|
+
- Fast transitions and split-second actions
|
|
185
|
+
- Causality between frames (what "happened" vs. what's visible)
|
|
186
|
+
|
|
187
|
+
The model is good at describing **what is there** but bad at detecting **what happened**. Structure your map prompts and queries accordingly — ask the model to describe scenes, then use `query_media` (Claude) to reason about patterns and events across the descriptive data.
|
|
188
|
+
|
|
113
189
|
## Operator Runbook
|
|
114
190
|
|
|
115
191
|
### Monitoring Progress
|
|
@@ -137,16 +213,7 @@ After fixing the root cause, re-run the failed stage. The pipeline is resumable
|
|
|
137
213
|
|
|
138
214
|
### Cost Expectations
|
|
139
215
|
|
|
140
|
-
The Map phase (Gemini
|
|
141
|
-
|
|
142
|
-
| Video Duration | Interval | Keyframes | Segments (~10 frames each) | Estimated Map Cost |
|
|
143
|
-
|----------------|----------|-----------|----------------------------|--------------------|
|
|
144
|
-
| 30 min | 3s | ~600 | ~60 | ~$0.06 |
|
|
145
|
-
| 60 min | 3s | ~1,200 | ~120 | ~$0.12 |
|
|
146
|
-
| 90 min | 3s | ~1,800 | ~180 | ~$0.18 |
|
|
147
|
-
| 90 min | 5s | ~1,080 | ~108 | ~$0.11 |
|
|
148
|
-
|
|
149
|
-
The Reduce phase (Claude) adds a small additional cost per query. The `media_diagnostics` tool provides per-asset cost estimates.
|
|
216
|
+
Use `media_diagnostics` to get per-asset cost estimates. The Map phase (Gemini) is the primary cost driver — it scales with video duration and keyframe interval. The Q&A phase (Claude) is negligible per query.
|
|
150
217
|
|
|
151
218
|
### Known Limitations
|
|
152
219
|
|
|
@@ -67,11 +67,11 @@
|
|
|
67
67
|
},
|
|
68
68
|
"interval_seconds": {
|
|
69
69
|
"type": "number",
|
|
70
|
-
"description": "Interval between keyframes in seconds. Default:
|
|
70
|
+
"description": "Interval between keyframes in seconds. Default: 1. Use 0.5 for sports/action content."
|
|
71
71
|
},
|
|
72
72
|
"segment_duration": {
|
|
73
73
|
"type": "number",
|
|
74
|
-
"description": "Duration of each segment window in seconds. Default:
|
|
74
|
+
"description": "Duration of each segment window in seconds. Default: 15"
|
|
75
75
|
},
|
|
76
76
|
"dead_time_threshold": {
|
|
77
77
|
"type": "number",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
},
|
|
84
84
|
"skip_dead_time": {
|
|
85
85
|
"type": "boolean",
|
|
86
|
-
"description": "Whether to detect and skip dead time. Default:
|
|
86
|
+
"description": "Whether to detect and skip dead time. Default: false. Can be too aggressive for continuous action video like sports."
|
|
87
87
|
},
|
|
88
88
|
"short_edge": {
|
|
89
89
|
"type": "number",
|
|
@@ -355,13 +355,13 @@ export async function preprocessForAsset(
|
|
|
355
355
|
onProgress?: (msg: string) => void,
|
|
356
356
|
): Promise<PreprocessManifest> {
|
|
357
357
|
const config: PreprocessConfig = {
|
|
358
|
-
intervalSeconds: options.intervalSeconds ??
|
|
359
|
-
segmentDuration: options.segmentDuration ??
|
|
358
|
+
intervalSeconds: options.intervalSeconds ?? 1,
|
|
359
|
+
segmentDuration: options.segmentDuration ?? 15,
|
|
360
360
|
deadTimeThreshold: options.deadTimeThreshold ?? 0.02,
|
|
361
361
|
shortEdge: options.shortEdge ?? 480,
|
|
362
362
|
};
|
|
363
363
|
|
|
364
|
-
const skipDeadTime = options.skipDeadTime ??
|
|
364
|
+
const skipDeadTime = options.skipDeadTime ?? false;
|
|
365
365
|
|
|
366
366
|
const asset = getMediaAssetById(assetId);
|
|
367
367
|
if (!asset) {
|
package/src/config/defaults.ts
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const NotificationsConfigSchema = z.object({
|
|
4
|
+
enabled: z
|
|
5
|
+
.boolean({ error: 'notifications.enabled must be a boolean' })
|
|
6
|
+
.default(false),
|
|
7
|
+
shadowMode: z
|
|
8
|
+
.boolean({ error: 'notifications.shadowMode must be a boolean' })
|
|
9
|
+
.default(true),
|
|
10
|
+
decisionModel: z
|
|
11
|
+
.string({ error: 'notifications.decisionModel must be a string' })
|
|
12
|
+
.default('claude-haiku-4-5-20251001'),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type NotificationsConfig = z.infer<typeof NotificationsConfigSchema>;
|
package/src/config/schema.ts
CHANGED
|
@@ -90,6 +90,13 @@ export type {
|
|
|
90
90
|
WorkspaceGitConfig,
|
|
91
91
|
} from './agent-schema.js';
|
|
92
92
|
|
|
93
|
+
export {
|
|
94
|
+
NotificationsConfigSchema,
|
|
95
|
+
} from './notifications-schema.js';
|
|
96
|
+
export type {
|
|
97
|
+
NotificationsConfig,
|
|
98
|
+
} from './notifications-schema.js';
|
|
99
|
+
|
|
93
100
|
export {
|
|
94
101
|
TimeoutConfigSchema,
|
|
95
102
|
RateLimitConfigSchema,
|
|
@@ -129,6 +136,7 @@ import { CallsConfigSchema } from './calls-schema.js';
|
|
|
129
136
|
import { SandboxConfigSchema } from './sandbox-schema.js';
|
|
130
137
|
import { SkillsConfigSchema } from './skills-schema.js';
|
|
131
138
|
import { AgentHeartbeatConfigSchema, SwarmConfigSchema, WorkspaceGitConfigSchema } from './agent-schema.js';
|
|
139
|
+
import { NotificationsConfigSchema } from './notifications-schema.js';
|
|
132
140
|
import {
|
|
133
141
|
TimeoutConfigSchema,
|
|
134
142
|
RateLimitConfigSchema,
|
|
@@ -437,6 +445,11 @@ export const AssistantConfigSchema = z.object({
|
|
|
437
445
|
sigkillGracePeriodMs: 2000,
|
|
438
446
|
titleGenerationMaxTokens: 30,
|
|
439
447
|
}),
|
|
448
|
+
notifications: NotificationsConfigSchema.default({
|
|
449
|
+
enabled: false,
|
|
450
|
+
shadowMode: true,
|
|
451
|
+
decisionModel: 'claude-haiku-4-5-20251001',
|
|
452
|
+
}),
|
|
440
453
|
}).superRefine((config, ctx) => {
|
|
441
454
|
if (config.contextWindow.targetInputTokens >= config.contextWindow.maxInputTokens) {
|
|
442
455
|
ctx.addIssue({
|
package/src/config/types.ts
CHANGED
|
@@ -19,6 +19,10 @@ const DAEMON_TIMEOUT_DEFAULTS = {
|
|
|
19
19
|
sigkillGracePeriodMs: 2000,
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
function isPositiveInteger(v: unknown): v is number {
|
|
23
|
+
return typeof v === 'number' && Number.isInteger(v) && v > 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
/**
|
|
23
27
|
* Read daemon timeout values directly from the config JSON file, bypassing
|
|
24
28
|
* loadConfig() and its ensureMigratedDataDir()/ensureDataDir() side effects.
|
|
@@ -30,18 +34,15 @@ function readDaemonTimeouts(): typeof DAEMON_TIMEOUT_DEFAULTS {
|
|
|
30
34
|
const raw = JSON.parse(readFileSync(getWorkspaceConfigPath(), 'utf-8'));
|
|
31
35
|
if (raw.daemon && typeof raw.daemon === 'object') {
|
|
32
36
|
return {
|
|
33
|
-
startupSocketWaitMs:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
typeof raw.daemon.sigkillGracePeriodMs === 'number'
|
|
43
|
-
? raw.daemon.sigkillGracePeriodMs
|
|
44
|
-
: DAEMON_TIMEOUT_DEFAULTS.sigkillGracePeriodMs,
|
|
37
|
+
startupSocketWaitMs: isPositiveInteger(raw.daemon.startupSocketWaitMs)
|
|
38
|
+
? raw.daemon.startupSocketWaitMs
|
|
39
|
+
: DAEMON_TIMEOUT_DEFAULTS.startupSocketWaitMs,
|
|
40
|
+
stopTimeoutMs: isPositiveInteger(raw.daemon.stopTimeoutMs)
|
|
41
|
+
? raw.daemon.stopTimeoutMs
|
|
42
|
+
: DAEMON_TIMEOUT_DEFAULTS.stopTimeoutMs,
|
|
43
|
+
sigkillGracePeriodMs: isPositiveInteger(raw.daemon.sigkillGracePeriodMs)
|
|
44
|
+
? raw.daemon.sigkillGracePeriodMs
|
|
45
|
+
: DAEMON_TIMEOUT_DEFAULTS.sigkillGracePeriodMs,
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
48
|
} catch {
|
|
@@ -109,10 +109,17 @@ export function handleSubagentMessage(
|
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
const
|
|
112
|
+
const result = manager.sendMessage(msg.subagentId, msg.content);
|
|
113
113
|
|
|
114
|
-
if (
|
|
115
|
-
log.warn({ subagentId: msg.subagentId }, '
|
|
114
|
+
if (result === 'queue_full') {
|
|
115
|
+
log.warn({ subagentId: msg.subagentId }, 'Subagent message rejected — queue full');
|
|
116
|
+
ctx.send(socket, {
|
|
117
|
+
type: 'error',
|
|
118
|
+
message: `Subagent "${msg.subagentId}" message queue is full. Please wait for current messages to be processed.`,
|
|
119
|
+
category: 'queue_full',
|
|
120
|
+
});
|
|
121
|
+
} else if (result !== 'sent') {
|
|
122
|
+
log.warn({ subagentId: msg.subagentId, reason: result }, 'Client sent message to terminal subagent');
|
|
116
123
|
ctx.send(socket, {
|
|
117
124
|
type: 'error',
|
|
118
125
|
message: `Subagent "${msg.subagentId}" not found or in terminal state.`,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Broadcast to connected macOS clients when a notification should be displayed. */
|
|
2
|
+
export interface NotificationIntent {
|
|
3
|
+
type: 'notification_intent';
|
|
4
|
+
sourceEventName: string;
|
|
5
|
+
title: string;
|
|
6
|
+
body: string;
|
|
7
|
+
/** Optional deep-link metadata so the client can navigate to the relevant context. */
|
|
8
|
+
deepLinkMetadata?: Record<string, unknown>;
|
|
9
|
+
}
|
|
@@ -201,6 +201,7 @@
|
|
|
201
201
|
"MessageQueued",
|
|
202
202
|
"MessageQueuedDeleted",
|
|
203
203
|
"ModelInfo",
|
|
204
|
+
"NotificationIntent",
|
|
204
205
|
"OpenBundleResponse",
|
|
205
206
|
"OpenTasksWindow",
|
|
206
207
|
"OpenUrl",
|
|
@@ -486,6 +487,7 @@
|
|
|
486
487
|
"message_queued",
|
|
487
488
|
"message_queued_deleted",
|
|
488
489
|
"model_info",
|
|
490
|
+
"notification_intent",
|
|
489
491
|
"open_bundle_response",
|
|
490
492
|
"open_tasks_window",
|
|
491
493
|
"open_url",
|
|
@@ -27,6 +27,7 @@ export * from './ipc-contract/diagnostics.js';
|
|
|
27
27
|
export * from './ipc-contract/parental-control.js';
|
|
28
28
|
export * from './ipc-contract/inbox.js';
|
|
29
29
|
export * from './ipc-contract/pairing.js';
|
|
30
|
+
export * from './ipc-contract/notifications.js';
|
|
30
31
|
|
|
31
32
|
// Import types needed for aggregate unions and SubagentEvent
|
|
32
33
|
import type { AuthMessage, PingMessage, CancelRequest, DeleteQueuedMessage, ModelGetRequest, ModelSetRequest, ImageGenModelSetRequest, HistoryRequest, UndoRequest, RegenerateRequest, UsageRequest, SandboxSetRequest, SessionListRequest, SessionCreateRequest, SessionSwitchRequest, SessionRenameRequest, SessionsClearRequest, ConversationSearchRequest } from './ipc-contract/sessions.js';
|
|
@@ -64,6 +65,7 @@ import type { SchedulesList, ScheduleToggle, ScheduleRemove, ScheduleRunNow, Rem
|
|
|
64
65
|
import type { ParentalControlGetRequest, ParentalControlVerifyPinRequest, ParentalControlSetPinRequest, ParentalControlUpdateRequest, ParentalControlGetResponse, ParentalControlVerifyPinResponse, ParentalControlSetPinResponse, ParentalControlUpdateResponse } from './ipc-contract/parental-control.js';
|
|
65
66
|
import type { IngressInviteRequest, IngressMemberRequest, AssistantInboxRequest, AssistantInboxEscalationRequest, AssistantInboxReplyRequest, IngressInviteResponse, IngressMemberResponse, AssistantInboxResponse, AssistantInboxEscalationResponse, AssistantInboxReplyResponse } from './ipc-contract/inbox.js';
|
|
66
67
|
import type { PairingApprovalResponse, ApprovedDevicesList, ApprovedDeviceRemove, ApprovedDevicesClear, PairingApprovalRequest, ApprovedDevicesListResponse, ApprovedDeviceRemoveResponse } from './ipc-contract/pairing.js';
|
|
68
|
+
import type { NotificationIntent } from './ipc-contract/notifications.js';
|
|
67
69
|
|
|
68
70
|
// === SubagentEvent — defined here because it references ServerMessage ===
|
|
69
71
|
|
|
@@ -361,7 +363,8 @@ export type ServerMessage =
|
|
|
361
363
|
| AssistantInboxReplyResponse
|
|
362
364
|
| PairingApprovalRequest
|
|
363
365
|
| ApprovedDevicesListResponse
|
|
364
|
-
| ApprovedDeviceRemoveResponse
|
|
366
|
+
| ApprovedDeviceRemoveResponse
|
|
367
|
+
| NotificationIntent;
|
|
365
368
|
|
|
366
369
|
// === Contract schema ===
|
|
367
370
|
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { ensurePromptFiles } from '../config/system-prompt.js';
|
|
|
26
26
|
import { loadPrebuiltHtml } from '../home-base/prebuilt/seed.js';
|
|
27
27
|
import { DaemonServer } from './server.js';
|
|
28
28
|
import { setRelayBroadcast } from '../calls/relay-server.js';
|
|
29
|
+
import { setVoiceBridgeOrchestrator } from '../calls/voice-session-bridge.js';
|
|
29
30
|
import { listWorkItems, updateWorkItem } from '../work-items/work-item-store.js';
|
|
30
31
|
import { getLogger, initLogger } from '../util/logger.js';
|
|
31
32
|
import { initSentry } from '../instrument.js';
|
|
@@ -35,6 +36,8 @@ import { QdrantManager } from '../memory/qdrant-manager.js';
|
|
|
35
36
|
import { initQdrantClient } from '../memory/qdrant-client.js';
|
|
36
37
|
import { startScheduler } from '../schedule/scheduler.js';
|
|
37
38
|
import { RuntimeHttpServer } from '../runtime/http-server.js';
|
|
39
|
+
import { assistantEventHub } from '../runtime/assistant-event-hub.js';
|
|
40
|
+
import * as attachmentsStore from '../memory/attachments-store.js';
|
|
38
41
|
import { getHookManager } from '../hooks/manager.js';
|
|
39
42
|
import { installTemplates } from '../hooks/templates.js';
|
|
40
43
|
import { installCliLaunchers } from './install-cli-launchers.js';
|
|
@@ -42,10 +45,12 @@ import { HeartbeatService } from '../workspace/heartbeat-service.js';
|
|
|
42
45
|
import { AgentHeartbeatService } from '../agent-heartbeat/agent-heartbeat-service.js';
|
|
43
46
|
import { reconcileCallsOnStartup } from '../calls/call-recovery.js';
|
|
44
47
|
import { TwilioConversationRelayProvider } from '../calls/twilio-provider.js';
|
|
48
|
+
import { emitNotificationSignal, registerBroadcastFn } from '../notifications/emit-signal.js';
|
|
45
49
|
import { createApprovalCopyGenerator, createApprovalConversationGenerator } from './approval-generators.js';
|
|
46
50
|
import { initializeProvidersAndTools, registerWatcherProviders, registerMessagingProviders } from './providers-setup.js';
|
|
47
51
|
import { installShutdownHandlers } from './shutdown-handlers.js';
|
|
48
52
|
import { writePid, cleanupPidFile } from './daemon-control.js';
|
|
53
|
+
import { initPairingHandlers } from './handlers/pairing.js';
|
|
49
54
|
|
|
50
55
|
// Re-export public API so existing consumers don't need to change imports
|
|
51
56
|
export {
|
|
@@ -184,38 +189,112 @@ export async function runDaemon(): Promise<void> {
|
|
|
184
189
|
registerWatcherProviders();
|
|
185
190
|
registerMessagingProviders();
|
|
186
191
|
|
|
192
|
+
// Register the IPC broadcast function for the notification signal pipeline's
|
|
193
|
+
// macOS adapter so it can deliver notification_intent messages to desktop clients.
|
|
194
|
+
registerBroadcastFn((msg) => server.broadcast(msg));
|
|
195
|
+
|
|
187
196
|
const scheduler = startScheduler(
|
|
188
197
|
async (conversationId, message) => {
|
|
189
198
|
await server.processMessage(conversationId, message);
|
|
190
199
|
},
|
|
191
200
|
(reminder) => {
|
|
201
|
+
// Legacy IPC broadcast for backward compatibility with desktop client UI
|
|
192
202
|
server.broadcast({
|
|
193
203
|
type: 'reminder_fired',
|
|
194
204
|
reminderId: reminder.id,
|
|
195
205
|
label: reminder.label,
|
|
196
206
|
message: reminder.message,
|
|
197
207
|
});
|
|
208
|
+
// Signal pipeline: fire-and-forget
|
|
209
|
+
void emitNotificationSignal({
|
|
210
|
+
sourceEventName: 'reminder.fired',
|
|
211
|
+
sourceChannel: 'scheduler',
|
|
212
|
+
sourceSessionId: reminder.id,
|
|
213
|
+
attentionHints: {
|
|
214
|
+
requiresAction: true,
|
|
215
|
+
urgency: 'high',
|
|
216
|
+
isAsyncBackground: false,
|
|
217
|
+
visibleInSourceNow: false,
|
|
218
|
+
},
|
|
219
|
+
contextPayload: {
|
|
220
|
+
reminderId: reminder.id,
|
|
221
|
+
label: reminder.label,
|
|
222
|
+
message: reminder.message,
|
|
223
|
+
},
|
|
224
|
+
dedupeKey: `reminder:${reminder.id}`,
|
|
225
|
+
});
|
|
198
226
|
},
|
|
199
227
|
(schedule) => {
|
|
228
|
+
// Legacy IPC broadcast for backward compatibility with desktop client UI
|
|
200
229
|
server.broadcast({
|
|
201
230
|
type: 'schedule_complete',
|
|
202
231
|
scheduleId: schedule.id,
|
|
203
232
|
name: schedule.name,
|
|
204
233
|
});
|
|
234
|
+
// Signal pipeline: fire-and-forget
|
|
235
|
+
void emitNotificationSignal({
|
|
236
|
+
sourceEventName: 'schedule.complete',
|
|
237
|
+
sourceChannel: 'scheduler',
|
|
238
|
+
sourceSessionId: schedule.id,
|
|
239
|
+
attentionHints: {
|
|
240
|
+
requiresAction: false,
|
|
241
|
+
urgency: 'medium',
|
|
242
|
+
isAsyncBackground: true,
|
|
243
|
+
visibleInSourceNow: false,
|
|
244
|
+
},
|
|
245
|
+
contextPayload: {
|
|
246
|
+
scheduleId: schedule.id,
|
|
247
|
+
name: schedule.name,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
205
250
|
},
|
|
206
251
|
(notification) => {
|
|
252
|
+
// Legacy IPC broadcast for backward compatibility with desktop client UI
|
|
207
253
|
server.broadcast({
|
|
208
254
|
type: 'watcher_notification',
|
|
209
255
|
title: notification.title,
|
|
210
256
|
body: notification.body,
|
|
211
257
|
});
|
|
258
|
+
// Signal pipeline: fire-and-forget
|
|
259
|
+
void emitNotificationSignal({
|
|
260
|
+
sourceEventName: 'watcher.notification',
|
|
261
|
+
sourceChannel: 'watcher',
|
|
262
|
+
sourceSessionId: `watcher-${Date.now()}`,
|
|
263
|
+
attentionHints: {
|
|
264
|
+
requiresAction: false,
|
|
265
|
+
urgency: 'low',
|
|
266
|
+
isAsyncBackground: true,
|
|
267
|
+
visibleInSourceNow: false,
|
|
268
|
+
},
|
|
269
|
+
contextPayload: {
|
|
270
|
+
title: notification.title,
|
|
271
|
+
body: notification.body,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
212
274
|
},
|
|
213
275
|
(params) => {
|
|
276
|
+
// Legacy IPC broadcast for backward compatibility with desktop client UI
|
|
214
277
|
server.broadcast({
|
|
215
278
|
type: 'watcher_escalation',
|
|
216
279
|
title: params.title,
|
|
217
280
|
body: params.body,
|
|
218
281
|
});
|
|
282
|
+
// Signal pipeline: fire-and-forget
|
|
283
|
+
void emitNotificationSignal({
|
|
284
|
+
sourceEventName: 'watcher.escalation',
|
|
285
|
+
sourceChannel: 'watcher',
|
|
286
|
+
sourceSessionId: `watcher-escalation-${Date.now()}`,
|
|
287
|
+
attentionHints: {
|
|
288
|
+
requiresAction: true,
|
|
289
|
+
urgency: 'high',
|
|
290
|
+
isAsyncBackground: false,
|
|
291
|
+
visibleInSourceNow: false,
|
|
292
|
+
},
|
|
293
|
+
contextPayload: {
|
|
294
|
+
title: params.title,
|
|
295
|
+
body: params.body,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
219
298
|
},
|
|
220
299
|
);
|
|
221
300
|
|
|
@@ -247,6 +326,8 @@ export async function runDaemon(): Promise<void> {
|
|
|
247
326
|
|
|
248
327
|
const hostname = getRuntimeHttpHost();
|
|
249
328
|
|
|
329
|
+
const runOrchestrator = server.createRunOrchestrator();
|
|
330
|
+
|
|
250
331
|
runtimeHttp = new RuntimeHttpServer({
|
|
251
332
|
port: httpPort,
|
|
252
333
|
hostname,
|
|
@@ -255,15 +336,33 @@ export async function runDaemon(): Promise<void> {
|
|
|
255
336
|
server.processMessage(conversationId, content, attachmentIds, options, sourceChannel),
|
|
256
337
|
persistAndProcessMessage: (conversationId, content, attachmentIds, options, sourceChannel) =>
|
|
257
338
|
server.persistAndProcessMessage(conversationId, content, attachmentIds, options, sourceChannel),
|
|
258
|
-
runOrchestrator
|
|
339
|
+
runOrchestrator,
|
|
259
340
|
interfacesDir: getInterfacesDir(),
|
|
260
341
|
approvalCopyGenerator: createApprovalCopyGenerator(),
|
|
261
342
|
approvalConversationGenerator: createApprovalConversationGenerator(),
|
|
343
|
+
sendMessageDeps: {
|
|
344
|
+
getOrCreateSession: (conversationId) =>
|
|
345
|
+
server.getSessionForMessages(conversationId),
|
|
346
|
+
assistantEventHub,
|
|
347
|
+
resolveAttachments: (attachmentIds) =>
|
|
348
|
+
attachmentsStore.getAttachmentsByIds(attachmentIds).map((a) => ({
|
|
349
|
+
id: a.id,
|
|
350
|
+
filename: a.originalFilename,
|
|
351
|
+
mimeType: a.mimeType,
|
|
352
|
+
data: a.dataBase64,
|
|
353
|
+
})),
|
|
354
|
+
},
|
|
262
355
|
});
|
|
356
|
+
|
|
357
|
+
// Inject the voice bridge orchestrator BEFORE attempting to start the HTTP
|
|
358
|
+
// server. The bridge only needs the RunOrchestrator instance (already created
|
|
359
|
+
// above) and must be available even when the HTTP server fails to bind.
|
|
360
|
+
setVoiceBridgeOrchestrator(runOrchestrator);
|
|
263
361
|
try {
|
|
264
362
|
await runtimeHttp.start();
|
|
265
363
|
setRelayBroadcast((msg) => server.broadcast(msg));
|
|
266
364
|
runtimeHttp.setPairingBroadcast((msg) => server.broadcast(msg));
|
|
365
|
+
initPairingHandlers(runtimeHttp.getPairingStore(), bearerToken);
|
|
267
366
|
server.setHttpPort(httpPort);
|
|
268
367
|
log.info({ port: httpPort, hostname }, 'Daemon startup: runtime HTTP server listening');
|
|
269
368
|
} catch (err) {
|
package/src/daemon/server.ts
CHANGED
|
@@ -819,6 +819,14 @@ export class DaemonServer {
|
|
|
819
819
|
return { messageId };
|
|
820
820
|
}
|
|
821
821
|
|
|
822
|
+
/**
|
|
823
|
+
* Expose session lookup for the POST /v1/messages handler.
|
|
824
|
+
* The handler manages busy-state checking and queueing itself.
|
|
825
|
+
*/
|
|
826
|
+
async getSessionForMessages(conversationId: string): Promise<Session> {
|
|
827
|
+
return this.getOrCreateSession(conversationId, undefined, true);
|
|
828
|
+
}
|
|
829
|
+
|
|
822
830
|
createRunOrchestrator(): RunOrchestrator {
|
|
823
831
|
return new RunOrchestrator({
|
|
824
832
|
getOrCreateSession: (conversationId, transport) =>
|
|
@@ -100,6 +100,7 @@ export interface AgentLoopSessionContext {
|
|
|
100
100
|
channelCapabilities?: ChannelCapabilities;
|
|
101
101
|
commandIntent?: { type: string; payload?: string; languageCode?: string };
|
|
102
102
|
guardianContext?: GuardianRuntimeContext;
|
|
103
|
+
voiceCallControlPrompt?: string;
|
|
103
104
|
|
|
104
105
|
readonly coreToolNames: Set<string>;
|
|
105
106
|
allowedToolNames?: Set<string>;
|
|
@@ -321,6 +322,7 @@ export async function runAgentLoopImpl(
|
|
|
321
322
|
channelTurnContext,
|
|
322
323
|
guardianContext: ctx.guardianContext ?? null,
|
|
323
324
|
temporalContext,
|
|
325
|
+
voiceCallControlPrompt: ctx.voiceCallControlPrompt ?? null,
|
|
324
326
|
});
|
|
325
327
|
|
|
326
328
|
// Pre-run repair
|
|
@@ -431,6 +433,7 @@ export async function runAgentLoopImpl(
|
|
|
431
433
|
channelTurnContext,
|
|
432
434
|
guardianContext: ctx.guardianContext ?? null,
|
|
433
435
|
temporalContext,
|
|
436
|
+
voiceCallControlPrompt: ctx.voiceCallControlPrompt ?? null,
|
|
434
437
|
});
|
|
435
438
|
preRepairMessages = runMessages;
|
|
436
439
|
preRunHistoryLength = runMessages.length;
|
|
@@ -466,6 +469,7 @@ export async function runAgentLoopImpl(
|
|
|
466
469
|
channelTurnContext,
|
|
467
470
|
guardianContext: ctx.guardianContext ?? null,
|
|
468
471
|
temporalContext,
|
|
472
|
+
voiceCallControlPrompt: ctx.voiceCallControlPrompt ?? null,
|
|
469
473
|
});
|
|
470
474
|
preRepairMessages = runMessages;
|
|
471
475
|
preRunHistoryLength = runMessages.length;
|
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
import { answerCall } from '../calls/call-domain.js';
|
|
25
25
|
import { resolveSlash, type SlashContext } from './session-slash.js';
|
|
26
26
|
import { getConfig } from '../config/loader.js';
|
|
27
|
+
import { extractPreferences } from '../notifications/preference-extractor.js';
|
|
28
|
+
import { createPreference } from '../notifications/preferences-store.js';
|
|
27
29
|
import { getLogger } from '../util/logger.js';
|
|
28
30
|
|
|
29
31
|
const log = getLogger('session-process');
|
|
@@ -68,6 +70,8 @@ export interface ProcessSessionContext {
|
|
|
68
70
|
readonly usageStats: UsageStats;
|
|
69
71
|
/** Request-scoped skill IDs preactivated via slash resolution. */
|
|
70
72
|
preactivatedSkillIds?: string[];
|
|
73
|
+
/** Assistant identity — used for scoping notification preferences. */
|
|
74
|
+
readonly assistantId?: string;
|
|
71
75
|
persistUserMessage(content: string, attachments: UserMessageAttachment[], requestId?: string, metadata?: Record<string, unknown>): string;
|
|
72
76
|
runAgentLoop(
|
|
73
77
|
content: string,
|
|
@@ -231,6 +235,29 @@ export function drainQueue(session: ProcessSessionContext, reason: QueueDrainRea
|
|
|
231
235
|
session.currentActiveSurfaceId = next.activeSurfaceId;
|
|
232
236
|
session.currentPage = next.currentPage;
|
|
233
237
|
|
|
238
|
+
// Fire-and-forget: detect notification preferences in the queued message
|
|
239
|
+
// and persist any that are found, mirroring the logic in processMessage.
|
|
240
|
+
if (session.assistantId && getConfig().notifications.enabled) {
|
|
241
|
+
const aid = session.assistantId;
|
|
242
|
+
extractPreferences(resolvedContent)
|
|
243
|
+
.then((result) => {
|
|
244
|
+
if (!result.detected) return;
|
|
245
|
+
for (const pref of result.preferences) {
|
|
246
|
+
createPreference({
|
|
247
|
+
assistantId: aid,
|
|
248
|
+
preferenceText: pref.preferenceText,
|
|
249
|
+
appliesWhen: pref.appliesWhen,
|
|
250
|
+
priority: pref.priority,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
log.info({ count: result.preferences.length, conversationId: session.conversationId }, 'Persisted extracted notification preferences (queued)');
|
|
254
|
+
})
|
|
255
|
+
.catch((err) => {
|
|
256
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
257
|
+
log.warn({ err: errMsg, conversationId: session.conversationId }, 'Background preference extraction failed (queued)');
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
234
261
|
// Fire-and-forget: persistUserMessage set session.processing = true
|
|
235
262
|
// so subsequent messages will still be enqueued.
|
|
236
263
|
// runAgentLoop's finally block will call drainQueue when this run completes.
|
|
@@ -375,6 +402,30 @@ export async function processMessage(
|
|
|
375
402
|
return '';
|
|
376
403
|
}
|
|
377
404
|
|
|
405
|
+
// Fire-and-forget: detect notification preferences in the user message
|
|
406
|
+
// and persist any that are found. Runs in the background so it doesn't
|
|
407
|
+
// block the main conversation flow.
|
|
408
|
+
if (session.assistantId && getConfig().notifications.enabled) {
|
|
409
|
+
const aid = session.assistantId;
|
|
410
|
+
extractPreferences(resolvedContent)
|
|
411
|
+
.then((result) => {
|
|
412
|
+
if (!result.detected) return;
|
|
413
|
+
for (const pref of result.preferences) {
|
|
414
|
+
createPreference({
|
|
415
|
+
assistantId: aid,
|
|
416
|
+
preferenceText: pref.preferenceText,
|
|
417
|
+
appliesWhen: pref.appliesWhen,
|
|
418
|
+
priority: pref.priority,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
log.info({ count: result.preferences.length, conversationId: session.conversationId }, 'Persisted extracted notification preferences');
|
|
422
|
+
})
|
|
423
|
+
.catch((err) => {
|
|
424
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
425
|
+
log.warn({ err: errMsg, conversationId: session.conversationId }, 'Background preference extraction failed');
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
378
429
|
await session.runAgentLoop(resolvedContent, userMessageId, onEvent);
|
|
379
430
|
return userMessageId;
|
|
380
431
|
}
|