@vellumai/assistant 0.12.3 → 0.12.4-dev.202609231620.a15f321
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/Dockerfile +7 -0
- package/README.md +12 -12
- package/docker/mousepad/90-vellum.gschema.override +4 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
- package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
- package/openapi.yaml +97 -4
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +4 -2
- package/src/__tests__/call-routes-http.test.ts +4 -2
- package/src/__tests__/channel-readiness-discord.test.ts +8 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
- package/src/__tests__/config-loader-backfill.test.ts +1 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-resurface-done.test.ts +289 -0
- package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
- package/src/__tests__/gemini-provider.test.ts +31 -0
- package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
- package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
- package/src/__tests__/platform-callback-registration.test.ts +46 -1
- package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/pricing.test.ts +33 -0
- package/src/__tests__/provider-error-scenarios.test.ts +46 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +45 -15
- package/src/__tests__/retry-fallback-escalation.test.ts +22 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -2
- package/src/__tests__/skill-load-inline-command.test.ts +35 -0
- package/src/__tests__/skill-load-tool.test.ts +42 -11
- package/src/__tests__/skill-tool-factory.test.ts +50 -2
- package/src/__tests__/steer-on-enqueue-question.test.ts +265 -34
- package/src/__tests__/validate-input.test.ts +73 -0
- package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
- package/src/api/ask-question-tool.ts +50 -0
- package/src/api/events/tool-result.ts +11 -0
- package/src/api/index.ts +7 -0
- package/src/api/requests/dictation.ts +2 -2
- package/src/api/responses/home.ts +27 -0
- package/src/avatar/notification-avatar.test.ts +42 -34
- package/src/avatar/notification-avatar.ts +1 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +63 -1
- package/src/calls/voice-escalation-judge.ts +6 -5
- package/src/calls/voice-screen-annotation.ts +44 -0
- package/src/calls/voice-session-bridge.ts +22 -0
- package/src/cli/commands/__tests__/plugins.test.ts +29 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +12 -2
- package/src/cli/commands/platform/index.help.ts +2 -1
- package/src/cli/commands/platform/index.ts +8 -2
- package/src/cli/commands/plugins.ts +16 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +92 -2
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
- package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
- package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
- package/src/cli/lib/bundled-marketplace.json +4 -4
- package/src/cli/lib/inspect-plugin.ts +5 -0
- package/src/cli/lib/plugin-catalog-cache.ts +41 -4
- package/src/cli/lib/plugin-catalog-local.ts +9 -0
- package/src/cli/lib/plugin-catalog-platform.ts +48 -9
- package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
- package/src/cli/lib/plugin-details.ts +10 -7
- package/src/cli/lib/search-plugins.ts +2 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +61 -1
- package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
- package/src/config/bundled-skills/screen-annotation/SKILL.md +6 -3
- package/src/config/call-site-defaults.ts +17 -11
- package/src/config/default-profile-catalog.ts +118 -19
- package/src/config/default-profile-names.ts +69 -0
- package/src/config/feature-flag-registry.json +25 -9
- package/src/config/inference-profile-validation.ts +7 -6
- package/src/config/llm-resolver.ts +16 -0
- package/src/config/profile-text-generation.test.ts +9 -0
- package/src/config/schemas/call-site-catalog.ts +8 -1
- package/src/config/schemas/llm.ts +70 -9
- package/src/config/sidebar-done-gate.ts +27 -0
- package/src/config/sync-gated-profiles.ts +96 -44
- package/src/daemon/__tests__/auto-profile-router.test.ts +228 -0
- package/src/daemon/auto-profile-router.ts +220 -0
- package/src/daemon/conversation-agent-loop.ts +77 -4
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-turn-finalize.ts +12 -0
- package/src/daemon/dictation-text-processing.ts +4 -4
- package/src/daemon/handlers/__tests__/conversation-history-search.test.ts +61 -0
- package/src/daemon/handlers/config-channels.ts +3 -1
- package/src/daemon/handlers/conversation-history.ts +11 -1
- package/src/daemon/handlers/conversations.ts +118 -10
- package/src/daemon/pkb-reminder-builder.test.ts +10 -3
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/desktop/desktop-browser-endpoint.ts +1 -0
- package/src/home/feed-types.ts +2 -0
- package/src/inbound/platform-callback-registration.ts +19 -6
- package/src/ipc/routes/__tests__/conversation-sync-ipc-routes.test.ts +49 -1
- package/src/ipc/routes/conversation-sync-ipc-routes.ts +38 -2
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
- package/src/live-voice/__tests__/protocol.test.ts +22 -0
- package/src/live-voice/continuation-judge.ts +1 -1
- package/src/live-voice/live-voice-session.ts +7 -6
- package/src/live-voice/protocol.ts +16 -5
- package/src/messaging/providers/gmail/client.ts +4 -0
- package/src/notifications/README.md +11 -3
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +87 -3
- package/src/notifications/conversation-pairing.ts +23 -8
- package/src/notifications/home-feed-mirror.ts +39 -0
- package/src/notifications/home-feed-side-effect.ts +46 -19
- package/src/oauth/AGENTS.md +7 -1
- package/src/permissions/question-prompter.ts +14 -4
- package/src/persistence/conversation-crud.ts +114 -3
- package/src/persistence/conversation-queries.ts +48 -3
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +228 -32
- package/src/persistence/embeddings/embedding-backend-shutdown.test.ts +47 -0
- package/src/persistence/embeddings/embedding-backend.ts +23 -41
- package/src/persistence/embeddings/embedding-gemini.test.ts +17 -0
- package/src/persistence/embeddings/embedding-gemini.ts +3 -1
- package/src/persistence/embeddings/embedding-local.ts +72 -17
- package/src/persistence/embeddings/embedding-types.ts +0 -19
- package/src/persistence/jobs-store.ts +229 -0
- package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
- package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
- package/src/persistence/steps.ts +12 -0
- package/src/platform/sync-avatar.test.ts +29 -0
- package/src/plugin-api/model-profiles.ts +5 -3
- package/src/plugins/defaults/memory/AGENTS.md +5 -3
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
- package/src/plugins/defaults/memory/graph/tools.ts +9 -5
- package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
- package/src/plugins/defaults/memory/job-handlers.ts +8 -2
- package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +5 -0
- package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
- package/src/plugins/defaults/memory/tools.test.ts +17 -0
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/anthropic/client.ts +2 -2
- package/src/providers/gemini/client.ts +17 -1
- package/src/providers/inference/auth.ts +25 -0
- package/src/providers/jev/ask.test.ts +66 -0
- package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +75 -36
- package/src/providers/malformed-tool-call.ts +55 -0
- package/src/providers/model-catalog.ts +257 -4
- package/src/providers/openai/__tests__/chat-completions-malformed-tool-call.test.ts +105 -0
- package/src/providers/openai/chat-completions-provider.ts +30 -0
- package/src/providers/openai/codex-models.ts +2 -0
- package/src/providers/platform-proxy/constants.ts +5 -0
- package/src/providers/retry.ts +68 -36
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/vellum-model-routing.test.ts +1 -0
- package/src/runtime/__tests__/agent-wake-archived-gate.test.ts +90 -0
- package/src/runtime/__tests__/question-resolution.test.ts +32 -0
- package/src/runtime/agent-wake.ts +9 -2
- package/src/runtime/auth/__tests__/middleware.test.ts +138 -0
- package/src/runtime/auth/middleware.ts +15 -11
- package/src/runtime/middleware/auth.ts +7 -1
- package/src/runtime/pending-interactions.ts +8 -8
- package/src/runtime/question-resolution.ts +54 -8
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +146 -0
- package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +74 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
- package/src/runtime/routes/conversation-list-routes.ts +16 -14
- package/src/runtime/routes/conversation-query-routes.ts +4 -0
- package/src/runtime/routes/conversation-routes.ts +4 -2
- package/src/runtime/routes/diagnostics-routes.ts +66 -229
- package/src/runtime/routes/global-search-routes.ts +22 -7
- package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
- package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
- package/src/runtime/routes/platform-routes.ts +6 -1
- package/src/runtime/routes/plugins-routes.ts +5 -2
- package/src/runtime/sync/documents-sidecar-publish.test.ts +50 -4
- package/src/runtime/sync/resource-sync-events.ts +11 -0
- package/src/runtime/sync/worker-daemon-notify.test.ts +30 -0
- package/src/runtime/sync/worker-daemon-notify.ts +44 -0
- package/src/runtime/verification-outbound-actions.ts +2 -2
- package/src/skills/validate-input.ts +75 -11
- package/src/telegram/__tests__/webhook-health.test.ts +8 -0
- package/src/tools/ask-question/ask-question-tool.test.ts +38 -0
- package/src/tools/ask-question/ask-question-tool.ts +12 -1
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
- package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
- package/src/tools/network/__tests__/web-search.test.ts +96 -0
- package/src/tools/network/web-fetch.ts +30 -9
- package/src/tools/network/web-search.ts +235 -4
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/format-input-schema.ts +15 -0
- package/src/tools/skills/load.ts +15 -43
- package/src/tools/skills/scaffold-managed.ts +86 -71
- package/src/tools/skills/skill-tool-factory.ts +2 -1
- package/src/tools/workflows/manage-workflows.ts +4 -2
- package/src/util/worker-pipe.test.ts +146 -0
- package/src/util/worker-pipe.ts +49 -0
- package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
- package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
- package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
package/src/tools/skills/load.ts
CHANGED
|
@@ -35,6 +35,7 @@ import type {
|
|
|
35
35
|
ToolDefinition,
|
|
36
36
|
ToolExecutionResult,
|
|
37
37
|
} from "../types.js";
|
|
38
|
+
import { formatSkillInputSchema } from "./format-input-schema.js";
|
|
38
39
|
|
|
39
40
|
/** Skill sources eligible for inline command expansion in v1. */
|
|
40
41
|
const INLINE_COMMAND_ELIGIBLE_SOURCES = new Set([
|
|
@@ -123,32 +124,7 @@ function formatToolSchemas(
|
|
|
123
124
|
tool.description.replaceAll("{workspaceDir}", getWorkspaceDirDisplay()),
|
|
124
125
|
);
|
|
125
126
|
|
|
126
|
-
|
|
127
|
-
const properties = schema.properties as
|
|
128
|
-
| Record<string, Record<string, unknown>>
|
|
129
|
-
| undefined;
|
|
130
|
-
if (properties && Object.keys(properties).length > 0) {
|
|
131
|
-
const requiredSet = new Set<string>(
|
|
132
|
-
Array.isArray(schema.required) ? (schema.required as string[]) : [],
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
lines.push("Parameters:");
|
|
136
|
-
for (const [paramName, paramDef] of Object.entries(properties)) {
|
|
137
|
-
const paramType =
|
|
138
|
-
typeof paramDef.type === "string" ? paramDef.type : "any";
|
|
139
|
-
const requiredLabel = requiredSet.has(paramName)
|
|
140
|
-
? "required"
|
|
141
|
-
: "optional";
|
|
142
|
-
const descPart =
|
|
143
|
-
typeof paramDef.description === "string"
|
|
144
|
-
? `: ${paramDef.description.replaceAll("{workspaceDir}", getWorkspaceDirDisplay())}`
|
|
145
|
-
: "";
|
|
146
|
-
lines.push(
|
|
147
|
-
`- ${paramName} (${paramType}, ${requiredLabel})${descPart}`,
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
127
|
+
lines.push(formatSkillInputSchema(tool.input_schema));
|
|
152
128
|
lines.push("");
|
|
153
129
|
}
|
|
154
130
|
|
|
@@ -181,6 +157,7 @@ export const skillLoadTool = {
|
|
|
181
157
|
async execute(
|
|
182
158
|
input: Record<string, unknown>,
|
|
183
159
|
context: ToolContext,
|
|
160
|
+
options?: { readOnly?: boolean },
|
|
184
161
|
): Promise<ToolExecutionResult> {
|
|
185
162
|
const selector = input.skill;
|
|
186
163
|
if (typeof selector !== "string" || selector.trim().length === 0) {
|
|
@@ -195,13 +172,17 @@ export const skillLoadTool = {
|
|
|
195
172
|
// command execution. Instructions are still returned so the assistant can
|
|
196
173
|
// load the system-storage-cleanup skill (and any already-installed skill).
|
|
197
174
|
const cleanupMode = context.diskPressureCleanupModeActive === true;
|
|
175
|
+
const readOnly = cleanupMode || options?.readOnly === true;
|
|
176
|
+
const inlineCommandStub = cleanupMode
|
|
177
|
+
? INLINE_COMMAND_CLEANUP_STUB
|
|
178
|
+
: "[inline command not executed]";
|
|
198
179
|
|
|
199
180
|
let loaded = loadSkillBySelector(selector);
|
|
200
181
|
|
|
201
182
|
// Auto-install from catalog if the skill isn't found locally
|
|
202
183
|
if (
|
|
203
184
|
!loaded.skill &&
|
|
204
|
-
!
|
|
185
|
+
!readOnly &&
|
|
205
186
|
(loaded.errorCode === "not_found" || loaded.errorCode === "empty_catalog")
|
|
206
187
|
) {
|
|
207
188
|
try {
|
|
@@ -297,9 +278,8 @@ export const skillLoadTool = {
|
|
|
297
278
|
break;
|
|
298
279
|
}
|
|
299
280
|
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
if (cleanupMode) {
|
|
281
|
+
// Read-only loads leave missing includes advisory ("not loaded").
|
|
282
|
+
if (readOnly) {
|
|
303
283
|
break;
|
|
304
284
|
}
|
|
305
285
|
|
|
@@ -373,17 +353,11 @@ export const skillLoadTool = {
|
|
|
373
353
|
const hasInlineCommands =
|
|
374
354
|
skill.inlineCommandExpansions && skill.inlineCommandExpansions.length > 0;
|
|
375
355
|
|
|
376
|
-
if (hasInlineCommands &&
|
|
377
|
-
|
|
378
|
-
// Strip inline command tokens instead of rendering them; the rest of the
|
|
379
|
-
// instructions are still returned.
|
|
380
|
-
body = body.replace(
|
|
381
|
-
INLINE_COMMAND_TOKEN_PATTERN,
|
|
382
|
-
INLINE_COMMAND_CLEANUP_STUB,
|
|
383
|
-
);
|
|
356
|
+
if (hasInlineCommands && readOnly) {
|
|
357
|
+
body = body.replace(INLINE_COMMAND_TOKEN_PATTERN, inlineCommandStub);
|
|
384
358
|
log.info(
|
|
385
359
|
{ skillId: skill.id },
|
|
386
|
-
"Skipped inline command expansion during
|
|
360
|
+
"Skipped inline command expansion during read-only skill load",
|
|
387
361
|
);
|
|
388
362
|
} else if (hasInlineCommands) {
|
|
389
363
|
if (skill.source === "extra" || skill.source === "plugin") {
|
|
@@ -475,12 +449,10 @@ export const skillLoadTool = {
|
|
|
475
449
|
childLoaded.skill.inlineCommandExpansions &&
|
|
476
450
|
childLoaded.skill.inlineCommandExpansions.length > 0;
|
|
477
451
|
|
|
478
|
-
if (childHasInlineCommands &&
|
|
479
|
-
// No shell execution under the disk-pressure lock — strip the
|
|
480
|
-
// child's inline command tokens rather than rendering them.
|
|
452
|
+
if (childHasInlineCommands && readOnly) {
|
|
481
453
|
childBody = childBody.replace(
|
|
482
454
|
INLINE_COMMAND_TOKEN_PATTERN,
|
|
483
|
-
|
|
455
|
+
inlineCommandStub,
|
|
484
456
|
);
|
|
485
457
|
} else if (childHasInlineCommands) {
|
|
486
458
|
if (
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
+
import { v4 as uuid } from "uuid";
|
|
5
|
+
|
|
4
6
|
import type { SkillSource } from "../../config/skills.js";
|
|
5
7
|
import { loadSkillCatalog } from "../../config/skills.js";
|
|
6
8
|
import { refreshSkillCapabilityMemories } from "../../daemon/skill-memory-refresh.js";
|
|
7
|
-
import { emitNotificationSignal } from "../../notifications/emit-signal.js";
|
|
8
9
|
import { sanitizeMultilineMessagePreview } from "../../notifications/notification-utils.js";
|
|
9
10
|
import { getConversation } from "../../persistence/conversation-crud.js";
|
|
10
11
|
import { upsertSkillCardInsertJob } from "../../persistence/jobs-store.js";
|
|
11
12
|
import { MEMORY_RETROSPECTIVE_ORIGIN } from "../../plugins/defaults/memory/memory-retrospective-constants.js";
|
|
13
|
+
import { recordSkillUpdate } from "../../plugins/defaults/memory/skill-update-receipt-job.js";
|
|
12
14
|
import { readInstallMeta } from "../../skills/install-meta.js";
|
|
13
15
|
import {
|
|
14
16
|
createManagedSkill,
|
|
@@ -90,78 +92,90 @@ function normalizeOptionalStringArray(
|
|
|
90
92
|
return { value: normalized };
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
/** Watchdog check_name counted when a receipt entry could not be recorded. */
|
|
96
|
+
const SKILL_UPDATE_RECEIPT_RECORD_FAILED_CHECK_NAME =
|
|
97
|
+
"skill_update_receipt_record_failed";
|
|
98
|
+
|
|
93
99
|
/**
|
|
94
|
-
*
|
|
100
|
+
* Record that a background pass changed a skill the user already has.
|
|
95
101
|
*
|
|
96
102
|
* A newly authored skill announces itself in the conversation through the
|
|
97
|
-
* `skill_card` surface; an update
|
|
98
|
-
* runs in a background fork and rewrites the body
|
|
99
|
-
* relying on, and `createManagedSkill` writes
|
|
100
|
-
* keeping no prior version, so the
|
|
101
|
-
* change in the background feed alongside the other unattended
|
|
102
|
-
* scheduled jobs, heartbeat), where a user already looks to
|
|
103
|
-
* assistant did on its own.
|
|
103
|
+
* `skill_card` surface; an update is announced through a skill-update
|
|
104
|
+
* receipt instead. The pass runs in a background fork and rewrites the body
|
|
105
|
+
* of a skill the user may be relying on, and `createManagedSkill` writes
|
|
106
|
+
* through an atomic rename keeping no prior version, so the receipt is what
|
|
107
|
+
* puts the change in the background feed alongside the other unattended
|
|
108
|
+
* work (sweeps, scheduled jobs, heartbeat), where a user already looks to
|
|
109
|
+
* see what the assistant did on its own. One pass can rewrite several skills
|
|
110
|
+
* within minutes, so the rewrites accumulate on one pending receipt job
|
|
111
|
+
* (`plugins/defaults/memory/skill-update-receipt-job.ts`), which announces
|
|
112
|
+
* the whole burst once it has settled rather than one notification per
|
|
113
|
+
* rewrite.
|
|
104
114
|
*
|
|
105
115
|
* `changeSummary` is the pass's own account of what it changed and is the
|
|
106
|
-
*
|
|
107
|
-
* reader triaging it there has no diff to look at, so
|
|
116
|
+
* entry's body: the receipt is the only place the change surfaces, and a
|
|
117
|
+
* reader triaging it there has no diff to look at, so an entry that only
|
|
108
118
|
* names the skill sends them into the skill to find out what happened. The
|
|
109
119
|
* executor refuses the overwrite without one, so this never runs without it.
|
|
110
120
|
*
|
|
111
|
-
* `
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* the
|
|
116
|
-
*
|
|
117
|
-
*
|
|
121
|
+
* `sourceConversationId` is the conversation the receipt's link resolves to
|
|
122
|
+
* for this entry (see `home-updates-list.tsx`): the source conversation when
|
|
123
|
+
* lineage resolved, else none. The run's own conversation is never used in
|
|
124
|
+
* its place: it is an ephemeral fork the next successful retrospective
|
|
125
|
+
* garbage-collects, and the receipt job leaves out any entry whose source
|
|
126
|
+
* is gone by the time it announces, so naming the fork would drop the
|
|
127
|
+
* entry rather than link it. `entryId` is the producing tool call's id, so a
|
|
128
|
+
* re-executed call records nothing new. A context with no tool call id (a
|
|
129
|
+
* direct caller) has no re-execution to guard against, so it gets a fresh
|
|
130
|
+
* id: a stable key on the run and skill would drop a second rewrite of the
|
|
131
|
+
* same skill in the same run.
|
|
132
|
+
*
|
|
133
|
+
* Best-effort and non-blocking: the skill is already written, and a receipt
|
|
134
|
+
* that cannot be recorded is a receipt the user will not get, not a reason
|
|
135
|
+
* to fail or retry a write that already landed. The failure is logged with
|
|
136
|
+
* the ids and counted, and the tool-invocation history keeps the summary as
|
|
137
|
+
* audit evidence.
|
|
118
138
|
*/
|
|
119
|
-
function
|
|
139
|
+
function recordBackgroundSkillUpdate(args: {
|
|
120
140
|
skillId: string;
|
|
121
141
|
name: string;
|
|
122
|
-
conversationId: string;
|
|
123
142
|
changeSummary: string;
|
|
143
|
+
runConversationId: string;
|
|
144
|
+
sourceConversationId: string | undefined;
|
|
145
|
+
toolUseId: string | undefined;
|
|
124
146
|
}): void {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// `fromAssistant` from it, so labelling this "scheduler" would drop the
|
|
130
|
-
// item from the feed's assistant-initiated filter despite being exactly
|
|
131
|
-
// that.
|
|
132
|
-
sourceChannel: "assistant_tool",
|
|
133
|
-
sourceContextId: args.conversationId,
|
|
134
|
-
sourceEventName: "activity.complete",
|
|
135
|
-
dedupeKey: `skill-updated:${args.skillId}:${day}`,
|
|
136
|
-
contextPayload: {
|
|
137
|
-
// `summary` feeds the copy composer and `title` is the headline both
|
|
138
|
-
// it and the home feed keep; `title`/`body` are also the feed's
|
|
139
|
-
// fallback when no channel copy was rendered. Without them a fully
|
|
140
|
-
// suppressed delivery (the intended shape for this signal: low urgency,
|
|
141
|
-
// background, no interruption) leaves the feed writer with no summary
|
|
142
|
-
// and it skips the item entirely, so the quiet case would surface
|
|
143
|
-
// nothing at all.
|
|
144
|
-
summary: args.changeSummary,
|
|
145
|
-
// Named, not just "Skill updated": the feed sits several rows deep and
|
|
146
|
-
// a generic title is unscannable next to entries that name their
|
|
147
|
-
// subject (`Background job failed: memory.v2.sweep`). The word "Skill"
|
|
148
|
-
// stays because a bare skill name does not always read as one.
|
|
149
|
-
title: `Skill updated: ${args.name}`,
|
|
150
|
-
body: args.changeSummary,
|
|
147
|
+
const entryId = `${args.runConversationId}:${args.toolUseId ?? uuid()}`;
|
|
148
|
+
try {
|
|
149
|
+
recordSkillUpdate({
|
|
150
|
+
entryId,
|
|
151
151
|
skillId: args.skillId,
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
}
|
|
152
|
+
name: args.name,
|
|
153
|
+
changeSummary: args.changeSummary,
|
|
154
|
+
runConversationId: args.runConversationId,
|
|
155
|
+
...(args.sourceConversationId
|
|
156
|
+
? { sourceConversationId: args.sourceConversationId }
|
|
157
|
+
: {}),
|
|
158
|
+
});
|
|
159
|
+
} catch (err) {
|
|
160
160
|
log.warn(
|
|
161
|
-
{
|
|
162
|
-
|
|
161
|
+
{
|
|
162
|
+
err,
|
|
163
|
+
skillId: args.skillId,
|
|
164
|
+
runConversationId: args.runConversationId,
|
|
165
|
+
entryId,
|
|
166
|
+
},
|
|
167
|
+
"skill update receipt recording failed; the skill write stands and no receipt will name this update",
|
|
163
168
|
);
|
|
164
|
-
|
|
169
|
+
try {
|
|
170
|
+
recordWatchdogEvent({
|
|
171
|
+
checkName: SKILL_UPDATE_RECEIPT_RECORD_FAILED_CHECK_NAME,
|
|
172
|
+
value: 1,
|
|
173
|
+
});
|
|
174
|
+
} catch {
|
|
175
|
+
// recordWatchdogEvent already no-ops on opt-out and a missing
|
|
176
|
+
// telemetry DB; anything past that is not worth surfacing here.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
165
179
|
}
|
|
166
180
|
|
|
167
181
|
/**
|
|
@@ -333,10 +347,11 @@ export async function executeScaffoldManagedSkill(
|
|
|
333
347
|
category = input.category.trim().toLowerCase();
|
|
334
348
|
}
|
|
335
349
|
|
|
336
|
-
// The
|
|
350
|
+
// The receipt entry's body. Model-authored text bound for a notification
|
|
337
351
|
// surface, so it gets the same control-character strip and preview clamp as
|
|
338
352
|
// any other producer-supplied body; blank collapses to absent so the
|
|
339
|
-
// requirement below treats it as missing rather than
|
|
353
|
+
// requirement below treats it as missing rather than recording an empty
|
|
354
|
+
// entry.
|
|
340
355
|
let changeSummary: string | undefined;
|
|
341
356
|
if (input.change_summary !== undefined) {
|
|
342
357
|
if (typeof input.change_summary !== "string") {
|
|
@@ -404,9 +419,9 @@ export async function executeScaffoldManagedSkill(
|
|
|
404
419
|
}
|
|
405
420
|
}
|
|
406
421
|
|
|
407
|
-
// A background overwrite announces itself through the
|
|
408
|
-
//
|
|
409
|
-
// refused without one. The error returns to the pass in the same turn and
|
|
422
|
+
// A background overwrite announces itself through the receipt entry
|
|
423
|
+
// recorded in recordBackgroundSkillUpdate, whose body is this summary, so
|
|
424
|
+
// the write is refused without one. The error returns to the pass in the same turn and
|
|
410
425
|
// it retries with the field, the way a missing activation_hints does.
|
|
411
426
|
// Checked after the ownership backstop so a pass that may not touch the
|
|
412
427
|
// skill hears that first, and only for a call that asked to overwrite: a
|
|
@@ -500,23 +515,23 @@ export async function executeScaffoldManagedSkill(
|
|
|
500
515
|
}
|
|
501
516
|
|
|
502
517
|
// A background pass changed a skill that already existed. Creates announce
|
|
503
|
-
// themselves through the skill card below; updates
|
|
504
|
-
// the
|
|
505
|
-
// refinement as well as any other background write onto
|
|
506
|
-
// skill.
|
|
507
|
-
const notifyConversationId =
|
|
508
|
-
sourceConversationId ?? retrospectiveConversationId;
|
|
518
|
+
// themselves through the skill card below; updates land on the skill-update
|
|
519
|
+
// receipt that announces the whole burst. Covers an explicit
|
|
520
|
+
// `overwrite: true` refinement as well as any other background write onto
|
|
521
|
+
// a pre-existing skill.
|
|
509
522
|
if (
|
|
510
523
|
fromRetrospective &&
|
|
511
524
|
managedSkillExistedBefore &&
|
|
512
|
-
|
|
525
|
+
retrospectiveConversationId &&
|
|
513
526
|
changeSummary
|
|
514
527
|
) {
|
|
515
|
-
|
|
528
|
+
recordBackgroundSkillUpdate({
|
|
516
529
|
skillId: id,
|
|
517
530
|
name: normalizedName,
|
|
518
|
-
conversationId: notifyConversationId,
|
|
519
531
|
changeSummary,
|
|
532
|
+
runConversationId: retrospectiveConversationId,
|
|
533
|
+
sourceConversationId,
|
|
534
|
+
toolUseId: context.toolUseId,
|
|
520
535
|
});
|
|
521
536
|
}
|
|
522
537
|
|
|
@@ -15,6 +15,7 @@ import { bundledToolInputMisuseMessage } from "../shared/input-misuse.js";
|
|
|
15
15
|
import { bundledToolInputRepairs } from "../shared/input-repairs.js";
|
|
16
16
|
import type { ExecutionTarget } from "../tool-types.js";
|
|
17
17
|
import type { Tool, ToolContext, ToolExecutionResult } from "../types.js";
|
|
18
|
+
import { formatSkillInputSchema } from "./format-input-schema.js";
|
|
18
19
|
import { runSkillToolScript } from "./skill-script-runner.js";
|
|
19
20
|
|
|
20
21
|
const riskMap: Record<SkillToolEntry["risk"], RiskLevel> = {
|
|
@@ -94,7 +95,7 @@ export function createSkillTool(
|
|
|
94
95
|
return {
|
|
95
96
|
content:
|
|
96
97
|
misuse ??
|
|
97
|
-
`Invalid input for tool "${entry.name}": ${validation.errors.join("; ")}.
|
|
98
|
+
`Invalid input for tool "${entry.name}": ${validation.errors.join("; ")}. If skill_load is available, load the owning skill for its current instructions. Retry with arguments matching this schema.\n\n${formatSkillInputSchema(entry.input_schema)}`,
|
|
98
99
|
isError: true,
|
|
99
100
|
};
|
|
100
101
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { getConversationProfilesForProvider } from "../../config/default-profile-catalog.js";
|
|
19
19
|
import { loadConfig } from "../../config/loader.js";
|
|
20
20
|
import { callerOwnsWorkflowRun } from "../../workflows/capabilities.js";
|
|
21
21
|
import type { WorkflowRun } from "../../workflows/journal-store.js";
|
|
@@ -199,7 +199,9 @@ export async function executeManageWorkflows(
|
|
|
199
199
|
// workspace-wide active profile. Read-only — leaves use this to pick a
|
|
200
200
|
// valid `profile` for `run_workflow` (an unknown profile throws).
|
|
201
201
|
const { llm } = loadConfig();
|
|
202
|
-
|
|
202
|
+
// A workflow leaf runs chat turns, so this is the conversation view,
|
|
203
|
+
// which leaves out the managed Jev profile.
|
|
204
|
+
const profiles = getConversationProfilesForProvider(
|
|
203
205
|
llm?.profiles,
|
|
204
206
|
llm?.defaultProvider ?? null,
|
|
205
207
|
);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { writeWorkerLine } from "./worker-pipe.js";
|
|
4
|
+
|
|
5
|
+
/** Comfortably larger than any OS pipe buffer, so the write cannot complete. */
|
|
6
|
+
const OVERSIZED_LINE = "x".repeat(4 * 1024 * 1024);
|
|
7
|
+
|
|
8
|
+
const spawned: { kill(signal?: NodeJS.Signals): void }[] = [];
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
for (const proc of spawned.splice(0)) {
|
|
12
|
+
try {
|
|
13
|
+
proc.kill("SIGKILL");
|
|
14
|
+
} catch {
|
|
15
|
+
/* already gone */
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/** A live child that never reads stdin, the way a worker busy in ONNX does not. */
|
|
21
|
+
function spawnBusyWorker() {
|
|
22
|
+
const proc = Bun.spawn({
|
|
23
|
+
cmd: [process.execPath, "-e", "setTimeout(() => {}, 60_000)"],
|
|
24
|
+
windowsHide: true,
|
|
25
|
+
stdin: "pipe",
|
|
26
|
+
stdout: "ignore",
|
|
27
|
+
stderr: "ignore",
|
|
28
|
+
});
|
|
29
|
+
spawned.push(proc);
|
|
30
|
+
return proc;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("writeWorkerLine", () => {
|
|
34
|
+
/**
|
|
35
|
+
* The contract this module rests on, against the real Bun API: a payload
|
|
36
|
+
* that outruns the pipe buffer comes back as a `Promise`, and killing the
|
|
37
|
+
* worker settles it. Which way it settles is Bun's to choose. It rejects
|
|
38
|
+
* with EPIPE on nearly every kill and, rarely on Linux, resolves with a
|
|
39
|
+
* short count instead, so this asserts only that nothing escapes either
|
|
40
|
+
* way. Demanding the rejection is what made this case flaky on CI.
|
|
41
|
+
*/
|
|
42
|
+
test("observes a real pending write however Bun settles it", async () => {
|
|
43
|
+
const proc = spawnBusyWorker();
|
|
44
|
+
const failures: unknown[] = [];
|
|
45
|
+
const settling: Promise<number>[] = [];
|
|
46
|
+
const record = (result: number | Promise<number>) => {
|
|
47
|
+
if (result instanceof Promise) {
|
|
48
|
+
settling.push(result);
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
writeWorkerLine(
|
|
54
|
+
{
|
|
55
|
+
write: (chunk) => record(proc.stdin.write(chunk)),
|
|
56
|
+
flush: () => record(proc.stdin.flush()),
|
|
57
|
+
},
|
|
58
|
+
OVERSIZED_LINE,
|
|
59
|
+
(err) => failures.push(err),
|
|
60
|
+
);
|
|
61
|
+
proc.kill("SIGKILL");
|
|
62
|
+
const outcomes = await Promise.race([
|
|
63
|
+
Promise.allSettled(settling),
|
|
64
|
+
Bun.sleep(10_000).then(() => null),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
expect(settling.length).toBeGreaterThan(0);
|
|
68
|
+
expect(outcomes).not.toBeNull();
|
|
69
|
+
expect(failures).toHaveLength(
|
|
70
|
+
outcomes!.some((o) => o.status === "rejected") ? 1 : 0,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The production failure: the payload outruns the pipe buffer, Bun hands
|
|
76
|
+
* `write` and `flush` one pending promise, and the worker dies before
|
|
77
|
+
* draining it. Bun rejects that promise from its own event loop, after this
|
|
78
|
+
* call has returned. Unobserved, the rejection is an `unhandledRejection`,
|
|
79
|
+
* which `bun test` reports as a failure of this test.
|
|
80
|
+
*/
|
|
81
|
+
test("reports a write still pending when the worker dies", async () => {
|
|
82
|
+
const failure = Promise.withResolvers<unknown>();
|
|
83
|
+
const epipe = Object.assign(new Error("EPIPE: broken pipe, write"), {
|
|
84
|
+
code: "EPIPE",
|
|
85
|
+
});
|
|
86
|
+
const pending = Promise.withResolvers<number>();
|
|
87
|
+
const stdin = {
|
|
88
|
+
write: () => pending.promise,
|
|
89
|
+
flush: () => pending.promise,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
writeWorkerLine(stdin, "line", failure.resolve);
|
|
93
|
+
pending.reject(epipe);
|
|
94
|
+
|
|
95
|
+
expect(await failure.promise).toBe(epipe);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("reports a synchronous throw", () => {
|
|
99
|
+
const failures: unknown[] = [];
|
|
100
|
+
const epipe = Object.assign(new Error("EPIPE: broken pipe, write"), {
|
|
101
|
+
code: "EPIPE",
|
|
102
|
+
});
|
|
103
|
+
const stdin = {
|
|
104
|
+
write() {
|
|
105
|
+
throw epipe;
|
|
106
|
+
},
|
|
107
|
+
flush: () => 0,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
writeWorkerLine(stdin, "line", (err) => failures.push(err));
|
|
111
|
+
|
|
112
|
+
expect(failures).toEqual([epipe]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/** `write` and `flush` both report the same broken pipe. */
|
|
116
|
+
test("reports one failure when write and flush both reject", async () => {
|
|
117
|
+
const failures: unknown[] = [];
|
|
118
|
+
const epipe = new Error("EPIPE: broken pipe, send");
|
|
119
|
+
const stdin = {
|
|
120
|
+
write: () => Promise.reject(epipe),
|
|
121
|
+
flush: () => Promise.reject(epipe),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
writeWorkerLine(stdin, "line", (err) => failures.push(err));
|
|
125
|
+
await Bun.sleep(0);
|
|
126
|
+
|
|
127
|
+
expect(failures).toEqual([epipe]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("stays silent when the line is delivered", async () => {
|
|
131
|
+
const failures: unknown[] = [];
|
|
132
|
+
const proc = Bun.spawn({
|
|
133
|
+
cmd: [process.execPath, "-e", "process.stdin.on('data', () => {})"],
|
|
134
|
+
windowsHide: true,
|
|
135
|
+
stdin: "pipe",
|
|
136
|
+
stdout: "ignore",
|
|
137
|
+
stderr: "ignore",
|
|
138
|
+
});
|
|
139
|
+
spawned.push(proc);
|
|
140
|
+
|
|
141
|
+
writeWorkerLine(proc.stdin, "hello", (err) => failures.push(err));
|
|
142
|
+
await Bun.sleep(50);
|
|
143
|
+
|
|
144
|
+
expect(failures).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { FileSink } from "bun";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Write one line to a worker subprocess's stdin, reporting a broken pipe to
|
|
5
|
+
* `onFailure` however Bun surfaces it.
|
|
6
|
+
*
|
|
7
|
+
* `FileSink.write` and `FileSink.flush` return `number | Promise<number>`.
|
|
8
|
+
* While the pipe has room they complete synchronously. Once a payload outruns
|
|
9
|
+
* the pipe buffer, which is what happens when the worker is busy and not
|
|
10
|
+
* draining stdin, they return a pending promise instead, and that promise
|
|
11
|
+
* rejects with EPIPE once the worker dies, from Bun's event loop, after this
|
|
12
|
+
* call has returned. A `try/catch` sees only the synchronous half: the
|
|
13
|
+
* rejection escapes it, reaches the process's `unhandledRejection` handler,
|
|
14
|
+
* and in the daemon that handler shuts the whole process down.
|
|
15
|
+
*
|
|
16
|
+
* On Linux, Bun occasionally treats a zero-byte write to the dead worker's
|
|
17
|
+
* pipe as end-of-file and resolves that promise with the bytes written so
|
|
18
|
+
* far instead. `onFailure` then never runs, and the caller learns of the
|
|
19
|
+
* death only when the worker's stdout ends.
|
|
20
|
+
*
|
|
21
|
+
* `onFailure` runs at most once per call, even though `write` and `flush` can
|
|
22
|
+
* each report the same broken pipe.
|
|
23
|
+
*/
|
|
24
|
+
export function writeWorkerLine(
|
|
25
|
+
stdin: Pick<FileSink, "write" | "flush">,
|
|
26
|
+
line: string,
|
|
27
|
+
onFailure: (err: unknown) => void,
|
|
28
|
+
): void {
|
|
29
|
+
let failed = false;
|
|
30
|
+
const fail = (err: unknown): void => {
|
|
31
|
+
if (failed) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
failed = true;
|
|
35
|
+
onFailure(err);
|
|
36
|
+
};
|
|
37
|
+
const observe = (result: number | Promise<number>): void => {
|
|
38
|
+
if (result instanceof Promise) {
|
|
39
|
+
result.catch(fail);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
observe(stdin.write(line + "\n"));
|
|
45
|
+
observe(stdin.flush());
|
|
46
|
+
} catch (err) {
|
|
47
|
+
fail(err);
|
|
48
|
+
}
|
|
49
|
+
}
|