vellum 0.2.0 → 0.2.1
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 +28 -0
- package/src/__tests__/app-bundler.test.ts +12 -33
- package/src/__tests__/browser-skill-endstate.test.ts +1 -5
- package/src/__tests__/call-orchestrator.test.ts +328 -0
- package/src/__tests__/call-state.test.ts +133 -0
- package/src/__tests__/call-store.test.ts +476 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +409 -0
- package/src/__tests__/config-schema.test.ts +49 -0
- package/src/__tests__/doordash-session.test.ts +9 -0
- package/src/__tests__/ipc-snapshot.test.ts +34 -0
- package/src/__tests__/registry.test.ts +13 -8
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +218 -0
- package/src/__tests__/run-orchestrator.test.ts +3 -3
- package/src/__tests__/runtime-attachment-metadata.test.ts +17 -19
- package/src/__tests__/runtime-runs-http.test.ts +1 -19
- package/src/__tests__/runtime-runs.test.ts +7 -7
- package/src/__tests__/session-queue.test.ts +50 -0
- package/src/__tests__/turn-commit.test.ts +56 -0
- package/src/__tests__/workspace-git-service.test.ts +217 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +129 -0
- package/src/bundler/app-bundler.ts +29 -12
- package/src/calls/call-constants.ts +10 -0
- package/src/calls/call-orchestrator.ts +364 -0
- package/src/calls/call-state.ts +64 -0
- package/src/calls/call-store.ts +229 -0
- package/src/calls/relay-server.ts +298 -0
- package/src/calls/twilio-config.ts +34 -0
- package/src/calls/twilio-provider.ts +169 -0
- package/src/calls/twilio-routes.ts +236 -0
- package/src/calls/types.ts +37 -0
- package/src/calls/voice-provider.ts +14 -0
- package/src/cli/doordash.ts +5 -24
- package/src/config/bundled-skills/doordash/SKILL.md +104 -0
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -1
- package/src/config/defaults.ts +11 -0
- package/src/config/schema.ts +57 -0
- package/src/config/system-prompt.ts +50 -1
- package/src/config/types.ts +1 -0
- package/src/daemon/handlers/config.ts +30 -0
- package/src/daemon/handlers/index.ts +6 -0
- package/src/daemon/handlers/work-items.ts +142 -2
- package/src/daemon/ipc-contract-inventory.json +12 -0
- package/src/daemon/ipc-contract.ts +52 -0
- package/src/daemon/lifecycle.ts +27 -5
- package/src/daemon/server.ts +10 -12
- package/src/daemon/session-tool-setup.ts +6 -0
- package/src/daemon/session.ts +40 -1
- package/src/index.ts +2 -0
- package/src/media/gemini-image-service.ts +1 -1
- package/src/memory/db.ts +266 -0
- package/src/memory/schema.ts +42 -0
- package/src/runtime/http-server.ts +189 -25
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/routes/attachment-routes.ts +6 -6
- package/src/runtime/routes/channel-routes.ts +16 -18
- package/src/runtime/routes/conversation-routes.ts +5 -9
- package/src/runtime/routes/run-routes.ts +4 -8
- package/src/runtime/run-orchestrator.ts +32 -5
- package/src/tools/calls/call-end.ts +117 -0
- package/src/tools/calls/call-start.ts +134 -0
- package/src/tools/calls/call-status.ts +97 -0
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/registry.ts +2 -4
- package/src/tools/tasks/index.ts +2 -0
- package/src/tools/tasks/task-delete.ts +49 -8
- package/src/tools/tasks/task-run.ts +9 -1
- package/src/tools/tasks/work-item-enqueue.ts +93 -3
- package/src/tools/tasks/work-item-list.ts +10 -25
- package/src/tools/tasks/work-item-remove.ts +112 -0
- package/src/tools/tasks/work-item-update.ts +186 -0
- package/src/tools/tool-manifest.ts +39 -31
- package/src/tools/ui-surface/definitions.ts +3 -0
- package/src/work-items/work-item-store.ts +209 -0
- package/src/workspace/commit-message-enrichment-service.ts +260 -0
- package/src/workspace/commit-message-provider.ts +95 -0
- package/src/workspace/git-service.ts +187 -32
- package/src/workspace/heartbeat-service.ts +70 -13
- package/src/workspace/turn-commit.ts +39 -49
|
@@ -10,6 +10,7 @@ import { postToSlackWebhook } from '../../slack/slack-webhook.js';
|
|
|
10
10
|
import { getApp } from '../../memory/app-store.js';
|
|
11
11
|
import type {
|
|
12
12
|
ModelSetRequest,
|
|
13
|
+
ImageGenModelSetRequest,
|
|
13
14
|
AddTrustRule,
|
|
14
15
|
RemoveTrustRule,
|
|
15
16
|
UpdateTrustRule,
|
|
@@ -126,6 +127,35 @@ export function handleModelSet(
|
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
export function handleImageGenModelSet(
|
|
131
|
+
msg: ImageGenModelSetRequest,
|
|
132
|
+
_socket: net.Socket,
|
|
133
|
+
ctx: HandlerContext,
|
|
134
|
+
): void {
|
|
135
|
+
try {
|
|
136
|
+
const raw = loadRawConfig();
|
|
137
|
+
raw.imageGenModel = msg.model;
|
|
138
|
+
|
|
139
|
+
ctx.setSuppressConfigReload(true);
|
|
140
|
+
try {
|
|
141
|
+
saveRawConfig(raw);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
ctx.setSuppressConfigReload(false);
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
const existingSuppressTimer = ctx.debounceTimers.get('__suppress_reset__');
|
|
147
|
+
if (existingSuppressTimer) clearTimeout(existingSuppressTimer);
|
|
148
|
+
const resetTimer = setTimeout(() => { ctx.setSuppressConfigReload(false); }, 300);
|
|
149
|
+
ctx.debounceTimers.set('__suppress_reset__', resetTimer);
|
|
150
|
+
|
|
151
|
+
ctx.updateConfigFingerprint();
|
|
152
|
+
log.info({ model: msg.model }, 'Image generation model updated');
|
|
153
|
+
} catch (err) {
|
|
154
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
155
|
+
log.error({ err }, `Failed to set image gen model: ${message}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
129
159
|
export function handleAddTrustRule(
|
|
130
160
|
msg: AddTrustRule,
|
|
131
161
|
_socket: net.Socket,
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
import {
|
|
56
56
|
handleModelGet,
|
|
57
57
|
handleModelSet,
|
|
58
|
+
handleImageGenModelSet,
|
|
58
59
|
handleAddTrustRule,
|
|
59
60
|
handleTrustRulesList,
|
|
60
61
|
handleRemoveTrustRule,
|
|
@@ -103,7 +104,9 @@ import {
|
|
|
103
104
|
handleWorkItemCreate,
|
|
104
105
|
handleWorkItemUpdate,
|
|
105
106
|
handleWorkItemComplete,
|
|
107
|
+
handleWorkItemDelete,
|
|
106
108
|
handleWorkItemRunTask,
|
|
109
|
+
handleWorkItemOutput,
|
|
107
110
|
} from './work-items.js';
|
|
108
111
|
|
|
109
112
|
import {
|
|
@@ -152,6 +155,7 @@ const handlers: DispatchMap = {
|
|
|
152
155
|
delete_queued_message: handleDeleteQueuedMessage,
|
|
153
156
|
model_get: (_msg, socket, ctx) => handleModelGet(socket, ctx),
|
|
154
157
|
model_set: handleModelSet,
|
|
158
|
+
image_gen_model_set: handleImageGenModelSet,
|
|
155
159
|
history_request: handleHistoryRequest,
|
|
156
160
|
undo: handleUndo,
|
|
157
161
|
regenerate: handleRegenerate,
|
|
@@ -329,7 +333,9 @@ const handlers: DispatchMap = {
|
|
|
329
333
|
work_item_create: handleWorkItemCreate,
|
|
330
334
|
work_item_update: handleWorkItemUpdate,
|
|
331
335
|
work_item_complete: handleWorkItemComplete,
|
|
336
|
+
work_item_delete: handleWorkItemDelete,
|
|
332
337
|
work_item_run_task: handleWorkItemRunTask,
|
|
338
|
+
work_item_output: handleWorkItemOutput,
|
|
333
339
|
|
|
334
340
|
subagent_abort: handleSubagentAbort,
|
|
335
341
|
subagent_status: handleSubagentStatus,
|
|
@@ -5,18 +5,22 @@ import type {
|
|
|
5
5
|
WorkItemCreateRequest,
|
|
6
6
|
WorkItemUpdateRequest,
|
|
7
7
|
WorkItemCompleteRequest,
|
|
8
|
+
WorkItemDeleteRequest,
|
|
8
9
|
WorkItemRunTaskRequest,
|
|
10
|
+
WorkItemOutputRequest,
|
|
9
11
|
} from '../ipc-protocol.js';
|
|
10
12
|
import { log, type HandlerContext } from './shared.js';
|
|
11
13
|
import {
|
|
12
14
|
createWorkItem,
|
|
15
|
+
deleteWorkItem,
|
|
13
16
|
getWorkItem,
|
|
14
17
|
listWorkItems,
|
|
15
18
|
updateWorkItem,
|
|
16
19
|
type WorkItemStatus,
|
|
17
20
|
} from '../../work-items/work-item-store.js';
|
|
18
|
-
import { getTask } from '../../tasks/task-store.js';
|
|
21
|
+
import { getTask, getTaskRun } from '../../tasks/task-store.js';
|
|
19
22
|
import { runTask } from '../../tasks/task-runner.js';
|
|
23
|
+
import { getMessages } from '../../memory/conversation-store.js';
|
|
20
24
|
|
|
21
25
|
export function handleWorkItemsList(
|
|
22
26
|
msg: WorkItemsListRequest,
|
|
@@ -57,6 +61,7 @@ export function handleWorkItemCreate(
|
|
|
57
61
|
|
|
58
62
|
// Notify all connected clients so open Task Queue views refresh immediately
|
|
59
63
|
broadcastWorkItemStatus(ctx, item.id);
|
|
64
|
+
ctx.broadcast({ type: 'tasks_changed' });
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
export function handleWorkItemUpdate(
|
|
@@ -78,6 +83,7 @@ export function handleWorkItemUpdate(
|
|
|
78
83
|
// (e.g. priority/sort changes made by one client are reflected everywhere)
|
|
79
84
|
if (item) {
|
|
80
85
|
broadcastWorkItemStatus(ctx, item.id);
|
|
86
|
+
ctx.broadcast({ type: 'tasks_changed' });
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
|
|
@@ -86,6 +92,18 @@ export function handleWorkItemComplete(
|
|
|
86
92
|
socket: net.Socket,
|
|
87
93
|
ctx: HandlerContext,
|
|
88
94
|
): void {
|
|
95
|
+
// Only allow completion from the 'awaiting_review' state — this ensures
|
|
96
|
+
// items go through the full run lifecycle before being marked done.
|
|
97
|
+
const existing = getWorkItem(msg.id);
|
|
98
|
+
if (!existing) {
|
|
99
|
+
ctx.send(socket, { type: 'error', message: `Work item not found: ${msg.id}` });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (existing.status !== 'awaiting_review') {
|
|
103
|
+
ctx.send(socket, { type: 'error', message: `Cannot complete work item: status is '${existing.status}', expected 'awaiting_review'` });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
89
107
|
const item = updateWorkItem(msg.id, { status: 'done' }) ?? null;
|
|
90
108
|
ctx.send(socket, { type: 'work_item_update_response', item });
|
|
91
109
|
if (item) {
|
|
@@ -102,9 +120,25 @@ export function handleWorkItemComplete(
|
|
|
102
120
|
updatedAt: item.updatedAt,
|
|
103
121
|
},
|
|
104
122
|
});
|
|
123
|
+
ctx.broadcast({ type: 'tasks_changed' });
|
|
105
124
|
}
|
|
106
125
|
}
|
|
107
126
|
|
|
127
|
+
export function handleWorkItemDelete(
|
|
128
|
+
msg: WorkItemDeleteRequest,
|
|
129
|
+
socket: net.Socket,
|
|
130
|
+
ctx: HandlerContext,
|
|
131
|
+
): void {
|
|
132
|
+
const existing = getWorkItem(msg.id);
|
|
133
|
+
if (!existing) {
|
|
134
|
+
ctx.send(socket, { type: 'work_item_delete_response', id: msg.id, success: false });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
deleteWorkItem(msg.id);
|
|
138
|
+
ctx.send(socket, { type: 'work_item_delete_response', id: msg.id, success: true });
|
|
139
|
+
ctx.broadcast({ type: 'tasks_changed' });
|
|
140
|
+
}
|
|
141
|
+
|
|
108
142
|
function broadcastWorkItemStatus(ctx: HandlerContext, id: string): void {
|
|
109
143
|
const item = getWorkItem(id);
|
|
110
144
|
if (item) {
|
|
@@ -124,6 +158,92 @@ function broadcastWorkItemStatus(ctx: HandlerContext, id: string): void {
|
|
|
124
158
|
}
|
|
125
159
|
}
|
|
126
160
|
|
|
161
|
+
export function handleWorkItemOutput(
|
|
162
|
+
msg: WorkItemOutputRequest,
|
|
163
|
+
socket: net.Socket,
|
|
164
|
+
ctx: HandlerContext,
|
|
165
|
+
): void {
|
|
166
|
+
try {
|
|
167
|
+
const workItem = getWorkItem(msg.id);
|
|
168
|
+
if (!workItem) {
|
|
169
|
+
ctx.send(socket, { type: 'work_item_output_response', id: msg.id, success: false, error: 'Work item not found' });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// If the work item has never been run, return an error so the client
|
|
174
|
+
// can show "No output yet" instead of an empty loaded state.
|
|
175
|
+
if (!workItem.lastRunConversationId) {
|
|
176
|
+
ctx.send(socket, { type: 'work_item_output_response', id: msg.id, success: false, error: 'This task has not been run yet. No output is available.' });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let summary = '';
|
|
181
|
+
const highlights: string[] = [];
|
|
182
|
+
|
|
183
|
+
const msgs = getMessages(workItem.lastRunConversationId);
|
|
184
|
+
// Find the last assistant message with text content (not tool calls)
|
|
185
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
186
|
+
const m = msgs[i];
|
|
187
|
+
if (m.role !== 'assistant') continue;
|
|
188
|
+
|
|
189
|
+
let text = m.content;
|
|
190
|
+
// Content may be JSON array of content blocks — extract text blocks only
|
|
191
|
+
try {
|
|
192
|
+
const parsed = JSON.parse(text);
|
|
193
|
+
if (Array.isArray(parsed)) {
|
|
194
|
+
text = parsed
|
|
195
|
+
.filter((b: { type: string }) => b.type === 'text')
|
|
196
|
+
.map((b: { text: string }) => b.text)
|
|
197
|
+
.join('\n');
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// Plain text content — use as-is
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!text.trim()) continue;
|
|
204
|
+
|
|
205
|
+
summary = text.length > 2000 ? text.slice(0, 2000) : text;
|
|
206
|
+
|
|
207
|
+
// Extract up to 5 notable lines (bullet points or key findings)
|
|
208
|
+
const lines = text.split('\n');
|
|
209
|
+
for (const line of lines) {
|
|
210
|
+
const trimmed = line.trim();
|
|
211
|
+
if ((trimmed.startsWith('-') || trimmed.startsWith('*')) && trimmed.length > 2) {
|
|
212
|
+
highlights.push(trimmed);
|
|
213
|
+
if (highlights.length >= 5) break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Convert finishedAt from milliseconds (Date.now()) to seconds for the
|
|
220
|
+
// client, which uses Date(timeIntervalSince1970:) expecting seconds.
|
|
221
|
+
let completedAt: number | null = null;
|
|
222
|
+
if (workItem.lastRunId) {
|
|
223
|
+
const run = getTaskRun(workItem.lastRunId);
|
|
224
|
+
completedAt = run?.finishedAt != null ? Math.floor(run.finishedAt / 1000) : null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
ctx.send(socket, {
|
|
228
|
+
type: 'work_item_output_response',
|
|
229
|
+
id: msg.id,
|
|
230
|
+
success: true,
|
|
231
|
+
output: {
|
|
232
|
+
title: workItem.title,
|
|
233
|
+
status: workItem.lastRunStatus ?? workItem.status,
|
|
234
|
+
runId: workItem.lastRunId,
|
|
235
|
+
conversationId: workItem.lastRunConversationId,
|
|
236
|
+
completedAt,
|
|
237
|
+
summary,
|
|
238
|
+
highlights,
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
} catch (err) {
|
|
242
|
+
log.error({ err, workItemId: msg.id }, 'handleWorkItemOutput failed');
|
|
243
|
+
ctx.send(socket, { type: 'work_item_output_response', id: msg.id, success: false, error: 'Failed to load task output' });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
127
247
|
export async function handleWorkItemRunTask(
|
|
128
248
|
msg: WorkItemRunTaskRequest,
|
|
129
249
|
socket: net.Socket,
|
|
@@ -131,7 +251,24 @@ export async function handleWorkItemRunTask(
|
|
|
131
251
|
): Promise<void> {
|
|
132
252
|
const workItem = getWorkItem(msg.id);
|
|
133
253
|
if (!workItem) {
|
|
134
|
-
ctx.send(socket, { type: 'work_item_run_task_response', id: msg.id, lastRunId: '', success: false, error: 'Work item not found' });
|
|
254
|
+
ctx.send(socket, { type: 'work_item_run_task_response', id: msg.id, lastRunId: '', success: false, error: 'Work item not found', errorCode: 'not_found' });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (workItem.status === 'running') {
|
|
259
|
+
ctx.send(socket, { type: 'work_item_run_task_response', id: msg.id, lastRunId: workItem.lastRunId ?? '', success: false, error: 'Work item is already running', errorCode: 'already_running' });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const NON_RUNNABLE_STATUSES: readonly string[] = ['done', 'archived'];
|
|
264
|
+
if (NON_RUNNABLE_STATUSES.includes(workItem.status)) {
|
|
265
|
+
ctx.send(socket, { type: 'work_item_run_task_response', id: msg.id, lastRunId: workItem.lastRunId ?? '', success: false, error: `Work item has status '${workItem.status}' and cannot be run`, errorCode: 'invalid_status' });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const task = getTask(workItem.taskId);
|
|
270
|
+
if (!task) {
|
|
271
|
+
ctx.send(socket, { type: 'work_item_run_task_response', id: msg.id, lastRunId: '', success: false, error: `Associated task not found: ${workItem.taskId}`, errorCode: 'no_task' });
|
|
135
272
|
return;
|
|
136
273
|
}
|
|
137
274
|
|
|
@@ -143,6 +280,7 @@ export async function handleWorkItemRunTask(
|
|
|
143
280
|
|
|
144
281
|
// Broadcast the running state
|
|
145
282
|
broadcastWorkItemStatus(ctx, msg.id);
|
|
283
|
+
ctx.broadcast({ type: 'tasks_changed' });
|
|
146
284
|
|
|
147
285
|
// Execute task asynchronously — create a session and wire processMessage
|
|
148
286
|
try {
|
|
@@ -165,6 +303,7 @@ export async function handleWorkItemRunTask(
|
|
|
165
303
|
});
|
|
166
304
|
|
|
167
305
|
broadcastWorkItemStatus(ctx, msg.id);
|
|
306
|
+
ctx.broadcast({ type: 'tasks_changed' });
|
|
168
307
|
} catch (err) {
|
|
169
308
|
log.error({ err, workItemId: msg.id }, 'work_item_run_task failed');
|
|
170
309
|
updateWorkItem(msg.id, {
|
|
@@ -172,5 +311,6 @@ export async function handleWorkItemRunTask(
|
|
|
172
311
|
lastRunStatus: 'failed',
|
|
173
312
|
});
|
|
174
313
|
broadcastWorkItemStatus(ctx, msg.id);
|
|
314
|
+
ctx.broadcast({ type: 'tasks_changed' });
|
|
175
315
|
}
|
|
176
316
|
}
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"GetSigningIdentityResponse",
|
|
32
32
|
"HistoryRequest",
|
|
33
33
|
"HomeBaseGetRequest",
|
|
34
|
+
"ImageGenModelSetRequest",
|
|
34
35
|
"IntegrationConnectRequest",
|
|
35
36
|
"IntegrationDisconnectRequest",
|
|
36
37
|
"IntegrationListRequest",
|
|
@@ -90,7 +91,9 @@
|
|
|
90
91
|
"WatchObservation",
|
|
91
92
|
"WorkItemCompleteRequest",
|
|
92
93
|
"WorkItemCreateRequest",
|
|
94
|
+
"WorkItemDeleteRequest",
|
|
93
95
|
"WorkItemGetRequest",
|
|
96
|
+
"WorkItemOutputRequest",
|
|
94
97
|
"WorkItemRunTaskRequest",
|
|
95
98
|
"WorkItemUpdateRequest",
|
|
96
99
|
"WorkItemsListRequest"
|
|
@@ -174,6 +177,7 @@
|
|
|
174
177
|
"SubagentStatusChanged",
|
|
175
178
|
"SuggestionResponse",
|
|
176
179
|
"TaskRouted",
|
|
180
|
+
"TasksChanged",
|
|
177
181
|
"ToolInputDelta",
|
|
178
182
|
"ToolOutputChunk",
|
|
179
183
|
"ToolResult",
|
|
@@ -196,7 +200,9 @@
|
|
|
196
200
|
"WatcherEscalation",
|
|
197
201
|
"WatcherNotification",
|
|
198
202
|
"WorkItemCreateResponse",
|
|
203
|
+
"WorkItemDeleteResponse",
|
|
199
204
|
"WorkItemGetResponse",
|
|
205
|
+
"WorkItemOutputResponse",
|
|
200
206
|
"WorkItemRunTaskResponse",
|
|
201
207
|
"WorkItemStatusChanged",
|
|
202
208
|
"WorkItemUpdateResponse",
|
|
@@ -234,6 +240,7 @@
|
|
|
234
240
|
"get_signing_identity_response",
|
|
235
241
|
"history_request",
|
|
236
242
|
"home_base_get",
|
|
243
|
+
"image_gen_model_set",
|
|
237
244
|
"integration_connect",
|
|
238
245
|
"integration_disconnect",
|
|
239
246
|
"integration_list",
|
|
@@ -293,7 +300,9 @@
|
|
|
293
300
|
"watch_observation",
|
|
294
301
|
"work_item_complete",
|
|
295
302
|
"work_item_create",
|
|
303
|
+
"work_item_delete",
|
|
296
304
|
"work_item_get",
|
|
305
|
+
"work_item_output",
|
|
297
306
|
"work_item_run_task",
|
|
298
307
|
"work_item_update",
|
|
299
308
|
"work_items_list"
|
|
@@ -377,6 +386,7 @@
|
|
|
377
386
|
"subagent_status_changed",
|
|
378
387
|
"suggestion_response",
|
|
379
388
|
"task_routed",
|
|
389
|
+
"tasks_changed",
|
|
380
390
|
"tool_input_delta",
|
|
381
391
|
"tool_output_chunk",
|
|
382
392
|
"tool_result",
|
|
@@ -398,7 +408,9 @@
|
|
|
398
408
|
"watcher_escalation",
|
|
399
409
|
"watcher_notification",
|
|
400
410
|
"work_item_create_response",
|
|
411
|
+
"work_item_delete_response",
|
|
401
412
|
"work_item_get_response",
|
|
413
|
+
"work_item_output_response",
|
|
402
414
|
"work_item_run_task_response",
|
|
403
415
|
"work_item_status_changed",
|
|
404
416
|
"work_item_update_response",
|
|
@@ -125,6 +125,11 @@ export interface ModelSetRequest {
|
|
|
125
125
|
model: string;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
export interface ImageGenModelSetRequest {
|
|
129
|
+
type: 'image_gen_model_set';
|
|
130
|
+
model: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
128
133
|
export interface HistoryRequest {
|
|
129
134
|
type: 'history_request';
|
|
130
135
|
sessionId: string;
|
|
@@ -793,11 +798,21 @@ export interface WorkItemCompleteRequest {
|
|
|
793
798
|
id: string;
|
|
794
799
|
}
|
|
795
800
|
|
|
801
|
+
export interface WorkItemDeleteRequest {
|
|
802
|
+
type: 'work_item_delete';
|
|
803
|
+
id: string;
|
|
804
|
+
}
|
|
805
|
+
|
|
796
806
|
export interface WorkItemRunTaskRequest {
|
|
797
807
|
type: 'work_item_run_task';
|
|
798
808
|
id: string;
|
|
799
809
|
}
|
|
800
810
|
|
|
811
|
+
export interface WorkItemOutputRequest {
|
|
812
|
+
type: 'work_item_output';
|
|
813
|
+
id: string;
|
|
814
|
+
}
|
|
815
|
+
|
|
801
816
|
export type ClientMessage =
|
|
802
817
|
| AuthMessage
|
|
803
818
|
| UserMessage
|
|
@@ -811,6 +826,7 @@ export type ClientMessage =
|
|
|
811
826
|
| DeleteQueuedMessage
|
|
812
827
|
| ModelGetRequest
|
|
813
828
|
| ModelSetRequest
|
|
829
|
+
| ImageGenModelSetRequest
|
|
814
830
|
| HistoryRequest
|
|
815
831
|
| UndoRequest
|
|
816
832
|
| RegenerateRequest
|
|
@@ -889,7 +905,9 @@ export type ClientMessage =
|
|
|
889
905
|
| WorkItemCreateRequest
|
|
890
906
|
| WorkItemUpdateRequest
|
|
891
907
|
| WorkItemCompleteRequest
|
|
908
|
+
| WorkItemDeleteRequest
|
|
892
909
|
| WorkItemRunTaskRequest
|
|
910
|
+
| WorkItemOutputRequest
|
|
893
911
|
| SubagentAbortRequest
|
|
894
912
|
| SubagentStatusRequest
|
|
895
913
|
| SubagentMessageRequest;
|
|
@@ -1914,12 +1932,38 @@ export interface WorkItemUpdateResponse {
|
|
|
1914
1932
|
} | null;
|
|
1915
1933
|
}
|
|
1916
1934
|
|
|
1935
|
+
export interface WorkItemDeleteResponse {
|
|
1936
|
+
type: 'work_item_delete_response';
|
|
1937
|
+
id: string;
|
|
1938
|
+
success: boolean;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
export type WorkItemRunTaskErrorCode = 'not_found' | 'already_running' | 'invalid_status' | 'no_task';
|
|
1942
|
+
|
|
1917
1943
|
export interface WorkItemRunTaskResponse {
|
|
1918
1944
|
type: 'work_item_run_task_response';
|
|
1919
1945
|
id: string;
|
|
1920
1946
|
lastRunId: string;
|
|
1921
1947
|
success: boolean;
|
|
1922
1948
|
error?: string;
|
|
1949
|
+
/** Structured error code so the client can deterministically re-enable buttons or show contextual UI. */
|
|
1950
|
+
errorCode?: WorkItemRunTaskErrorCode;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
export interface WorkItemOutputResponse {
|
|
1954
|
+
type: 'work_item_output_response';
|
|
1955
|
+
id: string;
|
|
1956
|
+
success: boolean;
|
|
1957
|
+
error?: string;
|
|
1958
|
+
output?: {
|
|
1959
|
+
title: string;
|
|
1960
|
+
status: string;
|
|
1961
|
+
runId: string | null;
|
|
1962
|
+
conversationId: string | null;
|
|
1963
|
+
completedAt: number | null;
|
|
1964
|
+
summary: string;
|
|
1965
|
+
highlights: string[];
|
|
1966
|
+
};
|
|
1923
1967
|
}
|
|
1924
1968
|
|
|
1925
1969
|
/** Server push — tells the client to open/focus the tasks window. */
|
|
@@ -1927,6 +1971,11 @@ export interface OpenTasksWindow {
|
|
|
1927
1971
|
type: 'open_tasks_window';
|
|
1928
1972
|
}
|
|
1929
1973
|
|
|
1974
|
+
/** Server push — lightweight invalidation signal: the task queue has been mutated, refetch your list. */
|
|
1975
|
+
export interface TasksChanged {
|
|
1976
|
+
type: 'tasks_changed';
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1930
1979
|
/** Server push — broadcast when a work item status changes (e.g. running -> awaiting_review). */
|
|
1931
1980
|
export interface WorkItemStatusChanged {
|
|
1932
1981
|
type: 'work_item_status_changed';
|
|
@@ -2042,8 +2091,11 @@ export type ServerMessage =
|
|
|
2042
2091
|
| WorkItemGetResponse
|
|
2043
2092
|
| WorkItemCreateResponse
|
|
2044
2093
|
| WorkItemUpdateResponse
|
|
2094
|
+
| WorkItemDeleteResponse
|
|
2045
2095
|
| WorkItemRunTaskResponse
|
|
2096
|
+
| WorkItemOutputResponse
|
|
2046
2097
|
| WorkItemStatusChanged
|
|
2098
|
+
| TasksChanged
|
|
2047
2099
|
| OpenTasksWindow
|
|
2048
2100
|
| SubagentSpawned
|
|
2049
2101
|
| SubagentStatusChanged
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { initializeTools } from '../tools/registry.js';
|
|
|
22
22
|
import { loadConfig } from '../config/loader.js';
|
|
23
23
|
import { ensurePromptFiles } from '../config/system-prompt.js';
|
|
24
24
|
import { DaemonServer } from './server.js';
|
|
25
|
+
import { listWorkItems, updateWorkItem } from '../work-items/work-item-store.js';
|
|
25
26
|
import { getLogger, initLogger } from '../util/logger.js';
|
|
26
27
|
import { DaemonError } from '../util/errors.js';
|
|
27
28
|
import { initSentry } from '../instrument.js';
|
|
@@ -43,6 +44,7 @@ import { RuntimeHttpServer } from '../runtime/http-server.js';
|
|
|
43
44
|
import { getHookManager } from '../hooks/manager.js';
|
|
44
45
|
import { installTemplates } from '../hooks/templates.js';
|
|
45
46
|
import { HeartbeatService } from '../workspace/heartbeat-service.js';
|
|
47
|
+
import { getEnrichmentService } from '../workspace/commit-message-enrichment-service.js';
|
|
46
48
|
|
|
47
49
|
const log = getLogger('lifecycle');
|
|
48
50
|
|
|
@@ -279,6 +281,17 @@ export async function runDaemon(): Promise<void> {
|
|
|
279
281
|
initializeDb();
|
|
280
282
|
log.info('Daemon startup: DB initialized');
|
|
281
283
|
|
|
284
|
+
// Recover orphaned work items that were left in 'running' state when the
|
|
285
|
+
// daemon previously crashed or was killed mid-task.
|
|
286
|
+
const orphanedRunning = listWorkItems({ status: 'running' });
|
|
287
|
+
if (orphanedRunning.length > 0) {
|
|
288
|
+
for (const item of orphanedRunning) {
|
|
289
|
+
updateWorkItem(item.id, { status: 'failed', lastRunStatus: 'interrupted' });
|
|
290
|
+
log.info({ workItemId: item.id, title: item.title }, 'Recovered orphaned running work item → failed (interrupted)');
|
|
291
|
+
}
|
|
292
|
+
log.info({ count: orphanedRunning.length }, 'Recovered orphaned running work items');
|
|
293
|
+
}
|
|
294
|
+
|
|
282
295
|
log.info('Daemon startup: loading config');
|
|
283
296
|
const config = loadConfig();
|
|
284
297
|
|
|
@@ -328,7 +341,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
328
341
|
|
|
329
342
|
const scheduler = startScheduler(
|
|
330
343
|
async (conversationId, message) => {
|
|
331
|
-
await server.processMessage(
|
|
344
|
+
await server.processMessage(conversationId, message);
|
|
332
345
|
},
|
|
333
346
|
(reminder) => {
|
|
334
347
|
server.broadcast({
|
|
@@ -382,10 +395,10 @@ export async function runDaemon(): Promise<void> {
|
|
|
382
395
|
port,
|
|
383
396
|
hostname,
|
|
384
397
|
bearerToken,
|
|
385
|
-
processMessage: (
|
|
386
|
-
server.processMessage(
|
|
387
|
-
persistAndProcessMessage: (
|
|
388
|
-
server.persistAndProcessMessage(
|
|
398
|
+
processMessage: (conversationId, content, attachmentIds, options, sourceChannel) =>
|
|
399
|
+
server.processMessage(conversationId, content, attachmentIds, options, sourceChannel),
|
|
400
|
+
persistAndProcessMessage: (conversationId, content, attachmentIds, options, sourceChannel) =>
|
|
401
|
+
server.persistAndProcessMessage(conversationId, content, attachmentIds, options, sourceChannel),
|
|
389
402
|
runOrchestrator: server.createRunOrchestrator(),
|
|
390
403
|
interfacesDir: getInterfacesDir(),
|
|
391
404
|
});
|
|
@@ -477,6 +490,15 @@ export async function runDaemon(): Promise<void> {
|
|
|
477
490
|
} catch (err) {
|
|
478
491
|
log.warn({ err, phase: 'post_stop' }, 'Post-stop workspace commit failed');
|
|
479
492
|
}
|
|
493
|
+
|
|
494
|
+
// Flush in-flight enrichment jobs so shutdown commit notes are not dropped.
|
|
495
|
+
// The enrichment service's shutdown() drains active jobs and discards pending ones.
|
|
496
|
+
try {
|
|
497
|
+
await getEnrichmentService().shutdown();
|
|
498
|
+
} catch (err) {
|
|
499
|
+
log.warn({ err }, 'Enrichment service shutdown failed (non-fatal)');
|
|
500
|
+
}
|
|
501
|
+
|
|
480
502
|
if (runtimeHttp) await runtimeHttp.stop();
|
|
481
503
|
await browserManager.closeAllPages();
|
|
482
504
|
scheduler.stop();
|
package/src/daemon/server.ts
CHANGED
|
@@ -967,7 +967,6 @@ export class DaemonServer {
|
|
|
967
967
|
* is not blocked for the duration of the agent loop.
|
|
968
968
|
*/
|
|
969
969
|
async persistAndProcessMessage(
|
|
970
|
-
assistantId: string,
|
|
971
970
|
conversationId: string,
|
|
972
971
|
content: string,
|
|
973
972
|
attachmentIds?: string[],
|
|
@@ -988,14 +987,14 @@ export class DaemonServer {
|
|
|
988
987
|
throw new Error('Session is already processing a message');
|
|
989
988
|
}
|
|
990
989
|
|
|
991
|
-
// Set
|
|
992
|
-
// doesn't mutate the session state visible to an in-flight request.
|
|
993
|
-
session.setAssistantId(
|
|
990
|
+
// Set the session identity AFTER the isProcessing check so a rejected
|
|
991
|
+
// request doesn't mutate the session state visible to an in-flight request.
|
|
992
|
+
session.setAssistantId('self');
|
|
994
993
|
session.setChannelCapabilities(resolveChannelCapabilities(sourceChannel));
|
|
995
994
|
|
|
996
995
|
// Resolve attachment IDs to full attachment data for the session
|
|
997
996
|
const attachments = attachmentIds
|
|
998
|
-
? attachmentsStore.getAttachmentsByIds(
|
|
997
|
+
? attachmentsStore.getAttachmentsByIds('self', attachmentIds).map((a) => ({
|
|
999
998
|
id: a.id,
|
|
1000
999
|
filename: a.originalFilename,
|
|
1001
1000
|
mimeType: a.mimeType,
|
|
@@ -1022,7 +1021,6 @@ export class DaemonServer {
|
|
|
1022
1021
|
* Used by the channel inbound endpoint which needs the assistant reply.
|
|
1023
1022
|
*/
|
|
1024
1023
|
async processMessage(
|
|
1025
|
-
assistantId: string,
|
|
1026
1024
|
conversationId: string,
|
|
1027
1025
|
content: string,
|
|
1028
1026
|
attachmentIds?: string[],
|
|
@@ -1041,14 +1039,14 @@ export class DaemonServer {
|
|
|
1041
1039
|
throw new Error('Session is already processing a message');
|
|
1042
1040
|
}
|
|
1043
1041
|
|
|
1044
|
-
// Set
|
|
1045
|
-
// doesn't mutate the session state visible to an in-flight request.
|
|
1046
|
-
session.setAssistantId(
|
|
1042
|
+
// Set the session identity AFTER the isProcessing check so a rejected
|
|
1043
|
+
// request doesn't mutate the session state visible to an in-flight request.
|
|
1044
|
+
session.setAssistantId('self');
|
|
1047
1045
|
session.setChannelCapabilities(resolveChannelCapabilities(sourceChannel));
|
|
1048
1046
|
|
|
1049
1047
|
// Resolve attachment IDs to full attachment data for the session
|
|
1050
1048
|
const attachments = attachmentIds
|
|
1051
|
-
? attachmentsStore.getAttachmentsByIds(
|
|
1049
|
+
? attachmentsStore.getAttachmentsByIds('self', attachmentIds).map((a) => ({
|
|
1052
1050
|
id: a.id,
|
|
1053
1051
|
filename: a.originalFilename,
|
|
1054
1052
|
mimeType: a.mimeType,
|
|
@@ -1072,8 +1070,8 @@ export class DaemonServer {
|
|
|
1072
1070
|
return new RunOrchestrator({
|
|
1073
1071
|
getOrCreateSession: (conversationId) =>
|
|
1074
1072
|
this.getOrCreateSession(conversationId),
|
|
1075
|
-
resolveAttachments: (
|
|
1076
|
-
attachmentsStore.getAttachmentsByIds(
|
|
1073
|
+
resolveAttachments: (attachmentIds) =>
|
|
1074
|
+
attachmentsStore.getAttachmentsByIds('self', attachmentIds).map((a) => ({
|
|
1077
1075
|
id: a.id,
|
|
1078
1076
|
filename: a.originalFilename,
|
|
1079
1077
|
mimeType: a.mimeType,
|
|
@@ -181,6 +181,12 @@ export function createToolExecutor(
|
|
|
181
181
|
ctx.sendToClient({ type: 'open_tasks_window' });
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
// Broadcast tasks_changed so connected clients (e.g. macOS Tasks window)
|
|
185
|
+
// auto-refresh when the LLM mutates the task queue via tools
|
|
186
|
+
if ((name === 'task_list_add' || name === 'task_list_update' || name === 'task_list_remove') && !result.isError) {
|
|
187
|
+
broadcastToAllClients?.({ type: 'tasks_changed' });
|
|
188
|
+
}
|
|
189
|
+
|
|
184
190
|
// Auto-refresh workspace surfaces when app files are edited
|
|
185
191
|
if ((name === 'app_file_edit' || name === 'app_file_write') && !result.isError) {
|
|
186
192
|
const appId = input.app_id as string | undefined;
|
package/src/daemon/session.ts
CHANGED
|
@@ -1422,7 +1422,21 @@ export class Session {
|
|
|
1422
1422
|
// the agent loop started, even if post-processing threw.
|
|
1423
1423
|
if (turnStarted) {
|
|
1424
1424
|
this.turnCount++;
|
|
1425
|
-
|
|
1425
|
+
const config = getConfig();
|
|
1426
|
+
const maxWait = config.workspaceGit?.turnCommitMaxWaitMs ?? 4000;
|
|
1427
|
+
const deadlineMs = Date.now() + maxWait;
|
|
1428
|
+
const commitPromise = commitTurnChanges(
|
|
1429
|
+
this.workingDir, this.conversationId, this.turnCount,
|
|
1430
|
+
undefined, // use default commit message provider
|
|
1431
|
+
deadlineMs,
|
|
1432
|
+
);
|
|
1433
|
+
const outcome = await raceWithTimeout(commitPromise, maxWait);
|
|
1434
|
+
if (outcome === 'timed_out') {
|
|
1435
|
+
rlog.warn(
|
|
1436
|
+
{ turnNumber: this.turnCount, maxWaitMs: maxWait, conversationId: this.conversationId },
|
|
1437
|
+
'Turn-boundary commit timed out — continuing without waiting (commit still runs in background)',
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1426
1440
|
}
|
|
1427
1441
|
|
|
1428
1442
|
this.profiler.emitSummary(this.traceEmitter, reqId);
|
|
@@ -1649,3 +1663,28 @@ function isToolResultOnlyMessage(message: Message): boolean {
|
|
|
1649
1663
|
return message.content.length > 0
|
|
1650
1664
|
&& message.content.every((block) => block.type === 'tool_result');
|
|
1651
1665
|
}
|
|
1666
|
+
|
|
1667
|
+
/**
|
|
1668
|
+
* Race a promise against a timeout. Returns 'completed' if the promise
|
|
1669
|
+
* resolves/rejects within the budget, or 'timed_out' if the timeout fires
|
|
1670
|
+
* first. The timer is always cleared in `finally` to prevent handle leaks.
|
|
1671
|
+
*/
|
|
1672
|
+
async function raceWithTimeout<T>(
|
|
1673
|
+
promise: Promise<T>,
|
|
1674
|
+
timeoutMs: number,
|
|
1675
|
+
): Promise<'completed' | 'timed_out'> {
|
|
1676
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1677
|
+
try {
|
|
1678
|
+
const result = await Promise.race([
|
|
1679
|
+
promise.then(() => 'completed' as const),
|
|
1680
|
+
new Promise<'timed_out'>((resolve) => {
|
|
1681
|
+
timer = setTimeout(() => resolve('timed_out'), timeoutMs);
|
|
1682
|
+
}),
|
|
1683
|
+
]);
|
|
1684
|
+
return result;
|
|
1685
|
+
} finally {
|
|
1686
|
+
if (timer !== undefined) {
|
|
1687
|
+
clearTimeout(timer);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|