@vellumai/assistant 0.11.10 → 0.11.11-staging.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/AGENTS.md +1 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/openapi.yaml +152 -55
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +60 -0
- package/src/__tests__/config-get-vision-flag.test.ts +36 -0
- package/src/__tests__/conversation-error.test.ts +38 -0
- package/src/__tests__/copy-composer-tc-templates.test.ts +2 -2
- package/src/__tests__/guardian-action-service-scope.test.ts +53 -0
- package/src/__tests__/headless-browser-mode.test.ts +63 -13
- package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
- package/src/__tests__/heartbeat-service.test.ts +118 -27
- package/src/__tests__/host-app-control-proxy.test.ts +2 -2
- package/src/__tests__/host-app-control-routes.test.ts +41 -0
- package/src/__tests__/host-bash-routes.test.ts +30 -1
- package/src/__tests__/host-browser-routes.test.ts +34 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +29 -0
- package/src/__tests__/host-file-routes-targeted.test.ts +29 -0
- package/src/__tests__/host-proxy-base.test.ts +1 -0
- package/src/__tests__/host-transfer-routes-targeted.test.ts +86 -0
- package/src/__tests__/llm-schema.test.ts +32 -0
- package/src/__tests__/mcp-config-secret-boundary.test.ts +2 -0
- package/src/__tests__/memory-config-file-parity.test.ts +18 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +15 -0
- package/src/__tests__/messaging-send-tool.test.ts +159 -13
- package/src/__tests__/messaging-shared.test.ts +19 -0
- package/src/__tests__/notification-events-source-context-probe.test.ts +211 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +40 -6
- package/src/__tests__/outlook-messaging-provider.test.ts +9 -0
- package/src/__tests__/provider-catalog-visibility.test.ts +2 -2
- package/src/__tests__/schedule-routes.test.ts +30 -1
- package/src/__tests__/scheduler-result-notification.test.ts +209 -0
- package/src/__tests__/skills-install-extract.test.ts +26 -0
- package/src/__tests__/skills-install-staging.test.ts +11 -2
- package/src/api/constants/personality-sliders.test.ts +21 -0
- package/src/api/constants/personality-sliders.ts +64 -0
- package/src/api/index.ts +8 -0
- package/src/archive/ustar-size.test.ts +32 -0
- package/src/archive/ustar-size.ts +23 -0
- package/src/background-wake/next-wake.test.ts +13 -0
- package/src/background-wake/next-wake.ts +4 -2
- package/src/calls/voice-session-bridge.ts +26 -0
- package/src/cli/__tests__/catalog-search-help.test.ts +1 -0
- package/src/cli/commands/__tests__/plugins-search.test.ts +81 -0
- package/src/cli/commands/config.help.ts +3 -1
- package/src/cli/commands/memory/worker.ts +1 -1
- package/src/cli/commands/platform/__tests__/credits-format.test.ts +94 -5
- package/src/cli/commands/platform/__tests__/credits.test.ts +10 -0
- package/src/cli/commands/platform/credits-format.ts +57 -0
- package/src/cli/commands/platform/index.help.ts +19 -3
- package/src/cli/commands/plugins.help.ts +4 -1
- package/src/cli/commands/plugins.ts +2 -1
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/cli/lib/install-from-platform.ts +8 -10
- package/src/config/bundled-skills/messaging/SKILL.md +2 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +4 -4
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +85 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +6 -0
- package/src/config/bundled-skills/schedule/SKILL.md +9 -1
- package/src/config/bundled-skills/screen-annotation/SKILL.md +87 -20
- package/src/config/bundled-skills/screen-annotation/TOOLS.json +21 -5
- package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +67 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/input-modalities.test.ts +71 -0
- package/src/config/input-modalities.ts +95 -0
- package/src/config/schemas/heartbeat.ts +3 -3
- package/src/config/schemas/llm.ts +13 -3
- package/src/config/schemas/memory-storage.test.ts +39 -0
- package/src/config/schemas/memory-storage.ts +21 -0
- package/src/context/token-estimator.ts +2 -2
- package/src/daemon/__tests__/embedding-reconcile.test.ts +77 -0
- package/src/daemon/__tests__/lifecycle-embedding-reconcile.test.ts +3 -0
- package/src/daemon/conversation-error.ts +20 -7
- package/src/daemon/embedding-reconcile.ts +64 -1
- package/src/daemon/handlers/config-embeddings.ts +31 -0
- package/src/daemon/host-app-control-proxy.ts +2 -0
- package/src/daemon/host-bash-proxy.ts +1 -0
- package/src/daemon/host-cu-proxy.ts +6 -6
- package/src/daemon/host-file-proxy.ts +6 -6
- package/src/daemon/host-proxy-base.ts +10 -9
- package/src/daemon/host-transfer-proxy.ts +21 -24
- package/src/daemon/mcp-reload-service.ts +6 -0
- package/src/daemon/providers-setup.ts +5 -26
- package/src/heartbeat/heartbeat-service.ts +72 -13
- package/src/live-voice/__tests__/live-voice-photo.test.ts +72 -0
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +81 -0
- package/src/live-voice/live-voice-photo.ts +68 -4
- package/src/live-voice/live-voice-session.ts +36 -2
- package/src/live-voice/protocol.ts +98 -1
- package/src/mcp/__tests__/reload-signal-emission.test.ts +54 -0
- package/src/mcp/__tests__/reload-signal.test.ts +57 -0
- package/src/mcp/__tests__/startup.test.ts +150 -0
- package/src/mcp/reload-signal.ts +50 -0
- package/src/mcp/startup.ts +101 -0
- package/src/messaging/providers/outlook/adapter.ts +2 -13
- package/src/messaging/providers/outlook/client.ts +13 -1
- package/src/messaging/providers/outlook/types.ts +3 -0
- package/src/notifications/__tests__/decision-engine.test.ts +79 -6
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +27 -0
- package/src/notifications/__tests__/schedule-result-producer.test.ts +397 -0
- package/src/notifications/copy-composer.ts +33 -11
- package/src/notifications/decision-engine.ts +48 -11
- package/src/notifications/events-store.ts +68 -2
- package/src/notifications/home-feed-side-effect.ts +15 -3
- package/src/notifications/schedule-result-producer.ts +275 -0
- package/src/notifications/signal.ts +5 -0
- package/src/oauth/seed-providers.ts +3 -2
- package/src/persistence/conversation-crud.ts +38 -0
- package/src/persistence/embeddings/embedding-backend.test.ts +162 -0
- package/src/persistence/embeddings/embedding-backend.ts +148 -11
- package/src/persistence/embeddings/embedding-openai.test.ts +113 -0
- package/src/persistence/embeddings/embedding-openai.ts +55 -4
- package/src/persistence/embeddings/embedding-types.test.ts +24 -0
- package/src/persistence/embeddings/embedding-types.ts +25 -1
- package/src/persistence/job-utils.ts +6 -2
- package/src/plugin-api/index.ts +2 -1
- package/src/plugin-api/vision-support.test.ts +81 -0
- package/src/plugin-api/vision-support.ts +57 -10
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +8 -0
- package/src/plugins/defaults/memory/jobs/__tests__/embed-concept-page.test.ts +1 -0
- package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +11 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +20 -0
- package/src/plugins/defaults/memory/src/memory-worker-routes.ts +3 -1
- package/src/plugins/defaults/memory/substrate/__tests__/reembed-job.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/__tests__/backfill-jobs.test.ts +1 -0
- package/src/plugins/defaults/memory/v3/__tests__/section-dense-store.test.ts +82 -0
- package/src/plugins/defaults/memory/v3/section-dense-store.ts +9 -27
- package/src/providers/__tests__/provider-secret-catalog.test.ts +4 -0
- package/src/providers/__tests__/xml-tool-call-salvage.test.ts +172 -0
- package/src/providers/anthropic/client.ts +9 -4
- package/src/providers/inline-audio-support.test.ts +34 -0
- package/src/providers/inline-audio-support.ts +27 -0
- package/src/providers/model-catalog.ts +22 -2
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +50 -0
- package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +51 -0
- package/src/providers/openai/__tests__/chat-completions-provider-xml-salvage.test.ts +291 -0
- package/src/providers/openai/api-error-normalization.ts +18 -3
- package/src/providers/openai/chat-completions-provider.ts +113 -13
- package/src/providers/openai/responses-provider.ts +10 -8
- package/src/providers/provider-secret-catalog.ts +11 -1
- package/src/providers/vellum/client.ts +14 -1
- package/src/providers/vellum/personality-directions.test.ts +163 -0
- package/src/providers/vellum/personality-directions.ts +120 -0
- package/src/providers/xml-tool-call-salvage.ts +212 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +42 -4
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -6
- package/src/runtime/auth/__tests__/route-policy.test.ts +65 -0
- package/src/runtime/auth/same-actor.ts +29 -5
- package/src/runtime/background-job-runner.ts +38 -29
- package/src/runtime/guardian-action-service.ts +20 -1
- package/src/runtime/migrations/vbundle-validator.ts +3 -3
- package/src/runtime/pending-interactions.ts +4 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +24 -0
- package/src/runtime/routes/__tests__/platform-credits-routes.test.ts +99 -5
- package/src/runtime/routes/conversation-query-routes.ts +53 -11
- package/src/runtime/routes/document-comments-routes.ts +8 -2
- package/src/runtime/routes/guardian-action-routes.ts +7 -0
- package/src/runtime/routes/heartbeat-routes.ts +41 -53
- package/src/runtime/routes/host-app-control-routes.ts +10 -36
- package/src/runtime/routes/host-bash-routes.ts +11 -45
- package/src/runtime/routes/host-browser-routes.ts +24 -0
- package/src/runtime/routes/host-cu-routes.ts +11 -45
- package/src/runtime/routes/host-file-routes.ts +11 -44
- package/src/runtime/routes/host-proxy-result-binding.ts +57 -0
- package/src/runtime/routes/host-transfer-routes.ts +33 -25
- package/src/runtime/routes/index.ts +0 -2
- package/src/runtime/routes/integrations/a2a.ts +12 -3
- package/src/runtime/routes/integrations/vercel.ts +13 -3
- package/src/runtime/routes/platform-routes.ts +121 -24
- package/src/runtime/routes/schedule-routes.ts +15 -1
- package/src/schedule/__tests__/schedule-timezone.test.ts +21 -3
- package/src/schedule/__tests__/worker-mcp-tools.test.ts +88 -0
- package/src/schedule/schedule-timezone.ts +25 -0
- package/src/schedule/scheduler.ts +19 -0
- package/src/schedule/worker.ts +119 -2
- package/src/skills/catalog-install.ts +40 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +3 -0
- package/src/tools/browser/browser-execution.ts +117 -53
- package/src/tools/browser/browser-status-constants.ts +6 -6
- package/src/tools/capability-offer.test.ts +87 -0
- package/src/tools/capability-offer.ts +93 -0
- package/src/tools/terminal/__tests__/safe-env.test.ts +17 -0
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/util/provider-error-patterns.ts +34 -0
- package/src/watcher/__tests__/engine.test.ts +5 -2
- package/src/watcher/engine.ts +7 -4
- package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +0 -487
- package/src/runtime/routes/sight-frame-routes.ts +0 -136
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
getVercelConfig,
|
|
17
17
|
setVercelConfig,
|
|
18
18
|
} from "../../../daemon/handlers/config-vercel.js";
|
|
19
|
+
import { ACTOR_PRINCIPALS } from "../../auth/route-policy.js";
|
|
19
20
|
import { BadRequestError } from "../errors.js";
|
|
20
21
|
import type { RouteDefinition, RouteHandlerArgs } from "../types.js";
|
|
21
22
|
|
|
@@ -70,7 +71,10 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
70
71
|
operationId: "integrations_vercel_config_get",
|
|
71
72
|
endpoint: "integrations/vercel/config",
|
|
72
73
|
method: "GET",
|
|
73
|
-
policy:
|
|
74
|
+
policy: {
|
|
75
|
+
requiredScopes: ["settings.read"],
|
|
76
|
+
allowedPrincipalTypes: ACTOR_PRINCIPALS,
|
|
77
|
+
},
|
|
74
78
|
summary: "Get Vercel config",
|
|
75
79
|
description: "Check if a Vercel API token is stored.",
|
|
76
80
|
tags: ["integrations"],
|
|
@@ -81,7 +85,10 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
81
85
|
operationId: "integrations_vercel_config_post",
|
|
82
86
|
endpoint: "integrations/vercel/config",
|
|
83
87
|
method: "POST",
|
|
84
|
-
policy:
|
|
88
|
+
policy: {
|
|
89
|
+
requiredScopes: ["settings.write"],
|
|
90
|
+
allowedPrincipalTypes: ACTOR_PRINCIPALS,
|
|
91
|
+
},
|
|
85
92
|
summary: "Set or delete Vercel config",
|
|
86
93
|
description:
|
|
87
94
|
"Set or delete the Vercel API token. Action is determined by the body action field.",
|
|
@@ -97,7 +104,10 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
97
104
|
operationId: "integrations_vercel_config_delete",
|
|
98
105
|
endpoint: "integrations/vercel/config",
|
|
99
106
|
method: "DELETE",
|
|
100
|
-
policy:
|
|
107
|
+
policy: {
|
|
108
|
+
requiredScopes: ["settings.write"],
|
|
109
|
+
allowedPrincipalTypes: ACTOR_PRINCIPALS,
|
|
110
|
+
},
|
|
101
111
|
summary: "Delete Vercel config",
|
|
102
112
|
description: "Delete the stored Vercel API token.",
|
|
103
113
|
tags: ["integrations"],
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
* - platform_callback_routes_list (GET platform/callback-routes): lists
|
|
16
16
|
* registered callback routes for this assistant.
|
|
17
17
|
* - platform_credits (GET platform/credits): fetches the org's remaining
|
|
18
|
-
* credit balance,
|
|
19
|
-
*
|
|
18
|
+
* credit balance, plan-included credit usage, extra credit, upcoming
|
|
19
|
+
* expiry, today's spend, and daily-limit / low-balance state from the
|
|
20
|
+
* platform billing summary.
|
|
20
21
|
* - platform_subscription (GET platform/subscription): fetches the org's
|
|
21
22
|
* current plan, subscription status, and entitlements.
|
|
22
23
|
* - platform_plans (GET platform/plans): fetches the plan catalog with pricing.
|
|
@@ -26,6 +27,11 @@
|
|
|
26
27
|
* invoice list and returns a single invoice by Stripe invoice ID.
|
|
27
28
|
*/
|
|
28
29
|
|
|
30
|
+
import {
|
|
31
|
+
type BillingPlanId,
|
|
32
|
+
extraCreditUsd,
|
|
33
|
+
planCreditUsedFraction,
|
|
34
|
+
} from "@vellumai/service-contracts/plan-credit";
|
|
29
35
|
import {
|
|
30
36
|
type PlatformCredentialVerificationStatus,
|
|
31
37
|
type PlatformVerifyCredentialResponse,
|
|
@@ -172,6 +178,48 @@ const PlatformCreditsResponseSchema = z.object({
|
|
|
172
178
|
.describe(
|
|
173
179
|
"True when the balance is above zero but below the low-balance threshold and auto top-up is not enabled.",
|
|
174
180
|
),
|
|
181
|
+
plan_credit_remaining: z
|
|
182
|
+
.number()
|
|
183
|
+
.nullable()
|
|
184
|
+
.describe(
|
|
185
|
+
"Unused credit (USD) remaining on the unexpired plan-included grants (initial credit, Pro credit bundle). Null when the platform did not report grant figures.",
|
|
186
|
+
),
|
|
187
|
+
plan_credit_total: z
|
|
188
|
+
.number()
|
|
189
|
+
.nullable()
|
|
190
|
+
.describe(
|
|
191
|
+
"Total credit (USD) those plan-included grants were worth, net of refunds. Null when the platform did not report grant figures.",
|
|
192
|
+
),
|
|
193
|
+
plan_credit_used_fraction: z
|
|
194
|
+
.number()
|
|
195
|
+
.nullable()
|
|
196
|
+
.describe(
|
|
197
|
+
"Share of the plan-included credit already used, from 0 to 1: the same reading as the in-app usage meter. A Pro subscription whose unexpired grants total nothing reads as 1 (everything granted is used or expired). Null when there is no usable reading: grant figures missing, or a zero total on a base or unknown plan.",
|
|
198
|
+
),
|
|
199
|
+
plan_credits_spent: z
|
|
200
|
+
.boolean()
|
|
201
|
+
.nullable()
|
|
202
|
+
.describe(
|
|
203
|
+
"True when the plan-included credit is used up or expired. Whether further managed usage is funded is extra_credit_remaining (zero means the organization is out of credit). Null when there is no plan-credit reading.",
|
|
204
|
+
),
|
|
205
|
+
extra_credit_remaining: z
|
|
206
|
+
.number()
|
|
207
|
+
.nullable()
|
|
208
|
+
.describe(
|
|
209
|
+
"Credit (USD) bought or earned on top of the plan-included grants: remaining minus plan_credit_remaining, floored at zero. Null when grant figures are missing.",
|
|
210
|
+
),
|
|
211
|
+
credits_expiring_soon: z
|
|
212
|
+
.number()
|
|
213
|
+
.nullable()
|
|
214
|
+
.describe(
|
|
215
|
+
"Unexpired credit (USD) on grants that expire within the next 30 days. Null when the platform did not report it.",
|
|
216
|
+
),
|
|
217
|
+
next_credit_expiry_at: z
|
|
218
|
+
.string()
|
|
219
|
+
.nullable()
|
|
220
|
+
.describe(
|
|
221
|
+
"Earliest upcoming expiry (ISO 8601) across grants that still hold credit, or null when none is scheduled.",
|
|
222
|
+
),
|
|
175
223
|
});
|
|
176
224
|
type PlatformCreditsResponse = z.infer<typeof PlatformCreditsResponseSchema>;
|
|
177
225
|
|
|
@@ -513,10 +561,40 @@ async function handlePlatformCredits(
|
|
|
513
561
|
daily_limit_snoozed?: boolean;
|
|
514
562
|
low_balance_threshold_usd?: string;
|
|
515
563
|
low_balance_warning?: boolean;
|
|
564
|
+
available_usage_balance?: string;
|
|
565
|
+
total_usage_balance?: string;
|
|
566
|
+
credits_expiring_soon_usd?: string;
|
|
567
|
+
next_credit_expiry_at?: string | null;
|
|
516
568
|
};
|
|
517
569
|
|
|
570
|
+
const remaining = Number(summary.effective_balance_usd);
|
|
571
|
+
const planCreditRemaining = parseUsd(summary.available_usage_balance);
|
|
572
|
+
const planCreditTotal = parseUsd(summary.total_usage_balance);
|
|
573
|
+
let usedFraction = planCreditUsedFraction(
|
|
574
|
+
planCreditTotal,
|
|
575
|
+
planCreditRemaining,
|
|
576
|
+
null,
|
|
577
|
+
);
|
|
578
|
+
if (
|
|
579
|
+
usedFraction === null &&
|
|
580
|
+
planCreditTotal !== null &&
|
|
581
|
+
planCreditTotal <= 0
|
|
582
|
+
) {
|
|
583
|
+
// Only the zero-grant reading depends on the plan, so the subscription is
|
|
584
|
+
// read lazily and best-effort: a slow or failed read degrades that one
|
|
585
|
+
// reading to null instead of delaying or failing the balance.
|
|
586
|
+
const planId = await fetchBillingSubscription()
|
|
587
|
+
.then((subscription) => subscription.plan_id)
|
|
588
|
+
.catch((): BillingPlanId | null => null);
|
|
589
|
+
usedFraction = planCreditUsedFraction(
|
|
590
|
+
planCreditTotal,
|
|
591
|
+
planCreditRemaining,
|
|
592
|
+
planId,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
518
596
|
return {
|
|
519
|
-
remaining
|
|
597
|
+
remaining,
|
|
520
598
|
settled: Number(summary.settled_balance_usd),
|
|
521
599
|
pending: Number(summary.pending_compute_usd),
|
|
522
600
|
unit: "USD",
|
|
@@ -530,6 +608,19 @@ async function handlePlatformCredits(
|
|
|
530
608
|
daily_limit_snoozed: summary.daily_limit_snoozed === true,
|
|
531
609
|
low_balance_threshold: parseUsd(summary.low_balance_threshold_usd),
|
|
532
610
|
low_balance_warning: summary.low_balance_warning === true,
|
|
611
|
+
plan_credit_remaining: planCreditRemaining,
|
|
612
|
+
plan_credit_total: planCreditTotal,
|
|
613
|
+
plan_credit_used_fraction: usedFraction,
|
|
614
|
+
plan_credits_spent: usedFraction === null ? null : usedFraction >= 1,
|
|
615
|
+
extra_credit_remaining:
|
|
616
|
+
planCreditRemaining === null
|
|
617
|
+
? null
|
|
618
|
+
: extraCreditUsd(remaining, planCreditRemaining),
|
|
619
|
+
credits_expiring_soon: parseUsd(summary.credits_expiring_soon_usd),
|
|
620
|
+
next_credit_expiry_at:
|
|
621
|
+
typeof summary.next_credit_expiry_at === "string"
|
|
622
|
+
? summary.next_credit_expiry_at
|
|
623
|
+
: null,
|
|
533
624
|
};
|
|
534
625
|
}
|
|
535
626
|
|
|
@@ -624,28 +715,34 @@ async function fetchPlatformJson(
|
|
|
624
715
|
return response.json();
|
|
625
716
|
}
|
|
626
717
|
|
|
718
|
+
interface BillingSubscriptionData {
|
|
719
|
+
plan_id: BillingPlanId;
|
|
720
|
+
status: string | null;
|
|
721
|
+
renewal_date: string | null;
|
|
722
|
+
current_period_end: string | null;
|
|
723
|
+
cancel_at_period_end: boolean;
|
|
724
|
+
cancel_at: string | null;
|
|
725
|
+
selected_credit_tier: string | null;
|
|
726
|
+
package: {
|
|
727
|
+
key: string;
|
|
728
|
+
name: string;
|
|
729
|
+
version: number;
|
|
730
|
+
customized: boolean;
|
|
731
|
+
} | null;
|
|
732
|
+
entitlements: { managed_email: boolean; phone_number: boolean };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function fetchBillingSubscription(): Promise<BillingSubscriptionData> {
|
|
736
|
+
return fetchPlatformJson(
|
|
737
|
+
"/v1/organizations/billing/subscription/",
|
|
738
|
+
"subscription",
|
|
739
|
+
) as Promise<BillingSubscriptionData>;
|
|
740
|
+
}
|
|
741
|
+
|
|
627
742
|
async function handlePlatformSubscription(
|
|
628
743
|
_args: RouteHandlerArgs,
|
|
629
744
|
): Promise<PlatformSubscriptionResponse> {
|
|
630
|
-
const data =
|
|
631
|
-
"/v1/organizations/billing/subscription/",
|
|
632
|
-
"subscription",
|
|
633
|
-
)) as {
|
|
634
|
-
plan_id: "base" | "pro";
|
|
635
|
-
status: string | null;
|
|
636
|
-
renewal_date: string | null;
|
|
637
|
-
current_period_end: string | null;
|
|
638
|
-
cancel_at_period_end: boolean;
|
|
639
|
-
cancel_at: string | null;
|
|
640
|
-
selected_credit_tier: string | null;
|
|
641
|
-
package: {
|
|
642
|
-
key: string;
|
|
643
|
-
name: string;
|
|
644
|
-
version: number;
|
|
645
|
-
customized: boolean;
|
|
646
|
-
} | null;
|
|
647
|
-
entitlements: { managed_email: boolean; phone_number: boolean };
|
|
648
|
-
};
|
|
745
|
+
const data = await fetchBillingSubscription();
|
|
649
746
|
|
|
650
747
|
return {
|
|
651
748
|
planId: data.plan_id,
|
|
@@ -914,9 +1011,9 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
914
1011
|
allowedPrincipalTypes: ACTOR_PRINCIPALS,
|
|
915
1012
|
},
|
|
916
1013
|
summary:
|
|
917
|
-
"Get the organization's
|
|
1014
|
+
"Get the organization's credit balance, plan-credit usage, and daily-limit state",
|
|
918
1015
|
description:
|
|
919
|
-
"Fetches the org's settled, pending, and effective (remaining) credit balance in USD from the platform billing summary, plus the daily-limit state: today's spend counted against the daily credit limit (which excludes spend covered by plan-included credits, so it is not total spend), the limit itself, and the low-balance warning state.",
|
|
1016
|
+
"Fetches the org's settled, pending, and effective (remaining) credit balance in USD from the platform billing summary, plus plan-included credit usage (remaining, total, and the used fraction shown by the in-app usage meter), extra credit bought or earned on top, credit expiring within 30 days, and the daily-limit state: today's spend counted against the daily credit limit (which excludes spend covered by plan-included credits, so it is not total spend), the limit itself, and the low-balance warning state.",
|
|
920
1017
|
tags: ["platform"],
|
|
921
1018
|
handler: handlePlatformCredits,
|
|
922
1019
|
responseBody: PlatformCreditsResponseSchema,
|
|
@@ -14,6 +14,7 @@ import { getOrCreateConversation } from "../../daemon/conversation-store.js";
|
|
|
14
14
|
import { INTERNAL_GUARDIAN_TRUST_CONTEXT } from "../../daemon/trust-context.js";
|
|
15
15
|
import { bootstrapConversation } from "../../persistence/conversation-bootstrap.js";
|
|
16
16
|
import { getConversation } from "../../persistence/conversation-crud.js";
|
|
17
|
+
import { isLifecycleQuiesced } from "../../persistence/lifecycle-quiesce.js";
|
|
17
18
|
import {
|
|
18
19
|
getUsageCostForRun,
|
|
19
20
|
listRunConversationIds,
|
|
@@ -62,6 +63,7 @@ import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
|
|
|
62
63
|
import { parseEpochMillisRange } from "./epoch-millis-range.js";
|
|
63
64
|
import {
|
|
64
65
|
BadRequestError,
|
|
66
|
+
ConflictError,
|
|
65
67
|
ForbiddenError,
|
|
66
68
|
InternalError,
|
|
67
69
|
NotFoundError,
|
|
@@ -1302,11 +1304,17 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
1302
1304
|
},
|
|
1303
1305
|
summary: "Run schedule now",
|
|
1304
1306
|
description:
|
|
1305
|
-
"Trigger an immediate execution of a schedule. A plugin-sourced schedule is rejected with a 400 when its plugin is disabled or no longer declares it.",
|
|
1307
|
+
"Trigger an immediate execution of a schedule. A plugin-sourced schedule is rejected with a 400 when its plugin is disabled or no longer declares it. Returns 409 while a drain quiesce lease is active so run-now cannot start work the shutdown snapshot would miss.",
|
|
1306
1308
|
tags: ["schedules"],
|
|
1307
1309
|
responseBody: z.object({
|
|
1308
1310
|
schedules: z.array(scheduleSchema).describe("Updated schedule list"),
|
|
1309
1311
|
}),
|
|
1312
|
+
additionalResponses: {
|
|
1313
|
+
"409": {
|
|
1314
|
+
description:
|
|
1315
|
+
"A drain quiesce lease is active, so run-now is not starting new work.",
|
|
1316
|
+
},
|
|
1317
|
+
},
|
|
1310
1318
|
handler: ({ pathParams, headers }: RouteHandlerArgs) =>
|
|
1311
1319
|
handleRunScheduleNow(pathParams!.id, headers),
|
|
1312
1320
|
},
|
|
@@ -1321,6 +1329,12 @@ async function handleRunScheduleNow(
|
|
|
1321
1329
|
throw new NotFoundError("Schedule not found");
|
|
1322
1330
|
}
|
|
1323
1331
|
|
|
1332
|
+
if (isLifecycleQuiesced()) {
|
|
1333
|
+
throw new ConflictError(
|
|
1334
|
+
"The assistant is shutting down and is not starting new schedule runs.",
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1324
1338
|
// A plugin-sourced row runs the plugin's own script or prompt, so run-now is
|
|
1325
1339
|
// only offered while that plugin is something the runtime would activate.
|
|
1326
1340
|
// `pluginScheduleSourceAvailable` covers a plugin the daemon never
|
|
@@ -9,8 +9,11 @@ mock.module("../../config/loader.js", () => ({
|
|
|
9
9
|
getConfigReadOnly: () => ({ ui: mockUi }),
|
|
10
10
|
}));
|
|
11
11
|
|
|
12
|
-
const {
|
|
13
|
-
|
|
12
|
+
const {
|
|
13
|
+
resolveScheduleTimezone,
|
|
14
|
+
expressionCarriesOwnTimezone,
|
|
15
|
+
hourInTimeZone,
|
|
16
|
+
} = await import("../schedule-timezone.js");
|
|
14
17
|
|
|
15
18
|
afterEach(() => {
|
|
16
19
|
mockUi = {};
|
|
@@ -42,12 +45,27 @@ describe("resolveScheduleTimezone", () => {
|
|
|
42
45
|
expect(resolveScheduleTimezone(null)).toBeNull();
|
|
43
46
|
});
|
|
44
47
|
|
|
45
|
-
test("
|
|
48
|
+
test("an invalid explicit value is ignored and falls back", () => {
|
|
46
49
|
mockUi = { userTimezone: "America/New_York" };
|
|
47
50
|
expect(resolveScheduleTimezone("Not/AZone")).toBe("America/New_York");
|
|
48
51
|
});
|
|
49
52
|
});
|
|
50
53
|
|
|
54
|
+
describe("hourInTimeZone", () => {
|
|
55
|
+
test("returns the wall-clock hour in the given zone", () => {
|
|
56
|
+
// 23:00 UTC on a Pacific Daylight date is 16:00 in America/Los_Angeles.
|
|
57
|
+
const now = new Date("2026-09-08T23:00:00Z");
|
|
58
|
+
expect(hourInTimeZone("UTC", now)).toBe(23);
|
|
59
|
+
expect(hourInTimeZone("America/Los_Angeles", now)).toBe(16);
|
|
60
|
+
expect(hourInTimeZone("America/New_York", now)).toBe(19);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("normalizes a 24-hour midnight encoding to 0", () => {
|
|
64
|
+
const midnightUtc = new Date("2026-09-08T00:00:00Z");
|
|
65
|
+
expect(hourInTimeZone("UTC", midnightUtc)).toBe(0);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
51
69
|
describe("expressionCarriesOwnTimezone", () => {
|
|
52
70
|
test("cron never carries its own zone", () => {
|
|
53
71
|
expect(expressionCarriesOwnTimezone("cron", "30 8 * * *")).toBe(false);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard over the schedule worker entrypoint's MCP bootstrap.
|
|
3
|
+
*
|
|
4
|
+
* The tool registry is process-local and MCP tools reach it only by connecting
|
|
5
|
+
* to each server and listing what it offers. `initializeTools()` does not do
|
|
6
|
+
* that: it loads core built-ins and workspace tools from disk. So a worker
|
|
7
|
+
* that stops connecting fails every `mcp__*` call an execute-mode schedule
|
|
8
|
+
* makes as "Unknown tool", while `assistant tools list` reads the daemon's
|
|
9
|
+
* registry over IPC and reports the same tool as registered. The mismatch is
|
|
10
|
+
* what makes the failure hard to place, so it is worth a guard.
|
|
11
|
+
*
|
|
12
|
+
* The worker is also long-lived, so the set it connects at startup goes stale
|
|
13
|
+
* as soon as the config moves. It watches for the daemon's reload signal to
|
|
14
|
+
* rebuild, which is covered here too.
|
|
15
|
+
*
|
|
16
|
+
* The entrypoint installs signal handlers and calls `process.exit`, so it
|
|
17
|
+
* cannot be imported into a test process; these assertions read its source the
|
|
18
|
+
* way `worker-feature-flags.test.ts` does. The behavior of the steps
|
|
19
|
+
* themselves is covered in `src/mcp/__tests__/startup.test.ts` and
|
|
20
|
+
* `src/mcp/__tests__/reload-signal.test.ts`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { describe, expect, test } from "bun:test";
|
|
26
|
+
|
|
27
|
+
const MCP_CALL = "startConfiguredMcpServers(";
|
|
28
|
+
|
|
29
|
+
/** The bounded wait that keeps unreachable servers off the critical path. */
|
|
30
|
+
const MCP_GRACE = "MCP_STARTUP_GRACE_MS";
|
|
31
|
+
|
|
32
|
+
/** The reload watcher that keeps the connected set current. */
|
|
33
|
+
const MCP_WATCH = "watchForMcpReload(";
|
|
34
|
+
|
|
35
|
+
/** The worker's first call that can execute a schedule. */
|
|
36
|
+
const WORK_START = "void tick()";
|
|
37
|
+
|
|
38
|
+
function readWorkerSource(): string {
|
|
39
|
+
return readFileSync(join(process.cwd(), "src/schedule/worker.ts"), "utf8");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("schedule worker MCP tools", () => {
|
|
43
|
+
test("connects its own MCP servers before the first schedule tick", () => {
|
|
44
|
+
const source = readWorkerSource();
|
|
45
|
+
const mcpAt = source.indexOf(MCP_CALL);
|
|
46
|
+
const workAt = source.indexOf(WORK_START);
|
|
47
|
+
|
|
48
|
+
expect(
|
|
49
|
+
mcpAt,
|
|
50
|
+
`The schedule worker must call \`${MCP_CALL})\` at startup. This ` +
|
|
51
|
+
"process hosts execute-mode schedule turns, and its tool registry is " +
|
|
52
|
+
"its own: without the call it has no mcp__* tools at all.",
|
|
53
|
+
).toBeGreaterThanOrEqual(0);
|
|
54
|
+
expect(workAt).toBeGreaterThanOrEqual(0);
|
|
55
|
+
expect(
|
|
56
|
+
mcpAt < workAt,
|
|
57
|
+
"The connect starts and is waited on before the first tick, so a " +
|
|
58
|
+
"schedule that fires immediately runs with the tools it was written " +
|
|
59
|
+
"against. The wait is bounded (see below), so a server that never " +
|
|
60
|
+
"answers delays it rather than blocking it.",
|
|
61
|
+
).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("bounds the startup wait so unrelated schedules still fire", () => {
|
|
65
|
+
const source = readWorkerSource();
|
|
66
|
+
|
|
67
|
+
expect(
|
|
68
|
+
source.includes(MCP_GRACE),
|
|
69
|
+
"`McpServerManager.start()` walks servers one at a time and allows " +
|
|
70
|
+
"each 30s to connect and 30s more to list its tools. An unbounded " +
|
|
71
|
+
"wait lets a few unreachable servers hold every due schedule, " +
|
|
72
|
+
"including the notify and script ones that never touch MCP, while " +
|
|
73
|
+
"the PID file already reports this worker as running.",
|
|
74
|
+
).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("rebuilds its MCP set when the daemon reports a reload", () => {
|
|
78
|
+
const source = readWorkerSource();
|
|
79
|
+
|
|
80
|
+
expect(
|
|
81
|
+
source.includes(MCP_WATCH),
|
|
82
|
+
"This process is long-lived and holds its own connections. Without " +
|
|
83
|
+
"the watcher, a server disabled or removed in the config stays " +
|
|
84
|
+
"registered and callable by background schedules until the worker " +
|
|
85
|
+
"restarts.",
|
|
86
|
+
).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -61,3 +61,28 @@ export function resolveScheduleTimezone(
|
|
|
61
61
|
null
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Wall-clock hour (0-23) of `now` in `timeZone`.
|
|
67
|
+
*
|
|
68
|
+
* Heartbeat active hours are stored as 0-23 integers. Evaluating them against
|
|
69
|
+
* the host clock (UTC on a managed container) shifts the window by the user's
|
|
70
|
+
* offset, so 8:00-22:00 becomes 1am-3pm Pacific. This reads the hour in the
|
|
71
|
+
* same zone `resolveScheduleTimezone` selected.
|
|
72
|
+
*/
|
|
73
|
+
export function hourInTimeZone(
|
|
74
|
+
timeZone: string,
|
|
75
|
+
now: Date = new Date(),
|
|
76
|
+
): number {
|
|
77
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
78
|
+
timeZone,
|
|
79
|
+
hourCycle: "h23",
|
|
80
|
+
hour: "numeric",
|
|
81
|
+
}).formatToParts(now);
|
|
82
|
+
const raw = Number(parts.find((part) => part.type === "hour")?.value);
|
|
83
|
+
if (!Number.isFinite(raw)) {
|
|
84
|
+
return now.getHours();
|
|
85
|
+
}
|
|
86
|
+
// Some runtimes emit "24" for midnight under hourCycle h23.
|
|
87
|
+
return raw === 24 ? 0 : raw;
|
|
88
|
+
}
|
|
@@ -10,6 +10,7 @@ import { processMessage } from "../daemon/process-message.js";
|
|
|
10
10
|
import { INTERNAL_GUARDIAN_TRUST_CONTEXT } from "../daemon/trust-context.js";
|
|
11
11
|
import { emitBackgroundFailureSignal } from "../notifications/background-failure-signal.js";
|
|
12
12
|
import { emitNotificationSignal } from "../notifications/emit-signal.js";
|
|
13
|
+
import { emitScheduleResultNotification } from "../notifications/schedule-result-producer.js";
|
|
13
14
|
import { getConversation } from "../persistence/conversation-crud.js";
|
|
14
15
|
import { isLifecycleQuiesced } from "../persistence/lifecycle-quiesce.js";
|
|
15
16
|
import { invalidateAssistantInferredItemsForConversation } from "../plugins/defaults/memory/task-memory-cleanup.js";
|
|
@@ -963,6 +964,11 @@ export async function runDueSchedulesOnce(
|
|
|
963
964
|
let failedTurn: TurnFailure | undefined;
|
|
964
965
|
const conversationReused = reusedConversationId != null;
|
|
965
966
|
let runConversationId = reusedConversationId;
|
|
967
|
+
// Captured before the run so the post-run "did this run notify?" probe has
|
|
968
|
+
// a lower bound. A reused conversation carries every prior run's
|
|
969
|
+
// notifications, and without this bound the first run to notify would
|
|
970
|
+
// silence the fallback for every run after it.
|
|
971
|
+
const runStartedAt = Date.now();
|
|
966
972
|
const runId = await createScheduleRun(job.id, reusedConversationId);
|
|
967
973
|
|
|
968
974
|
if (reusedConversationId) {
|
|
@@ -1067,6 +1073,19 @@ export async function runDueSchedulesOnce(
|
|
|
1067
1073
|
|
|
1068
1074
|
if (ok) {
|
|
1069
1075
|
await completeScheduleRun(runId, { status: "ok" });
|
|
1076
|
+
// The run succeeded; make sure it was not invisible. No-ops when the run
|
|
1077
|
+
// already notified or produced nothing user-facing, so a schedule whose
|
|
1078
|
+
// prompt ends in an explicit `assistant notifications send` is unaffected.
|
|
1079
|
+
// Awaited rather than fired and forgotten: the schedule worker can exit
|
|
1080
|
+
// once the tick's loop drains, and a detached emit would race that exit.
|
|
1081
|
+
await emitScheduleResultNotification({
|
|
1082
|
+
scheduleId: job.id,
|
|
1083
|
+
scheduleName: job.name,
|
|
1084
|
+
conversationId,
|
|
1085
|
+
runId,
|
|
1086
|
+
runStartedAt,
|
|
1087
|
+
rlog: log,
|
|
1088
|
+
});
|
|
1070
1089
|
if (isOneShot) {
|
|
1071
1090
|
await completeOneShot(job.id);
|
|
1072
1091
|
}
|
package/src/schedule/worker.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* user-facing traffic, and keep running during a main-thread freeze in the
|
|
12
12
|
* daemon.
|
|
13
13
|
*/
|
|
14
|
-
import { writeFileSync } from "node:fs";
|
|
14
|
+
import { watch, writeFileSync } from "node:fs";
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
17
|
initFeatureFlagOverrides,
|
|
@@ -20,12 +20,18 @@ import {
|
|
|
20
20
|
import { getConfig } from "../config/loader.js";
|
|
21
21
|
import { rehydratePlatformCredentials } from "../config/platform-rehydration.js";
|
|
22
22
|
import { startConversationEvictor } from "../daemon/conversation-evictor.js";
|
|
23
|
+
import { stopMcpServerManager } from "../mcp/manager.js";
|
|
24
|
+
import { MCP_RELOAD_SIGNAL_FILE } from "../mcp/reload-signal.js";
|
|
25
|
+
import {
|
|
26
|
+
restartConfiguredMcpServers,
|
|
27
|
+
startConfiguredMcpServers,
|
|
28
|
+
} from "../mcp/startup.js";
|
|
23
29
|
import { resetDb } from "../persistence/db-connection.js";
|
|
24
30
|
import { registerWorkerPluginSurface } from "../plugins/worker-plugin-surface.js";
|
|
25
31
|
import { disableStreamSeqStamping } from "../runtime/assistant-stream-state.js";
|
|
26
32
|
import { initializeTools } from "../tools/registry.js";
|
|
27
33
|
import { getLogger } from "../util/logger.js";
|
|
28
|
-
import { getScheduleWorkerPidPath } from "../util/platform.js";
|
|
34
|
+
import { getScheduleWorkerPidPath, getSignalsDir } from "../util/platform.js";
|
|
29
35
|
import {
|
|
30
36
|
cleanupWorkerPidFile,
|
|
31
37
|
startWorkerPidFileGuard,
|
|
@@ -40,6 +46,80 @@ const TICK_INTERVAL_MS = 15_000;
|
|
|
40
46
|
/** How often this process re-reads flag overrides from the gateway. */
|
|
41
47
|
const FLAG_REFRESH_INTERVAL_MS = 60_000;
|
|
42
48
|
|
|
49
|
+
/**
|
|
50
|
+
* How long startup waits for MCP servers before arming the tick.
|
|
51
|
+
*
|
|
52
|
+
* Connecting is worth waiting for: a schedule that fires first would fail its
|
|
53
|
+
* `mcp__*` calls as unknown tools. It is not worth waiting for without limit.
|
|
54
|
+
* `McpServerManager.start()` walks servers one at a time and allows each 30s to
|
|
55
|
+
* connect and 30s more to list its tools, so a handful of unreachable ones can
|
|
56
|
+
* hold the first tick for minutes, including the notify and script schedules
|
|
57
|
+
* that never touch MCP. Past this deadline the connect continues in the
|
|
58
|
+
* background and the tick starts without it.
|
|
59
|
+
*/
|
|
60
|
+
const MCP_STARTUP_GRACE_MS = 20_000;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Rebuild this process's MCP connections whenever the daemon reports a reload.
|
|
64
|
+
*
|
|
65
|
+
* Watches the signals directory rather than config.json: a reload also follows
|
|
66
|
+
* an OAuth reauthentication and a plugin install, neither of which edits that
|
|
67
|
+
* file, and the daemon already funnels all of them through one reload that
|
|
68
|
+
* writes this signal. Restarts are serialized and coalesced, so a burst of
|
|
69
|
+
* writes (an editor saving config.json, several servers reauthenticating)
|
|
70
|
+
* rebuilds once rather than once per event.
|
|
71
|
+
*
|
|
72
|
+
* Never throws: a signals directory that cannot be watched leaves this process
|
|
73
|
+
* on the servers it connected at startup, which is where it already was.
|
|
74
|
+
*/
|
|
75
|
+
function watchForMcpReload(): void {
|
|
76
|
+
const signalsDir = getSignalsDir();
|
|
77
|
+
let restarting: Promise<unknown> | null = null;
|
|
78
|
+
let restartQueued = false;
|
|
79
|
+
|
|
80
|
+
const restart = (): void => {
|
|
81
|
+
if (restarting) {
|
|
82
|
+
// A reload landed mid-rebuild, so the rebuild in flight may have read
|
|
83
|
+
// the config as it stood before. Run once more after it, not once per
|
|
84
|
+
// event that arrived while it ran.
|
|
85
|
+
restartQueued = true;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
restarting = restartConfiguredMcpServers()
|
|
89
|
+
.then((toolCount) => {
|
|
90
|
+
log.info({ toolCount }, "MCP servers reloaded in schedule worker");
|
|
91
|
+
})
|
|
92
|
+
.catch((err: unknown) => {
|
|
93
|
+
log.warn({ err }, "MCP reload failed in schedule worker");
|
|
94
|
+
})
|
|
95
|
+
.finally(() => {
|
|
96
|
+
restarting = null;
|
|
97
|
+
if (restartQueued) {
|
|
98
|
+
restartQueued = false;
|
|
99
|
+
restart();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const watcher = watch(signalsDir, (_eventType, filename) => {
|
|
106
|
+
if (String(filename ?? "") === MCP_RELOAD_SIGNAL_FILE) {
|
|
107
|
+
restart();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
watcher.on("error", (err) => {
|
|
111
|
+
log.warn({ err, signalsDir }, "MCP reload watcher failed");
|
|
112
|
+
});
|
|
113
|
+
watcher.unref();
|
|
114
|
+
log.info({ dir: signalsDir }, "Watching for MCP reload signals");
|
|
115
|
+
} catch (err) {
|
|
116
|
+
log.warn(
|
|
117
|
+
{ err, signalsDir },
|
|
118
|
+
"Failed to watch for MCP reload signals; MCP servers stay as connected at startup",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
43
123
|
async function main(): Promise<void> {
|
|
44
124
|
// Only the daemon stamps SSE seqs and writes the shared reservation file.
|
|
45
125
|
disableStreamSeqStamping();
|
|
@@ -102,6 +182,36 @@ async function main(): Promise<void> {
|
|
|
102
182
|
);
|
|
103
183
|
}
|
|
104
184
|
|
|
185
|
+
// Connect this process's own MCP servers. The tool registry is per-process
|
|
186
|
+
// and MCP tools reach it only by connecting to each server and listing what
|
|
187
|
+
// it offers, so `initializeTools()` above leaves them out: it loads core
|
|
188
|
+
// built-ins and workspace tools from disk and nothing else. Without this an
|
|
189
|
+
// execute-mode schedule fails every `mcp__*` call as "Unknown tool" while the
|
|
190
|
+
// daemon, which does connect at boot, lists the same tool as registered.
|
|
191
|
+
//
|
|
192
|
+
// Connecting here rather than borrowing the daemon's connections is what
|
|
193
|
+
// keeps this process independent of the daemon's event loop, which is the
|
|
194
|
+
// reason it is a separate process at all.
|
|
195
|
+
//
|
|
196
|
+
// Bounded by MCP_STARTUP_GRACE_MS: unreachable servers must not hold the
|
|
197
|
+
// schedules that do not use them. The connect runs on past the deadline.
|
|
198
|
+
const mcpStartup = startConfiguredMcpServers(getConfig().mcp);
|
|
199
|
+
await Promise.race([
|
|
200
|
+
mcpStartup,
|
|
201
|
+
// Unref'd so a connect that beats the deadline leaves nothing pending.
|
|
202
|
+
new Promise((resolve) => {
|
|
203
|
+
setTimeout(resolve, MCP_STARTUP_GRACE_MS).unref();
|
|
204
|
+
}),
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
// React to an MCP reload in the daemon. The server set belongs to the config
|
|
208
|
+
// and the daemon owns the watcher that notices it change; this process holds
|
|
209
|
+
// its own connections to the same servers, so without this it keeps serving
|
|
210
|
+
// whatever was configured when it started, including servers since disabled
|
|
211
|
+
// or removed. Best-effort: a signals directory that cannot be watched leaves
|
|
212
|
+
// this process on its startup connections until it restarts.
|
|
213
|
+
watchForMcpReload();
|
|
214
|
+
|
|
105
215
|
// Sweep idle conversations out of the in-memory pool. The daemon starts
|
|
106
216
|
// this at startup; this process hosts conversations too, so without it
|
|
107
217
|
// every scheduled run's conversation is retained for the process lifetime.
|
|
@@ -122,6 +232,13 @@ async function main(): Promise<void> {
|
|
|
122
232
|
clearInterval(flagRefreshTimer);
|
|
123
233
|
}
|
|
124
234
|
disposePidGuard?.();
|
|
235
|
+
// Best-effort, deliberately not awaited: this process exits immediately on
|
|
236
|
+
// a signal by design, and the exit is what everything else here relies on.
|
|
237
|
+
// Starting the close still lets a stdio server see its stdin shut rather
|
|
238
|
+
// than only noticing when the pipe breaks.
|
|
239
|
+
void stopMcpServerManager().catch((err) => {
|
|
240
|
+
log.warn({ err }, "MCP server shutdown failed (non-fatal)");
|
|
241
|
+
});
|
|
125
242
|
cleanupWorkerPidFile(pidPath);
|
|
126
243
|
process.exit(0);
|
|
127
244
|
};
|