@vellumai/assistant 0.11.4-staging.2 → 0.11.4-staging.4
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 +8 -2
- package/ARCHITECTURE.md +2 -0
- package/docs/architecture/memory.md +15 -0
- package/docs/browser-use-architecture-phase2.md +128 -56
- package/docs/flux-turn-detection-spike.md +11 -6
- package/docs/guardian-request-flow.md +35 -0
- package/knip.json +3 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/openapi.yaml +136 -76
- package/package.json +1 -1
- package/scripts/write-plugin-api-shim.ts +10 -0
- package/src/__tests__/app-control-flow.test.ts +1 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -2
- package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
- package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
- package/src/__tests__/compaction-events.test.ts +8 -10
- package/src/__tests__/conversation-agent-loop.test.ts +4 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +112 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +209 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +39 -62
- package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +53 -0
- package/src/__tests__/conversation-slash-queue.test.ts +3 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
- package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/document-sync-tags.test.ts +0 -75
- package/src/__tests__/file-ops-service.test.ts +163 -30
- package/src/__tests__/filesystem-tools.test.ts +23 -24
- package/src/__tests__/gateway-only-guard.test.ts +2 -5
- package/src/__tests__/host-file-read-tool.test.ts +16 -19
- package/src/__tests__/http-user-message-parity.test.ts +1 -1
- package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
- package/src/__tests__/media-generate-image.test.ts +131 -21
- package/src/__tests__/memory-retrieval-hook.test.ts +94 -2
- package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -3
- package/src/__tests__/proxy-approval-callback.test.ts +1 -0
- package/src/__tests__/qdrant-manager.test.ts +14 -1
- package/src/__tests__/run-due-schedules.test.ts +21 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
- package/src/__tests__/schedule-routes.test.ts +23 -0
- package/src/__tests__/schedule-store.test.ts +17 -0
- package/src/__tests__/secret-ingress-http.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +3 -3
- package/src/__tests__/starter-task-flow.test.ts +5 -4
- package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
- package/src/__tests__/subagent-spawn-and-await.test.ts +4 -7
- package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
- package/src/__tests__/subagent-tools.test.ts +81 -101
- package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
- package/src/__tests__/tool-executor.test.ts +5 -1
- package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
- package/src/__tests__/ui-visual-surface.test.ts +1 -1
- package/src/__tests__/ui-voice-picker-surface.test.ts +1 -1
- package/src/__tests__/ui-work-result-surface.test.ts +1 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
- package/src/__tests__/voice-session-bridge.test.ts +85 -29
- package/src/acp/session-manager.ts +8 -1
- package/src/api/events/host-file.ts +2 -2
- package/src/api/surfaces.ts +5 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +21 -10
- package/src/calls/__tests__/voice-triage-escalate.test.ts +8 -0
- package/src/calls/voice-session-bridge.ts +44 -25
- package/src/calls/voice-triage-escalate.ts +1 -0
- package/src/cli/bundled-modules.ts +29 -0
- package/src/cli/commands/db/repair.ts +4 -8
- package/src/cli/commands/domain.ts +6 -3
- package/src/cli/commands/email.ts +6 -3
- package/src/cli/commands/keys.ts +8 -3
- package/src/cli/commands/plugins.ts +85 -36
- package/src/cli/commands/schedules.ts +35 -1
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/config/__tests__/balanced-model-experiment.test.ts +278 -0
- package/src/config/assistant-feature-flags.ts +36 -15
- package/src/config/balanced-model-experiment.ts +35 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +5 -4
- package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
- package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
- package/src/config/bundled-skills/subagent/SKILL.md +17 -12
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
- package/src/config/call-site-defaults.ts +7 -0
- package/src/config/default-profile-catalog.ts +96 -4
- package/src/config/feature-flag-registry.json +11 -11
- package/src/config/skills.ts +9 -2
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
- package/src/daemon/conversation-agent-loop.ts +14 -13
- package/src/daemon/conversation-notifiers.ts +11 -9
- package/src/daemon/conversation-process.ts +0 -27
- package/src/daemon/conversation-runtime-assembly.ts +9 -2
- package/src/daemon/conversation-store.ts +4 -4
- package/src/daemon/conversation-surfaces.ts +27 -13
- package/src/daemon/conversation-tool-setup.ts +2 -4
- package/src/daemon/conversation.ts +104 -51
- package/src/daemon/doordash-steps.ts +2 -2
- package/src/daemon/lifecycle.ts +14 -1
- package/src/daemon/process-message.ts +0 -13
- package/src/daemon/windows-compiled-entry.ts +4 -0
- package/src/documents/document-store.ts +5 -235
- package/src/hooks/types.ts +5 -0
- package/src/ipc/gateway-flag-listener.ts +17 -3
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +118 -0
- package/src/live-voice/live-voice-manager.ts +16 -3
- package/src/live-voice/live-voice-session.ts +63 -9
- package/src/live-voice/windows-compiled-live-voice.ts +4 -0
- package/src/monitoring/control.ts +1 -0
- package/src/monitoring/db-integrity-sample.ts +4 -5
- package/src/notifications/AGENTS.md +2 -0
- package/src/notifications/approval-card-data.ts +33 -0
- package/src/permissions/prompter.ts +1 -5
- package/src/persistence/conversation-queries.ts +66 -16
- package/src/persistence/embeddings/qdrant-manager.ts +84 -49
- package/src/persistence/migrations/360-add-document-workspace-path.ts +5 -14
- package/src/persistence/schema/documents.ts +4 -4
- package/src/plugin-api/constants.ts +8 -0
- package/src/plugin-api/index.ts +5 -1
- package/src/plugin-api/webhook-url.ts +13 -11
- package/src/plugins/defaults/main.ts +6 -7
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
- package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +4 -4
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +18 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
- package/src/plugins/defaults/memory/v3/injector.ts +8 -0
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +23 -9
- package/src/plugins/defaults/memory/worker-control.ts +1 -0
- package/src/plugins/defaults/worker-entrypoints.ts +3 -0
- package/src/plugins/mtime-cache.ts +17 -0
- package/src/prompts/templates/system-sections.ts +0 -7
- package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
- package/src/providers/__tests__/retry-callsite.test.ts +20 -0
- package/src/providers/openai/chat-completions-provider.ts +11 -1
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +11 -6
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +9 -49
- package/src/routes/control.ts +1 -0
- package/src/routes/route-host-client.ts +1 -0
- package/src/runtime/AGENTS.md +16 -17
- package/src/runtime/agent-wake.ts +15 -12
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +170 -1
- package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
- package/src/runtime/routes/conversation-list-routes.ts +54 -22
- package/src/runtime/routes/conversation-management-routes.ts +2 -3
- package/src/runtime/routes/conversation-routes.ts +11 -13
- package/src/runtime/routes/documents-routes.ts +3 -222
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +2 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
- package/src/runtime/routes/playground/inject-failures.ts +2 -2
- package/src/runtime/routes/playground/reset-circuit.ts +1 -1
- package/src/runtime/routes/schedule-routes.ts +94 -7
- package/src/runtime/routes/workspace-routes.ts +0 -9
- package/src/runtime/routes/workspace-utils.ts +3 -13
- package/src/runtime/services/conversation-serializer.ts +7 -2
- package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
- package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
- package/src/schedule/plugin-schedule-availability.ts +58 -0
- package/src/schedule/plugin-schedule-declarations.ts +23 -27
- package/src/schedule/plugin-schedule-reconciler.ts +12 -3
- package/src/schedule/schedule-store.ts +5 -1
- package/src/schedule/scheduler.ts +9 -4
- package/src/schedule/worker-control.ts +1 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +26 -15
- package/src/subagent/consult-context.ts +11 -11
- package/src/subagent/consult-prompt.ts +26 -35
- package/src/subagent/manager.ts +20 -37
- package/src/subagent/notify.ts +7 -1
- package/src/subagent/types.ts +15 -13
- package/src/tools/__tests__/tool-input-schemas.test.ts +7 -7
- package/src/tools/acp/spawn.ts +6 -4
- package/src/tools/filesystem/read.ts +27 -10
- package/src/tools/host-filesystem/read.ts +27 -15
- package/src/tools/shared/filesystem/file-ops-service.ts +63 -35
- package/src/tools/shared/filesystem/legacy-read-args.ts +22 -0
- package/src/tools/shared/filesystem/types.ts +5 -5
- package/src/tools/skills/scaffold-managed.ts +25 -7
- package/src/tools/subagent/spawn.ts +28 -88
- package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
- package/src/util/__tests__/worker-process-command.test.ts +37 -0
- package/src/util/logger.ts +16 -0
- package/src/util/worker-process.ts +37 -4
- package/src/windows-compiled-cli.ts +32 -0
- package/src/windows-compiled-entry.ts +4 -0
- package/src/windows-compiled-logger.ts +6 -0
- package/src/windows-compiled-worker-entry.ts +29 -0
- package/src/__tests__/document-workspace-file.test.ts +0 -467
- package/src/daemon/interactive-turn-sender.ts +0 -59
- package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
- package/src/subagent/consult-transcript.ts +0 -90
|
@@ -72,7 +72,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
72
72
|
if (circuitOpenForMs === 0) {
|
|
73
73
|
if (circuit.compactionCircuitOpenUntil !== null) {
|
|
74
74
|
circuit.compactionCircuitOpenUntil = null;
|
|
75
|
-
conversation.
|
|
75
|
+
conversation.emit({
|
|
76
76
|
type: "compaction_circuit_closed",
|
|
77
77
|
conversationId: conversation.conversationId,
|
|
78
78
|
});
|
|
@@ -80,7 +80,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
80
80
|
} else {
|
|
81
81
|
const openUntil = Date.now() + circuitOpenForMs;
|
|
82
82
|
circuit.compactionCircuitOpenUntil = openUntil;
|
|
83
|
-
conversation.
|
|
83
|
+
conversation.emit({
|
|
84
84
|
type: "compaction_circuit_open",
|
|
85
85
|
conversationId: conversation.conversationId,
|
|
86
86
|
reason: "3_consecutive_failures",
|
|
@@ -52,7 +52,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
52
52
|
circuit.consecutiveCompactionFailures = 0;
|
|
53
53
|
if (circuit.compactionCircuitOpenUntil !== null) {
|
|
54
54
|
circuit.compactionCircuitOpenUntil = null;
|
|
55
|
-
conversation.
|
|
55
|
+
conversation.emit({
|
|
56
56
|
type: "compaction_circuit_closed",
|
|
57
57
|
conversationId: conversation.conversationId,
|
|
58
58
|
});
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* the shared ROUTES array.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { statSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
8
11
|
import { z } from "zod";
|
|
9
12
|
|
|
10
13
|
import { getOrCreateConversation } from "../../daemon/conversation-store.js";
|
|
@@ -15,9 +18,10 @@ import {
|
|
|
15
18
|
getUsageCostForRun,
|
|
16
19
|
listRunConversationIds,
|
|
17
20
|
} from "../../persistence/llm-usage-store.js";
|
|
21
|
+
import { isPluginDisabled } from "../../plugins/disabled-state.js";
|
|
18
22
|
import { isDeferSchedule } from "../../schedule/defer-provenance.js";
|
|
19
23
|
import { validateScheduleInferenceProfile } from "../../schedule/inference-profile.js";
|
|
20
|
-
import {
|
|
24
|
+
import { pluginScheduleSourceAvailable } from "../../schedule/plugin-schedule-availability.js";
|
|
21
25
|
import { isPluginSchedulesEnabled } from "../../schedule/plugin-schedules-gate.js";
|
|
22
26
|
import {
|
|
23
27
|
describeRRuleExpression,
|
|
@@ -38,6 +42,7 @@ import {
|
|
|
38
42
|
getLastScheduleConversationId,
|
|
39
43
|
getSchedule,
|
|
40
44
|
getScheduleRuns,
|
|
45
|
+
isEngineLatched,
|
|
41
46
|
listSchedules,
|
|
42
47
|
resolveScheduleConversationGroupId,
|
|
43
48
|
type ScheduleJob,
|
|
@@ -49,6 +54,7 @@ import { buildWakeScheduleOptions } from "../../schedule/wake-schedule-options.j
|
|
|
49
54
|
import { initializeTools } from "../../tools/registry.js";
|
|
50
55
|
import { UserError } from "../../util/errors.js";
|
|
51
56
|
import { getLogger } from "../../util/logger.js";
|
|
57
|
+
import { getWorkspacePluginsDir } from "../../util/platform.js";
|
|
52
58
|
import { normalizeCapabilityManifest } from "../../workflows/capabilities.js";
|
|
53
59
|
import { getWorkflowRunManager } from "../../workflows/run-manager.js";
|
|
54
60
|
import { isOwnerCaller } from "../auth/owner-caller.js";
|
|
@@ -108,6 +114,76 @@ async function assertWakeMutationAllowed(
|
|
|
108
114
|
// Response schemas (shared by all schedule routes)
|
|
109
115
|
// ---------------------------------------------------------------------------
|
|
110
116
|
|
|
117
|
+
const DISARM_REASONS = [
|
|
118
|
+
"user_disabled",
|
|
119
|
+
"plugin_removed",
|
|
120
|
+
"plugin_disabled",
|
|
121
|
+
"declaration_removed",
|
|
122
|
+
"declaration_disabled",
|
|
123
|
+
] as const;
|
|
124
|
+
|
|
125
|
+
type DisarmReason = (typeof DISARM_REASONS)[number];
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Why a plugin-sourced schedule sits disarmed, so a client can say so instead
|
|
129
|
+
* of showing an off row with no explanation.
|
|
130
|
+
*
|
|
131
|
+
* Only a sourced row that is off can have one, which also bounds the disk
|
|
132
|
+
* probes below to those rows. The first matching cause wins, ordered so the
|
|
133
|
+
* most specific answer comes first: the user's own override outranks anything
|
|
134
|
+
* the plugin's files say, a plugin that is gone outranks one that is merely
|
|
135
|
+
* disabled, and a declaration that is gone outranks one that is still there.
|
|
136
|
+
* `declaration_disabled` is the fallback the plugin's own files account for:
|
|
137
|
+
* a declared `enabled: false`, a declaration too broken to arm, or a manifest
|
|
138
|
+
* that no longer parses.
|
|
139
|
+
*
|
|
140
|
+
* Two off states never get a reason, because in neither did the plugin's files
|
|
141
|
+
* turn the schedule off. A row the engine latched (a fired or cancelled
|
|
142
|
+
* one-shot, a bounded recurrence the claim path exhausted) is a schedule that
|
|
143
|
+
* finished, not one that is paused. And when the `plugin-schedules` kill switch
|
|
144
|
+
* is off the reconciler disarms every sourced row regardless of what its
|
|
145
|
+
* declaration says, so no per-row cause is the true one. Both return null, and
|
|
146
|
+
* clients show the row like any other schedule that is off.
|
|
147
|
+
*/
|
|
148
|
+
function deriveDisarmReason(
|
|
149
|
+
job: Pick<
|
|
150
|
+
ScheduleJob,
|
|
151
|
+
| "sourceKey"
|
|
152
|
+
| "enabled"
|
|
153
|
+
| "userEnabled"
|
|
154
|
+
| "status"
|
|
155
|
+
| "nextRunAt"
|
|
156
|
+
| "lastRunAt"
|
|
157
|
+
>,
|
|
158
|
+
): DisarmReason | null {
|
|
159
|
+
if (job.sourceKey === null || job.enabled) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
if (job.userEnabled === false) {
|
|
163
|
+
return "user_disabled";
|
|
164
|
+
}
|
|
165
|
+
if (isEngineLatched(job) || !isPluginSchedulesEnabled()) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const match = /^plugin:([^/]+)\/(.+)$/.exec(job.sourceKey);
|
|
169
|
+
if (!match) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const [, pluginName, scheduleName] = match;
|
|
173
|
+
const pluginDir = join(getWorkspacePluginsDir(), pluginName!);
|
|
174
|
+
if (!statSync(pluginDir, { throwIfNoEntry: false })?.isDirectory()) {
|
|
175
|
+
return "plugin_removed";
|
|
176
|
+
}
|
|
177
|
+
if (isPluginDisabled(pluginName!)) {
|
|
178
|
+
return "plugin_disabled";
|
|
179
|
+
}
|
|
180
|
+
const declarationDir = join(pluginDir, "schedules", scheduleName!);
|
|
181
|
+
if (!statSync(declarationDir, { throwIfNoEntry: false })?.isDirectory()) {
|
|
182
|
+
return "declaration_removed";
|
|
183
|
+
}
|
|
184
|
+
return "declaration_disabled";
|
|
185
|
+
}
|
|
186
|
+
|
|
111
187
|
const scheduleSchema = z.object({
|
|
112
188
|
id: z.string(),
|
|
113
189
|
name: z.string(),
|
|
@@ -150,6 +226,12 @@ const scheduleSchema = z.object({
|
|
|
150
226
|
.describe(
|
|
151
227
|
"User enable/disable override on a plugin-sourced schedule; null when the declaration's own enabled value applies. Always null for user-created schedules.",
|
|
152
228
|
),
|
|
229
|
+
disarmReason: z
|
|
230
|
+
.enum(DISARM_REASONS)
|
|
231
|
+
.nullable()
|
|
232
|
+
.describe(
|
|
233
|
+
"Why a plugin-sourced schedule is off: the user turned it off, the plugin is gone, the plugin is disabled, the declaration is gone, or the plugin's own files turned it off. Null whenever the schedule is on, and always null for user-created schedules.",
|
|
234
|
+
),
|
|
153
235
|
isOneShot: z.boolean(),
|
|
154
236
|
// A deferred wake ("remind me about this tomorrow") is an ordinary schedule
|
|
155
237
|
// row created by the defer path, distinguishable only by `createdBy`, which
|
|
@@ -302,6 +384,7 @@ function serializeSchedule(
|
|
|
302
384
|
workflowName: j.workflowName,
|
|
303
385
|
sourceKey: j.sourceKey,
|
|
304
386
|
userEnabled: j.userEnabled,
|
|
387
|
+
disarmReason: deriveDisarmReason(j),
|
|
305
388
|
isOneShot: isOneShotForDisplay(j),
|
|
306
389
|
isDeferred: isDeferSchedule(j.createdBy),
|
|
307
390
|
};
|
|
@@ -1240,15 +1323,19 @@ async function handleRunScheduleNow(
|
|
|
1240
1323
|
|
|
1241
1324
|
// A plugin-sourced row runs the plugin's own script or prompt, so run-now is
|
|
1242
1325
|
// only offered while that plugin is something the runtime would activate.
|
|
1243
|
-
// `
|
|
1244
|
-
// a disabled plugin, an unreadable or invalid manifest, and a
|
|
1245
|
-
// that is simply gone. Turning the feature flag off retires the
|
|
1246
|
-
// wholesale and takes the same path. The row can still be armed at
|
|
1247
|
-
// point, because the reconciler that disarms it runs on its own
|
|
1326
|
+
// `pluginScheduleSourceAvailable` covers a plugin the daemon never
|
|
1327
|
+
// activated, a disabled plugin, an unreadable or invalid manifest, and a
|
|
1328
|
+
// declaration that is simply gone. Turning the feature flag off retires the
|
|
1329
|
+
// surface wholesale and takes the same path. The row can still be armed at
|
|
1330
|
+
// this point, because the reconciler that disarms it runs on its own
|
|
1331
|
+
// schedule.
|
|
1332
|
+
// Routes are served by the daemon, so the activation half of the probe reads
|
|
1333
|
+
// a live ledger here. Paths that run in worker processes cannot ask that
|
|
1334
|
+
// question and use the on-disk probe alone.
|
|
1248
1335
|
if (
|
|
1249
1336
|
schedule.sourceKey !== null &&
|
|
1250
1337
|
(!isPluginSchedulesEnabled() ||
|
|
1251
|
-
!(await
|
|
1338
|
+
!(await pluginScheduleSourceAvailable(schedule.sourceKey)))
|
|
1252
1339
|
) {
|
|
1253
1340
|
throw new BadRequestError(
|
|
1254
1341
|
"This schedule's plugin is disabled or no longer declares it, so it cannot be run.",
|
|
@@ -19,7 +19,6 @@ import { basename, dirname, join } from "node:path";
|
|
|
19
19
|
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
|
|
22
|
-
import { rebindDocumentsToRenamedPath } from "../../documents/document-store.js";
|
|
23
22
|
import { getWorkspaceDir } from "../../util/platform.js";
|
|
24
23
|
import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
|
|
25
24
|
import { publishSoundsConfigUpdated } from "../sync/resource-sync-events.js";
|
|
@@ -35,7 +34,6 @@ import {
|
|
|
35
34
|
isTextMimeType,
|
|
36
35
|
MAX_INLINE_TEXT_SIZE,
|
|
37
36
|
resolveWorkspacePath,
|
|
38
|
-
toWorkspaceRelativePath,
|
|
39
37
|
writeWorkspaceFile,
|
|
40
38
|
} from "./workspace-utils.js";
|
|
41
39
|
|
|
@@ -505,13 +503,6 @@ function handleWorkspaceRename({ body, headers }: RouteHandlerArgs) {
|
|
|
505
503
|
|
|
506
504
|
mkdirSync(dirname(resolvedNew), { recursive: true });
|
|
507
505
|
renameSync(resolvedOld, resolvedNew);
|
|
508
|
-
// File-backed documents key off the canonical relative path, so they follow
|
|
509
|
-
// the file to its new name. The request strings are not canonical, hence the
|
|
510
|
-
// resolved paths.
|
|
511
|
-
rebindDocumentsToRenamedPath({
|
|
512
|
-
oldPath: toWorkspaceRelativePath(resolvedOld),
|
|
513
|
-
newPath: toWorkspaceRelativePath(resolvedNew),
|
|
514
|
-
});
|
|
515
506
|
publishSoundsConfigUpdatedForPaths(
|
|
516
507
|
[oldPath, newPath],
|
|
517
508
|
headers?.["x-vellum-client-id"]?.trim() || undefined,
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
statSync,
|
|
7
7
|
writeFileSync,
|
|
8
8
|
} from "node:fs";
|
|
9
|
-
import { dirname,
|
|
9
|
+
import { dirname, resolve, sep } from "node:path";
|
|
10
10
|
|
|
11
11
|
import { getWorkspaceDir } from "../../util/platform.js";
|
|
12
12
|
import { BadRequestError, ConflictError } from "./errors.js";
|
|
@@ -85,22 +85,12 @@ export function resolveWorkspacePath(
|
|
|
85
85
|
return resolved;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
/**
|
|
89
|
-
* Normalise a workspace path to the canonical relative form used as a stable
|
|
90
|
-
* key for the file — forward slashes, no `.`/`..` segments, no leading slash.
|
|
91
|
-
* `absolutePath` must already have passed {@link resolveWorkspacePath}.
|
|
92
|
-
*/
|
|
93
|
-
export function toWorkspaceRelativePath(absolutePath: string): string {
|
|
94
|
-
return relative(getWorkspaceDir(), absolutePath).split(sep).join("/");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
88
|
/**
|
|
98
89
|
* Write bytes to a workspace-relative path, enforcing the workspace boundary.
|
|
99
90
|
*
|
|
100
91
|
* This is the one write primitive behind every daemon-side workspace file
|
|
101
|
-
* write
|
|
102
|
-
*
|
|
103
|
-
* directory-conflict rules cannot drift apart.
|
|
92
|
+
* write, so the traversal and directory-conflict rules cannot drift apart
|
|
93
|
+
* between callers.
|
|
104
94
|
*
|
|
105
95
|
* Returns the number of bytes written. Throws {@link BadRequestError} when the
|
|
106
96
|
* path escapes the workspace and {@link ConflictError} when it names a
|
|
@@ -244,13 +244,18 @@ export function serializeConversationSummary(params: {
|
|
|
244
244
|
|
|
245
245
|
/**
|
|
246
246
|
* Build a full conversation detail response from a conversation ID.
|
|
247
|
-
* Returns null if the conversation doesn't exist
|
|
247
|
+
* Returns null if the conversation doesn't exist, or if it is a legacy
|
|
248
|
+
* `private` row: every listing hides those by type (see
|
|
249
|
+
* `standardListingVisibilitySql`) and migration cleanup deletes them, and the
|
|
250
|
+
* wire type collapses the value to `standard`, so serving one by id would let
|
|
251
|
+
* a client that only holds a stale id (a persisted last-viewed conversation)
|
|
252
|
+
* resurrect a row it can never see listed.
|
|
248
253
|
*/
|
|
249
254
|
export function buildConversationDetailResponse(
|
|
250
255
|
conversationId: string,
|
|
251
256
|
): { conversation: ReturnType<typeof serializeConversationSummary> } | null {
|
|
252
257
|
const conversation = getConversation(conversationId);
|
|
253
|
-
if (!conversation) {
|
|
258
|
+
if (!conversation || conversation.conversationType === "private") {
|
|
254
259
|
return null;
|
|
255
260
|
}
|
|
256
261
|
|
|
@@ -487,11 +487,39 @@ describe("parsePluginScheduleDeclarations", () => {
|
|
|
487
487
|
expect(after).not.toBe(before);
|
|
488
488
|
});
|
|
489
489
|
|
|
490
|
-
test("changes when
|
|
490
|
+
test("changes when config.json is edited", () => {
|
|
491
|
+
const pluginDir = makePlugin(FULL_DIGEST);
|
|
492
|
+
const before = parsePluginScheduleDeclarations(pluginDir, "p")
|
|
493
|
+
.declarations[0]!.definitionHash;
|
|
494
|
+
writeFileSync(
|
|
495
|
+
join(pluginDir, "schedules", "digest", "config.json"),
|
|
496
|
+
JSON.stringify({ expression: "0 10 * * *" }),
|
|
497
|
+
);
|
|
498
|
+
const after = parsePluginScheduleDeclarations(pluginDir, "p")
|
|
499
|
+
.declarations[0]!.definitionHash;
|
|
500
|
+
expect(after).not.toBe(before);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("changes when the index.sh entrypoint is edited", () => {
|
|
504
|
+
const pluginDir = makePlugin({
|
|
505
|
+
"backup/config.json": JSON.stringify({ expression: "0 3 * * *" }),
|
|
506
|
+
"backup/index.sh": "echo v1\n",
|
|
507
|
+
});
|
|
508
|
+
const parse = () =>
|
|
509
|
+
parsePluginScheduleDeclarations(pluginDir, "p").declarations[0]!
|
|
510
|
+
.definitionHash;
|
|
511
|
+
const before = parse();
|
|
512
|
+
writeFileSync(
|
|
513
|
+
join(pluginDir, "schedules", "backup", "index.sh"),
|
|
514
|
+
"echo v2\n",
|
|
515
|
+
);
|
|
516
|
+
expect(parse()).not.toBe(before);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test("ignores every other file under the declaration directory", () => {
|
|
491
520
|
const pluginDir = makePlugin({
|
|
492
521
|
"backup/config.json": JSON.stringify({ expression: "0 3 * * *" }),
|
|
493
522
|
"backup/index.sh": "sh lib/helper.sh\n",
|
|
494
|
-
"backup/lib/helper.sh": "echo v1\n",
|
|
495
523
|
});
|
|
496
524
|
const parse = () =>
|
|
497
525
|
parsePluginScheduleDeclarations(pluginDir, "p").declarations[0]!
|
|
@@ -505,15 +533,50 @@ describe("parsePluginScheduleDeclarations", () => {
|
|
|
505
533
|
"lib",
|
|
506
534
|
"helper.sh",
|
|
507
535
|
);
|
|
536
|
+
mkdirSync(dirname(helperPath), { recursive: true });
|
|
537
|
+
writeFileSync(helperPath, "echo v1\n");
|
|
538
|
+
const statePath = join(
|
|
539
|
+
pluginDir,
|
|
540
|
+
"schedules",
|
|
541
|
+
"backup",
|
|
542
|
+
"data",
|
|
543
|
+
"state.json",
|
|
544
|
+
);
|
|
545
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
546
|
+
writeFileSync(statePath, JSON.stringify({ lastRun: 1 }));
|
|
547
|
+
expect(parse()).toBe(original);
|
|
548
|
+
|
|
508
549
|
writeFileSync(helperPath, "echo v2\n");
|
|
509
|
-
|
|
510
|
-
expect(
|
|
550
|
+
writeFileSync(statePath, JSON.stringify({ lastRun: 2 }));
|
|
551
|
+
expect(parse()).toBe(original);
|
|
511
552
|
|
|
512
553
|
renameSync(
|
|
513
554
|
helperPath,
|
|
514
555
|
join(pluginDir, "schedules", "backup", "lib", "helper2.sh"),
|
|
515
556
|
);
|
|
516
|
-
expect(parse()).
|
|
557
|
+
expect(parse()).toBe(original);
|
|
558
|
+
|
|
559
|
+
rmSync(join(pluginDir, "schedules", "backup", "data"), {
|
|
560
|
+
recursive: true,
|
|
561
|
+
force: true,
|
|
562
|
+
});
|
|
563
|
+
expect(parse()).toBe(original);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test("hashes a two-file declaration to its known value", () => {
|
|
567
|
+
const pluginDir = makePlugin({
|
|
568
|
+
"backup/config.json": JSON.stringify({ expression: "0 3 * * *" }),
|
|
569
|
+
"backup/index.sh": "echo backup\n",
|
|
570
|
+
});
|
|
571
|
+
// Pinned so stored rows keep their hash across upgrades. It holds only
|
|
572
|
+
// while the hashed paths stay `<name>/config.json` and
|
|
573
|
+
// `<name>/<entrypoint>`, relative to schedules/.
|
|
574
|
+
expect(
|
|
575
|
+
parsePluginScheduleDeclarations(pluginDir, "p").declarations[0]!
|
|
576
|
+
.definitionHash,
|
|
577
|
+
).toBe(
|
|
578
|
+
"0b5722fc45abdd2c46672f275e3bcf548016ee5c0722da5a0588ef45fa7bbcab",
|
|
579
|
+
);
|
|
517
580
|
});
|
|
518
581
|
|
|
519
582
|
test("differs between two schedules with different names but same content", () => {
|
|
@@ -45,9 +45,29 @@ mock.module("../../notifications/emit-signal.js", () => ({
|
|
|
45
45
|
},
|
|
46
46
|
}));
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Plugin directories the daemon has not activated. The reconciler arms only
|
|
50
|
+
* plugins this process brought up, and no test here runs the plugin loader, so
|
|
51
|
+
* the double answers "activated" unless a case says otherwise.
|
|
52
|
+
*/
|
|
53
|
+
const notActivatedDirs = new Set<string>();
|
|
54
|
+
|
|
55
|
+
/** Every directory the reconciler put through the activation predicate. */
|
|
56
|
+
const activationProbes: string[] = [];
|
|
57
|
+
|
|
58
|
+
const realMtimeCache = await import("../../plugins/mtime-cache.js");
|
|
59
|
+
mock.module("../../plugins/mtime-cache.js", () => ({
|
|
60
|
+
...realMtimeCache,
|
|
61
|
+
isPluginDirActivated: (dir: string) => {
|
|
62
|
+
activationProbes.push(dir);
|
|
63
|
+
return !notActivatedDirs.has(dir);
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
66
|
+
|
|
48
67
|
import { setOverridesForTesting } from "../../__tests__/feature-flag-test-helpers.js";
|
|
49
68
|
import { getDb } from "../../persistence/db-connection.js";
|
|
50
69
|
import { initializeDb } from "../../persistence/db-init.js";
|
|
70
|
+
import { listInstalledPluginDirs } from "../../plugins/installed-plugin-dirs.js";
|
|
51
71
|
import { getWorkspacePluginsDir } from "../../util/platform.js";
|
|
52
72
|
import {
|
|
53
73
|
reconcilePluginSchedules,
|
|
@@ -175,6 +195,8 @@ function resetReconcilerFixtures(): void {
|
|
|
175
195
|
emittedSignals.length = 0;
|
|
176
196
|
emitResultOverride = null;
|
|
177
197
|
emitGate = null;
|
|
198
|
+
notActivatedDirs.clear();
|
|
199
|
+
activationProbes.length = 0;
|
|
178
200
|
resetDefinitionErrorEmitGuardForTests();
|
|
179
201
|
}
|
|
180
202
|
|
|
@@ -970,6 +992,65 @@ describe("reconcilePluginSchedules", () => {
|
|
|
970
992
|
expect(payload.reason).toContain("no upcoming occurrences");
|
|
971
993
|
});
|
|
972
994
|
|
|
995
|
+
test("a plugin the daemon never activated arms nothing", async () => {
|
|
996
|
+
// Everything the parser needs is on disk; only the activation is missing,
|
|
997
|
+
// which is what a directory dropped into the plugins root looks like.
|
|
998
|
+
const dir = writePlugin("news", digest());
|
|
999
|
+
notActivatedDirs.add(dir);
|
|
1000
|
+
|
|
1001
|
+
await reconcilePluginSchedules();
|
|
1002
|
+
|
|
1003
|
+
expect(listDeclaredSchedules()).toHaveLength(0);
|
|
1004
|
+
expect(emittedSignals).toHaveLength(0);
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
test("a row disarms once its plugin is no longer activated", async () => {
|
|
1008
|
+
const dir = writePlugin("news", digest());
|
|
1009
|
+
await reconcilePluginSchedules();
|
|
1010
|
+
const created = listDeclaredSchedules()[0]!;
|
|
1011
|
+
expect(created.enabled).toBe(true);
|
|
1012
|
+
|
|
1013
|
+
notActivatedDirs.add(dir);
|
|
1014
|
+
await reconcilePluginSchedules();
|
|
1015
|
+
|
|
1016
|
+
const row = listDeclaredSchedules()[0]!;
|
|
1017
|
+
expect(row.id).toBe(created.id);
|
|
1018
|
+
expect(row.enabled).toBe(false);
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
test("a plugin activated between passes arms on the second one", async () => {
|
|
1022
|
+
const dir = writePlugin("news", digest());
|
|
1023
|
+
notActivatedDirs.add(dir);
|
|
1024
|
+
await reconcilePluginSchedules();
|
|
1025
|
+
expect(listDeclaredSchedules()).toHaveLength(0);
|
|
1026
|
+
|
|
1027
|
+
notActivatedDirs.delete(dir);
|
|
1028
|
+
await reconcilePluginSchedules();
|
|
1029
|
+
|
|
1030
|
+
const rows = listDeclaredSchedules();
|
|
1031
|
+
expect(rows).toHaveLength(1);
|
|
1032
|
+
expect(rows[0]!.sourceKey).toBe(DIGEST_KEY);
|
|
1033
|
+
expect(rows[0]!.enabled).toBe(true);
|
|
1034
|
+
// The row arms for the first time here, so it announces its arrival.
|
|
1035
|
+
const declared = emittedSignals.filter(
|
|
1036
|
+
(s) => s.sourceEventName === "schedule.declared",
|
|
1037
|
+
);
|
|
1038
|
+
expect(declared).toHaveLength(1);
|
|
1039
|
+
expect(declared[0]!.sourceContextId).toBe(DIGEST_KEY);
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
test("the activation probe sees the directory string the walk yields", async () => {
|
|
1043
|
+
// The predicate is keyed by directory path, so the enumeration the
|
|
1044
|
+
// reconciler walks and the map the daemon activates into have to agree on
|
|
1045
|
+
// the exact string. Both come from `listInstalledPluginDirs`.
|
|
1046
|
+
const dir = writePlugin("news", digest());
|
|
1047
|
+
|
|
1048
|
+
await reconcilePluginSchedules();
|
|
1049
|
+
|
|
1050
|
+
expect(listInstalledPluginDirs().map((p) => p.dir)).toEqual([dir]);
|
|
1051
|
+
expect(activationProbes).toEqual([dir]);
|
|
1052
|
+
});
|
|
1053
|
+
|
|
973
1054
|
test("a suppressed error emit still latches the day guard", async () => {
|
|
974
1055
|
writePlugin("news", brokenDigest());
|
|
975
1056
|
emitResultOverride = {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Availability probe for a plugin-declared schedule's source.
|
|
3
|
+
*
|
|
4
|
+
* Composes the daemon's activation state with the on-disk declaration probe:
|
|
5
|
+
* did this process bring the plugin up, and is its declaration still
|
|
6
|
+
* sourceable.
|
|
7
|
+
*
|
|
8
|
+
* **This probe is only meaningful in the main daemon process.** Activation is
|
|
9
|
+
* tracked in an in-memory map that `bringUpPlugin` writes, so only the process
|
|
10
|
+
* that ran the plugin loader can answer for it. HTTP routes and the plugin
|
|
11
|
+
* schedule reconciler run there, so they may use this. Worker processes (the
|
|
12
|
+
* schedule worker in `schedule/worker.ts`, sidecar turn workers) activate no
|
|
13
|
+
* plugins, so their ledger is empty and every plugin would read as
|
|
14
|
+
* unactivated. Those paths call {@link declarationExistsOnDisk} directly.
|
|
15
|
+
*
|
|
16
|
+
* That split is safe because the reconciler owns arming and runs in the
|
|
17
|
+
* daemon: a plugin it has not activated never arms, and its sweep disarms the
|
|
18
|
+
* rows of a plugin that stops being activated within one pass. The window
|
|
19
|
+
* between those passes is the whole exposure of the worker-side disk-only
|
|
20
|
+
* probes.
|
|
21
|
+
*
|
|
22
|
+
* The composition lives here rather than in `./plugin-schedule-declarations.ts`
|
|
23
|
+
* because that parser is reachable from the plugin loader's own static graph
|
|
24
|
+
* and must stay free of daemon runtime state. `plugins/mtime-cache.ts` reaches
|
|
25
|
+
* the schedule side only through a dynamic import, so importing it here does
|
|
26
|
+
* not close a static cycle.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
|
|
31
|
+
import { isPluginDirActivated } from "../plugins/mtime-cache.js";
|
|
32
|
+
import { getWorkspacePluginsDir } from "../util/platform.js";
|
|
33
|
+
import { declarationExistsOnDisk } from "./plugin-schedule-declarations.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* True when the schedule behind `sourceKey`
|
|
37
|
+
* (`plugin:<pluginName>/<scheduleName>`) may run: the daemon activated the
|
|
38
|
+
* plugin, and {@link declarationExistsOnDisk} still finds the declaration.
|
|
39
|
+
*
|
|
40
|
+
* The activation half is what keeps a plugin directory dropped into the
|
|
41
|
+
* plugins root out of the scheduler. Such a directory has files the disk probe
|
|
42
|
+
* accepts, but no `init` has run for it, so its hooks and tools are not live
|
|
43
|
+
* and its schedules must not be either.
|
|
44
|
+
*/
|
|
45
|
+
export async function pluginScheduleSourceAvailable(
|
|
46
|
+
sourceKey: string,
|
|
47
|
+
): Promise<boolean> {
|
|
48
|
+
const match = /^plugin:([^/]+)\/(.+)$/.exec(sourceKey);
|
|
49
|
+
if (!match) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const [, pluginName] = match;
|
|
53
|
+
const pluginDir = join(getWorkspacePluginsDir(), pluginName!);
|
|
54
|
+
return (
|
|
55
|
+
isPluginDirActivated(pluginDir) &&
|
|
56
|
+
(await declarationExistsOnDisk(sourceKey))
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -30,7 +30,6 @@ import { z } from "zod";
|
|
|
30
30
|
import { isPluginDisabled } from "../plugins/disabled-state.js";
|
|
31
31
|
import { parsePluginManifest } from "../plugins/external-plugin-loader.js";
|
|
32
32
|
import { isInsidePluginRoot } from "../plugins/installed-plugin-dirs.js";
|
|
33
|
-
import { walkPluginTree } from "../plugins/plugin-tree-walk.js";
|
|
34
33
|
import { FRONTMATTER_REGEX } from "../skills/frontmatter.js";
|
|
35
34
|
import { getWorkspacePluginsDir } from "../util/platform.js";
|
|
36
35
|
import {
|
|
@@ -77,9 +76,13 @@ export interface ScheduleDeclaration {
|
|
|
77
76
|
scriptInvocation: string | null;
|
|
78
77
|
config: PluginScheduleConfig;
|
|
79
78
|
/**
|
|
80
|
-
* Stable sha256 over the declaration's
|
|
81
|
-
*
|
|
82
|
-
* declaration
|
|
79
|
+
* Stable sha256 over the declaration's `config.json` and its entrypoint,
|
|
80
|
+
* each hashed together with its path relative to `schedules/`. Nothing else
|
|
81
|
+
* under the declaration directory takes part. Helper scripts, and any state
|
|
82
|
+
* a script writes beside itself, do not change the stored row definition, so
|
|
83
|
+
* hashing them would report a definition change every time a stateful
|
|
84
|
+
* schedule runs. Scripts execute by path, so an edit to a helper takes
|
|
85
|
+
* effect without a definition change.
|
|
83
86
|
*/
|
|
84
87
|
definitionHash: string;
|
|
85
88
|
}
|
|
@@ -292,21 +295,6 @@ function hashDeclarationFiles(files: DeclarationFile[]): string {
|
|
|
292
295
|
return hash.digest("hex");
|
|
293
296
|
}
|
|
294
297
|
|
|
295
|
-
/**
|
|
296
|
-
* Collect every regular file under `dir` (recursively, dot entries and
|
|
297
|
-
* symlinks skipped) with paths relative to the schedules/ root.
|
|
298
|
-
*/
|
|
299
|
-
function collectDeclarationFiles(
|
|
300
|
-
dir: string,
|
|
301
|
-
relPrefix: string,
|
|
302
|
-
): DeclarationFile[] {
|
|
303
|
-
const files: DeclarationFile[] = [];
|
|
304
|
-
walkPluginTree(dir, { excludeDotEntries: true }, (rel, abs) => {
|
|
305
|
-
files.push({ relPath: `${relPrefix}${rel}`, content: readFileSync(abs) });
|
|
306
|
-
});
|
|
307
|
-
return files;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
298
|
function shellQuote(value: string): string {
|
|
311
299
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
312
300
|
}
|
|
@@ -395,25 +383,24 @@ function parseDirectoryDeclaration(
|
|
|
395
383
|
let message: string | null = null;
|
|
396
384
|
let scriptInvocation: string | null = null;
|
|
397
385
|
let mode: PluginScheduleMode;
|
|
386
|
+
let entrypointText: string;
|
|
398
387
|
if (entrypoint === "index.md") {
|
|
399
|
-
|
|
400
|
-
if (FRONTMATTER_REGEX.test(
|
|
388
|
+
entrypointText = readFileSync(join(dirPath, "index.md"), "utf8");
|
|
389
|
+
if (FRONTMATTER_REGEX.test(entrypointText)) {
|
|
401
390
|
return {
|
|
402
391
|
error:
|
|
403
392
|
"index.md must not contain frontmatter: directory-form config belongs in config.json",
|
|
404
393
|
};
|
|
405
394
|
}
|
|
406
|
-
message =
|
|
395
|
+
message = entrypointText.trim();
|
|
407
396
|
if (!message) {
|
|
408
397
|
return { error: "prompt body is empty" };
|
|
409
398
|
}
|
|
410
399
|
mode = "execute";
|
|
411
400
|
} else {
|
|
412
401
|
const scriptPath = join(dirPath, "index.sh");
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
readFileSync(scriptPath, "utf8"),
|
|
416
|
-
);
|
|
402
|
+
entrypointText = readFileSync(scriptPath, "utf8");
|
|
403
|
+
scriptInvocation = buildScriptInvocation(scriptPath, entrypointText);
|
|
417
404
|
mode = "script";
|
|
418
405
|
}
|
|
419
406
|
|
|
@@ -422,7 +409,16 @@ function parseDirectoryDeclaration(
|
|
|
422
409
|
message,
|
|
423
410
|
scriptInvocation,
|
|
424
411
|
config: configResult.config,
|
|
425
|
-
files:
|
|
412
|
+
files: [
|
|
413
|
+
{
|
|
414
|
+
relPath: `${dirName}/config.json`,
|
|
415
|
+
content: Buffer.from(rawConfigText),
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
relPath: `${dirName}/${entrypoint}`,
|
|
419
|
+
content: Buffer.from(entrypointText),
|
|
420
|
+
},
|
|
421
|
+
],
|
|
426
422
|
};
|
|
427
423
|
}
|
|
428
424
|
|
|
@@ -33,6 +33,7 @@ import type { AttentionHints } from "../notifications/signal.js";
|
|
|
33
33
|
import { isPluginDisabled } from "../plugins/disabled-state.js";
|
|
34
34
|
import { parsePluginManifest } from "../plugins/external-plugin-loader.js";
|
|
35
35
|
import { listInstalledPluginDirs } from "../plugins/installed-plugin-dirs.js";
|
|
36
|
+
import { isPluginDirActivated } from "../plugins/mtime-cache.js";
|
|
36
37
|
import { getLogger } from "../util/logger.js";
|
|
37
38
|
import {
|
|
38
39
|
type DeclarationError,
|
|
@@ -264,9 +265,14 @@ function isCompletedRecurrence(
|
|
|
264
265
|
* as the plugin source collector) and gather their schedule declarations.
|
|
265
266
|
* Identity = the directory basename, mirroring `parsePluginManifest`.
|
|
266
267
|
*
|
|
267
|
-
* Disabled plugins
|
|
268
|
-
*
|
|
269
|
-
*
|
|
268
|
+
* Disabled plugins, plugins this daemon has not activated
|
|
269
|
+
* ({@link isPluginDirActivated}), and plugins without a `schedules/` directory
|
|
270
|
+
* are skipped entirely; in particular, only schedule-declaring plugins pay the
|
|
271
|
+
* manifest parse each pass. The activation gate is what keeps a directory
|
|
272
|
+
* dropped into the plugins root out of the desired set: nothing has run its
|
|
273
|
+
* `init`, so its hooks and tools are not live and its schedules must not arm
|
|
274
|
+
* either. It joins the set on the pass after the boot scan or an imperative
|
|
275
|
+
* reconcile brings it up. A schedule-declaring plugin whose manifest fails
|
|
270
276
|
* `parsePluginManifest` (unreadable or schema-invalid `package.json`) also
|
|
271
277
|
* contributes nothing to the desired set: the runtime loader refuses to
|
|
272
278
|
* bring such a plugin up, so its schedules must not stay armed either. Each
|
|
@@ -282,6 +288,9 @@ async function collectDesiredDeclarations(): Promise<CollectedDeclarations> {
|
|
|
282
288
|
if (isPluginDisabled(name)) {
|
|
283
289
|
continue;
|
|
284
290
|
}
|
|
291
|
+
if (!isPluginDirActivated(dir)) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
285
294
|
if (!existsSync(join(dir, "schedules"))) {
|
|
286
295
|
continue;
|
|
287
296
|
}
|