@vellumai/assistant 0.4.23 → 0.4.26
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/bun.lock +3 -0
- package/package.json +2 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -15
- package/src/__tests__/assistant-events-sse-hardening.test.ts +9 -3
- package/src/__tests__/call-controller.test.ts +80 -0
- package/src/__tests__/config-schema.test.ts +38 -178
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +4 -1
- package/src/__tests__/credential-security-invariants.test.ts +0 -2
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +2 -2
- package/src/__tests__/ipc-snapshot.test.ts +0 -9
- package/src/__tests__/onboarding-template-contract.test.ts +10 -20
- package/src/__tests__/relay-server.test.ts +3 -3
- package/src/__tests__/runtime-events-sse-parity.test.ts +10 -0
- package/src/__tests__/runtime-events-sse.test.ts +7 -0
- package/src/__tests__/session-runtime-assembly.test.ts +34 -8
- package/src/__tests__/system-prompt.test.ts +7 -1
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +12 -8
- package/src/__tests__/twilio-routes-twiml.test.ts +2 -2
- package/src/__tests__/twilio-routes.test.ts +2 -3
- package/src/__tests__/voice-quality.test.ts +21 -132
- package/src/calls/call-controller.ts +34 -29
- package/src/calls/relay-server.ts +11 -5
- package/src/calls/twilio-routes.ts +4 -38
- package/src/calls/voice-quality.ts +7 -63
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +7 -10
- package/src/config/bundled-skills/messaging/SKILL.md +3 -5
- package/src/config/bundled-skills/phone-calls/SKILL.md +144 -83
- package/src/config/bundled-skills/sms-setup/SKILL.md +0 -20
- package/src/config/bundled-skills/twilio-setup/SKILL.md +9 -17
- package/src/config/bundled-skills/voice-setup/SKILL.md +36 -1
- package/src/config/bundled-skills/voice-setup/icon.svg +20 -0
- package/src/config/calls-schema.ts +3 -53
- package/src/config/elevenlabs-schema.ts +33 -0
- package/src/config/schema.ts +183 -137
- package/src/config/types.ts +0 -1
- package/src/daemon/handlers/browser.ts +1 -6
- package/src/daemon/ipc-contract/browser.ts +5 -14
- package/src/daemon/ipc-contract-inventory.json +0 -2
- package/src/daemon/session-agent-loop-handlers.ts +3 -0
- package/src/daemon/session-runtime-assembly.ts +9 -7
- package/src/mcp/client.ts +2 -1
- package/src/memory/conversation-crud.ts +339 -166
- package/src/runtime/auth/middleware.ts +87 -26
- package/src/runtime/routes/events-routes.ts +7 -0
- package/src/runtime/routes/inbound-message-handler.ts +3 -4
- package/src/schedule/scheduler.ts +159 -45
- package/src/security/secure-keys.ts +3 -3
- package/src/tools/browser/browser-manager.ts +72 -228
- package/src/tools/browser/browser-screencast.ts +0 -5
- package/src/tools/network/script-proxy/certs.ts +7 -237
- package/src/tools/network/script-proxy/connect-tunnel.ts +1 -82
- package/src/tools/network/script-proxy/http-forwarder.ts +2 -151
- package/src/tools/network/script-proxy/logging.ts +12 -196
- package/src/tools/network/script-proxy/mitm-handler.ts +2 -270
- package/src/tools/network/script-proxy/policy.ts +4 -152
- package/src/tools/network/script-proxy/router.ts +2 -60
- package/src/tools/network/script-proxy/server.ts +5 -137
- package/src/tools/network/script-proxy/types.ts +19 -125
- package/src/tools/system/voice-config.ts +23 -1
- package/src/util/logger.ts +4 -1
- package/src/__tests__/elevenlabs-config.test.ts +0 -95
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +0 -407
- package/src/calls/elevenlabs-config.ts +0 -32
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* JWT bearer auth middleware for the runtime HTTP server.
|
|
3
3
|
*
|
|
4
|
-
* Extracts `Authorization: Bearer <token>`, verifies the JWT
|
|
5
|
-
*
|
|
4
|
+
* Extracts `Authorization: Bearer <token>`, verifies the JWT, and
|
|
5
|
+
* builds an AuthContext from the claims.
|
|
6
|
+
*
|
|
7
|
+
* Accepts two JWT audiences:
|
|
8
|
+
* - `vellum-daemon` — primary audience, used by the gateway's runtime
|
|
9
|
+
* proxy after token exchange and by daemon-minted delivery tokens.
|
|
10
|
+
* - `vellum-gateway` — fallback audience, used by direct local clients
|
|
11
|
+
* (e.g., the macOS app's SettingsStore) that hold a guardian-issued
|
|
12
|
+
* JWT but call daemon endpoints directly without routing through the
|
|
13
|
+
* gateway's runtime proxy. Both daemon and gateway share the same
|
|
14
|
+
* HMAC signing key (~/.vellum/protected/actor-token-signing-key),
|
|
15
|
+
* so the signature is valid regardless of audience.
|
|
6
16
|
*
|
|
7
17
|
* Replaces both the legacy bearer shared-secret check and the
|
|
8
18
|
* actor-token HMAC middleware with a single JWT verification path.
|
|
@@ -12,16 +22,16 @@
|
|
|
12
22
|
* so downstream code always has a typed context to consume.
|
|
13
23
|
*/
|
|
14
24
|
|
|
15
|
-
import { isHttpAuthDisabled } from
|
|
16
|
-
import { getLogger } from
|
|
17
|
-
import { DAEMON_INTERNAL_ASSISTANT_ID } from
|
|
18
|
-
import { extractBearerToken } from
|
|
19
|
-
import { buildAuthContext } from
|
|
20
|
-
import { resolveScopeProfile } from
|
|
21
|
-
import { verifyToken } from
|
|
22
|
-
import type { AuthContext } from
|
|
25
|
+
import { isHttpAuthDisabled } from "../../config/env.js";
|
|
26
|
+
import { getLogger } from "../../util/logger.js";
|
|
27
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
|
|
28
|
+
import { extractBearerToken } from "../middleware/auth.js";
|
|
29
|
+
import { buildAuthContext } from "./context.js";
|
|
30
|
+
import { resolveScopeProfile } from "./scopes.js";
|
|
31
|
+
import { verifyToken } from "./token-service.js";
|
|
32
|
+
import type { AuthContext } from "./types.js";
|
|
23
33
|
|
|
24
|
-
const log = getLogger(
|
|
34
|
+
const log = getLogger("auth-middleware");
|
|
25
35
|
|
|
26
36
|
// ---------------------------------------------------------------------------
|
|
27
37
|
// Result type
|
|
@@ -43,11 +53,11 @@ export type AuthenticateResult =
|
|
|
43
53
|
function buildDevBypassContext(): AuthContext {
|
|
44
54
|
return {
|
|
45
55
|
subject: `actor:${DAEMON_INTERNAL_ASSISTANT_ID}:dev-bypass`,
|
|
46
|
-
principalType:
|
|
56
|
+
principalType: "actor",
|
|
47
57
|
assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
|
|
48
|
-
actorPrincipalId:
|
|
49
|
-
scopeProfile:
|
|
50
|
-
scopes: resolveScopeProfile(
|
|
58
|
+
actorPrincipalId: "dev-bypass",
|
|
59
|
+
scopeProfile: "actor_client_v1",
|
|
60
|
+
scopes: resolveScopeProfile("actor_client_v1"),
|
|
51
61
|
policyEpoch: Number.MAX_SAFE_INTEGER,
|
|
52
62
|
};
|
|
53
63
|
}
|
|
@@ -72,36 +82,82 @@ export function authenticateRequest(req: Request): AuthenticateResult {
|
|
|
72
82
|
|
|
73
83
|
const rawToken = extractBearerToken(req);
|
|
74
84
|
if (!rawToken) {
|
|
75
|
-
log.warn(
|
|
85
|
+
log.warn(
|
|
86
|
+
{ reason: "missing_token", path },
|
|
87
|
+
"Auth denied: missing Authorization header",
|
|
88
|
+
);
|
|
76
89
|
return {
|
|
77
90
|
ok: false,
|
|
78
91
|
response: Response.json(
|
|
79
|
-
{
|
|
92
|
+
{
|
|
93
|
+
error: {
|
|
94
|
+
code: "UNAUTHORIZED",
|
|
95
|
+
message: "Missing Authorization header",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
80
98
|
{ status: 401 },
|
|
81
99
|
),
|
|
82
100
|
};
|
|
83
101
|
}
|
|
84
102
|
|
|
85
|
-
// Verify the JWT
|
|
86
|
-
|
|
103
|
+
// Verify the JWT — prefer vellum-daemon audience (gateway-proxied requests
|
|
104
|
+
// and daemon-minted tokens), but also accept vellum-gateway audience for
|
|
105
|
+
// direct local clients (macOS SettingsStore) that hold a guardian-issued JWT
|
|
106
|
+
// and call daemon endpoints without routing through the gateway runtime proxy.
|
|
107
|
+
let verifyResult = verifyToken(rawToken, "vellum-daemon");
|
|
108
|
+
if (
|
|
109
|
+
!verifyResult.ok &&
|
|
110
|
+
verifyResult.reason?.startsWith("audience_mismatch")
|
|
111
|
+
) {
|
|
112
|
+
verifyResult = verifyToken(rawToken, "vellum-gateway");
|
|
113
|
+
// Normalize gateway-audience claims to daemon context so that
|
|
114
|
+
// buildAuthContext applies the same assistantId normalization
|
|
115
|
+
// (aud=vellum-daemon → assistantId='self') that gateway-exchanged
|
|
116
|
+
// tokens receive. Without this rewrite, the external assistant ID
|
|
117
|
+
// from the guardian-issued JWT would leak into daemon-internal
|
|
118
|
+
// scoping (storage keys, routing), violating the invariant
|
|
119
|
+
// documented in context.ts:30-33.
|
|
120
|
+
if (verifyResult.ok) {
|
|
121
|
+
verifyResult = {
|
|
122
|
+
ok: true,
|
|
123
|
+
claims: { ...verifyResult.claims, aud: "vellum-daemon" },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
87
127
|
if (!verifyResult.ok) {
|
|
88
128
|
// Stale policy epoch gets a specific error code so clients can refresh
|
|
89
|
-
if (verifyResult.reason ===
|
|
90
|
-
log.warn(
|
|
129
|
+
if (verifyResult.reason === "stale_policy_epoch") {
|
|
130
|
+
log.warn(
|
|
131
|
+
{ reason: "stale_policy_epoch", path },
|
|
132
|
+
"Auth denied: stale policy epoch",
|
|
133
|
+
);
|
|
91
134
|
return {
|
|
92
135
|
ok: false,
|
|
93
136
|
response: Response.json(
|
|
94
|
-
{
|
|
137
|
+
{
|
|
138
|
+
error: {
|
|
139
|
+
code: "refresh_required",
|
|
140
|
+
message: "Token policy epoch is stale; refresh required",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
95
143
|
{ status: 401 },
|
|
96
144
|
),
|
|
97
145
|
};
|
|
98
146
|
}
|
|
99
147
|
|
|
100
|
-
log.warn(
|
|
148
|
+
log.warn(
|
|
149
|
+
{ reason: verifyResult.reason, path },
|
|
150
|
+
"Auth denied: JWT verification failed",
|
|
151
|
+
);
|
|
101
152
|
return {
|
|
102
153
|
ok: false,
|
|
103
154
|
response: Response.json(
|
|
104
|
-
{
|
|
155
|
+
{
|
|
156
|
+
error: {
|
|
157
|
+
code: "UNAUTHORIZED",
|
|
158
|
+
message: `Invalid token: ${verifyResult.reason}`,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
105
161
|
{ status: 401 },
|
|
106
162
|
),
|
|
107
163
|
};
|
|
@@ -112,12 +168,17 @@ export function authenticateRequest(req: Request): AuthenticateResult {
|
|
|
112
168
|
if (!contextResult.ok) {
|
|
113
169
|
log.warn(
|
|
114
170
|
{ reason: contextResult.reason, path, sub: verifyResult.claims.sub },
|
|
115
|
-
|
|
171
|
+
"Auth denied: invalid JWT claims",
|
|
116
172
|
);
|
|
117
173
|
return {
|
|
118
174
|
ok: false,
|
|
119
175
|
response: Response.json(
|
|
120
|
-
{
|
|
176
|
+
{
|
|
177
|
+
error: {
|
|
178
|
+
code: "UNAUTHORIZED",
|
|
179
|
+
message: `Invalid token claims: ${contextResult.reason}`,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
121
182
|
{ status: 401 },
|
|
122
183
|
),
|
|
123
184
|
};
|
|
@@ -116,6 +116,13 @@ export function handleSubscribeAssistantEvents(
|
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// Immediately enqueue a heartbeat comment so the HTTP status line and
|
|
120
|
+
// headers are flushed to the client without waiting for a real event.
|
|
121
|
+
// Without this, Bun may buffer the headers until the first data chunk
|
|
122
|
+
// arrives, causing clients (e.g. Python `requests`) to hang until the
|
|
123
|
+
// periodic heartbeat fires or an event is published.
|
|
124
|
+
controller.enqueue(encoder.encode(formatSseHeartbeat()));
|
|
125
|
+
|
|
119
126
|
// Send a keep-alive comment on each interval to prevent proxies and
|
|
120
127
|
// load-balancers from treating idle connections as timed out.
|
|
121
128
|
heartbeatTimer = setInterval(() => {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { ChannelId, InterfaceId } from '../../channels/types.js';
|
|
10
10
|
import { CHANNEL_IDS, INTERFACE_IDS, isChannelId, parseInterfaceId } from '../../channels/types.js';
|
|
11
11
|
import { getGatewayInternalBaseUrl } from '../../config/env.js';
|
|
12
|
+
import { resolveUserReference } from '../../config/user-reference.js';
|
|
12
13
|
import { RESEND_COOLDOWN_MS } from '../../daemon/handlers/config-channels.js';
|
|
13
14
|
import * as attachmentsStore from '../../memory/attachments-store.js';
|
|
14
15
|
import {
|
|
@@ -1577,10 +1578,8 @@ function startTrustedContactApprovalNotifier(params: {
|
|
|
1577
1578
|
const guardianName = resolveGuardianDisplayName(
|
|
1578
1579
|
assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
|
|
1579
1580
|
sourceChannel,
|
|
1580
|
-
);
|
|
1581
|
-
const waitingText = guardianName
|
|
1582
|
-
? `Waiting for ${guardianName}'s approval...`
|
|
1583
|
-
: 'Waiting for your guardian\'s approval...';
|
|
1581
|
+
) ?? resolveUserReference();
|
|
1582
|
+
const waitingText = `Waiting for ${guardianName}'s approval...`;
|
|
1584
1583
|
try {
|
|
1585
1584
|
await deliverChannelReply(replyCallbackUrl, {
|
|
1586
1585
|
chatId: externalChatId,
|
|
@@ -1,18 +1,31 @@
|
|
|
1
|
-
import { createConversation } from
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
1
|
+
import { createConversation } from "../memory/conversation-store.js";
|
|
2
|
+
import {
|
|
3
|
+
GENERATING_TITLE,
|
|
4
|
+
queueGenerateConversationTitle,
|
|
5
|
+
} from "../memory/conversation-title-service.js";
|
|
6
|
+
import { invalidateAssistantInferredItemsForConversation } from "../memory/task-memory-cleanup.js";
|
|
7
|
+
import { runSequencesOnce } from "../sequence/engine.js";
|
|
8
|
+
import {
|
|
9
|
+
claimDueReminders,
|
|
10
|
+
completeReminder,
|
|
11
|
+
failReminder,
|
|
12
|
+
type RoutingIntent,
|
|
13
|
+
setReminderConversationId,
|
|
14
|
+
} from "../tools/reminder/reminder-store.js";
|
|
15
|
+
import { getLogger } from "../util/logger.js";
|
|
16
|
+
import {
|
|
17
|
+
runWatchersOnce,
|
|
18
|
+
type WatcherEscalator,
|
|
19
|
+
type WatcherNotifier,
|
|
20
|
+
} from "../watcher/engine.js";
|
|
21
|
+
import { hasSetConstructs } from "./recurrence-engine.js";
|
|
9
22
|
import {
|
|
10
23
|
claimDueSchedules,
|
|
11
24
|
completeScheduleRun,
|
|
12
25
|
createScheduleRun,
|
|
13
|
-
} from
|
|
26
|
+
} from "./schedule-store.js";
|
|
14
27
|
|
|
15
|
-
const log = getLogger(
|
|
28
|
+
const log = getLogger("scheduler");
|
|
16
29
|
|
|
17
30
|
export type ScheduleMessageProcessor = (
|
|
18
31
|
conversationId: string,
|
|
@@ -50,21 +63,35 @@ export function startScheduler(
|
|
|
50
63
|
if (stopped || tickRunning) return;
|
|
51
64
|
tickRunning = true;
|
|
52
65
|
try {
|
|
53
|
-
await runScheduleOnce(
|
|
66
|
+
await runScheduleOnce(
|
|
67
|
+
processMessage,
|
|
68
|
+
notifyReminder,
|
|
69
|
+
notifySchedule,
|
|
70
|
+
watcherNotifier,
|
|
71
|
+
watcherEscalator,
|
|
72
|
+
);
|
|
54
73
|
} catch (err) {
|
|
55
|
-
log.error({ err },
|
|
74
|
+
log.error({ err }, "Schedule tick failed");
|
|
56
75
|
} finally {
|
|
57
76
|
tickRunning = false;
|
|
58
77
|
}
|
|
59
78
|
};
|
|
60
79
|
|
|
61
|
-
const timer = setInterval(() => {
|
|
80
|
+
const timer = setInterval(() => {
|
|
81
|
+
void tick();
|
|
82
|
+
}, TICK_INTERVAL_MS);
|
|
62
83
|
timer.unref();
|
|
63
84
|
void tick();
|
|
64
85
|
|
|
65
86
|
return {
|
|
66
87
|
async runOnce(): Promise<number> {
|
|
67
|
-
return runScheduleOnce(
|
|
88
|
+
return runScheduleOnce(
|
|
89
|
+
processMessage,
|
|
90
|
+
notifyReminder,
|
|
91
|
+
notifySchedule,
|
|
92
|
+
watcherNotifier,
|
|
93
|
+
watcherEscalator,
|
|
94
|
+
);
|
|
68
95
|
},
|
|
69
96
|
stop(): void {
|
|
70
97
|
stopped = true;
|
|
@@ -90,62 +117,123 @@ async function runScheduleOnce(
|
|
|
90
117
|
const taskMatch = job.message.match(/^run_task:(\S+)$/);
|
|
91
118
|
if (taskMatch) {
|
|
92
119
|
const taskId = taskMatch[1];
|
|
93
|
-
const isRruleSet =
|
|
120
|
+
const isRruleSet =
|
|
121
|
+
job.syntax === "rrule" && hasSetConstructs(job.expression);
|
|
94
122
|
try {
|
|
95
|
-
log.info(
|
|
96
|
-
|
|
123
|
+
log.info(
|
|
124
|
+
{
|
|
125
|
+
jobId: job.id,
|
|
126
|
+
name: job.name,
|
|
127
|
+
taskId,
|
|
128
|
+
syntax: job.syntax,
|
|
129
|
+
expression: job.expression,
|
|
130
|
+
isRruleSet,
|
|
131
|
+
},
|
|
132
|
+
"Executing scheduled task",
|
|
133
|
+
);
|
|
134
|
+
const { runTask } = await import("../tasks/task-runner.js");
|
|
97
135
|
const result = await runTask(
|
|
98
|
-
{ taskId, workingDir: process.cwd(), source:
|
|
99
|
-
processMessage as (
|
|
136
|
+
{ taskId, workingDir: process.cwd(), source: "schedule" },
|
|
137
|
+
processMessage as (
|
|
138
|
+
conversationId: string,
|
|
139
|
+
message: string,
|
|
140
|
+
taskRunId: string,
|
|
141
|
+
) => Promise<void>,
|
|
100
142
|
);
|
|
101
143
|
|
|
102
144
|
// Track the schedule run using the task's conversation
|
|
103
145
|
const runId = createScheduleRun(job.id, result.conversationId);
|
|
104
|
-
if (result.status ===
|
|
105
|
-
completeScheduleRun(runId, {
|
|
146
|
+
if (result.status === "failed") {
|
|
147
|
+
completeScheduleRun(runId, {
|
|
148
|
+
status: "error",
|
|
149
|
+
error: result.error ?? "Task run failed",
|
|
150
|
+
});
|
|
106
151
|
} else {
|
|
107
|
-
completeScheduleRun(runId, { status:
|
|
152
|
+
completeScheduleRun(runId, { status: "ok" });
|
|
108
153
|
notifySchedule({ id: job.id, name: job.name });
|
|
109
154
|
}
|
|
110
155
|
processed += 1;
|
|
111
156
|
} catch (err) {
|
|
112
157
|
const message = err instanceof Error ? err.message : String(err);
|
|
113
|
-
log.warn(
|
|
158
|
+
log.warn(
|
|
159
|
+
{
|
|
160
|
+
err,
|
|
161
|
+
jobId: job.id,
|
|
162
|
+
name: job.name,
|
|
163
|
+
taskId,
|
|
164
|
+
syntax: job.syntax,
|
|
165
|
+
expression: job.expression,
|
|
166
|
+
isRruleSet,
|
|
167
|
+
},
|
|
168
|
+
"Scheduled task execution failed",
|
|
169
|
+
);
|
|
114
170
|
// Create a fallback conversation for the schedule run record
|
|
115
|
-
const fallbackConversation = createConversation({
|
|
171
|
+
const fallbackConversation = createConversation({
|
|
172
|
+
title: GENERATING_TITLE,
|
|
173
|
+
source: "schedule",
|
|
174
|
+
scheduleJobId: job.id,
|
|
175
|
+
});
|
|
116
176
|
queueGenerateConversationTitle({
|
|
117
177
|
conversationId: fallbackConversation.id,
|
|
118
|
-
context: { origin:
|
|
178
|
+
context: { origin: "schedule", systemHint: `Schedule: ${job.name}` },
|
|
119
179
|
});
|
|
120
180
|
const runId = createScheduleRun(job.id, fallbackConversation.id);
|
|
121
|
-
completeScheduleRun(runId, { status:
|
|
181
|
+
completeScheduleRun(runId, { status: "error", error: message });
|
|
122
182
|
}
|
|
123
183
|
continue;
|
|
124
184
|
}
|
|
125
185
|
|
|
126
|
-
const conversation = createConversation({
|
|
186
|
+
const conversation = createConversation({
|
|
187
|
+
title: GENERATING_TITLE,
|
|
188
|
+
source: "schedule",
|
|
189
|
+
scheduleJobId: job.id,
|
|
190
|
+
});
|
|
127
191
|
queueGenerateConversationTitle({
|
|
128
192
|
conversationId: conversation.id,
|
|
129
|
-
context: { origin:
|
|
193
|
+
context: { origin: "schedule", systemHint: `Schedule: ${job.name}` },
|
|
130
194
|
});
|
|
131
195
|
const runId = createScheduleRun(job.id, conversation.id);
|
|
132
|
-
const isRruleSetMsg =
|
|
196
|
+
const isRruleSetMsg =
|
|
197
|
+
job.syntax === "rrule" && hasSetConstructs(job.expression);
|
|
133
198
|
|
|
134
199
|
try {
|
|
135
|
-
log.info(
|
|
200
|
+
log.info(
|
|
201
|
+
{
|
|
202
|
+
jobId: job.id,
|
|
203
|
+
name: job.name,
|
|
204
|
+
syntax: job.syntax,
|
|
205
|
+
expression: job.expression,
|
|
206
|
+
isRruleSet: isRruleSetMsg,
|
|
207
|
+
conversationId: conversation.id,
|
|
208
|
+
},
|
|
209
|
+
"Executing schedule",
|
|
210
|
+
);
|
|
136
211
|
await processMessage(conversation.id, job.message);
|
|
137
|
-
completeScheduleRun(runId, { status:
|
|
212
|
+
completeScheduleRun(runId, { status: "ok" });
|
|
138
213
|
notifySchedule({ id: job.id, name: job.name });
|
|
139
214
|
processed += 1;
|
|
140
215
|
} catch (err) {
|
|
141
216
|
const message = err instanceof Error ? err.message : String(err);
|
|
142
|
-
log.warn(
|
|
143
|
-
|
|
217
|
+
log.warn(
|
|
218
|
+
{
|
|
219
|
+
err,
|
|
220
|
+
jobId: job.id,
|
|
221
|
+
name: job.name,
|
|
222
|
+
syntax: job.syntax,
|
|
223
|
+
expression: job.expression,
|
|
224
|
+
isRruleSet: isRruleSetMsg,
|
|
225
|
+
},
|
|
226
|
+
"Schedule execution failed",
|
|
227
|
+
);
|
|
228
|
+
completeScheduleRun(runId, { status: "error", error: message });
|
|
144
229
|
|
|
145
230
|
try {
|
|
146
231
|
invalidateAssistantInferredItemsForConversation(conversation.id);
|
|
147
232
|
} catch (cleanupErr) {
|
|
148
|
-
log.warn(
|
|
233
|
+
log.warn(
|
|
234
|
+
{ err: cleanupErr, conversationId: conversation.id },
|
|
235
|
+
"Failed to invalidate assistant-inferred memory items",
|
|
236
|
+
);
|
|
149
237
|
}
|
|
150
238
|
}
|
|
151
239
|
}
|
|
@@ -153,24 +241,43 @@ async function runScheduleOnce(
|
|
|
153
241
|
// ── One-shot reminders ──────────────────────────────────────────────
|
|
154
242
|
const dueReminders = claimDueReminders(now);
|
|
155
243
|
for (const reminder of dueReminders) {
|
|
156
|
-
if (reminder.mode ===
|
|
157
|
-
const conversation = createConversation({
|
|
244
|
+
if (reminder.mode === "execute") {
|
|
245
|
+
const conversation = createConversation({
|
|
246
|
+
title: GENERATING_TITLE,
|
|
247
|
+
source: "reminder",
|
|
248
|
+
});
|
|
158
249
|
queueGenerateConversationTitle({
|
|
159
250
|
conversationId: conversation.id,
|
|
160
|
-
context: {
|
|
251
|
+
context: {
|
|
252
|
+
origin: "reminder",
|
|
253
|
+
systemHint: `Reminder: ${reminder.label}`,
|
|
254
|
+
},
|
|
161
255
|
});
|
|
162
256
|
setReminderConversationId(reminder.id, conversation.id);
|
|
163
257
|
try {
|
|
164
|
-
log.info(
|
|
258
|
+
log.info(
|
|
259
|
+
{
|
|
260
|
+
reminderId: reminder.id,
|
|
261
|
+
label: reminder.label,
|
|
262
|
+
conversationId: conversation.id,
|
|
263
|
+
},
|
|
264
|
+
"Executing reminder",
|
|
265
|
+
);
|
|
165
266
|
await processMessage(conversation.id, reminder.message);
|
|
166
267
|
completeReminder(reminder.id);
|
|
167
268
|
} catch (err) {
|
|
168
|
-
log.warn(
|
|
269
|
+
log.warn(
|
|
270
|
+
{ err, reminderId: reminder.id },
|
|
271
|
+
"Reminder execution failed, reverting to pending",
|
|
272
|
+
);
|
|
169
273
|
failReminder(reminder.id);
|
|
170
274
|
}
|
|
171
275
|
} else {
|
|
172
276
|
try {
|
|
173
|
-
log.info(
|
|
277
|
+
log.info(
|
|
278
|
+
{ reminderId: reminder.id, label: reminder.label },
|
|
279
|
+
"Firing reminder notification",
|
|
280
|
+
);
|
|
174
281
|
notifyReminder({
|
|
175
282
|
id: reminder.id,
|
|
176
283
|
label: reminder.label,
|
|
@@ -180,7 +287,10 @@ async function runScheduleOnce(
|
|
|
180
287
|
});
|
|
181
288
|
completeReminder(reminder.id);
|
|
182
289
|
} catch (err) {
|
|
183
|
-
log.warn(
|
|
290
|
+
log.warn(
|
|
291
|
+
{ err, reminderId: reminder.id },
|
|
292
|
+
"Reminder notification failed, reverting to pending",
|
|
293
|
+
);
|
|
184
294
|
failReminder(reminder.id);
|
|
185
295
|
}
|
|
186
296
|
}
|
|
@@ -190,10 +300,14 @@ async function runScheduleOnce(
|
|
|
190
300
|
// ── Watchers (event-driven polling) ────────────────────────────────
|
|
191
301
|
if (watcherNotifier && watcherEscalator) {
|
|
192
302
|
try {
|
|
193
|
-
const watcherProcessed = await runWatchersOnce(
|
|
303
|
+
const watcherProcessed = await runWatchersOnce(
|
|
304
|
+
processMessage,
|
|
305
|
+
watcherNotifier,
|
|
306
|
+
watcherEscalator,
|
|
307
|
+
);
|
|
194
308
|
processed += watcherProcessed;
|
|
195
309
|
} catch (err) {
|
|
196
|
-
log.error({ err },
|
|
310
|
+
log.error({ err }, "Watcher tick failed");
|
|
197
311
|
}
|
|
198
312
|
}
|
|
199
313
|
|
|
@@ -202,11 +316,11 @@ async function runScheduleOnce(
|
|
|
202
316
|
const sequenceProcessed = await runSequencesOnce(processMessage);
|
|
203
317
|
processed += sequenceProcessed;
|
|
204
318
|
} catch (err) {
|
|
205
|
-
log.error({ err },
|
|
319
|
+
log.error({ err }, "Sequence engine tick failed");
|
|
206
320
|
}
|
|
207
321
|
|
|
208
322
|
if (processed > 0) {
|
|
209
|
-
log.info({ processed },
|
|
323
|
+
log.info({ processed }, "Schedule tick complete");
|
|
210
324
|
}
|
|
211
325
|
return processed;
|
|
212
326
|
}
|
|
@@ -161,7 +161,7 @@ export function setSecureKey(account: string, value: string): boolean {
|
|
|
161
161
|
try {
|
|
162
162
|
// Only attempt deletion if the key actually exists in keychain to
|
|
163
163
|
// avoid spawning a subprocess on every write.
|
|
164
|
-
if (keychain.getKey(account)
|
|
164
|
+
if (keychain.getKey(account) != null) {
|
|
165
165
|
keychain.deleteKey(account);
|
|
166
166
|
}
|
|
167
167
|
} catch { /* best-effort */ }
|
|
@@ -301,7 +301,7 @@ export async function setSecureKeyAsync(
|
|
|
301
301
|
// Only attempt deletion if the key actually exists in keychain to
|
|
302
302
|
// avoid spawning a subprocess on every write.
|
|
303
303
|
const exists = await keychain.getKeyAsync(account);
|
|
304
|
-
if (exists
|
|
304
|
+
if (exists != null) {
|
|
305
305
|
await keychain.deleteKeyAsync(account);
|
|
306
306
|
}
|
|
307
307
|
} catch { /* best-effort */ }
|
|
@@ -323,7 +323,7 @@ export async function setSecureKeyAsync(
|
|
|
323
323
|
keychainMissCache.delete(account);
|
|
324
324
|
try {
|
|
325
325
|
const exists = await keychain.getKeyAsync(account);
|
|
326
|
-
if (exists
|
|
326
|
+
if (exists != null) {
|
|
327
327
|
await keychain.deleteKeyAsync(account);
|
|
328
328
|
}
|
|
329
329
|
} catch { /* best-effort */ }
|