@vellumai/assistant 0.9.0 → 0.9.1-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/ARCHITECTURE.md +18 -34
- package/bun.lock +7 -8
- package/docs/activation-funnel-telemetry.md +4 -4
- package/docs/architecture/security.md +29 -28
- package/docs/stt-provider-onboarding.md +3 -5
- package/docs/workflows-testing.md +13 -44
- package/docs/workflows.md +3 -5
- package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
- package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
- package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
- package/node_modules/@vellumai/gateway-client/src/index.ts +17 -6
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
- package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
- package/openapi.yaml +135 -59
- package/package.json +2 -1
- package/scripts/sync-llm-catalog.ts +6 -15
- package/scripts/sync-web-search-catalog.ts +3 -11
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +14 -26
- package/src/__tests__/agent-loop-compaction-strip.test.ts +240 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
- package/src/__tests__/app-dir-path-guard.test.ts +0 -1
- package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
- package/src/__tests__/avatar-identity-sync.test.ts +2 -27
- package/src/__tests__/btw-routes.test.ts +6 -8
- package/src/__tests__/checker.test.ts +0 -3
- package/src/__tests__/config-loader-backfill.test.ts +103 -6
- package/src/__tests__/config-watcher.test.ts +0 -18
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
- package/src/__tests__/credential-broker.test.ts +449 -1
- package/src/__tests__/credential-execution-tools.test.ts +0 -1
- package/src/__tests__/credential-prompt-route.test.ts +3 -4
- package/src/__tests__/credential-routes.test.ts +360 -0
- package/src/__tests__/credential-security-invariants.test.ts +2 -13
- package/src/__tests__/dynamic-page-surface.test.ts +101 -1
- package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
- package/src/__tests__/gateway-only-guard.test.ts +3 -7
- package/src/__tests__/identity-routes.test.ts +0 -189
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/invite-redemption-service.test.ts +4 -4
- package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
- package/src/__tests__/llm-catalog-parity.test.ts +0 -22
- package/src/__tests__/llm-resolver.test.ts +49 -24
- package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
- package/src/__tests__/onboarding-persona-write.test.ts +1 -1
- package/src/__tests__/persona-resolver.test.ts +11 -14
- package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
- package/src/__tests__/registry.test.ts +2 -7
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
- package/src/__tests__/schedule-routes.test.ts +0 -30
- package/src/__tests__/schedule-tools.test.ts +2 -18
- package/src/__tests__/skill-execute-input.test.ts +46 -1
- package/src/__tests__/skill-runtime-path.test.ts +2 -3
- package/src/__tests__/subagent-tools.test.ts +116 -0
- package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
- package/src/__tests__/token-manager.test.ts +519 -0
- package/src/__tests__/tool-executor.test.ts +0 -79
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +6 -6
- package/src/__tests__/voice-invite-redemption.test.ts +2 -2
- package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
- package/src/__tests__/workspace-greetings.test.ts +152 -0
- package/src/agent/loop.ts +25 -5
- package/src/api/README.md +6 -6
- package/src/api/responses/conversation-message.ts +2 -4
- package/src/api/responses/home.ts +0 -4
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/calls/relay-access-wait.ts +1 -1
- package/src/calls/voice-session-bridge.ts +2 -2
- package/src/cli/commands/plugins.ts +143 -2
- package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +313 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +253 -2
- package/src/cli/lib/diff-plugin.ts +346 -0
- package/src/cli/lib/install-from-github.ts +105 -17
- package/src/cli/lib/merge-plugin-tree.ts +228 -0
- package/src/cli/lib/plugin-fingerprint.ts +14 -0
- package/src/cli/lib/upgrade-plugin.ts +270 -10
- package/src/cli/program.ts +0 -2
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/bundled-skills/workflows/SKILL.md +0 -1
- package/src/config/bundled-tool-registry.ts +2 -7
- package/src/config/call-site-defaults.ts +12 -2
- package/src/config/feature-flag-registry.json +1 -17
- package/src/config/inference-profile-validation.ts +26 -0
- package/src/config/loader.ts +4 -0
- package/src/config/profile-order.ts +28 -0
- package/src/config/schemas/elevenlabs.ts +0 -1
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/seed-inference-profiles.ts +25 -20
- package/src/contacts/contact-store.ts +87 -96
- package/src/contacts/contacts-write.ts +5 -21
- package/src/context/compactor.ts +2 -2
- package/src/credential-execution/process-manager.ts +55 -14
- package/src/credential-execution/prompted-credential.ts +2 -3
- package/src/daemon/config-watcher.ts +0 -4
- package/src/daemon/conversation-agent-loop.ts +15 -4
- package/src/daemon/conversation-slash.ts +2 -23
- package/src/daemon/conversation-tool-setup.ts +11 -3
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/handlers/config-channels.ts +20 -16
- package/src/daemon/handlers/config-slack-channel.ts +2 -3
- package/src/daemon/lifecycle.ts +0 -7
- package/src/daemon/message-types/conversations.ts +3 -3
- package/src/daemon/message-types/sync.ts +0 -1
- package/src/daemon/orphan-reaper.test.ts +0 -19
- package/src/daemon/orphan-reaper.ts +2 -24
- package/src/daemon/server.ts +0 -10
- package/src/home/relationship-state.ts +2 -4
- package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
- package/src/memory/bookmark-crud.ts +1 -2
- package/src/memory/db-init.ts +7 -17
- package/src/memory/embedding-backend.ts +23 -0
- package/src/memory/embedding-billing-breaker.ts +96 -0
- package/src/memory/jobs-store.ts +25 -13
- package/src/memory/jobs-worker.ts +52 -0
- package/src/memory/memory-retrospective-constants.ts +4 -4
- package/src/memory/memory-retrospective-job.ts +19 -227
- package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +62 -0
- package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +311 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/run-migrations.ts +41 -0
- package/src/memory/migrations/validate-migration-state.ts +1 -1
- package/src/memory/schema/contacts.ts +0 -4
- package/src/messaging/providers/slack/adapter.ts +1 -1
- package/src/notifications/adapters/shared.ts +29 -0
- package/src/notifications/adapters/slack.ts +5 -32
- package/src/notifications/adapters/telegram.ts +2 -20
- package/src/notifications/broadcaster.ts +10 -1
- package/src/notifications/home-feed-side-effect.ts +4 -3
- package/src/notifications/notification-utils.ts +17 -19
- package/src/notifications/types.ts +7 -0
- package/src/oauth/AGENTS.md +5 -24
- package/src/plugin-api/constants.ts +1 -1
- package/src/plugin-api/index.ts +6 -1
- package/src/plugin-api/model-profiles.ts +33 -0
- package/src/plugin-api/types.ts +50 -2
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
- package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
- package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +1 -1
- package/src/prompts/persona-resolver.ts +2 -2
- package/src/runtime/AGENTS.md +0 -1
- package/src/runtime/actor-trust-resolver.ts +12 -44
- package/src/runtime/btw-sidechain.ts +3 -6
- package/src/runtime/channel-approval-types.ts +18 -45
- package/src/runtime/channel-invite-transports/telegram.ts +4 -4
- package/src/runtime/channel-verification-service.ts +4 -3
- package/src/runtime/invite-redemption-service.ts +3 -3
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +218 -1
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
- package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
- package/src/runtime/routes/btw-routes.ts +1 -27
- package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/credential-routes.ts +40 -16
- package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
- package/src/runtime/routes/identity-routes.ts +1 -296
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +171 -5
- package/src/runtime/routes/schedule-routes.ts +0 -22
- package/src/runtime/routes/workflow-routes.test.ts +4 -43
- package/src/runtime/routes/workflow-routes.ts +0 -28
- package/src/runtime/routes/workspace-greetings.ts +55 -0
- package/src/runtime/sync/resource-sync-events.ts +1 -11
- package/src/schedule/inference-profile.ts +2 -14
- package/src/subagent/manager.ts +6 -0
- package/src/subagent/types.ts +6 -0
- package/src/tools/AGENTS.md +3 -3
- package/src/tools/browser/browser-execution.ts +1 -1
- package/src/tools/network/web-search-error.ts +1 -1
- package/src/tools/permission-checker.ts +1 -1
- package/src/tools/schedule/create.ts +3 -9
- package/src/tools/schedule/update.ts +2 -10
- package/src/tools/side-effects.ts +2 -17
- package/src/tools/skills/execute.ts +34 -0
- package/src/tools/subagent/spawn.ts +34 -9
- package/src/tools/tool-approval-handler.ts +1 -3
- package/src/tools/tool-manifest.ts +0 -2
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/workflows/run-workflow.test.ts +8 -18
- package/src/util/platform.ts +2 -2
- package/src/workflows/capabilities.ts +2 -3
- package/src/workflows/run-manager.test.ts +0 -25
- package/src/workflows/run-manager.ts +2 -24
- package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
- package/src/__tests__/credential-security-e2e.test.ts +0 -362
- package/src/__tests__/credential-vault-unit.test.ts +0 -1528
- package/src/__tests__/credential-vault.test.ts +0 -1706
- package/src/__tests__/identity-intro-cache.test.ts +0 -315
- package/src/__tests__/secret-onetime-send.test.ts +0 -182
- package/src/cli/commands/__tests__/task.test.ts +0 -914
- package/src/cli/commands/task.ts +0 -771
- package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
- package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
- package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
- package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
- package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
- package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
- package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
- package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
- package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
- package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
- package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
- package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
- package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
- package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
- package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
- package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
- package/src/memory/preloaded-apps.ts +0 -116
- package/src/runtime/routes/identity-intro-cache.ts +0 -172
- package/src/tools/credentials/vault.ts +0 -712
|
@@ -14,7 +14,10 @@ import { v4 as uuid } from "uuid";
|
|
|
14
14
|
import { getConversation } from "../memory/conversation-crud.js";
|
|
15
15
|
import type { ApprovalUIMetadata } from "../runtime/channel-approval-types.js";
|
|
16
16
|
import { getLogger } from "../util/logger.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
buildAccessRequestContractText,
|
|
19
|
+
parseAccessRequestPayload,
|
|
20
|
+
} from "./access-request-copy.js";
|
|
18
21
|
import { isGuardianSensitiveEvent } from "./adapters/macos.js";
|
|
19
22
|
import { pairDeliveryWithConversation } from "./conversation-pairing.js";
|
|
20
23
|
import { composeFallbackCopy } from "./copy-composer.js";
|
|
@@ -181,6 +184,11 @@ export class NotificationBroadcaster {
|
|
|
181
184
|
> | null = null;
|
|
182
185
|
|
|
183
186
|
const approvalContext = resolveApprovalContext(signal);
|
|
187
|
+
const accessRequestContext =
|
|
188
|
+
signal.sourceEventName === "ingress.access_request" &&
|
|
189
|
+
signal.contextPayload
|
|
190
|
+
? parseAccessRequestPayload(signal.contextPayload)
|
|
191
|
+
: undefined;
|
|
184
192
|
const results: NotificationDeliveryResult[] = [];
|
|
185
193
|
|
|
186
194
|
for (const channel of orderedChannels) {
|
|
@@ -411,6 +419,7 @@ export class NotificationBroadcaster {
|
|
|
411
419
|
contextPayload: signal.contextPayload,
|
|
412
420
|
urgency: signal.attentionHints.urgency,
|
|
413
421
|
approvalContext,
|
|
422
|
+
accessRequestContext,
|
|
414
423
|
};
|
|
415
424
|
|
|
416
425
|
// Compute conversation decision audit fields for the delivery record
|
|
@@ -217,9 +217,10 @@ function resolveHomeFeedMirror(
|
|
|
217
217
|
sourceConversationId?: string;
|
|
218
218
|
sourceScheduleJobId?: string;
|
|
219
219
|
} {
|
|
220
|
-
let sourceRow:
|
|
221
|
-
|
|
222
|
-
|
|
220
|
+
let sourceRow: {
|
|
221
|
+
conversationType?: string;
|
|
222
|
+
scheduleJobId?: string | null;
|
|
223
|
+
} | null = null;
|
|
223
224
|
if (signal.sourceContextId) {
|
|
224
225
|
try {
|
|
225
226
|
sourceRow = getConversation(signal.sourceContextId) ?? null;
|
|
@@ -29,38 +29,36 @@ export function readPayloadString(
|
|
|
29
29
|
return typeof value === "string" ? value : undefined;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** Truncate `text` to `maxLength`, appending "…" when exceeded. */
|
|
33
|
+
export function truncate(text: string, maxLength: number): string {
|
|
34
|
+
if (text.length <= maxLength) return text;
|
|
35
|
+
return text.slice(0, maxLength - 1) + "…";
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
// ── Sanitization ────────────────────────────────────────────────────────────
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
/** Strip control characters and newlines, then truncate to `maxLength`. */
|
|
41
|
+
function sanitize(value: string, maxLength: number): string {
|
|
42
|
+
return truncate(
|
|
43
|
+
value.replace(/[\x00-\x1f\x7f-\x9f\r\n]+/g, " ").trim(),
|
|
44
|
+
maxLength,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
35
47
|
|
|
36
48
|
/**
|
|
37
49
|
* Sanitize an untrusted identity field for inclusion in notification copy.
|
|
38
|
-
*
|
|
39
|
-
* - Strips control characters (U+0000–U+001F, U+007F–U+009F) and newlines.
|
|
40
|
-
* - Clamps to 120 characters.
|
|
50
|
+
* Strips control characters and clamps to 120 characters.
|
|
41
51
|
*/
|
|
42
52
|
export function sanitizeIdentityField(value: string): string {
|
|
43
|
-
|
|
44
|
-
const clamped =
|
|
45
|
-
stripped.length > IDENTITY_FIELD_MAX_LENGTH
|
|
46
|
-
? stripped.slice(0, IDENTITY_FIELD_MAX_LENGTH) + "…"
|
|
47
|
-
: stripped;
|
|
48
|
-
return clamped;
|
|
53
|
+
return sanitize(value, 120);
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
export const MESSAGE_PREVIEW_MAX_LENGTH = 200;
|
|
52
57
|
|
|
53
58
|
/**
|
|
54
59
|
* Sanitize an untrusted message preview for inclusion in notification copy.
|
|
55
|
-
*
|
|
56
|
-
* Same as {@link sanitizeIdentityField} but uses a higher length limit
|
|
57
|
-
* (200 chars) suited for message previews.
|
|
60
|
+
* Strips control characters and clamps to 200 characters.
|
|
58
61
|
*/
|
|
59
62
|
export function sanitizeMessagePreview(value: string): string {
|
|
60
|
-
|
|
61
|
-
const clamped =
|
|
62
|
-
stripped.length > MESSAGE_PREVIEW_MAX_LENGTH
|
|
63
|
-
? stripped.slice(0, MESSAGE_PREVIEW_MAX_LENGTH) + "…"
|
|
64
|
-
: stripped;
|
|
65
|
-
return clamped;
|
|
63
|
+
return sanitize(value, MESSAGE_PREVIEW_MAX_LENGTH);
|
|
66
64
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { ChannelPolicies } from "../channels/config.js";
|
|
9
9
|
import type { ChannelId } from "../channels/types.js";
|
|
10
10
|
import type { ApprovalUIMetadata } from "../runtime/channel-approval-types.js";
|
|
11
|
+
import type { ParsedAccessRequestPayload } from "./access-request-copy.js";
|
|
11
12
|
import type { AttentionHints } from "./signal.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -103,6 +104,12 @@ export interface ChannelDeliveryPayload {
|
|
|
103
104
|
* `contextPayload`.
|
|
104
105
|
*/
|
|
105
106
|
approvalContext?: ApprovalUIMetadata;
|
|
107
|
+
/**
|
|
108
|
+
* Pre-parsed access request context for `ingress.access_request` signals.
|
|
109
|
+
* Built centrally by the broadcaster so adapters can render structured
|
|
110
|
+
* access-request cards without re-parsing `contextPayload`.
|
|
111
|
+
*/
|
|
112
|
+
accessRequestContext?: ParsedAccessRequestPayload;
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
/**
|
package/src/oauth/AGENTS.md
CHANGED
|
@@ -29,34 +29,15 @@ Then add the key to `ServicesSchema`:
|
|
|
29
29
|
|
|
30
30
|
The key here **must** match the `managedServiceConfigKey` in `seed-providers.ts`. The cross-repo invariant test in `__tests__/seed-providers-managed.test.ts` will fail if they drift.
|
|
31
31
|
|
|
32
|
-
### 3. _(managed only)_ Enable by default during onboarding
|
|
32
|
+
### 3. _(managed only)_ Enable by default during onboarding
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Managed-sign-in users should get the integration pre-enabled by setting `services.acme-oauth.mode` to `"managed"` in the client's onboarding config defaults.
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
configValues["services.acme-oauth.mode"] = "managed"
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### 4. Add a cached logo — `clients/shared/Resources/IntegrationLogos/`
|
|
41
|
-
|
|
42
|
-
Drop a **vector PDF** named `{provider_key}.pdf` (e.g. `acme.pdf`) into the `IntegrationLogos/` directory. The file is automatically bundled via `.copy()` in `clients/Package.swift` and looked up at runtime by `IntegrationLogoBundle` using the provider key — no code changes needed.
|
|
43
|
-
|
|
44
|
-
Most existing logos come from [Simple Icons](https://simpleicons.org) (CC0-licensed). To get a PDF from Simple Icons:
|
|
45
|
-
|
|
46
|
-
1. Find the icon slug on https://simpleicons.org (e.g. `slack`, `linear`).
|
|
47
|
-
2. Download the SVG: `curl -o acme.svg https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/acme.svg`
|
|
48
|
-
3. Convert to PDF using `rsvg-convert` (same tool used by `clients/scripts/sync-lucide-icons.sh`):
|
|
49
|
-
```bash
|
|
50
|
-
# Install if needed: brew install librsvg
|
|
51
|
-
rsvg-convert -f pdf -o clients/shared/Resources/IntegrationLogos/acme.pdf acme.svg
|
|
52
|
-
```
|
|
53
|
-
4. Add the provider key to `clients/shared/Resources/integration-logos-manifest.json`.
|
|
54
|
-
|
|
55
|
-
If the service is not on Simple Icons, source or create an SVG and convert it the same way. The result must be a true vector PDF (not a rasterized image wrapped in PDF) so it scales cleanly.
|
|
36
|
+
### 4. Set the logo URL — `seed-providers.ts`
|
|
56
37
|
|
|
57
|
-
The `logoUrl` field in `seed-providers.ts`
|
|
38
|
+
The `logoUrl` field in `seed-providers.ts` is the source of truth for a provider's logo. Most providers use a [Simple Icons](https://simpleicons.org) (CC0-licensed) CDN URL like `https://cdn.simpleicons.org/acme`. The web client resolves logos from this field (see `clients/web/src/components/integrations/integration-icon.tsx`) and falls back to an initials avatar.
|
|
58
39
|
|
|
59
|
-
For brands Simple Icons doesn't host (e.g. Salesforce, which Simple Icons removed for trademark reasons), use the
|
|
40
|
+
For brands Simple Icons doesn't host (e.g. Salesforce, which Simple Icons removed for trademark reasons), use the `glincker/thesvg` source via jsDelivr — `https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/<key>/default.svg`. The recognised `logoUrl` prefixes are enforced by `oauth-provider-seed-logos.test.ts`; if you need a third source, extend that allowlist.
|
|
60
41
|
|
|
61
42
|
### 5. Secret patterns (if applicable) — `../security/secret-patterns.ts`
|
|
62
43
|
|
|
@@ -20,7 +20,7 @@ export const HOOKS = {
|
|
|
20
20
|
SHUTDOWN: "shutdown",
|
|
21
21
|
/** Fires once per user turn, immediately before the agent loop receives `runMessages`. */
|
|
22
22
|
USER_PROMPT_SUBMIT: "user-prompt-submit",
|
|
23
|
-
/** Fires immediately before each provider call. A hook may edit the outbound request (e.g. the system prompt) and opt the turn into deferred output streaming. */
|
|
23
|
+
/** Fires immediately before each provider call. A hook may edit the outbound request (e.g. the system prompt), route the call to a different inference profile, and opt the turn into deferred output streaming. */
|
|
24
24
|
PRE_MODEL_CALL: "pre-model-call",
|
|
25
25
|
/** Fires once per tool result, after the tool returns and before the result is sent to the provider. */
|
|
26
26
|
POST_TOOL_USE: "post-tool-use",
|
package/src/plugin-api/index.ts
CHANGED
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
*
|
|
33
33
|
* - {@link assistantEventHub} — the assistant's pub/sub hub for runtime events
|
|
34
34
|
* - {@link getSecureKeyAsync} — read a secret from secure storage
|
|
35
|
+
* - {@link getModelProfiles} — list the workspace inference profiles a plugin
|
|
36
|
+
* can route to (e.g. a model router building its category → profile map)
|
|
35
37
|
*
|
|
36
38
|
* - {@link PluginInitContext} — passed to `init` hook at bootstrap
|
|
37
39
|
* - {@link PluginShutdownContext} — passed to `shutdown` hook at teardown
|
|
@@ -40,7 +42,8 @@
|
|
|
40
42
|
* - {@link PostCompactContext} — passed to `post-compact` hook, fired after
|
|
41
43
|
* the agent loop compacts a conversation mid-turn to re-apply injections
|
|
42
44
|
* - {@link PreModelCallContext} — passed to `pre-model-call` hook, fired
|
|
43
|
-
* before each provider call to edit the request
|
|
45
|
+
* before each provider call to edit the request, route it to a different
|
|
46
|
+
* inference profile, or defer output streaming
|
|
44
47
|
* - {@link PostToolUseContext} — passed to `post-tool-use` hook, fired once
|
|
45
48
|
* per tool result before it joins the provider-bound history
|
|
46
49
|
* - {@link StopContext} — passed to `stop` hook, the definitive terminal hook
|
|
@@ -79,6 +82,7 @@ export type {
|
|
|
79
82
|
} from "../providers/types.js";
|
|
80
83
|
export type {
|
|
81
84
|
AgentLoopExitReason,
|
|
85
|
+
ModelProfileInfo,
|
|
82
86
|
PluginHookFn,
|
|
83
87
|
PluginInitContext,
|
|
84
88
|
PluginLogger,
|
|
@@ -110,3 +114,4 @@ export type {
|
|
|
110
114
|
} from "../runtime/assistant-event-hub.js";
|
|
111
115
|
export { assistantEventHub } from "../runtime/assistant-event-hub.js";
|
|
112
116
|
export { getSecureKeyAsync } from "../security/secure-keys.js";
|
|
117
|
+
export { getModelProfiles } from "./model-profiles.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getConfig } from "../config/loader.js";
|
|
2
|
+
import { orderProfileKeys } from "../config/profile-order.js";
|
|
3
|
+
import type { ModelProfileInfo } from "./types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List the workspace inference profiles a plugin can route to, in the order the
|
|
7
|
+
* `/model` picker presents them (`llm.profileOrder` first, then the rest
|
|
8
|
+
* alphabetically). Disabled profiles are included and flagged via
|
|
9
|
+
* {@link ModelProfileInfo.isDisabled}; weighted "mix" profiles are included and
|
|
10
|
+
* flagged via {@link ModelProfileInfo.isMix}, since a mix is itself a valid
|
|
11
|
+
* routing target (it resolves to one constituent per conversation).
|
|
12
|
+
*
|
|
13
|
+
* Reads the live in-memory config, so the result reflects the current profile
|
|
14
|
+
* set each time it is called.
|
|
15
|
+
*/
|
|
16
|
+
export function getModelProfiles(): ModelProfileInfo[] {
|
|
17
|
+
const { llm } = getConfig();
|
|
18
|
+
const { profiles, activeProfile } = llm;
|
|
19
|
+
const result: ModelProfileInfo[] = [];
|
|
20
|
+
for (const key of orderProfileKeys(profiles, llm.profileOrder)) {
|
|
21
|
+
const entry = profiles[key];
|
|
22
|
+
if (entry == null) continue;
|
|
23
|
+
result.push({
|
|
24
|
+
key,
|
|
25
|
+
label: entry.label ?? key,
|
|
26
|
+
description: entry.description ?? null,
|
|
27
|
+
isActive: key === activeProfile,
|
|
28
|
+
isDisabled: entry.status === "disabled",
|
|
29
|
+
isMix: entry.mix != null,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
package/src/plugin-api/types.ts
CHANGED
|
@@ -94,6 +94,35 @@ export interface PluginInitContext {
|
|
|
94
94
|
assistantVersion: string;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// ─── Model profiles ──────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A workspace inference profile a plugin can route to. Returned by
|
|
101
|
+
* {@link getModelProfiles}; {@link key} is the value a `pre-model-call` hook
|
|
102
|
+
* assigns to `PreModelCallContext.modelProfile` to route a call. A model router
|
|
103
|
+
* reads this list (typically at `init`) to learn which profiles exist before
|
|
104
|
+
* mapping a classified message onto one.
|
|
105
|
+
*/
|
|
106
|
+
export interface ModelProfileInfo {
|
|
107
|
+
/** Profile key in `llm.profiles`; assignable to `PreModelCallContext.modelProfile`. */
|
|
108
|
+
readonly key: string;
|
|
109
|
+
/** Human-readable label, falling back to {@link key} when none is set. */
|
|
110
|
+
readonly label: string;
|
|
111
|
+
/** Author-supplied description, or `null` when none is set. */
|
|
112
|
+
readonly description: string | null;
|
|
113
|
+
/** Whether this is the workspace's active profile. */
|
|
114
|
+
readonly isActive: boolean;
|
|
115
|
+
/** Whether the profile is disabled; routing to it is rejected by the resolver. */
|
|
116
|
+
readonly isDisabled: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Whether this is a weighted "mix" profile — an A/B blend that resolves to one
|
|
119
|
+
* of its constituent profiles per conversation via a seeded weighted pick.
|
|
120
|
+
* Routing to its {@link key} is valid; it directs the call into the blend
|
|
121
|
+
* rather than at a single fixed model.
|
|
122
|
+
*/
|
|
123
|
+
readonly isMix: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
97
126
|
// ─── Shutdown context ────────────────────────────────────────────────────────
|
|
98
127
|
|
|
99
128
|
/**
|
|
@@ -438,8 +467,9 @@ export interface StopContext {
|
|
|
438
467
|
* and compaction work can share a conversation), hooks MUST self-gate on
|
|
439
468
|
* {@link callSite} / {@link conversationId} before acting.
|
|
440
469
|
*
|
|
441
|
-
* A hook may edit the outbound request by replacing {@link systemPrompt},
|
|
442
|
-
*
|
|
470
|
+
* A hook may edit the outbound request by replacing {@link systemPrompt}, route
|
|
471
|
+
* the call to a different inference profile via {@link modelProfile}, and opt
|
|
472
|
+
* this turn into deferred output streaming via {@link deferAssistantOutput}.
|
|
443
473
|
* Mutate the context in place or return a new one; throwing is contained by the
|
|
444
474
|
* loop (the call proceeds with the original request).
|
|
445
475
|
*/
|
|
@@ -456,6 +486,24 @@ export interface PreModelCallContext {
|
|
|
456
486
|
* append a section); the loop sends the resulting value.
|
|
457
487
|
*/
|
|
458
488
|
systemPrompt: string | null;
|
|
489
|
+
/**
|
|
490
|
+
* Inference profile to route THIS provider call to, named by its key in the
|
|
491
|
+
* workspace `llm.profiles`. Seeded with the call's already-resolved override
|
|
492
|
+
* profile, or `null` when none applies. A hook may replace it to select a
|
|
493
|
+
* different profile per call — the lever a model router uses to map a
|
|
494
|
+
* classified message onto a profile (model + provider connection + sampling
|
|
495
|
+
* settings). For the user-facing `mainAgent` call the resolver layers the
|
|
496
|
+
* named profile at the top of precedence (above the workspace active
|
|
497
|
+
* profile), so the hook's choice wins; a key with no matching profile falls
|
|
498
|
+
* through unchanged (no throw). Honored only when {@link callSite} is set.
|
|
499
|
+
* Set to `null` to apply no override.
|
|
500
|
+
*
|
|
501
|
+
* Context-window sizing and overflow recovery for this call are computed from
|
|
502
|
+
* the profile resolved before the hook runs. Routing a near-budget
|
|
503
|
+
* conversation to a profile with a smaller context window relies on the loop's
|
|
504
|
+
* overflow recovery (compact and retry) rather than proactive compaction.
|
|
505
|
+
*/
|
|
506
|
+
modelProfile: string | null;
|
|
459
507
|
/**
|
|
460
508
|
* Seeded `false`. When a hook sets it `true`, the loop suppresses this turn's
|
|
461
509
|
* live assistant `text_delta` stream; a `post-model-call` hook is then
|
|
@@ -54,6 +54,10 @@ import memoryRetrievalPkg from "./memory-retrieval/package.json" with { type: "j
|
|
|
54
54
|
import memoryV3PostCompact from "./memory-v3-shadow/hooks/post-compact.js";
|
|
55
55
|
import memoryV3UserPromptSubmit from "./memory-v3-shadow/hooks/user-prompt-submit.js";
|
|
56
56
|
import memoryV3Pkg from "./memory-v3-shadow/package.json" with { type: "json" };
|
|
57
|
+
import surfaceCompletionNudgePostModelCall from "./surface-completion-nudge/hooks/post-model-call.js";
|
|
58
|
+
import surfaceCompletionNudgeStop from "./surface-completion-nudge/hooks/stop.js";
|
|
59
|
+
import { resetSurfaceCompletionNudgeStoreForTests } from "./surface-completion-nudge/nudge-state-store.js";
|
|
60
|
+
import surfaceCompletionNudgePkg from "./surface-completion-nudge/package.json" with { type: "json" };
|
|
57
61
|
import taskProgressNudgePostToolUse, {
|
|
58
62
|
resetTaskProgressNudgeStateForTests,
|
|
59
63
|
} from "./task-progress-nudge/hooks/post-tool-use.js";
|
|
@@ -259,6 +263,25 @@ export const defaultTaskProgressNudgePlugin: Plugin = {
|
|
|
259
263
|
},
|
|
260
264
|
};
|
|
261
265
|
|
|
266
|
+
/**
|
|
267
|
+
* `surface-completion-nudge` — a `post-model-call` hook that, when a user-facing
|
|
268
|
+
* turn is about to end with a progress surface (a `task_progress` card or
|
|
269
|
+
* `work_result`) the model showed but never advanced to a terminal status or
|
|
270
|
+
* dismissed, nudges the model once to close it and re-queries so it can act; the
|
|
271
|
+
* `stop` hook clears the one-shot bound on a terminal stop so the next run
|
|
272
|
+
* nudges afresh.
|
|
273
|
+
*/
|
|
274
|
+
export const defaultSurfaceCompletionNudgePlugin: Plugin = {
|
|
275
|
+
manifest: {
|
|
276
|
+
name: surfaceCompletionNudgePkg.name,
|
|
277
|
+
version: surfaceCompletionNudgePkg.version,
|
|
278
|
+
},
|
|
279
|
+
hooks: {
|
|
280
|
+
"post-model-call": surfaceCompletionNudgePostModelCall,
|
|
281
|
+
stop: surfaceCompletionNudgeStop,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
262
285
|
/**
|
|
263
286
|
* `tool-result-truncate` — a `post-tool-use` hook that tail-drops an oversized
|
|
264
287
|
* tool result down to a character budget derived from the model's context
|
|
@@ -289,6 +312,7 @@ function getAllDefaultPlugins(): readonly Plugin[] {
|
|
|
289
312
|
defaultToolErrorPlugin,
|
|
290
313
|
defaultExplorationDriftPlugin,
|
|
291
314
|
defaultTaskProgressNudgePlugin,
|
|
315
|
+
defaultSurfaceCompletionNudgePlugin,
|
|
292
316
|
defaultHistoryRepairPlugin,
|
|
293
317
|
defaultImageRecoveryPlugin,
|
|
294
318
|
defaultCompactionPlugin,
|
|
@@ -339,5 +363,6 @@ export function resetPluginRegistryAndRegisterDefaults(): void {
|
|
|
339
363
|
resetImageRecoveryStoreForTests();
|
|
340
364
|
resetExplorationDriftStateForTests();
|
|
341
365
|
resetTaskProgressNudgeStateForTests();
|
|
366
|
+
resetSurfaceCompletionNudgeStoreForTests();
|
|
342
367
|
registerDefaultPlugins();
|
|
343
368
|
}
|
|
@@ -71,6 +71,7 @@ describe("maintainJob", () => {
|
|
|
71
71
|
return makeIndex(slugs);
|
|
72
72
|
},
|
|
73
73
|
readPageBody: async (slug) => `body for ${slug}`,
|
|
74
|
+
readCapabilityBody: async (slug) => `capability body for ${slug}`,
|
|
74
75
|
deleteSectionsForArticle: async (_config, article) => {
|
|
75
76
|
calls.deleted.push(article);
|
|
76
77
|
},
|
|
@@ -283,6 +284,9 @@ describe("maintainJob", () => {
|
|
|
283
284
|
const { deps: d, calls } = deps({
|
|
284
285
|
loadCoreSet: () => ["page-live", "page-gone", "skills/example"],
|
|
285
286
|
listIndexedSlugs: async () => ["page-live", "skills/example"],
|
|
287
|
+
// The capability row is already in the store, so the reconcile stage
|
|
288
|
+
// no-ops here and this test exercises core-validation in isolation.
|
|
289
|
+
listSectionArticles: async () => ["skills/example"],
|
|
286
290
|
});
|
|
287
291
|
const outcome = await maintainJob(JOB, CONFIG, d);
|
|
288
292
|
|
|
@@ -317,8 +321,9 @@ describe("maintainJob", () => {
|
|
|
317
321
|
});
|
|
318
322
|
const outcome = await maintainJob(JOB, CONFIG, d);
|
|
319
323
|
expect(outcome.danglingCoreSlugs).toEqual([]);
|
|
320
|
-
// The prune
|
|
321
|
-
|
|
324
|
+
// The reconcile and prune stages each read the index once; the empty core
|
|
325
|
+
// stage adds no further read.
|
|
326
|
+
expect(indexReads).toBe(2);
|
|
322
327
|
});
|
|
323
328
|
|
|
324
329
|
test("a thrown core-validation stage is contained and does not abort lane invalidation", async () => {
|
|
@@ -351,6 +356,53 @@ describe("maintainJob", () => {
|
|
|
351
356
|
expect(calls.invalidate).toBe(1);
|
|
352
357
|
expect(outcome.invalidated).toBe(true);
|
|
353
358
|
});
|
|
359
|
+
|
|
360
|
+
test("reconciles capability rows missing from the section store", async () => {
|
|
361
|
+
setOverridesForTesting({ [FLAG_SHADOW]: true });
|
|
362
|
+
// The index lists a real page and three capability rows; the store already
|
|
363
|
+
// holds one of the caps. The change-delta excludes capability rows, so
|
|
364
|
+
// without this stage a skill enabled after the one-time backfill never
|
|
365
|
+
// reaches the dense lane. selectChangedPages stays empty so `calls.built`
|
|
366
|
+
// reflects reconcile embeds only.
|
|
367
|
+
const { deps: d, calls } = deps({
|
|
368
|
+
selectChangedPages: async () => [],
|
|
369
|
+
listIndexedSlugs: async () => [
|
|
370
|
+
"page-a",
|
|
371
|
+
"skills/already",
|
|
372
|
+
"skills/workflows",
|
|
373
|
+
"skills/another",
|
|
374
|
+
],
|
|
375
|
+
listSectionArticles: async () => ["page-a", "skills/already"],
|
|
376
|
+
});
|
|
377
|
+
const outcome = await maintainJob(JOB, CONFIG, d);
|
|
378
|
+
|
|
379
|
+
// Only the caps missing from the store are embedded; the real page (handled
|
|
380
|
+
// by the change-delta stage) and the already-stored cap are left alone.
|
|
381
|
+
expect(outcome.capabilitiesReconciled).toBe(2);
|
|
382
|
+
expect(outcome.reconcileFailures).toBe(0);
|
|
383
|
+
expect(calls.built).toEqual([["skills/workflows"], ["skills/another"]]);
|
|
384
|
+
expect(calls.deleted).toEqual(["skills/workflows", "skills/another"]);
|
|
385
|
+
expect(calls.upserted.flat().map((s) => s.article)).toEqual([
|
|
386
|
+
"skills/workflows",
|
|
387
|
+
"skills/another",
|
|
388
|
+
]);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("skips a cold capability row (empty body) without deleting its points", async () => {
|
|
392
|
+
setOverridesForTesting({ [FLAG_SHADOW]: true });
|
|
393
|
+
const { deps: d, calls } = deps({
|
|
394
|
+
listIndexedSlugs: async () => ["skills/cold"],
|
|
395
|
+
listSectionArticles: async () => [],
|
|
396
|
+
readCapabilityBody: async () => "", // capability store not seeded yet
|
|
397
|
+
});
|
|
398
|
+
const outcome = await maintainJob(JOB, CONFIG, d);
|
|
399
|
+
|
|
400
|
+
expect(outcome.capabilitiesReconciled).toBe(0);
|
|
401
|
+
expect(outcome.reconcileFailures).toBe(0);
|
|
402
|
+
// Never replace good points with a blank: no delete, no upsert.
|
|
403
|
+
expect(calls.deleted).toEqual([]);
|
|
404
|
+
expect(calls.upserted).toEqual([]);
|
|
405
|
+
});
|
|
354
406
|
});
|
|
355
407
|
|
|
356
408
|
describe("computeChangedPages", () => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Memory v3 — `memory_v3_maintain` job handler.
|
|
3
3
|
*
|
|
4
4
|
* A flag-gated, best-effort self-maintenance pass over the v3 section dense
|
|
5
|
-
* store and the in-memory lanes. It runs
|
|
5
|
+
* store and the in-memory lanes. It runs five independent stages, in order:
|
|
6
6
|
*
|
|
7
7
|
* 1. **Section re-embed** — diff the page index by `modifiedAt` against the
|
|
8
8
|
* last successful pass (the high-water mark below), and for every page that
|
|
@@ -15,19 +15,26 @@
|
|
|
15
15
|
* captured before any potential mtime bumps) so a page is not re-embedded
|
|
16
16
|
* forever, yet a page whose embed failed (and whose sections were therefore
|
|
17
17
|
* left deleted) stays above the mark and is retried next pass.
|
|
18
|
-
* 2. **
|
|
18
|
+
* 2. **Capability reconcile** — embed capability rows (synthetic skill/CLI
|
|
19
|
+
* slugs) present in the page index but missing from the section store. The
|
|
20
|
+
* re-embed delta above EXCLUDES capability rows (they have `modifiedAt` 0,
|
|
21
|
+
* no mtime to diff), so the only other embedder is the one-time
|
|
22
|
+
* `backfillAllSections`. Without this stage a skill enabled AFTER that
|
|
23
|
+
* backfill (e.g. a flag-gated skill flipped on at runtime) lands in the
|
|
24
|
+
* index but never reaches the dense lane. See {@link reconcileCapabilityRows}.
|
|
25
|
+
* 3. **Deleted-page prune** — diff the dense store's stored articles
|
|
19
26
|
* (`listSectionArticles`) against the live page-index slugs and
|
|
20
27
|
* `deleteSectionsForArticle` for any article that is no longer in the
|
|
21
28
|
* index. A deleted page's slug never reaches the re-embed delta selector
|
|
22
29
|
* (it only names live pages), so without this its section points would
|
|
23
30
|
* linger in Qdrant and the dense lane could still surface the deleted page.
|
|
24
31
|
* Synthetic capability rows are in the page index, so they are never pruned.
|
|
25
|
-
*
|
|
32
|
+
* 4. **Core-set validation** — load the maintainer-curated core set
|
|
26
33
|
* (`memory/core-pages.md`) and report entries whose page no longer exists
|
|
27
34
|
* in the page index (dangling slugs) via the log + outcome. The file is
|
|
28
35
|
* maintainer-owned, so this stage NEVER edits it — the maintainer fixes
|
|
29
36
|
* dangling entries at the next consolidation pass.
|
|
30
|
-
*
|
|
37
|
+
* 5. **Lane invalidation** — `invalidateLanes()` so the next turn rebuilds the
|
|
31
38
|
* in-memory section index, needle, and edge graph from the freshly-updated
|
|
32
39
|
* pages.
|
|
33
40
|
*
|
|
@@ -110,6 +117,13 @@ export interface MaintainJobDeps {
|
|
|
110
117
|
buildSectionIndex: typeof realBuildSectionIndex;
|
|
111
118
|
/** Read a page's frontmatter-stripped body for `buildSectionIndex`. */
|
|
112
119
|
readPageBody: (slug: Slug) => Promise<string>;
|
|
120
|
+
/**
|
|
121
|
+
* Capability-aware body reader for the reconcile stage: synthetic skill/CLI
|
|
122
|
+
* slugs resolve their rendered capability content; real pages read from disk.
|
|
123
|
+
* The re-embed stage uses `readPageBody` (disk-only) since it only processes
|
|
124
|
+
* real pages — the reconcile stage needs capability bodies.
|
|
125
|
+
*/
|
|
126
|
+
readCapabilityBody: (slug: Slug) => Promise<string>;
|
|
113
127
|
/** Clear an article's stale section points before re-upserting. */
|
|
114
128
|
deleteSectionsForArticle: typeof realDeleteSectionsForArticle;
|
|
115
129
|
/** Embed + upsert an article's current sections into the dense store. */
|
|
@@ -198,6 +212,15 @@ export interface MaintainOutcome {
|
|
|
198
212
|
reembedded: number;
|
|
199
213
|
/** Pages whose re-embed threw (and was contained). */
|
|
200
214
|
reembedFailures: number;
|
|
215
|
+
/**
|
|
216
|
+
* Capability rows (skills/CLI) present in the index but missing from the
|
|
217
|
+
* section store that were embedded this pass — how a skill enabled after the
|
|
218
|
+
* one-time backfill reaches the dense lane (the re-embed delta excludes
|
|
219
|
+
* capability rows).
|
|
220
|
+
*/
|
|
221
|
+
capabilitiesReconciled: number;
|
|
222
|
+
/** Capability reconcile embeds that threw (and were contained). */
|
|
223
|
+
reconcileFailures: number;
|
|
201
224
|
/**
|
|
202
225
|
* Deleted-page articles whose lingering section points were pruned this pass
|
|
203
226
|
* (present in the dense store but absent from the page index).
|
|
@@ -302,6 +325,8 @@ function defaultDeps(config: AssistantConfig): MaintainJobDeps {
|
|
|
302
325
|
selectChangedPages: () => selectChangedPagesFromWorkspace(workspaceDir),
|
|
303
326
|
buildSectionIndex: realBuildSectionIndex,
|
|
304
327
|
readPageBody: (slug) => readPageBodyFromWorkspace(workspaceDir, slug),
|
|
328
|
+
readCapabilityBody: (slug) =>
|
|
329
|
+
backfillPageBodyFromWorkspace(workspaceDir, slug),
|
|
305
330
|
deleteSectionsForArticle: realDeleteSectionsForArticle,
|
|
306
331
|
upsertSections: realUpsertSections,
|
|
307
332
|
commitEmbedHighWater,
|
|
@@ -357,6 +382,58 @@ async function reembedChangedPages(
|
|
|
357
382
|
return { reembedded, reembedFailures };
|
|
358
383
|
}
|
|
359
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Embed capability rows (synthetic skill/CLI slugs) present in the page index
|
|
387
|
+
* but missing from the section dense store. The incremental re-embed selector
|
|
388
|
+
* ({@link computeChangedPages}) excludes capability rows — they have no on-disk
|
|
389
|
+
* mtime to delta against — so the ONLY other embedder is the one-time
|
|
390
|
+
* {@link backfillAllSections}. Without this, a skill enabled AFTER that backfill
|
|
391
|
+
* (e.g. a flag-gated skill flipped on at runtime) lands in the page index but
|
|
392
|
+
* never reaches the dense lane. This stage makes the periodic maintain pass
|
|
393
|
+
* self-heal: diff the live capability slugs against the stored section articles
|
|
394
|
+
* and embed the missing ones.
|
|
395
|
+
*
|
|
396
|
+
* Each row is independent: a single build/upsert throw is logged and counted in
|
|
397
|
+
* `reconcileFailures` without aborting the rest. A capability row whose body
|
|
398
|
+
* resolves empty (its store has not seeded yet) is skipped WITHOUT deleting any
|
|
399
|
+
* points — never replace good points with a blank — and is retried next pass.
|
|
400
|
+
*/
|
|
401
|
+
async function reconcileCapabilityRows(
|
|
402
|
+
deps: MaintainJobDeps,
|
|
403
|
+
): Promise<{ capabilitiesReconciled: number; reconcileFailures: number }> {
|
|
404
|
+
const [indexedSlugs, storedArticles] = await Promise.all([
|
|
405
|
+
deps.listIndexedSlugs(),
|
|
406
|
+
deps.listSectionArticles(),
|
|
407
|
+
]);
|
|
408
|
+
const stored = new Set(storedArticles);
|
|
409
|
+
const missing = indexedSlugs.filter(
|
|
410
|
+
(slug) => isCapabilitySlug(slug) && !stored.has(slug),
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
let capabilitiesReconciled = 0;
|
|
414
|
+
let reconcileFailures = 0;
|
|
415
|
+
for (const slug of missing) {
|
|
416
|
+
try {
|
|
417
|
+
const body = await deps.readCapabilityBody(slug);
|
|
418
|
+
if (body.trim().length === 0) continue; // store cold — retry next pass
|
|
419
|
+
const index = await deps.buildSectionIndex(
|
|
420
|
+
[slug],
|
|
421
|
+
deps.readCapabilityBody,
|
|
422
|
+
);
|
|
423
|
+
await deps.deleteSectionsForArticle(deps.config, slug);
|
|
424
|
+
await deps.upsertSections(deps.config, index.sections);
|
|
425
|
+
capabilitiesReconciled += 1;
|
|
426
|
+
} catch (err) {
|
|
427
|
+
reconcileFailures += 1;
|
|
428
|
+
log.warn(
|
|
429
|
+
{ slug, err: err instanceof Error ? err.message : String(err) },
|
|
430
|
+
"memory-v3 maintain: capability reconcile embed failed (non-fatal)",
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return { capabilitiesReconciled, reconcileFailures };
|
|
435
|
+
}
|
|
436
|
+
|
|
360
437
|
/**
|
|
361
438
|
* Prune section points for articles that no longer exist in the page index. A
|
|
362
439
|
* deleted concept page leaves `getPageIndex` (so the needle and edge lanes both
|
|
@@ -537,6 +614,8 @@ export async function maintainJob(
|
|
|
537
614
|
disabled: false,
|
|
538
615
|
reembedded: 0,
|
|
539
616
|
reembedFailures: 0,
|
|
617
|
+
capabilitiesReconciled: 0,
|
|
618
|
+
reconcileFailures: 0,
|
|
540
619
|
pruned: 0,
|
|
541
620
|
pruneFailures: 0,
|
|
542
621
|
danglingCoreSlugs: [],
|
|
@@ -587,7 +666,26 @@ export async function maintainJob(
|
|
|
587
666
|
);
|
|
588
667
|
}
|
|
589
668
|
|
|
590
|
-
// Stage 2:
|
|
669
|
+
// Stage 2: embed capability rows (skills/CLI) present in the index but missing
|
|
670
|
+
// from the section store. They have modifiedAt 0, so computeChangedPages never
|
|
671
|
+
// selects them; the one-time backfill is otherwise their only embedder, so a
|
|
672
|
+
// skill enabled after that backfill stays invisible to the dense lane until
|
|
673
|
+
// this self-heals it. Contained: a failure is logged + recorded without
|
|
674
|
+
// aborting later stages.
|
|
675
|
+
try {
|
|
676
|
+
const { capabilitiesReconciled, reconcileFailures } =
|
|
677
|
+
await reconcileCapabilityRows(deps);
|
|
678
|
+
outcome.capabilitiesReconciled = capabilitiesReconciled;
|
|
679
|
+
outcome.reconcileFailures = reconcileFailures;
|
|
680
|
+
} catch (err) {
|
|
681
|
+
outcome.failures.push("capability-reconcile");
|
|
682
|
+
log.warn(
|
|
683
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
684
|
+
"memory-v3 maintain: capability reconcile failed (non-fatal)",
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Stage 3: prune section points for pages deleted from the index. A deleted
|
|
591
689
|
// page's slug never appears in `selectChangedPages` (the delta selector only
|
|
592
690
|
// names live pages), so its lingering points are cleared by diffing the dense
|
|
593
691
|
// store's stored articles against the live page-index slugs. Contained: a
|
|
@@ -604,7 +702,7 @@ export async function maintainJob(
|
|
|
604
702
|
);
|
|
605
703
|
}
|
|
606
704
|
|
|
607
|
-
// Stage
|
|
705
|
+
// Stage 4: validate the maintainer-curated core set. Diff `core-pages.md`
|
|
608
706
|
// entries against the live page-index slugs and REPORT dangling entries
|
|
609
707
|
// (pages that were renamed or deleted) through the log + outcome — the file
|
|
610
708
|
// is maintainer-owned, so it is never auto-edited; the maintainer fixes it at
|
|
@@ -632,7 +730,7 @@ export async function maintainJob(
|
|
|
632
730
|
);
|
|
633
731
|
}
|
|
634
732
|
|
|
635
|
-
// Stage
|
|
733
|
+
// Stage 5: rebuild section index + needle + edge graph on the next turn.
|
|
636
734
|
try {
|
|
637
735
|
deps.invalidateLanes();
|
|
638
736
|
outcome.invalidated = true;
|
|
@@ -648,6 +746,8 @@ export async function maintainJob(
|
|
|
648
746
|
{
|
|
649
747
|
reembedded: outcome.reembedded,
|
|
650
748
|
reembedFailures: outcome.reembedFailures,
|
|
749
|
+
capabilitiesReconciled: outcome.capabilitiesReconciled,
|
|
750
|
+
reconcileFailures: outcome.reconcileFailures,
|
|
651
751
|
pruned: outcome.pruned,
|
|
652
752
|
pruneFailures: outcome.pruneFailures,
|
|
653
753
|
danglingCoreSlugs: outcome.danglingCoreSlugs,
|