@vellumai/assistant 0.4.11 → 0.4.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +401 -385
- package/package.json +1 -1
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +75 -61
- package/src/__tests__/registry.test.ts +235 -187
- package/src/__tests__/secure-keys.test.ts +27 -0
- package/src/__tests__/session-agent-loop.test.ts +521 -256
- package/src/__tests__/session-surfaces-task-progress.test.ts +1 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/skills.test.ts +334 -276
- package/src/__tests__/slack-skill.test.ts +124 -0
- package/src/__tests__/starter-task-flow.test.ts +7 -17
- package/src/agent/loop.ts +10 -3
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +449 -0
- package/src/config/bundled-skills/doordash/SKILL.md +171 -0
- package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +203 -0
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +164 -0
- package/src/config/bundled-skills/doordash/doordash-cli.ts +1193 -0
- package/src/config/bundled-skills/doordash/doordash-entry.ts +22 -0
- package/src/config/bundled-skills/doordash/lib/cart-queries.ts +787 -0
- package/src/config/bundled-skills/doordash/lib/client.ts +1071 -0
- package/src/config/bundled-skills/doordash/lib/order-queries.ts +85 -0
- package/src/config/bundled-skills/doordash/lib/queries.ts +28 -0
- package/src/config/bundled-skills/doordash/lib/query-extractor.ts +94 -0
- package/src/config/bundled-skills/doordash/lib/search-queries.ts +203 -0
- package/src/config/bundled-skills/doordash/lib/session.ts +93 -0
- package/src/config/bundled-skills/doordash/lib/shared/errors.ts +61 -0
- package/src/config/bundled-skills/doordash/lib/shared/ipc.ts +32 -0
- package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +380 -0
- package/src/config/bundled-skills/doordash/lib/shared/platform.ts +35 -0
- package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +43 -0
- package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +49 -0
- package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +6 -0
- package/src/config/bundled-skills/doordash/lib/store-queries.ts +246 -0
- package/src/config/bundled-skills/doordash/lib/types.ts +367 -0
- package/src/config/bundled-skills/google-calendar/SKILL.md +4 -5
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +41 -41
- package/src/config/bundled-skills/messaging/SKILL.md +59 -42
- package/src/config/bundled-skills/messaging/TOOLS.json +14 -92
- package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +11 -2
- package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +8 -1
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +12 -4
- package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +5 -2
- package/src/config/bundled-skills/notion/SKILL.md +240 -0
- package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +127 -0
- package/src/config/bundled-skills/oauth-setup/SKILL.md +144 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +76 -45
- package/src/config/bundled-skills/skills-catalog/SKILL.md +32 -29
- package/src/config/bundled-skills/slack/SKILL.md +49 -0
- package/src/config/bundled-skills/slack/TOOLS.json +167 -0
- package/src/config/bundled-skills/slack/tools/shared.ts +23 -0
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-add-reaction.ts +2 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +33 -0
- package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +75 -0
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-delete-message.ts +2 -5
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-leave-channel.ts +2 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +193 -0
- package/src/config/{vellum-skills → bundled-skills}/sms-setup/SKILL.md +29 -22
- package/src/config/{vellum-skills → bundled-skills}/telegram-setup/SKILL.md +17 -14
- package/src/config/{vellum-skills → bundled-skills}/twilio-setup/SKILL.md +20 -5
- package/src/config/bundled-tool-registry.ts +292 -267
- package/src/config/schema.ts +1 -1
- package/src/daemon/handlers/skills.ts +334 -234
- package/src/daemon/ipc-contract/messages.ts +2 -0
- package/src/daemon/ipc-contract/surfaces.ts +2 -0
- package/src/daemon/lifecycle.ts +358 -221
- package/src/daemon/response-tier.ts +2 -0
- package/src/daemon/server.ts +453 -193
- package/src/daemon/session-agent-loop-handlers.ts +43 -2
- package/src/daemon/session-agent-loop.ts +3 -0
- package/src/daemon/session-lifecycle.ts +3 -0
- package/src/daemon/session-process.ts +1 -0
- package/src/daemon/session-surfaces.ts +22 -20
- package/src/daemon/session-tool-setup.ts +1 -0
- package/src/daemon/session.ts +5 -2
- package/src/messaging/outreach-classifier.ts +12 -5
- package/src/messaging/provider-types.ts +5 -0
- package/src/messaging/provider.ts +1 -1
- package/src/messaging/providers/gmail/adapter.ts +11 -5
- package/src/messaging/providers/gmail/client.ts +2 -0
- package/src/messaging/providers/slack/adapter.ts +1 -0
- package/src/messaging/providers/slack/client.ts +8 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/runtime/http-errors.ts +33 -20
- package/src/runtime/http-server.ts +706 -291
- package/src/runtime/http-types.ts +26 -16
- package/src/runtime/routes/secret-routes.ts +57 -2
- package/src/runtime/routes/surface-action-routes.ts +66 -0
- package/src/runtime/routes/trust-rules-routes.ts +140 -0
- package/src/security/keychain-to-encrypted-migration.ts +59 -0
- package/src/security/secure-keys.ts +17 -0
- package/src/skills/frontmatter.ts +9 -7
- package/src/tools/apps/executors.ts +2 -1
- package/src/tools/tool-manifest.ts +44 -42
- package/src/tools/types.ts +9 -0
- package/src/__tests__/skill-mirror-parity.test.ts +0 -176
- package/src/config/vellum-skills/catalog.json +0 -63
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +0 -295
- package/src/skills/vellum-catalog-remote.ts +0 -166
- package/src/tools/skills/vellum-catalog.ts +0 -168
- /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/TOOLS.json +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/deploy-fullstack-vercel/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/document-writer/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/guardian-verify-setup/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/slack-oauth-setup/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/trusted-contacts/SKILL.md +0 -0
package/src/daemon/server.ts
CHANGED
|
@@ -1,61 +1,91 @@
|
|
|
1
|
-
import { chmodSync, existsSync, readFileSync,statSync } from
|
|
2
|
-
import * as net from
|
|
3
|
-
import { join } from
|
|
4
|
-
import * as tls from
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { chmodSync, existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import * as net from "node:net";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import * as tls from "node:tls";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createAssistantMessage,
|
|
8
|
+
createUserMessage,
|
|
9
|
+
} from "../agent/message-types.js";
|
|
10
|
+
import {
|
|
11
|
+
type ChannelId,
|
|
12
|
+
type InterfaceId,
|
|
13
|
+
parseChannelId,
|
|
14
|
+
parseInterfaceId,
|
|
15
|
+
} from "../channels/types.js";
|
|
16
|
+
import { getConfig } from "../config/loader.js";
|
|
17
|
+
import { buildSystemPrompt } from "../config/system-prompt.js";
|
|
18
|
+
import type { HeartbeatService } from "../heartbeat/heartbeat-service.js";
|
|
19
|
+
import { bootstrapHomeBaseAppLink } from "../home-base/bootstrap.js";
|
|
20
|
+
import * as attachmentsStore from "../memory/attachments-store.js";
|
|
13
21
|
import {
|
|
14
22
|
createCanonicalGuardianRequest,
|
|
15
23
|
generateCanonicalRequestCode,
|
|
16
|
-
} from
|
|
17
|
-
import * as conversationStore from
|
|
18
|
-
import { provenanceFromGuardianContext } from
|
|
19
|
-
import { RateLimitProvider } from
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
} from "../memory/canonical-guardian-store.js";
|
|
25
|
+
import * as conversationStore from "../memory/conversation-store.js";
|
|
26
|
+
import { provenanceFromGuardianContext } from "../memory/conversation-store.js";
|
|
27
|
+
import { RateLimitProvider } from "../providers/ratelimit.js";
|
|
28
|
+
import {
|
|
29
|
+
getFailoverProvider,
|
|
30
|
+
initializeProviders,
|
|
31
|
+
} from "../providers/registry.js";
|
|
32
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
|
|
33
|
+
import { bridgeConfirmationRequestToGuardian } from "../runtime/confirmation-request-guardian-bridge.js";
|
|
34
|
+
import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
35
|
+
import { checkIngressForSecrets } from "../security/secret-ingress.js";
|
|
36
|
+
import { getSubagentManager } from "../subagent/index.js";
|
|
37
|
+
import { IngressBlockedError } from "../util/errors.js";
|
|
38
|
+
import { getLogger } from "../util/logger.js";
|
|
39
|
+
import { getLocalIPv4 } from "../util/network-info.js";
|
|
40
|
+
import {
|
|
41
|
+
getSandboxWorkingDir,
|
|
42
|
+
getSocketPath,
|
|
43
|
+
getTCPHost,
|
|
44
|
+
getTCPPort,
|
|
45
|
+
getWorkspacePromptPath,
|
|
46
|
+
isIOSPairingEnabled,
|
|
47
|
+
isTCPEnabled,
|
|
48
|
+
removeSocketFile,
|
|
49
|
+
} from "../util/platform.js";
|
|
50
|
+
import { registerDaemonCallbacks } from "../work-items/work-item-runner.js";
|
|
51
|
+
import { AuthManager } from "./auth-manager.js";
|
|
52
|
+
import { ComputerUseSession } from "./computer-use-session.js";
|
|
53
|
+
import { ConfigWatcher } from "./config-watcher.js";
|
|
54
|
+
import {
|
|
55
|
+
handleMessage,
|
|
56
|
+
type HandlerContext,
|
|
57
|
+
type SessionCreateOptions,
|
|
58
|
+
} from "./handlers.js";
|
|
59
|
+
import { parseIdentityFields } from "./handlers/identity.js";
|
|
60
|
+
import { cleanupRecordingsOnDisconnect } from "./handlers/recording.js";
|
|
61
|
+
import { ensureBlobDir, sweepStaleBlobs } from "./ipc-blob-store.js";
|
|
62
|
+
import { IpcSender } from "./ipc-handler.js";
|
|
39
63
|
import {
|
|
40
64
|
createMessageParser,
|
|
41
65
|
MAX_LINE_SIZE,
|
|
42
66
|
normalizeThreadType,
|
|
43
67
|
serialize,
|
|
44
68
|
type ServerMessage,
|
|
45
|
-
} from
|
|
46
|
-
import { validateClientMessage } from
|
|
47
|
-
import {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
} from "./ipc-protocol.js";
|
|
70
|
+
import { validateClientMessage } from "./ipc-validate.js";
|
|
71
|
+
import {
|
|
72
|
+
DEFAULT_MEMORY_POLICY,
|
|
73
|
+
Session,
|
|
74
|
+
type SessionMemoryPolicy,
|
|
75
|
+
} from "./session.js";
|
|
76
|
+
import { SessionEvictor } from "./session-evictor.js";
|
|
77
|
+
import { resolveChannelCapabilities } from "./session-runtime-assembly.js";
|
|
78
|
+
import { resolveSlash } from "./session-slash.js";
|
|
79
|
+
import { ensureTlsCert } from "./tls-certs.js";
|
|
52
80
|
|
|
53
|
-
const log = getLogger(
|
|
81
|
+
const log = getLogger("server");
|
|
54
82
|
|
|
55
83
|
function readPackageVersion(): string | undefined {
|
|
56
84
|
try {
|
|
57
|
-
const pkgPath = join(import.meta.dir,
|
|
58
|
-
const pkg = JSON.parse(readFileSync(pkgPath,
|
|
85
|
+
const pkgPath = join(import.meta.dir, "../../package.json");
|
|
86
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as {
|
|
87
|
+
version?: string;
|
|
88
|
+
};
|
|
59
89
|
return pkg.version;
|
|
60
90
|
} catch {
|
|
61
91
|
return undefined;
|
|
@@ -64,7 +94,10 @@ function readPackageVersion(): string | undefined {
|
|
|
64
94
|
|
|
65
95
|
const daemonVersion = readPackageVersion();
|
|
66
96
|
|
|
67
|
-
function resolveTurnChannel(
|
|
97
|
+
function resolveTurnChannel(
|
|
98
|
+
sourceChannel?: string,
|
|
99
|
+
transportChannelId?: string,
|
|
100
|
+
): ChannelId {
|
|
68
101
|
if (sourceChannel != null) {
|
|
69
102
|
const parsed = parseChannelId(sourceChannel);
|
|
70
103
|
if (!parsed) {
|
|
@@ -79,7 +112,7 @@ function resolveTurnChannel(sourceChannel?: string, transportChannelId?: string)
|
|
|
79
112
|
}
|
|
80
113
|
return parsed;
|
|
81
114
|
}
|
|
82
|
-
return
|
|
115
|
+
return "vellum";
|
|
83
116
|
}
|
|
84
117
|
|
|
85
118
|
function resolveTurnInterface(sourceInterface?: string): InterfaceId {
|
|
@@ -92,17 +125,19 @@ function resolveTurnInterface(sourceInterface?: string): InterfaceId {
|
|
|
92
125
|
}
|
|
93
126
|
// Interface and channel are orthogonal dimensions; default explicitly
|
|
94
127
|
// instead of deriving interface from channel.
|
|
95
|
-
return
|
|
128
|
+
return "vellum";
|
|
96
129
|
}
|
|
97
130
|
|
|
98
|
-
function resolveCanonicalRequestSourceType(
|
|
99
|
-
|
|
100
|
-
|
|
131
|
+
function resolveCanonicalRequestSourceType(
|
|
132
|
+
sourceChannel: string | undefined,
|
|
133
|
+
): "desktop" | "channel" | "voice" {
|
|
134
|
+
if (sourceChannel === "voice") {
|
|
135
|
+
return "voice";
|
|
101
136
|
}
|
|
102
|
-
if (sourceChannel ===
|
|
103
|
-
return
|
|
137
|
+
if (sourceChannel === "vellum") {
|
|
138
|
+
return "desktop";
|
|
104
139
|
}
|
|
105
|
-
return
|
|
140
|
+
return "channel";
|
|
106
141
|
}
|
|
107
142
|
|
|
108
143
|
/**
|
|
@@ -115,11 +150,11 @@ function makePendingInteractionRegistrar(
|
|
|
115
150
|
conversationId: string,
|
|
116
151
|
): (msg: ServerMessage) => void {
|
|
117
152
|
return (msg: ServerMessage) => {
|
|
118
|
-
if (msg.type ===
|
|
153
|
+
if (msg.type === "confirmation_request") {
|
|
119
154
|
pendingInteractions.register(msg.requestId, {
|
|
120
155
|
session,
|
|
121
156
|
conversationId,
|
|
122
|
-
kind:
|
|
157
|
+
kind: "confirmation",
|
|
123
158
|
confirmationDetails: {
|
|
124
159
|
toolName: msg.toolName,
|
|
125
160
|
input: msg.input,
|
|
@@ -135,19 +170,20 @@ function makePendingInteractionRegistrar(
|
|
|
135
170
|
// via applyCanonicalGuardianDecision.
|
|
136
171
|
try {
|
|
137
172
|
const guardianContext = session.guardianContext;
|
|
138
|
-
const sourceChannel = guardianContext?.sourceChannel ??
|
|
173
|
+
const sourceChannel = guardianContext?.sourceChannel ?? "vellum";
|
|
139
174
|
const canonicalRequest = createCanonicalGuardianRequest({
|
|
140
175
|
id: msg.requestId,
|
|
141
|
-
kind:
|
|
176
|
+
kind: "tool_approval",
|
|
142
177
|
sourceType: resolveCanonicalRequestSourceType(sourceChannel),
|
|
143
178
|
sourceChannel,
|
|
144
179
|
conversationId,
|
|
145
180
|
requesterExternalUserId: guardianContext?.requesterExternalUserId,
|
|
146
181
|
requesterChatId: guardianContext?.requesterChatId,
|
|
147
182
|
guardianExternalUserId: guardianContext?.guardianExternalUserId,
|
|
148
|
-
guardianPrincipalId:
|
|
183
|
+
guardianPrincipalId:
|
|
184
|
+
guardianContext?.guardianPrincipalId ?? undefined,
|
|
149
185
|
toolName: msg.toolName,
|
|
150
|
-
status:
|
|
186
|
+
status: "pending",
|
|
151
187
|
requestCode: generateCanonicalRequestCode(),
|
|
152
188
|
expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
|
|
153
189
|
});
|
|
@@ -166,14 +202,14 @@ function makePendingInteractionRegistrar(
|
|
|
166
202
|
} catch (err) {
|
|
167
203
|
log.debug(
|
|
168
204
|
{ err, requestId: msg.requestId, conversationId },
|
|
169
|
-
|
|
205
|
+
"Failed to create canonical request from pending interaction registrar",
|
|
170
206
|
);
|
|
171
207
|
}
|
|
172
|
-
} else if (msg.type ===
|
|
208
|
+
} else if (msg.type === "secret_request") {
|
|
173
209
|
pendingInteractions.register(msg.requestId, {
|
|
174
210
|
session,
|
|
175
211
|
conversationId,
|
|
176
|
-
kind:
|
|
212
|
+
kind: "secret",
|
|
177
213
|
});
|
|
178
214
|
}
|
|
179
215
|
};
|
|
@@ -206,7 +242,7 @@ export class DaemonServer {
|
|
|
206
242
|
/**
|
|
207
243
|
* Logical assistant identifier used when publishing to the assistant-events hub.
|
|
208
244
|
*/
|
|
209
|
-
assistantId: string =
|
|
245
|
+
assistantId: string = "default";
|
|
210
246
|
|
|
211
247
|
/** Optional heartbeat service reference for "Run Now" from the UI. */
|
|
212
248
|
private _heartbeatService?: HeartbeatService;
|
|
@@ -216,8 +252,9 @@ export class DaemonServer {
|
|
|
216
252
|
}
|
|
217
253
|
|
|
218
254
|
private deriveMemoryPolicy(conversationId: string): SessionMemoryPolicy {
|
|
219
|
-
const threadType =
|
|
220
|
-
|
|
255
|
+
const threadType =
|
|
256
|
+
conversationStore.getConversationThreadType(conversationId);
|
|
257
|
+
if (threadType === "private") {
|
|
221
258
|
return {
|
|
222
259
|
scopeId: conversationStore.getConversationMemoryScopeId(conversationId),
|
|
223
260
|
includeDefaultFallback: true,
|
|
@@ -227,10 +264,16 @@ export class DaemonServer {
|
|
|
227
264
|
return DEFAULT_MEMORY_POLICY;
|
|
228
265
|
}
|
|
229
266
|
|
|
230
|
-
private applyTransportMetadata(
|
|
267
|
+
private applyTransportMetadata(
|
|
268
|
+
_session: Session,
|
|
269
|
+
options: SessionCreateOptions | undefined,
|
|
270
|
+
): void {
|
|
231
271
|
const transport = options?.transport;
|
|
232
272
|
if (!transport) return;
|
|
233
|
-
log.debug(
|
|
273
|
+
log.debug(
|
|
274
|
+
{ channelId: transport.channelId },
|
|
275
|
+
"Transport metadata received",
|
|
276
|
+
);
|
|
234
277
|
}
|
|
235
278
|
|
|
236
279
|
constructor() {
|
|
@@ -242,26 +285,57 @@ export class DaemonServer {
|
|
|
242
285
|
};
|
|
243
286
|
this.evictor.shouldProtect = (sessionId: string) => {
|
|
244
287
|
const children = getSubagentManager().getChildrenOf(sessionId);
|
|
245
|
-
return children.some(
|
|
288
|
+
return children.some(
|
|
289
|
+
(c) => c.status === "running" || c.status === "pending",
|
|
290
|
+
);
|
|
246
291
|
};
|
|
247
|
-
getSubagentManager().onSubagentFinished = async (
|
|
292
|
+
getSubagentManager().onSubagentFinished = async (
|
|
293
|
+
parentSessionId,
|
|
294
|
+
message,
|
|
295
|
+
sendToClient,
|
|
296
|
+
notification,
|
|
297
|
+
) => {
|
|
248
298
|
const parentSession = this.sessions.get(parentSessionId);
|
|
249
299
|
if (!parentSession) {
|
|
250
|
-
log.warn(
|
|
300
|
+
log.warn(
|
|
301
|
+
{ parentSessionId },
|
|
302
|
+
"Subagent finished but parent session not found",
|
|
303
|
+
);
|
|
251
304
|
return;
|
|
252
305
|
}
|
|
253
306
|
const requestId = `subagent-notify-${Date.now()}`;
|
|
254
307
|
const metadata = { subagentNotification: notification };
|
|
255
|
-
const enqueueResult = parentSession.enqueueMessage(
|
|
308
|
+
const enqueueResult = parentSession.enqueueMessage(
|
|
309
|
+
message,
|
|
310
|
+
[],
|
|
311
|
+
sendToClient,
|
|
312
|
+
requestId,
|
|
313
|
+
undefined,
|
|
314
|
+
undefined,
|
|
315
|
+
metadata,
|
|
316
|
+
);
|
|
256
317
|
if (enqueueResult.rejected) {
|
|
257
|
-
log.warn(
|
|
318
|
+
log.warn(
|
|
319
|
+
{ parentSessionId },
|
|
320
|
+
"Parent session queue full, dropping subagent notification",
|
|
321
|
+
);
|
|
258
322
|
return;
|
|
259
323
|
}
|
|
260
324
|
if (!enqueueResult.queued) {
|
|
261
|
-
const messageId = await parentSession.persistUserMessage(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
325
|
+
const messageId = await parentSession.persistUserMessage(
|
|
326
|
+
message,
|
|
327
|
+
[],
|
|
328
|
+
undefined,
|
|
329
|
+
metadata,
|
|
330
|
+
);
|
|
331
|
+
parentSession
|
|
332
|
+
.runAgentLoop(message, messageId, sendToClient)
|
|
333
|
+
.catch((err) => {
|
|
334
|
+
log.error(
|
|
335
|
+
{ parentSessionId, err },
|
|
336
|
+
"Failed to process subagent notification in parent",
|
|
337
|
+
);
|
|
338
|
+
});
|
|
265
339
|
}
|
|
266
340
|
};
|
|
267
341
|
}
|
|
@@ -284,11 +358,13 @@ export class DaemonServer {
|
|
|
284
358
|
|
|
285
359
|
private broadcastIdentityChanged(): void {
|
|
286
360
|
try {
|
|
287
|
-
const identityPath = getWorkspacePromptPath(
|
|
288
|
-
const content = existsSync(identityPath)
|
|
361
|
+
const identityPath = getWorkspacePromptPath("IDENTITY.md");
|
|
362
|
+
const content = existsSync(identityPath)
|
|
363
|
+
? readFileSync(identityPath, "utf-8")
|
|
364
|
+
: "";
|
|
289
365
|
const fields = parseIdentityFields(content);
|
|
290
366
|
this.broadcast({
|
|
291
|
-
type:
|
|
367
|
+
type: "identity_changed",
|
|
292
368
|
name: fields.name,
|
|
293
369
|
role: fields.role,
|
|
294
370
|
personality: fields.personality,
|
|
@@ -296,7 +372,7 @@ export class DaemonServer {
|
|
|
296
372
|
home: fields.home,
|
|
297
373
|
});
|
|
298
374
|
} catch (err) {
|
|
299
|
-
log.error({ err },
|
|
375
|
+
log.error({ err }, "Failed to broadcast identity change");
|
|
300
376
|
}
|
|
301
377
|
}
|
|
302
378
|
|
|
@@ -312,22 +388,29 @@ export class DaemonServer {
|
|
|
312
388
|
try {
|
|
313
389
|
bootstrapHomeBaseAppLink();
|
|
314
390
|
} catch (err) {
|
|
315
|
-
log.warn(
|
|
391
|
+
log.warn(
|
|
392
|
+
{ err },
|
|
393
|
+
"Failed to bootstrap Home Base app link at daemon startup",
|
|
394
|
+
);
|
|
316
395
|
}
|
|
317
396
|
|
|
318
397
|
this.evictor.start();
|
|
319
398
|
|
|
320
399
|
registerDaemonCallbacks({
|
|
321
|
-
getOrCreateSession: (conversationId) =>
|
|
400
|
+
getOrCreateSession: (conversationId) =>
|
|
401
|
+
this.getOrCreateSession(conversationId),
|
|
322
402
|
broadcast: (msg) => this.broadcast(msg),
|
|
323
403
|
});
|
|
324
404
|
|
|
325
405
|
ensureBlobDir();
|
|
326
|
-
this.blobSweepTimer = setInterval(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
406
|
+
this.blobSweepTimer = setInterval(
|
|
407
|
+
() => {
|
|
408
|
+
sweepStaleBlobs(30 * 60 * 1000).catch((err) => {
|
|
409
|
+
log.warn({ err }, "Blob sweep failed");
|
|
410
|
+
});
|
|
411
|
+
},
|
|
412
|
+
5 * 60 * 1000,
|
|
413
|
+
);
|
|
331
414
|
|
|
332
415
|
this.configWatcher.start(
|
|
333
416
|
() => this.evictSessionsForReload(),
|
|
@@ -335,12 +418,16 @@ export class DaemonServer {
|
|
|
335
418
|
);
|
|
336
419
|
this.auth.initToken();
|
|
337
420
|
|
|
338
|
-
let tlsCreds: { cert: string; key: string; fingerprint: string } | null =
|
|
421
|
+
let tlsCreds: { cert: string; key: string; fingerprint: string } | null =
|
|
422
|
+
null;
|
|
339
423
|
if (isTCPEnabled()) {
|
|
340
424
|
try {
|
|
341
425
|
tlsCreds = await ensureTlsCert();
|
|
342
426
|
} catch (err) {
|
|
343
|
-
log.error(
|
|
427
|
+
log.error(
|
|
428
|
+
{ err },
|
|
429
|
+
"Failed to generate TLS certificate — TCP listener will not start",
|
|
430
|
+
);
|
|
344
431
|
}
|
|
345
432
|
}
|
|
346
433
|
|
|
@@ -351,53 +438,68 @@ export class DaemonServer {
|
|
|
351
438
|
|
|
352
439
|
const oldUmask = process.umask(0o177);
|
|
353
440
|
|
|
354
|
-
this.server.once(
|
|
441
|
+
this.server.once("error", (err) => {
|
|
355
442
|
process.umask(oldUmask);
|
|
356
|
-
log.error(
|
|
443
|
+
log.error(
|
|
444
|
+
{ err, socketPath: this.socketPath },
|
|
445
|
+
"Server failed to start (is another daemon already running?)",
|
|
446
|
+
);
|
|
357
447
|
reject(err);
|
|
358
448
|
});
|
|
359
449
|
|
|
360
450
|
this.server.listen(this.socketPath, () => {
|
|
361
451
|
process.umask(oldUmask);
|
|
362
|
-
this.server!.removeAllListeners(
|
|
363
|
-
this.server!.on(
|
|
364
|
-
log.error(
|
|
452
|
+
this.server!.removeAllListeners("error");
|
|
453
|
+
this.server!.on("error", (err) => {
|
|
454
|
+
log.error(
|
|
455
|
+
{ err, socketPath: this.socketPath },
|
|
456
|
+
"Server socket error while running",
|
|
457
|
+
);
|
|
365
458
|
});
|
|
366
459
|
chmodSync(this.socketPath, 0o600);
|
|
367
460
|
// Validate the chmod actually took effect — some filesystems
|
|
368
461
|
// (e.g. FAT32 mounts, container overlays) silently ignore chmod.
|
|
369
462
|
const socketStat = statSync(this.socketPath);
|
|
370
463
|
if ((socketStat.mode & 0o077) !== 0) {
|
|
371
|
-
const actual =
|
|
464
|
+
const actual = "0o" + (socketStat.mode & 0o777).toString(8);
|
|
372
465
|
log.error(
|
|
373
466
|
{ socketPath: this.socketPath, mode: actual },
|
|
374
|
-
|
|
467
|
+
"IPC socket is accessible by other users (expected 0600) — filesystem may not support Unix permissions",
|
|
375
468
|
);
|
|
376
469
|
}
|
|
377
|
-
log.info({ socketPath: this.socketPath },
|
|
470
|
+
log.info({ socketPath: this.socketPath }, "Daemon server listening");
|
|
378
471
|
|
|
379
472
|
if (tlsCreds) {
|
|
380
473
|
const tcpPort = getTCPPort();
|
|
381
474
|
const tcpHost = getTCPHost();
|
|
382
475
|
this.tcpServer = tls.createServer(
|
|
383
476
|
{ cert: tlsCreds.cert, key: tlsCreds.key },
|
|
384
|
-
(socket) => {
|
|
477
|
+
(socket) => {
|
|
478
|
+
this.handleConnection(socket);
|
|
479
|
+
},
|
|
385
480
|
);
|
|
386
|
-
this.tcpServer.on(
|
|
387
|
-
log.error({ err, tcpPort },
|
|
481
|
+
this.tcpServer.on("error", (err) => {
|
|
482
|
+
log.error({ err, tcpPort }, "TLS TCP server error");
|
|
388
483
|
});
|
|
389
484
|
const fingerprint = tlsCreds.fingerprint;
|
|
390
485
|
this.tcpServer.listen(tcpPort, tcpHost, () => {
|
|
391
486
|
const localIP = getLocalIPv4();
|
|
392
487
|
log.info(
|
|
393
|
-
{
|
|
394
|
-
|
|
488
|
+
{
|
|
489
|
+
tcpPort,
|
|
490
|
+
tcpHost,
|
|
491
|
+
fingerprint,
|
|
492
|
+
localIP,
|
|
493
|
+
iosPairing: isIOSPairingEnabled(),
|
|
494
|
+
},
|
|
495
|
+
"TLS TCP listener started",
|
|
395
496
|
);
|
|
396
497
|
if (isIOSPairingEnabled() && localIP) {
|
|
397
498
|
log.warn(
|
|
398
499
|
{ localIP, tcpPort },
|
|
399
|
-
|
|
400
|
-
localIP,
|
|
500
|
+
"iOS pairing enabled — daemon is reachable on the local network at %s:%d",
|
|
501
|
+
localIP,
|
|
502
|
+
tcpPort,
|
|
401
503
|
);
|
|
402
504
|
}
|
|
403
505
|
});
|
|
@@ -424,7 +526,10 @@ export class DaemonServer {
|
|
|
424
526
|
try {
|
|
425
527
|
removeSocketFile(this.socketPath);
|
|
426
528
|
} catch (err) {
|
|
427
|
-
log.warn(
|
|
529
|
+
log.warn(
|
|
530
|
+
{ err, socketPath: this.socketPath },
|
|
531
|
+
"Failed to remove socket file during shutdown",
|
|
532
|
+
);
|
|
428
533
|
}
|
|
429
534
|
resolve();
|
|
430
535
|
});
|
|
@@ -462,75 +567,117 @@ export class DaemonServer {
|
|
|
462
567
|
this.cuObservationParseSequence.clear();
|
|
463
568
|
|
|
464
569
|
await Promise.all([serverClosed, tcpServerClosed]);
|
|
465
|
-
log.info(
|
|
570
|
+
log.info("Daemon server stopped");
|
|
466
571
|
}
|
|
467
572
|
|
|
468
573
|
// ── Connection handling ─────────────────────────────────────────────
|
|
469
574
|
|
|
470
575
|
private handleConnection(socket: net.Socket): void {
|
|
471
576
|
if (this.connectedSockets.size >= DaemonServer.MAX_CONNECTIONS) {
|
|
472
|
-
log.warn(
|
|
473
|
-
|
|
474
|
-
|
|
577
|
+
log.warn(
|
|
578
|
+
{
|
|
579
|
+
current: this.connectedSockets.size,
|
|
580
|
+
max: DaemonServer.MAX_CONNECTIONS,
|
|
581
|
+
},
|
|
582
|
+
"Connection limit reached, rejecting client",
|
|
583
|
+
);
|
|
584
|
+
socket.once("error", (err) => {
|
|
585
|
+
log.error({ err }, "Socket error while rejecting connection");
|
|
475
586
|
});
|
|
476
|
-
socket.write(
|
|
587
|
+
socket.write(
|
|
588
|
+
serialize({
|
|
589
|
+
type: "error",
|
|
590
|
+
message: `Connection limit reached (max ${DaemonServer.MAX_CONNECTIONS})`,
|
|
591
|
+
}),
|
|
592
|
+
);
|
|
477
593
|
socket.destroy();
|
|
478
594
|
return;
|
|
479
595
|
}
|
|
480
596
|
|
|
481
|
-
log.info(
|
|
597
|
+
log.info("Client connected");
|
|
482
598
|
this.connectedSockets.add(socket);
|
|
483
599
|
const parser = createMessageParser({ maxLineSize: MAX_LINE_SIZE });
|
|
484
600
|
|
|
485
601
|
if (this.auth.shouldAutoAuth()) {
|
|
486
602
|
this.auth.markAuthenticated(socket);
|
|
487
|
-
log.warn(
|
|
488
|
-
|
|
603
|
+
log.warn(
|
|
604
|
+
"Auto-authenticated client (VELLUM_DAEMON_NOAUTH is set — token auth bypassed)",
|
|
605
|
+
);
|
|
606
|
+
this.send(socket, { type: "auth_result", success: true });
|
|
489
607
|
this.sendInitialSession(socket).catch((err) => {
|
|
490
|
-
log.error(
|
|
608
|
+
log.error(
|
|
609
|
+
{ err },
|
|
610
|
+
"Failed to send initial session info after auto-auth",
|
|
611
|
+
);
|
|
491
612
|
});
|
|
492
613
|
}
|
|
493
614
|
|
|
494
615
|
this.auth.startTimeout(socket, () => {
|
|
495
|
-
this.send(socket, { type:
|
|
616
|
+
this.send(socket, { type: "error", message: "Authentication timeout" });
|
|
496
617
|
socket.destroy();
|
|
497
618
|
});
|
|
498
619
|
|
|
499
|
-
socket.on(
|
|
620
|
+
socket.on("data", (data) => {
|
|
500
621
|
const chunkReceivedAtMs = Date.now();
|
|
501
622
|
const parseStartNs = process.hrtime.bigint();
|
|
502
623
|
let parsed;
|
|
503
624
|
try {
|
|
504
625
|
parsed = parser.feedRaw(data.toString());
|
|
505
626
|
} catch (err) {
|
|
506
|
-
log.error(
|
|
507
|
-
|
|
627
|
+
log.error(
|
|
628
|
+
{ err },
|
|
629
|
+
"IPC parse error (malformed JSON or message exceeded size limit), dropping client",
|
|
630
|
+
);
|
|
631
|
+
socket.write(
|
|
632
|
+
serialize({
|
|
633
|
+
type: "error",
|
|
634
|
+
message: `IPC parse error: ${(err as Error).message}`,
|
|
635
|
+
}),
|
|
636
|
+
);
|
|
508
637
|
socket.destroy();
|
|
509
638
|
return;
|
|
510
639
|
}
|
|
511
640
|
const parsedAtMs = Date.now();
|
|
512
|
-
const parseDurationMs =
|
|
641
|
+
const parseDurationMs =
|
|
642
|
+
Number(process.hrtime.bigint() - parseStartNs) / 1_000_000;
|
|
513
643
|
for (const entry of parsed) {
|
|
514
644
|
const msg = entry.msg;
|
|
515
|
-
if (
|
|
645
|
+
if (
|
|
646
|
+
typeof msg === "object" &&
|
|
647
|
+
msg != null &&
|
|
648
|
+
(msg as { type?: unknown }).type === "cu_observation"
|
|
649
|
+
) {
|
|
516
650
|
const maybeSessionId = (msg as { sessionId?: unknown }).sessionId;
|
|
517
|
-
const sessionId =
|
|
518
|
-
|
|
651
|
+
const sessionId =
|
|
652
|
+
typeof maybeSessionId === "string" ? maybeSessionId : "unknown";
|
|
653
|
+
const previousSequence =
|
|
654
|
+
this.cuObservationParseSequence.get(sessionId) ?? 0;
|
|
519
655
|
const sequence = previousSequence + 1;
|
|
520
656
|
this.cuObservationParseSequence.set(sessionId, sequence);
|
|
521
|
-
log.info(
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
657
|
+
log.info(
|
|
658
|
+
{
|
|
659
|
+
sessionId,
|
|
660
|
+
sequence,
|
|
661
|
+
chunkReceivedAtMs,
|
|
662
|
+
parsedAtMs,
|
|
663
|
+
parseDurationMs,
|
|
664
|
+
messageBytes: entry.rawByteLength,
|
|
665
|
+
},
|
|
666
|
+
"IPC_METRIC cu_observation_parse",
|
|
667
|
+
);
|
|
529
668
|
}
|
|
530
669
|
const result = validateClientMessage(msg);
|
|
531
670
|
if (!result.valid) {
|
|
532
|
-
log.warn(
|
|
533
|
-
|
|
671
|
+
log.warn(
|
|
672
|
+
{ reason: result.reason },
|
|
673
|
+
"Invalid IPC message, dropping client",
|
|
674
|
+
);
|
|
675
|
+
socket.write(
|
|
676
|
+
serialize({
|
|
677
|
+
type: "error",
|
|
678
|
+
message: `Invalid message: ${result.reason}`,
|
|
679
|
+
}),
|
|
680
|
+
);
|
|
534
681
|
socket.destroy();
|
|
535
682
|
return;
|
|
536
683
|
}
|
|
@@ -539,29 +686,42 @@ export class DaemonServer {
|
|
|
539
686
|
if (!this.auth.isAuthenticated(socket)) {
|
|
540
687
|
this.auth.clearTimeout(socket);
|
|
541
688
|
|
|
542
|
-
if (result.message.type ===
|
|
543
|
-
const authMsg = result.message as { type:
|
|
689
|
+
if (result.message.type === "auth") {
|
|
690
|
+
const authMsg = result.message as { type: "auth"; token: string };
|
|
544
691
|
if (this.auth.authenticate(socket, authMsg.token)) {
|
|
545
|
-
this.send(socket, { type:
|
|
692
|
+
this.send(socket, { type: "auth_result", success: true });
|
|
546
693
|
this.sendInitialSession(socket).catch((err) => {
|
|
547
|
-
log.error(
|
|
694
|
+
log.error(
|
|
695
|
+
{ err },
|
|
696
|
+
"Failed to send initial session info after auth",
|
|
697
|
+
);
|
|
548
698
|
});
|
|
549
699
|
} else {
|
|
550
|
-
this.send(socket, {
|
|
700
|
+
this.send(socket, {
|
|
701
|
+
type: "auth_result",
|
|
702
|
+
success: false,
|
|
703
|
+
message: "Invalid token",
|
|
704
|
+
});
|
|
551
705
|
socket.destroy();
|
|
552
706
|
}
|
|
553
707
|
continue;
|
|
554
708
|
}
|
|
555
709
|
|
|
556
|
-
log.warn(
|
|
557
|
-
|
|
710
|
+
log.warn(
|
|
711
|
+
{ type: result.message.type },
|
|
712
|
+
"Unauthenticated client sent non-auth message, disconnecting",
|
|
713
|
+
);
|
|
714
|
+
this.send(socket, {
|
|
715
|
+
type: "error",
|
|
716
|
+
message: "Authentication required",
|
|
717
|
+
});
|
|
558
718
|
socket.destroy();
|
|
559
719
|
return;
|
|
560
720
|
}
|
|
561
721
|
|
|
562
722
|
// Already-authenticated socket sending auth (e.g. auto-auth'd + local token)
|
|
563
|
-
if (result.message.type ===
|
|
564
|
-
this.send(socket, { type:
|
|
723
|
+
if (result.message.type === "auth") {
|
|
724
|
+
this.send(socket, { type: "auth_result", success: true });
|
|
565
725
|
continue;
|
|
566
726
|
}
|
|
567
727
|
|
|
@@ -569,7 +729,7 @@ export class DaemonServer {
|
|
|
569
729
|
}
|
|
570
730
|
});
|
|
571
731
|
|
|
572
|
-
socket.on(
|
|
732
|
+
socket.on("close", () => {
|
|
573
733
|
this.auth.cleanupSocket(socket);
|
|
574
734
|
this.connectedSockets.delete(socket);
|
|
575
735
|
this.socketSandboxOverride.delete(socket);
|
|
@@ -604,11 +764,14 @@ export class DaemonServer {
|
|
|
604
764
|
}
|
|
605
765
|
}
|
|
606
766
|
this.socketToCuSession.delete(socket);
|
|
607
|
-
log.info(
|
|
767
|
+
log.info("Client disconnected");
|
|
608
768
|
});
|
|
609
769
|
|
|
610
|
-
socket.on(
|
|
611
|
-
log.error(
|
|
770
|
+
socket.on("error", (err) => {
|
|
771
|
+
log.error(
|
|
772
|
+
{ err, remoteAddress: socket.remoteAddress },
|
|
773
|
+
"Client socket error",
|
|
774
|
+
);
|
|
612
775
|
});
|
|
613
776
|
}
|
|
614
777
|
|
|
@@ -617,7 +780,7 @@ export class DaemonServer {
|
|
|
617
780
|
setHttpPort(port: number): void {
|
|
618
781
|
this.httpPort = port;
|
|
619
782
|
this.broadcast({
|
|
620
|
-
type:
|
|
783
|
+
type: "daemon_status",
|
|
621
784
|
httpPort: port,
|
|
622
785
|
version: daemonVersion,
|
|
623
786
|
});
|
|
@@ -670,7 +833,7 @@ export class DaemonServer {
|
|
|
670
833
|
const conversation = conversationStore.getLatestConversation();
|
|
671
834
|
if (!conversation) {
|
|
672
835
|
this.send(socket, {
|
|
673
|
-
type:
|
|
836
|
+
type: "daemon_status",
|
|
674
837
|
httpPort: this.httpPort,
|
|
675
838
|
version: daemonVersion,
|
|
676
839
|
});
|
|
@@ -680,14 +843,14 @@ export class DaemonServer {
|
|
|
680
843
|
await this.getOrCreateSession(conversation.id, undefined, false);
|
|
681
844
|
|
|
682
845
|
this.send(socket, {
|
|
683
|
-
type:
|
|
846
|
+
type: "session_info",
|
|
684
847
|
sessionId: conversation.id,
|
|
685
|
-
title: conversation.title ??
|
|
848
|
+
title: conversation.title ?? "New Conversation",
|
|
686
849
|
threadType: normalizeThreadType(conversation.threadType),
|
|
687
850
|
});
|
|
688
851
|
|
|
689
852
|
this.send(socket, {
|
|
690
|
-
type:
|
|
853
|
+
type: "daemon_status",
|
|
691
854
|
httpPort: this.httpPort,
|
|
692
855
|
version: daemonVersion,
|
|
693
856
|
});
|
|
@@ -710,7 +873,7 @@ export class DaemonServer {
|
|
|
710
873
|
getSubagentManager().updateParentSender(conversationId, sendToClient);
|
|
711
874
|
};
|
|
712
875
|
|
|
713
|
-
if (options && Object.values(options).some(v => v !== undefined)) {
|
|
876
|
+
if (options && Object.values(options).some((v) => v !== undefined)) {
|
|
714
877
|
this.sessionOptions.set(conversationId, {
|
|
715
878
|
...this.sessionOptions.get(conversationId),
|
|
716
879
|
...options,
|
|
@@ -734,14 +897,25 @@ export class DaemonServer {
|
|
|
734
897
|
|
|
735
898
|
const createPromise = (async () => {
|
|
736
899
|
const config = getConfig();
|
|
737
|
-
let provider = getFailoverProvider(
|
|
900
|
+
let provider = getFailoverProvider(
|
|
901
|
+
config.provider,
|
|
902
|
+
config.providerOrder,
|
|
903
|
+
);
|
|
738
904
|
const { rateLimit } = config;
|
|
739
|
-
if (
|
|
740
|
-
|
|
905
|
+
if (
|
|
906
|
+
rateLimit.maxRequestsPerMinute > 0 ||
|
|
907
|
+
rateLimit.maxTokensPerSession > 0
|
|
908
|
+
) {
|
|
909
|
+
provider = new RateLimitProvider(
|
|
910
|
+
provider,
|
|
911
|
+
rateLimit,
|
|
912
|
+
this.sharedRequestTimestamps,
|
|
913
|
+
);
|
|
741
914
|
}
|
|
742
915
|
const workingDir = getSandboxWorkingDir();
|
|
743
916
|
|
|
744
|
-
const systemPrompt =
|
|
917
|
+
const systemPrompt =
|
|
918
|
+
storedOptions?.systemPromptOverride ?? buildSystemPrompt();
|
|
745
919
|
const maxTokens = storedOptions?.maxResponseTokens ?? config.maxTokens;
|
|
746
920
|
|
|
747
921
|
const memoryPolicy = this.deriveMemoryPolicy(conversationId);
|
|
@@ -795,7 +969,9 @@ export class DaemonServer {
|
|
|
795
969
|
sharedRequestTimestamps: this.sharedRequestTimestamps,
|
|
796
970
|
debounceTimers: this.configWatcher.timers,
|
|
797
971
|
suppressConfigReload: this.configWatcher.suppressConfigReload,
|
|
798
|
-
setSuppressConfigReload: (value: boolean) => {
|
|
972
|
+
setSuppressConfigReload: (value: boolean) => {
|
|
973
|
+
this.configWatcher.suppressConfigReload = value;
|
|
974
|
+
},
|
|
799
975
|
updateConfigFingerprint: () => {
|
|
800
976
|
this.configWatcher.updateFingerprint();
|
|
801
977
|
},
|
|
@@ -809,16 +985,25 @@ export class DaemonServer {
|
|
|
809
985
|
};
|
|
810
986
|
}
|
|
811
987
|
|
|
812
|
-
private dispatchMessage(
|
|
813
|
-
|
|
988
|
+
private dispatchMessage(
|
|
989
|
+
msg: Parameters<typeof handleMessage>[0],
|
|
990
|
+
socket: net.Socket,
|
|
991
|
+
): void {
|
|
992
|
+
if (msg.type !== "ping") {
|
|
814
993
|
const now = Date.now();
|
|
815
|
-
if (
|
|
994
|
+
if (
|
|
995
|
+
now - this.configWatcher.lastConfigRefreshTime >=
|
|
996
|
+
ConfigWatcher.REFRESH_INTERVAL_MS
|
|
997
|
+
) {
|
|
816
998
|
try {
|
|
817
999
|
const changed = this.configWatcher.refreshConfigFromSources();
|
|
818
1000
|
if (changed) this.evictSessionsForReload();
|
|
819
1001
|
this.configWatcher.lastConfigRefreshTime = now;
|
|
820
1002
|
} catch (err) {
|
|
821
|
-
log.warn(
|
|
1003
|
+
log.warn(
|
|
1004
|
+
{ err },
|
|
1005
|
+
"Failed to refresh config from secure sources before handling IPC message",
|
|
1006
|
+
);
|
|
822
1007
|
}
|
|
823
1008
|
}
|
|
824
1009
|
}
|
|
@@ -834,24 +1019,47 @@ export class DaemonServer {
|
|
|
834
1019
|
options: SessionCreateOptions | undefined,
|
|
835
1020
|
sourceChannel: string | undefined,
|
|
836
1021
|
sourceInterface: string | undefined,
|
|
837
|
-
): Promise<{
|
|
1022
|
+
): Promise<{
|
|
1023
|
+
session: Session;
|
|
1024
|
+
attachments: {
|
|
1025
|
+
id: string;
|
|
1026
|
+
filename: string;
|
|
1027
|
+
mimeType: string;
|
|
1028
|
+
data: string;
|
|
1029
|
+
}[];
|
|
1030
|
+
}> {
|
|
838
1031
|
const ingressCheck = checkIngressForSecrets(content);
|
|
839
1032
|
if (ingressCheck.blocked) {
|
|
840
|
-
throw new IngressBlockedError(
|
|
1033
|
+
throw new IngressBlockedError(
|
|
1034
|
+
ingressCheck.userNotice!,
|
|
1035
|
+
ingressCheck.detectedTypes,
|
|
1036
|
+
);
|
|
841
1037
|
}
|
|
842
1038
|
|
|
843
|
-
const session = await this.getOrCreateSession(
|
|
1039
|
+
const session = await this.getOrCreateSession(
|
|
1040
|
+
conversationId,
|
|
1041
|
+
undefined,
|
|
1042
|
+
true,
|
|
1043
|
+
options,
|
|
1044
|
+
);
|
|
844
1045
|
|
|
845
1046
|
if (session.isProcessing()) {
|
|
846
|
-
throw new Error(
|
|
1047
|
+
throw new Error("Session is already processing a message");
|
|
847
1048
|
}
|
|
848
1049
|
|
|
849
|
-
const resolvedChannel = resolveTurnChannel(
|
|
1050
|
+
const resolvedChannel = resolveTurnChannel(
|
|
1051
|
+
sourceChannel,
|
|
1052
|
+
options?.transport?.channelId,
|
|
1053
|
+
);
|
|
850
1054
|
const resolvedInterface = resolveTurnInterface(sourceInterface);
|
|
851
|
-
session.setAssistantId(
|
|
1055
|
+
session.setAssistantId(
|
|
1056
|
+
options?.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
|
|
1057
|
+
);
|
|
852
1058
|
session.setGuardianContext(options?.guardianContext ?? null);
|
|
853
1059
|
await session.ensureActorScopedHistory();
|
|
854
|
-
session.setChannelCapabilities(
|
|
1060
|
+
session.setChannelCapabilities(
|
|
1061
|
+
resolveChannelCapabilities(sourceChannel, sourceInterface),
|
|
1062
|
+
);
|
|
855
1063
|
session.setCommandIntent(options?.commandIntent ?? null);
|
|
856
1064
|
session.setTurnChannelContext({
|
|
857
1065
|
userMessageChannel: resolvedChannel,
|
|
@@ -883,11 +1091,20 @@ export class DaemonServer {
|
|
|
883
1091
|
sourceInterface?: string,
|
|
884
1092
|
): Promise<{ messageId: string }> {
|
|
885
1093
|
const { session, attachments } = await this.prepareSessionForMessage(
|
|
886
|
-
conversationId,
|
|
1094
|
+
conversationId,
|
|
1095
|
+
content,
|
|
1096
|
+
attachmentIds,
|
|
1097
|
+
options,
|
|
1098
|
+
sourceChannel,
|
|
1099
|
+
sourceInterface,
|
|
887
1100
|
);
|
|
888
1101
|
|
|
889
1102
|
const requestId = crypto.randomUUID();
|
|
890
|
-
const messageId = await session.persistUserMessage(
|
|
1103
|
+
const messageId = await session.persistUserMessage(
|
|
1104
|
+
content,
|
|
1105
|
+
attachments,
|
|
1106
|
+
requestId,
|
|
1107
|
+
);
|
|
891
1108
|
|
|
892
1109
|
// Register pending interactions so channel approval interception can
|
|
893
1110
|
// find the session by requestId when confirmation/secret events fire.
|
|
@@ -900,16 +1117,22 @@ export class DaemonServer {
|
|
|
900
1117
|
session.updateClient(onEvent, false);
|
|
901
1118
|
}
|
|
902
1119
|
|
|
903
|
-
session
|
|
1120
|
+
session
|
|
1121
|
+
.runAgentLoop(content, messageId, onEvent, {
|
|
1122
|
+
isInteractive: options?.isInteractive ?? false,
|
|
1123
|
+
})
|
|
904
1124
|
.finally(() => {
|
|
905
1125
|
// Only reset if no other caller (e.g. a real IPC client) has rebound
|
|
906
1126
|
// the session's sender while the agent loop was running.
|
|
907
|
-
if (
|
|
1127
|
+
if (
|
|
1128
|
+
options?.isInteractive === true &&
|
|
1129
|
+
session.getCurrentSender() === onEvent
|
|
1130
|
+
) {
|
|
908
1131
|
session.updateClient(() => {}, true);
|
|
909
1132
|
}
|
|
910
1133
|
})
|
|
911
1134
|
.catch((err) => {
|
|
912
|
-
log.error({ err, conversationId },
|
|
1135
|
+
log.error({ err, conversationId }, "Background agent loop failed");
|
|
913
1136
|
});
|
|
914
1137
|
|
|
915
1138
|
return { messageId };
|
|
@@ -924,28 +1147,42 @@ export class DaemonServer {
|
|
|
924
1147
|
sourceInterface?: string,
|
|
925
1148
|
): Promise<{ messageId: string }> {
|
|
926
1149
|
const { session, attachments } = await this.prepareSessionForMessage(
|
|
927
|
-
conversationId,
|
|
1150
|
+
conversationId,
|
|
1151
|
+
content,
|
|
1152
|
+
attachmentIds,
|
|
1153
|
+
options,
|
|
1154
|
+
sourceChannel,
|
|
1155
|
+
sourceInterface,
|
|
928
1156
|
);
|
|
929
1157
|
|
|
930
1158
|
const slashResult = resolveSlash(content);
|
|
931
1159
|
|
|
932
|
-
if (slashResult.kind ===
|
|
1160
|
+
if (slashResult.kind === "unknown") {
|
|
933
1161
|
const serverTurnCtx = session.getTurnChannelContext();
|
|
934
1162
|
const serverInterfaceCtx = session.getTurnInterfaceContext();
|
|
935
|
-
const serverProvenance = provenanceFromGuardianContext(
|
|
1163
|
+
const serverProvenance = provenanceFromGuardianContext(
|
|
1164
|
+
session.guardianContext,
|
|
1165
|
+
);
|
|
936
1166
|
const serverChannelMeta = {
|
|
937
1167
|
...serverProvenance,
|
|
938
1168
|
...(serverTurnCtx
|
|
939
|
-
? {
|
|
1169
|
+
? {
|
|
1170
|
+
userMessageChannel: serverTurnCtx.userMessageChannel,
|
|
1171
|
+
assistantMessageChannel: serverTurnCtx.assistantMessageChannel,
|
|
1172
|
+
}
|
|
940
1173
|
: {}),
|
|
941
1174
|
...(serverInterfaceCtx
|
|
942
|
-
? {
|
|
1175
|
+
? {
|
|
1176
|
+
userMessageInterface: serverInterfaceCtx.userMessageInterface,
|
|
1177
|
+
assistantMessageInterface:
|
|
1178
|
+
serverInterfaceCtx.assistantMessageInterface,
|
|
1179
|
+
}
|
|
943
1180
|
: {}),
|
|
944
1181
|
};
|
|
945
1182
|
const userMsg = createUserMessage(content, attachments);
|
|
946
1183
|
const persisted = await conversationStore.addMessage(
|
|
947
1184
|
conversationId,
|
|
948
|
-
|
|
1185
|
+
"user",
|
|
949
1186
|
JSON.stringify(userMsg.content),
|
|
950
1187
|
serverChannelMeta,
|
|
951
1188
|
);
|
|
@@ -953,23 +1190,35 @@ export class DaemonServer {
|
|
|
953
1190
|
|
|
954
1191
|
if (serverTurnCtx) {
|
|
955
1192
|
try {
|
|
956
|
-
conversationStore.setConversationOriginChannelIfUnset(
|
|
1193
|
+
conversationStore.setConversationOriginChannelIfUnset(
|
|
1194
|
+
conversationId,
|
|
1195
|
+
serverTurnCtx.userMessageChannel,
|
|
1196
|
+
);
|
|
957
1197
|
} catch (err) {
|
|
958
|
-
log.warn(
|
|
1198
|
+
log.warn(
|
|
1199
|
+
{ err, conversationId },
|
|
1200
|
+
"Failed to set origin channel (best-effort)",
|
|
1201
|
+
);
|
|
959
1202
|
}
|
|
960
1203
|
}
|
|
961
1204
|
if (serverInterfaceCtx) {
|
|
962
1205
|
try {
|
|
963
|
-
conversationStore.setConversationOriginInterfaceIfUnset(
|
|
1206
|
+
conversationStore.setConversationOriginInterfaceIfUnset(
|
|
1207
|
+
conversationId,
|
|
1208
|
+
serverInterfaceCtx.userMessageInterface,
|
|
1209
|
+
);
|
|
964
1210
|
} catch (err) {
|
|
965
|
-
log.warn(
|
|
1211
|
+
log.warn(
|
|
1212
|
+
{ err, conversationId },
|
|
1213
|
+
"Failed to set origin interface (best-effort)",
|
|
1214
|
+
);
|
|
966
1215
|
}
|
|
967
1216
|
}
|
|
968
1217
|
|
|
969
1218
|
const assistantMsg = createAssistantMessage(slashResult.message);
|
|
970
1219
|
await conversationStore.addMessage(
|
|
971
1220
|
conversationId,
|
|
972
|
-
|
|
1221
|
+
"assistant",
|
|
973
1222
|
JSON.stringify(assistantMsg.content),
|
|
974
1223
|
serverChannelMeta,
|
|
975
1224
|
);
|
|
@@ -979,14 +1228,18 @@ export class DaemonServer {
|
|
|
979
1228
|
|
|
980
1229
|
const resolvedContent = slashResult.content;
|
|
981
1230
|
|
|
982
|
-
if (slashResult.kind ===
|
|
1231
|
+
if (slashResult.kind === "rewritten") {
|
|
983
1232
|
session.setPreactivatedSkillIds([slashResult.skillId]);
|
|
984
1233
|
}
|
|
985
1234
|
|
|
986
1235
|
const requestId = crypto.randomUUID();
|
|
987
1236
|
let messageId: string;
|
|
988
1237
|
try {
|
|
989
|
-
messageId = await session.persistUserMessage(
|
|
1238
|
+
messageId = await session.persistUserMessage(
|
|
1239
|
+
resolvedContent,
|
|
1240
|
+
attachments,
|
|
1241
|
+
requestId,
|
|
1242
|
+
);
|
|
990
1243
|
} catch (err) {
|
|
991
1244
|
session.setPreactivatedSkillIds(undefined);
|
|
992
1245
|
throw err;
|
|
@@ -1004,16 +1257,16 @@ export class DaemonServer {
|
|
|
1004
1257
|
}
|
|
1005
1258
|
|
|
1006
1259
|
try {
|
|
1007
|
-
await session.runAgentLoop(
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
onEvent,
|
|
1011
|
-
{ isInteractive: options?.isInteractive ?? false },
|
|
1012
|
-
);
|
|
1260
|
+
await session.runAgentLoop(resolvedContent, messageId, onEvent, {
|
|
1261
|
+
isInteractive: options?.isInteractive ?? false,
|
|
1262
|
+
});
|
|
1013
1263
|
} finally {
|
|
1014
1264
|
// Only reset if no other caller (e.g. a real IPC client) has rebound
|
|
1015
1265
|
// the session's sender while the agent loop was running.
|
|
1016
|
-
if (
|
|
1266
|
+
if (
|
|
1267
|
+
options?.isInteractive === true &&
|
|
1268
|
+
session.getCurrentSender() === onEvent
|
|
1269
|
+
) {
|
|
1017
1270
|
session.updateClient(() => {}, true);
|
|
1018
1271
|
}
|
|
1019
1272
|
}
|
|
@@ -1029,4 +1282,11 @@ export class DaemonServer {
|
|
|
1029
1282
|
return this.getOrCreateSession(conversationId, undefined, true);
|
|
1030
1283
|
}
|
|
1031
1284
|
|
|
1285
|
+
/**
|
|
1286
|
+
* Look up an active session by ID without creating one.
|
|
1287
|
+
* Returns undefined if no session with that ID exists.
|
|
1288
|
+
*/
|
|
1289
|
+
findSession(sessionId: string): Session | undefined {
|
|
1290
|
+
return this.sessions.get(sessionId);
|
|
1291
|
+
}
|
|
1032
1292
|
}
|