@vellumai/assistant 0.8.11 → 0.8.12-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 +15 -17
- package/README.md +0 -6
- package/bun.lock +6 -122
- package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
- package/node_modules/@vellumai/gateway-client/package.json +3 -1
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
- package/openapi.yaml +126 -4
- package/package.json +1 -3
- package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
- package/src/__tests__/anthropic-provider.test.ts +129 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
- package/src/__tests__/btw-routes.test.ts +7 -34
- package/src/__tests__/checker.test.ts +6 -12
- package/src/__tests__/config-loader-backfill.test.ts +4 -2
- package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +2 -6
- package/src/__tests__/conversation-history-web-search.test.ts +8 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/disk-pressure-tools.test.ts +1 -1
- package/src/__tests__/exploration-drift-hook.test.ts +692 -0
- package/src/__tests__/filing-service.test.ts +8 -3
- package/src/__tests__/guardian-action-store.test.ts +0 -167
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
- package/src/__tests__/heartbeat-service.test.ts +5 -2
- package/src/__tests__/identity-intro-cache.test.ts +12 -5
- package/src/__tests__/identity-routes.test.ts +16 -57
- package/src/__tests__/injector-chain.test.ts +8 -3
- package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
- package/src/__tests__/llm-usage-store.test.ts +11 -0
- package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +19 -8
- package/src/__tests__/openai-provider.test.ts +34 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
- package/src/__tests__/recurrence-engine.test.ts +45 -0
- package/src/__tests__/schedule-routes.test.ts +34 -0
- package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
- package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
- package/src/__tests__/skill-tool-factory.test.ts +49 -0
- package/src/__tests__/subagent-role-registry.test.ts +24 -1
- package/src/__tests__/subagent-tools.test.ts +1 -0
- package/src/__tests__/system-prompt.test.ts +109 -11
- package/src/__tests__/tool-error-hook.test.ts +1 -0
- package/src/__tests__/tool-result-spool.test.ts +337 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/validate-input.test.ts +95 -1
- package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
- package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
- package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
- package/src/agent/loop.ts +81 -24
- package/src/api/events/usage-progress.ts +28 -0
- package/src/api/index.ts +6 -0
- package/src/background-wake/wake-intent-hooks.test.ts +2 -0
- package/src/bundler/app-bundler.ts +25 -42
- package/src/calls/call-controller.ts +1 -1
- package/src/cli/commands/plugins.ts +248 -15
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
- package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +301 -0
- package/src/cli/lib/inspect-plugin.ts +252 -0
- package/src/cli/lib/install-from-github.ts +214 -21
- package/src/cli/lib/list-installed-plugins.ts +17 -6
- package/src/cli/lib/plugin-artifact.ts +103 -0
- package/src/cli/lib/plugin-details.ts +18 -1
- package/src/cli/lib/plugin-fingerprint.ts +197 -0
- package/src/cli/lib/upgrade-plugin.ts +219 -0
- package/src/config/bundled-skills/subagent/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
- package/src/config/call-site-defaults.ts +13 -2
- package/src/config/feature-flag-registry.json +8 -16
- package/src/config/loader.ts +52 -59
- package/src/config/schema.ts +0 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
- package/src/config/schemas/llm.ts +10 -0
- package/src/config/schemas/memory-v2.ts +13 -0
- package/src/config/schemas/memory-v3.ts +92 -0
- package/src/context/post-turn-tool-result-truncation.ts +32 -18
- package/src/context/tool-result-spool.ts +104 -0
- package/src/credential-execution/feature-gates.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
- package/src/daemon/conversation-error.ts +6 -15
- package/src/daemon/conversation.ts +9 -0
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/lifecycle.ts +1 -20
- package/src/daemon/message-types/conversations.ts +2 -15
- package/src/daemon/trust-context.ts +1 -1
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
- package/src/home/__tests__/home-content-refresh.test.ts +114 -0
- package/src/home/__tests__/suggested-prompts.test.ts +86 -5
- package/src/home/home-content-refresh.ts +43 -31
- package/src/home/home-greeting-cache.ts +8 -1
- package/src/home/home-greeting.ts +13 -9
- package/src/home/suggested-prompts.ts +77 -24
- package/src/ipc/routes/trust-rules.test.ts +66 -72
- package/src/media/image-credentials.ts +2 -2
- package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
- package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
- package/src/memory/conversation-attention-store.ts +1 -0
- package/src/memory/conversation-bootstrap.ts +18 -9
- package/src/memory/conversation-crud.ts +12 -2
- package/src/memory/conversation-title-service.ts +53 -9
- package/src/memory/delivery-channels.ts +0 -69
- package/src/memory/graph/extraction-job.ts +0 -15
- package/src/memory/guardian-action-store.ts +1 -376
- package/src/memory/llm-usage-store.ts +5 -1
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
- package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
- package/src/memory/v2/__tests__/injection.test.ts +70 -0
- package/src/memory/v2/__tests__/static-context.test.ts +12 -0
- package/src/memory/v2/consolidation-job.ts +93 -9
- package/src/memory/v2/injection.ts +53 -0
- package/src/memory/v2/prompts/consolidation.ts +1 -0
- package/src/memory/v2/static-context.ts +13 -1
- package/src/memory/v2/sweep-job.ts +1 -1
- package/src/memory/v2/types.ts +5 -0
- package/src/plugin-api/types.ts +7 -0
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
- package/src/plugins/defaults/exploration-drift/package.json +15 -0
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
- package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
- package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
- package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
- package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
- package/src/prompts/cache-boundary.ts +17 -0
- package/src/prompts/sections.ts +50 -17
- package/src/prompts/system-prompt.ts +12 -4
- package/src/prompts/templates/system-sections.ts +22 -0
- package/src/providers/anthropic/client.ts +74 -28
- package/src/providers/gemini/client.ts +5 -1
- package/src/providers/minimax/client.ts +9 -0
- package/src/providers/model-intents.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +2 -1
- package/src/providers/openai/responses-provider.ts +5 -1
- package/src/providers/retry.ts +8 -0
- package/src/providers/types.ts +11 -0
- package/src/runtime/AGENTS.md +6 -0
- package/src/runtime/__tests__/agent-wake.test.ts +2 -2
- package/src/runtime/agent-wake.ts +5 -5
- package/src/runtime/background-job-runner.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
- package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
- package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
- package/src/runtime/migrations/vbundle-importer.ts +9 -4
- package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
- package/src/runtime/pre-first-message-gate.ts +1 -1
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
- package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
- package/src/runtime/routes/btw-routes.ts +0 -14
- package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
- package/src/runtime/routes/conversation-list-routes.ts +77 -5
- package/src/runtime/routes/conversation-management-routes.ts +54 -0
- package/src/runtime/routes/gateway-log-routes.ts +14 -64
- package/src/runtime/routes/home-feed-routes.ts +10 -0
- package/src/runtime/routes/identity-intro-cache.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +76 -20
- package/src/runtime/routes/inbound-message-handler.ts +0 -36
- package/src/runtime/routes/plugins-routes.ts +21 -0
- package/src/runtime/routes/schedule-routes.ts +19 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -67
- package/src/schedule/recurrence-engine.ts +34 -0
- package/src/schedule/scheduler.ts +1 -0
- package/src/skills/validate-input.ts +41 -1
- package/src/subagent/types.ts +26 -1
- package/src/telemetry/types.ts +15 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/apps/executors.ts +1 -1
- package/src/tools/skills/skill-tool-factory.ts +19 -8
- package/src/usage/types.ts +8 -1
- package/src/util/platform.ts +16 -0
- package/src/watcher/engine.ts +1 -0
- package/src/workspace/adaptive-thinking-repair.ts +113 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
- package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
- package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
- package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
- package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
- package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
- package/src/__tests__/update-bulletin-job.test.ts +0 -292
- package/src/config/schemas/updates.ts +0 -14
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
- package/src/memory/conversation-starters-cadence.ts +0 -78
- package/src/prompts/update-bulletin-job.ts +0 -180
- package/src/runtime/guardian-action-followup-executor.ts +0 -306
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2
2
|
|
|
3
|
+
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../prompts/cache-boundary.js";
|
|
3
4
|
import { isAbortReason } from "../../util/abort-reasons.js";
|
|
4
5
|
import { ProviderError } from "../../util/errors.js";
|
|
5
6
|
import { getLogger } from "../../util/logger.js";
|
|
@@ -809,6 +810,12 @@ export class AnthropicProvider implements Provider {
|
|
|
809
810
|
const mutableLatestUserMessage =
|
|
810
811
|
(config as Record<string, unknown> | undefined)
|
|
811
812
|
?.mutableLatestUserMessage === true;
|
|
813
|
+
// Full prompt-caching opt-out: send no cache breakpoints at all and strip
|
|
814
|
+
// caller-stamped block-level markers. Resolved per call site (see
|
|
815
|
+
// `disableCache` in the LLM config schema) for one-shot prompts where
|
|
816
|
+
// every breakpoint is a paid cache write with no future read.
|
|
817
|
+
const disableCache =
|
|
818
|
+
(config as Record<string, unknown> | undefined)?.disableCache === true;
|
|
812
819
|
let sentMessages: Anthropic.MessageParam[] | undefined;
|
|
813
820
|
const startedAt = Date.now();
|
|
814
821
|
// Hoisted so the catch block can distinguish our inner stream timeout
|
|
@@ -998,6 +1005,7 @@ export class AnthropicProvider implements Provider {
|
|
|
998
1005
|
cacheTtl: _cacheTtl,
|
|
999
1006
|
disableTurnStartCache: _disableTurnStartCache,
|
|
1000
1007
|
mutableLatestUserMessage: _mutableLatestUserMessage,
|
|
1008
|
+
disableCache: _disableCache,
|
|
1001
1009
|
max_tokens: callerMaxTokens,
|
|
1002
1010
|
usageAttributionHeaders,
|
|
1003
1011
|
...restConfig
|
|
@@ -1054,19 +1062,34 @@ export class AnthropicProvider implements Provider {
|
|
|
1054
1062
|
};
|
|
1055
1063
|
|
|
1056
1064
|
if (systemPrompt) {
|
|
1057
|
-
// The
|
|
1058
|
-
//
|
|
1059
|
-
//
|
|
1060
|
-
//
|
|
1061
|
-
|
|
1062
|
-
|
|
1065
|
+
// The system prompt may carry a cache boundary (placed by the
|
|
1066
|
+
// section pipeline — see `prompts/sections.ts`) splitting it into
|
|
1067
|
+
// a stable-prefix block and a volatile-suffix block, each with
|
|
1068
|
+
// its own breakpoint so a volatile-section change doesn't
|
|
1069
|
+
// re-create the stable prefix. A 1-hour cache TTL is used (when
|
|
1070
|
+
// supported by the model) so the breakpoints survive turn gaps
|
|
1071
|
+
// that exceed the default 5-minute window.
|
|
1072
|
+
params.system = systemPrompt
|
|
1073
|
+
.split(SYSTEM_PROMPT_CACHE_BOUNDARY)
|
|
1074
|
+
.filter((text) => text.length > 0)
|
|
1075
|
+
.map((text) => ({
|
|
1063
1076
|
type: "text" as const,
|
|
1064
|
-
text
|
|
1065
|
-
cache_control: cacheControl,
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1077
|
+
text,
|
|
1078
|
+
...(disableCache ? {} : { cache_control: cacheControl }),
|
|
1079
|
+
}));
|
|
1080
|
+
if (params.system.length === 0) delete params.system;
|
|
1068
1081
|
}
|
|
1069
1082
|
|
|
1083
|
+
// Tools precede the system blocks in the cached prefix, so the first
|
|
1084
|
+
// system breakpoint already covers the tool definitions. When the
|
|
1085
|
+
// system prompt is split into two blocks, skip the explicit last-tool
|
|
1086
|
+
// breakpoint to stay within Anthropic's 4-breakpoint budget; with a
|
|
1087
|
+
// single (or no) system block the tool breakpoint is kept.
|
|
1088
|
+
const systemBlockCount = Array.isArray(params.system)
|
|
1089
|
+
? params.system.length
|
|
1090
|
+
: 0;
|
|
1091
|
+
const applyToolCacheControl = systemBlockCount < 2;
|
|
1092
|
+
|
|
1070
1093
|
if (tools && tools.length > 0) {
|
|
1071
1094
|
if (
|
|
1072
1095
|
this.useNativeWebSearch &&
|
|
@@ -1077,7 +1100,9 @@ export class AnthropicProvider implements Provider {
|
|
|
1077
1100
|
name: t.name,
|
|
1078
1101
|
description: t.description,
|
|
1079
1102
|
input_schema: t.input_schema as Anthropic.Tool["input_schema"],
|
|
1080
|
-
...(
|
|
1103
|
+
...(applyToolCacheControl &&
|
|
1104
|
+
!disableCache &&
|
|
1105
|
+
i === otherTools.length - 1
|
|
1081
1106
|
? { cache_control: cacheControl }
|
|
1082
1107
|
: {}),
|
|
1083
1108
|
}));
|
|
@@ -1092,7 +1117,9 @@ export class AnthropicProvider implements Provider {
|
|
|
1092
1117
|
name: t.name,
|
|
1093
1118
|
description: t.description,
|
|
1094
1119
|
input_schema: t.input_schema as Anthropic.Tool["input_schema"],
|
|
1095
|
-
...(i === tools.length - 1
|
|
1120
|
+
...(applyToolCacheControl && !disableCache && i === tools.length - 1
|
|
1121
|
+
? { cache_control: cacheControl }
|
|
1122
|
+
: {}),
|
|
1096
1123
|
}));
|
|
1097
1124
|
}
|
|
1098
1125
|
}
|
|
@@ -1140,6 +1167,7 @@ export class AnthropicProvider implements Provider {
|
|
|
1140
1167
|
mutableLatestUserMessage && turnStartIdx === msgs.length - 1;
|
|
1141
1168
|
if (
|
|
1142
1169
|
turnStartIdx >= 0 &&
|
|
1170
|
+
!disableCache &&
|
|
1143
1171
|
!disableTurnStartCache &&
|
|
1144
1172
|
!skipVolatileTurnStartAnchor
|
|
1145
1173
|
) {
|
|
@@ -1156,7 +1184,11 @@ export class AnthropicProvider implements Provider {
|
|
|
1156
1184
|
// cache_creation tokens per new turn). Skipped during tool-use loops
|
|
1157
1185
|
// where the current turn-start already covers the same prefix and a
|
|
1158
1186
|
// second anchor would blow the 4-breakpoint budget.
|
|
1159
|
-
if (
|
|
1187
|
+
if (
|
|
1188
|
+
!disableCache &&
|
|
1189
|
+
turnStartIdx === msgs.length - 1 &&
|
|
1190
|
+
turnStartIdx > 0
|
|
1191
|
+
) {
|
|
1160
1192
|
const prevTurnAnchorIdx = findUserTextMsgIdx(turnStartIdx - 1);
|
|
1161
1193
|
if (prevTurnAnchorIdx >= 0)
|
|
1162
1194
|
applyCacheControlToLastBlock(prevTurnAnchorIdx);
|
|
@@ -1168,7 +1200,11 @@ export class AnthropicProvider implements Provider {
|
|
|
1168
1200
|
// cheaply without conflicting with the 1h breakpoints above.
|
|
1169
1201
|
// Skip thinking/redacted_thinking blocks — Anthropic doesn't allow
|
|
1170
1202
|
// cache_control on those types.
|
|
1171
|
-
if (
|
|
1203
|
+
if (
|
|
1204
|
+
!disableCache &&
|
|
1205
|
+
turnStartIdx >= 0 &&
|
|
1206
|
+
turnStartIdx < sentMessages.length - 1
|
|
1207
|
+
) {
|
|
1172
1208
|
const lastMsg = sentMessages[sentMessages.length - 1];
|
|
1173
1209
|
if (Array.isArray(lastMsg.content) && lastMsg.content.length > 0) {
|
|
1174
1210
|
const NON_CACHEABLE_TYPES = new Set([
|
|
@@ -1193,11 +1229,12 @@ export class AnthropicProvider implements Provider {
|
|
|
1193
1229
|
}
|
|
1194
1230
|
}
|
|
1195
1231
|
|
|
1196
|
-
// Cache-breakpoint accounting: system(
|
|
1197
|
-
//
|
|
1198
|
-
//
|
|
1199
|
-
//
|
|
1200
|
-
//
|
|
1232
|
+
// Cache-breakpoint accounting: system(≤2) + tools(1, only when the
|
|
1233
|
+
// system is a single block or absent) + turn-start(1) +
|
|
1234
|
+
// (tail OR prev-turn-anchor)(1) ≤ 4 — Anthropic's per-request cap.
|
|
1235
|
+
// Tail and prev-turn-anchor are mutually exclusive (the latter only
|
|
1236
|
+
// fires when turn-start is the last message, which suppresses the
|
|
1237
|
+
// tail), so the total can't drift past 4.
|
|
1201
1238
|
|
|
1202
1239
|
// Strip orphaned UTF-16 surrogates so the Anthropic JSON parser never
|
|
1203
1240
|
// sees invalid strings produced by upstream surrogate-splitting `.slice()` calls.
|
|
@@ -1208,19 +1245,28 @@ export class AnthropicProvider implements Provider {
|
|
|
1208
1245
|
sentMessages = params.messages;
|
|
1209
1246
|
}
|
|
1210
1247
|
|
|
1211
|
-
//
|
|
1212
|
-
//
|
|
1213
|
-
//
|
|
1214
|
-
//
|
|
1215
|
-
//
|
|
1216
|
-
|
|
1248
|
+
// Callers can stamp `cache_control` on message blocks before the
|
|
1249
|
+
// provider sees them. Two repairs apply:
|
|
1250
|
+
// - `disableCache`: strip the marker entirely — this call opted out of
|
|
1251
|
+
// prompt caching, and a leftover block-level marker would still incur
|
|
1252
|
+
// a cache write.
|
|
1253
|
+
// - Haiku: strip only the `ttl` field — Haiku does not support the
|
|
1254
|
+
// extended-cache-ttl beta, so a `ttl` would make the request invalid.
|
|
1255
|
+
// The client's own breakpoints already omit it for Haiku.
|
|
1256
|
+
if (disableCache || isHaiku) {
|
|
1217
1257
|
for (const msg of sentMessages) {
|
|
1218
1258
|
if (!Array.isArray(msg.content)) continue;
|
|
1219
1259
|
for (const block of msg.content) {
|
|
1220
1260
|
if (typeof block === "string") continue;
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1223
|
-
|
|
1261
|
+
const blockRecord = block as {
|
|
1262
|
+
cache_control?: { ttl?: unknown };
|
|
1263
|
+
};
|
|
1264
|
+
if (!blockRecord.cache_control) continue;
|
|
1265
|
+
if (disableCache) {
|
|
1266
|
+
delete blockRecord.cache_control;
|
|
1267
|
+
} else if ("ttl" in blockRecord.cache_control) {
|
|
1268
|
+
delete blockRecord.cache_control.ttl;
|
|
1269
|
+
}
|
|
1224
1270
|
}
|
|
1225
1271
|
}
|
|
1226
1272
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
THINKING_LEVELS,
|
|
6
6
|
type ThinkingLevel as ThinkingLevelName,
|
|
7
7
|
} from "../../config/schemas/llm.js";
|
|
8
|
+
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../prompts/cache-boundary.js";
|
|
8
9
|
import { isAbortReason } from "../../util/abort-reasons.js";
|
|
9
10
|
import { ProviderError } from "../../util/errors.js";
|
|
10
11
|
import { getLogger } from "../../util/logger.js";
|
|
@@ -314,7 +315,10 @@ export class GeminiProvider implements Provider {
|
|
|
314
315
|
const geminiConfig: genai.GenerateContentConfig = {};
|
|
315
316
|
|
|
316
317
|
if (systemPrompt) {
|
|
317
|
-
geminiConfig.systemInstruction = systemPrompt
|
|
318
|
+
geminiConfig.systemInstruction = systemPrompt.replaceAll(
|
|
319
|
+
SYSTEM_PROMPT_CACHE_BOUNDARY,
|
|
320
|
+
"\n\n",
|
|
321
|
+
);
|
|
318
322
|
}
|
|
319
323
|
if (maxTokens) {
|
|
320
324
|
geminiConfig.maxOutputTokens = maxTokens;
|
|
@@ -101,6 +101,15 @@ export class MinimaxProvider extends OpenAIChatCompletionsProvider {
|
|
|
101
101
|
providerName: "minimax",
|
|
102
102
|
providerLabel: "MiniMax",
|
|
103
103
|
streamTimeoutMs: options.streamTimeoutMs,
|
|
104
|
+
// Without reasoning_split, MiniMax embeds reasoning in `content`
|
|
105
|
+
// wrapped in <think>...</think> tags (and also mirrors it into
|
|
106
|
+
// reasoning deltas), so raw tags leak into user-visible text. With it,
|
|
107
|
+
// reasoning arrives only via `reasoning_content`/`reasoning_details`,
|
|
108
|
+
// which the base provider already parses into thinking blocks.
|
|
109
|
+
extraCreateParams: { reasoning_split: true },
|
|
110
|
+
// MiniMax models reason between tool calls (interleaved thinking) and
|
|
111
|
+
// expect prior-turn reasoning replayed on multi-turn requests.
|
|
112
|
+
assistantReasoningField: "reasoning_content",
|
|
104
113
|
});
|
|
105
114
|
}
|
|
106
115
|
}
|
|
@@ -13,7 +13,7 @@ const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
|
|
|
13
13
|
anthropic: {
|
|
14
14
|
balanced: "claude-sonnet-4-6",
|
|
15
15
|
"latency-optimized": "claude-haiku-4-5-20251001",
|
|
16
|
-
"quality-optimized": "claude-
|
|
16
|
+
"quality-optimized": "claude-fable-5",
|
|
17
17
|
"vision-optimized": "claude-opus-4-6",
|
|
18
18
|
},
|
|
19
19
|
openai: {
|
|
@@ -43,7 +43,7 @@ const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
|
|
|
43
43
|
openrouter: {
|
|
44
44
|
balanced: "anthropic/claude-sonnet-4.6",
|
|
45
45
|
"latency-optimized": "anthropic/claude-haiku-4.5",
|
|
46
|
-
"quality-optimized": "anthropic/claude-
|
|
46
|
+
"quality-optimized": "anthropic/claude-fable-5",
|
|
47
47
|
"vision-optimized": "anthropic/claude-opus-4.6",
|
|
48
48
|
},
|
|
49
49
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
2
|
|
|
3
|
+
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../prompts/cache-boundary.js";
|
|
3
4
|
import { isAbortReason } from "../../util/abort-reasons.js";
|
|
4
5
|
import { ProviderError } from "../../util/errors.js";
|
|
5
6
|
import { extractRetryAfterMs } from "../../util/retry.js";
|
|
@@ -797,7 +798,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
797
798
|
if (systemPrompt) {
|
|
798
799
|
result.push({
|
|
799
800
|
role: "system",
|
|
800
|
-
content: systemPrompt,
|
|
801
|
+
content: systemPrompt.replaceAll(SYSTEM_PROMPT_CACHE_BOUNDARY, "\n\n"),
|
|
801
802
|
});
|
|
802
803
|
}
|
|
803
804
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
2
|
|
|
3
|
+
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../prompts/cache-boundary.js";
|
|
3
4
|
import { isAbortReason } from "../../util/abort-reasons.js";
|
|
4
5
|
import { ProviderError } from "../../util/errors.js";
|
|
5
6
|
import { getLogger } from "../../util/logger.js";
|
|
@@ -216,7 +217,10 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
216
217
|
};
|
|
217
218
|
|
|
218
219
|
if (systemPrompt) {
|
|
219
|
-
params.instructions = systemPrompt
|
|
220
|
+
params.instructions = systemPrompt.replaceAll(
|
|
221
|
+
SYSTEM_PROMPT_CACHE_BOUNDARY,
|
|
222
|
+
"\n\n",
|
|
223
|
+
);
|
|
220
224
|
}
|
|
221
225
|
|
|
222
226
|
if (maxTokens && !this.codexSubscription) {
|
package/src/providers/retry.ts
CHANGED
|
@@ -268,6 +268,14 @@ function normalizeSendMessageOptions(
|
|
|
268
268
|
if (nextConfig.thinking === undefined && resolved.thinking !== undefined) {
|
|
269
269
|
nextConfig.thinking = resolved.thinking;
|
|
270
270
|
}
|
|
271
|
+
// Not a wire field: consumed (and stripped) by provider clients that
|
|
272
|
+
// implement prompt caching, like `cacheTtl` / `disableTurnStartCache`.
|
|
273
|
+
if (
|
|
274
|
+
nextConfig.disableCache === undefined &&
|
|
275
|
+
resolved.disableCache !== undefined
|
|
276
|
+
) {
|
|
277
|
+
nextConfig.disableCache = resolved.disableCache;
|
|
278
|
+
}
|
|
271
279
|
// Forward OpenRouter-only routing preferences so `OpenRouterProvider` can
|
|
272
280
|
// translate `openrouter.only` into the wire-format `provider: { only: [...] }`
|
|
273
281
|
// body field on both the OpenAI-compat and Anthropic-compat endpoints.
|
package/src/providers/types.ts
CHANGED
|
@@ -222,6 +222,17 @@ export interface SendMessageConfig {
|
|
|
222
222
|
* behavior.
|
|
223
223
|
*/
|
|
224
224
|
mutableLatestUserMessage?: boolean;
|
|
225
|
+
/**
|
|
226
|
+
* When true, the provider sends no prompt-cache breakpoints at all (and
|
|
227
|
+
* strips any block-level `cache_control` markers callers stamped on
|
|
228
|
+
* messages). For one-shot call sites whose prompts are unique per call or
|
|
229
|
+
* whose call cadence exceeds the cache TTL, every breakpoint is a paid
|
|
230
|
+
* cache write that will never be read — opting out saves the write
|
|
231
|
+
* premium. Resolved per call site via `resolveCallSiteConfig` (see
|
|
232
|
+
* `disableCache` in the LLM config schema); a per-call explicit value
|
|
233
|
+
* wins. Default false — existing behavior.
|
|
234
|
+
*/
|
|
235
|
+
disableCache?: boolean;
|
|
225
236
|
[key: string]: unknown;
|
|
226
237
|
}
|
|
227
238
|
|
package/src/runtime/AGENTS.md
CHANGED
|
@@ -20,6 +20,12 @@ SSE handlers built on `ReadableStream` shed slow subscribers when `controller.de
|
|
|
20
20
|
|
|
21
21
|
GET handlers must be safe and side-effect-free — they must not enqueue background jobs, mutate database state, or trigger writes. If a feature needs server-initiated work in response to a client request, use an explicit POST endpoint or a push-based flow (SSE event → client refetch). See [RFC 9110 §9.2.1 — Safe Methods](https://httpwg.org/specs/rfc9110.html#safe.methods).
|
|
22
22
|
|
|
23
|
+
Accepted exceptions (stale-while-revalidate caches): a GET handler may kick off a bounded, fire-and-forget background refresh of a generated-content cache when no fresh cache exists, provided the handler itself stays read-only and returns immediately with cached/fallback copy, the refresh is single-flight (concurrent GETs share one regeneration), and a TTL bounds regeneration frequency. Current instances:
|
|
24
|
+
|
|
25
|
+
- `GET /v1/identity/intro` — refreshes the generated greeting cache; the background prompt may only depend on static identity/soul context plus caller-supplied local hour/minute.
|
|
26
|
+
- `GET /v1/home/feed` — refreshes the personalized home greeting and suggested-prompt caches via `revalidateHomeContentInBackground()`, which publishes `home_feed_updated` when fresh content lands so clients refetch. This is intentional: home content is generated on demand (when a user actually views Home), never at daemon startup or on a timer.
|
|
27
|
+
- `GET /v1/conversation-starters` — enqueues a `generate_conversation_starters` memory job when the starter set is stale, cooldown-gated and deduped against in-flight jobs.
|
|
28
|
+
|
|
23
29
|
### Approvals (confirmations, secrets, trust rules)
|
|
24
30
|
|
|
25
31
|
Approvals are **orthogonal to message sending**. The assistant asks for approval whenever it needs one — this is a separate concern from how a message enters the system.
|
|
@@ -173,7 +173,7 @@ mock.module("../../config/loader.js", () => ({
|
|
|
173
173
|
getNestedValue: () => undefined,
|
|
174
174
|
setNestedValue: () => {},
|
|
175
175
|
API_KEY_PROVIDERS: [],
|
|
176
|
-
|
|
176
|
+
_writeQuarantineNotice: () => {},
|
|
177
177
|
invalidateConfigCache: () => {},
|
|
178
178
|
}));
|
|
179
179
|
|
|
@@ -837,7 +837,7 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
837
837
|
});
|
|
838
838
|
|
|
839
839
|
test("applies caller-supplied trustContext to the target before the agent loop runs", async () => {
|
|
840
|
-
// Background system jobs (memory consolidation
|
|
840
|
+
// Background system jobs (e.g. memory consolidation) need
|
|
841
841
|
// guardian trust to clear the side-effect approval gate. The wake must
|
|
842
842
|
// call setTrustContext BEFORE agentLoop.run so the per-turn snapshot
|
|
843
843
|
// captures the elevated trust.
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
import {
|
|
67
67
|
recordCompactionEndBestEffort,
|
|
68
68
|
recordCompactionStartBestEffort,
|
|
69
|
-
} from "../memory/compaction-log-
|
|
69
|
+
} from "../memory/compaction-log-store-clickhouse.js";
|
|
70
70
|
import { getConversationOverrideProfile } from "../memory/conversation-crud.js";
|
|
71
71
|
import {
|
|
72
72
|
buildProviderErrorResponsePayload,
|
|
@@ -167,10 +167,10 @@ export interface WakeOptions {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
|
-
* Reason a wake returned `invoked: false`. Callers (CLI
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
170
|
+
* Reason a wake returned `invoked: false`. Callers (e.g. the CLI) need to
|
|
171
|
+
* distinguish "conversation doesn't exist" from "conversation exists but
|
|
172
|
+
* stayed busy past the wait-until-idle timeout" — the former is a
|
|
173
|
+
* user-visible error, the latter is an expected transient condition.
|
|
174
174
|
*/
|
|
175
175
|
export type WakeSkipReason =
|
|
176
176
|
| "not_found"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `runBackgroundJob()` consolidates the bootstrap → processMessage → timeout
|
|
5
5
|
* pattern that every background producer (heartbeat, filing, scheduler, memory
|
|
6
|
-
* consolidation, watcher,
|
|
6
|
+
* consolidation, watcher, subagent, sequence) has been
|
|
7
7
|
* open-coding. Wrapping it here lets us:
|
|
8
8
|
*
|
|
9
9
|
* - apply a single timeout policy
|
|
@@ -182,7 +182,7 @@ export async function runBackgroundJob(
|
|
|
182
182
|
// failed" rows visible in the sidebar the moment a real user hatches the
|
|
183
183
|
// assistant — see `pre-first-message-gate.ts` for the rationale.
|
|
184
184
|
//
|
|
185
|
-
// Service-level callers (heartbeat
|
|
185
|
+
// Service-level callers (e.g. heartbeat) are expected to gate
|
|
186
186
|
// earlier and never reach this point; reaching the gate here means a
|
|
187
187
|
// caller either forgot to gate or deliberately opted in via
|
|
188
188
|
// `allowPreFirstUserMessage`. We log at `info` (not `warn`) because the
|
|
@@ -150,9 +150,6 @@ describe("DefaultPathResolver prompts/USER.md translation", () => {
|
|
|
150
150
|
expect(resolver.resolve("prompts/SOUL.md")).toBe(
|
|
151
151
|
join(WORKSPACE_ROOT, "SOUL.md"),
|
|
152
152
|
);
|
|
153
|
-
expect(resolver.resolve("prompts/UPDATES.md")).toBe(
|
|
154
|
-
join(WORKSPACE_ROOT, "UPDATES.md"),
|
|
155
|
-
);
|
|
156
153
|
});
|
|
157
154
|
|
|
158
155
|
test("skips unknown prompt filenames regardless of guardian state", () => {
|
|
@@ -230,6 +227,71 @@ describe("analyzeImport for legacy prompts/USER.md", () => {
|
|
|
230
227
|
const userMd = report.files.find((f) => f.path === "prompts/USER.md");
|
|
231
228
|
expect(userMd!.action).toBe("skip");
|
|
232
229
|
});
|
|
230
|
+
|
|
231
|
+
test("retired workspace/UPDATES.md (current format) is a non-blocking skip", () => {
|
|
232
|
+
const resolver = new DefaultPathResolver(
|
|
233
|
+
WORKSPACE_ROOT,
|
|
234
|
+
undefined,
|
|
235
|
+
() => null,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const { manifest } = buildVBundle({
|
|
239
|
+
files: [
|
|
240
|
+
{
|
|
241
|
+
path: "data/db/assistant.db",
|
|
242
|
+
data: new Uint8Array(),
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
path: "workspace/UPDATES.md",
|
|
246
|
+
data: new TextEncoder().encode("## Old release notes\n"),
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
...defaultV1Options(),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const report = analyzeImport({ manifest, pathResolver: resolver });
|
|
253
|
+
|
|
254
|
+
expect(report.can_import).toBe(true);
|
|
255
|
+
expect(report.conflicts).toHaveLength(0);
|
|
256
|
+
const updatesMd = report.files.find(
|
|
257
|
+
(f) => f.path === "workspace/UPDATES.md",
|
|
258
|
+
);
|
|
259
|
+
expect(updatesMd).toBeDefined();
|
|
260
|
+
expect(updatesMd!.action).toBe("skip");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("retired prompts/UPDATES.md in a legacy bundle is a non-blocking skip", () => {
|
|
264
|
+
const resolver = new DefaultPathResolver(
|
|
265
|
+
WORKSPACE_ROOT,
|
|
266
|
+
undefined,
|
|
267
|
+
() => null,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const { manifest } = buildVBundle({
|
|
271
|
+
files: [
|
|
272
|
+
{
|
|
273
|
+
path: "data/db/assistant.db",
|
|
274
|
+
data: new Uint8Array(),
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
path: "prompts/UPDATES.md",
|
|
278
|
+
data: new TextEncoder().encode("## Old release notes\n"),
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
...defaultV1Options(),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const report = analyzeImport({ manifest, pathResolver: resolver });
|
|
285
|
+
|
|
286
|
+
// Bundles exported while the update-bulletin feature existed must stay
|
|
287
|
+
// restorable — the retired file is dropped, never written back, and
|
|
288
|
+
// must not register an UNKNOWN_ARCHIVE_PATH conflict.
|
|
289
|
+
expect(report.can_import).toBe(true);
|
|
290
|
+
expect(report.conflicts).toHaveLength(0);
|
|
291
|
+
const updatesMd = report.files.find((f) => f.path === "prompts/UPDATES.md");
|
|
292
|
+
expect(updatesMd).toBeDefined();
|
|
293
|
+
expect(updatesMd!.action).toBe("skip");
|
|
294
|
+
});
|
|
233
295
|
});
|
|
234
296
|
|
|
235
297
|
// ---------------------------------------------------------------------------
|
|
@@ -358,6 +420,91 @@ describe("commitImport for legacy prompts/USER.md", () => {
|
|
|
358
420
|
expect(existsSync(join(USERS_DIR, "USER.md"))).toBe(false);
|
|
359
421
|
});
|
|
360
422
|
|
|
423
|
+
test("retired workspace/UPDATES.md (current format) is never written back", () => {
|
|
424
|
+
const resolver = new DefaultPathResolver(
|
|
425
|
+
WORKSPACE_ROOT,
|
|
426
|
+
undefined,
|
|
427
|
+
() => null,
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
const { archive } = buildVBundle({
|
|
431
|
+
files: [
|
|
432
|
+
{
|
|
433
|
+
path: "data/db/assistant.db",
|
|
434
|
+
data: new Uint8Array(),
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
path: "workspace/UPDATES.md",
|
|
438
|
+
data: new TextEncoder().encode("## Old release notes\n"),
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
...defaultV1Options(),
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const result = commitImport({
|
|
445
|
+
archiveData: archive,
|
|
446
|
+
pathResolver: resolver,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
expect(result.ok).toBe(true);
|
|
450
|
+
if (result.ok) {
|
|
451
|
+
expect(result.report.success).toBe(true);
|
|
452
|
+
expect(
|
|
453
|
+
result.report.files.find((f) => f.path === "workspace/UPDATES.md")
|
|
454
|
+
?.action,
|
|
455
|
+
).toBe("skipped");
|
|
456
|
+
expect(result.report.warnings.some((w) => w.includes("UPDATES.md"))).toBe(
|
|
457
|
+
false,
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// The retired bulletin file must not be resurrected in the workspace.
|
|
462
|
+
expect(existsSync(join(WORKSPACE_ROOT, "UPDATES.md"))).toBe(false);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("retired prompts/UPDATES.md skips silently — no warning, nothing written", () => {
|
|
466
|
+
const resolver = new DefaultPathResolver(
|
|
467
|
+
WORKSPACE_ROOT,
|
|
468
|
+
undefined,
|
|
469
|
+
() => null,
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
const { archive } = buildVBundle({
|
|
473
|
+
files: [
|
|
474
|
+
{
|
|
475
|
+
path: "data/db/assistant.db",
|
|
476
|
+
data: new Uint8Array(),
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
path: "prompts/UPDATES.md",
|
|
480
|
+
data: new TextEncoder().encode("## Old release notes\n"),
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
...defaultV1Options(),
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const result = commitImport({
|
|
487
|
+
archiveData: archive,
|
|
488
|
+
pathResolver: resolver,
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
expect(result.ok).toBe(true);
|
|
492
|
+
if (result.ok) {
|
|
493
|
+
expect(result.report.success).toBe(true);
|
|
494
|
+
expect(
|
|
495
|
+
result.report.files.find((f) => f.path === "prompts/UPDATES.md")
|
|
496
|
+
?.action,
|
|
497
|
+
).toBe("skipped");
|
|
498
|
+
// Silent skip: the commit report must agree with preflight, which
|
|
499
|
+
// raises neither a conflict nor a warning for retired paths.
|
|
500
|
+
expect(
|
|
501
|
+
result.report.warnings.some((w) => w.includes("prompts/UPDATES.md")),
|
|
502
|
+
).toBe(false);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
expect(existsSync(join(WORKSPACE_ROOT, "UPDATES.md"))).toBe(false);
|
|
506
|
+
});
|
|
507
|
+
|
|
361
508
|
test("does NOT overwrite a customized users/<slug>.md", () => {
|
|
362
509
|
const slug = "captain.md";
|
|
363
510
|
const guardianPath = join(USERS_DIR, slug);
|
|
@@ -19,6 +19,7 @@ import { join, resolve } from "node:path";
|
|
|
19
19
|
|
|
20
20
|
import { resolveGuardianPersonaPath } from "../../prompts/persona-resolver.js";
|
|
21
21
|
import { getLogger } from "../../util/logger.js";
|
|
22
|
+
import { isRetiredArchivePath } from "./vbundle-import-policy.js";
|
|
22
23
|
import type { ManifestType } from "./vbundle-validator.js";
|
|
23
24
|
|
|
24
25
|
const log = getLogger("vbundle-import-analyzer");
|
|
@@ -30,12 +31,7 @@ const log = getLogger("vbundle-import-analyzer");
|
|
|
30
31
|
* on import, its content is translated to `users/<slug>.md` at the
|
|
31
32
|
* current guardian's location (see `DefaultPathResolver.resolve`).
|
|
32
33
|
*/
|
|
33
|
-
const ALLOWED_PROMPT_FILENAMES = new Set([
|
|
34
|
-
"IDENTITY.md",
|
|
35
|
-
"SOUL.md",
|
|
36
|
-
"USER.md",
|
|
37
|
-
"UPDATES.md",
|
|
38
|
-
]);
|
|
34
|
+
const ALLOWED_PROMPT_FILENAMES = new Set(["IDENTITY.md", "SOUL.md", "USER.md"]);
|
|
39
35
|
|
|
40
36
|
/** Archive path for the legacy guardian user persona file. */
|
|
41
37
|
const LEGACY_USER_MD_ARCHIVE_PATH = "prompts/USER.md";
|
|
@@ -123,6 +119,14 @@ export class DefaultPathResolver implements PathResolver {
|
|
|
123
119
|
return null;
|
|
124
120
|
}
|
|
125
121
|
|
|
122
|
+
// Retired-feature files (both legacy and workspace-format spellings)
|
|
123
|
+
// must never resolve to a disk target — resolving would write them
|
|
124
|
+
// back into the workspace on import. The analyzer and both importers
|
|
125
|
+
// turn the resulting null into a silent non-blocking skip.
|
|
126
|
+
if (isRetiredArchivePath(archivePath)) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
126
130
|
// New format: workspace/ prefix — maps directly into the workspace dir
|
|
127
131
|
if (archivePath.startsWith("workspace/") && this.workspaceDir) {
|
|
128
132
|
const relPath = archivePath.slice("workspace/".length);
|
|
@@ -288,6 +292,25 @@ export function analyzeImport(
|
|
|
288
292
|
continue;
|
|
289
293
|
}
|
|
290
294
|
|
|
295
|
+
// Retired-feature file from a legacy bundle: nothing consumes it
|
|
296
|
+
// anymore, so drop it silently instead of blocking the restore with
|
|
297
|
+
// an UNKNOWN_ARCHIVE_PATH conflict.
|
|
298
|
+
if (isRetiredArchivePath(fileEntry.path)) {
|
|
299
|
+
log.info(
|
|
300
|
+
{ path: fileEntry.path },
|
|
301
|
+
"Retired archive path in legacy bundle — will be skipped on import",
|
|
302
|
+
);
|
|
303
|
+
files.push({
|
|
304
|
+
path: fileEntry.path,
|
|
305
|
+
action: "skip",
|
|
306
|
+
bundle_size: fileEntry.size_bytes,
|
|
307
|
+
bundle_sha256: fileEntry.sha256,
|
|
308
|
+
current_size: null,
|
|
309
|
+
current_sha256: null,
|
|
310
|
+
});
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
291
314
|
// Unknown archive path — would have nowhere to write
|
|
292
315
|
conflicts.push({
|
|
293
316
|
code: "UNKNOWN_ARCHIVE_PATH",
|
|
@@ -34,6 +34,29 @@ export function isLegacyPersonaArchivePath(archivePath: string): boolean {
|
|
|
34
34
|
return archivePath === LEGACY_USER_MD_ARCHIVE_PATH;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Archive paths for retired features. Bundles exported while those features
|
|
39
|
+
* existed still contain these entries; preflight and both importers must
|
|
40
|
+
* treat them as silent non-blocking skips (no conflict, no warning) so old
|
|
41
|
+
* backups stay restorable without noise, and the files are never written
|
|
42
|
+
* back to the workspace.
|
|
43
|
+
*
|
|
44
|
+
* `UPDATES.md` carried the retired update-bulletin release notes. Legacy
|
|
45
|
+
* bundles archived it as `prompts/UPDATES.md`; current-format bundles walk
|
|
46
|
+
* the workspace dir, so it appears as `workspace/UPDATES.md`. Both spellings
|
|
47
|
+
* must be retired — `DefaultPathResolver.resolve()` short-circuits them to
|
|
48
|
+
* `null` before any prefix handling so the workspace-format entry cannot
|
|
49
|
+
* resolve normally and recreate the file on import.
|
|
50
|
+
*/
|
|
51
|
+
export const RETIRED_ARCHIVE_PATHS: ReadonlySet<string> = new Set([
|
|
52
|
+
"prompts/UPDATES.md",
|
|
53
|
+
"workspace/UPDATES.md",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
export function isRetiredArchivePath(archivePath: string): boolean {
|
|
57
|
+
return RETIRED_ARCHIVE_PATHS.has(archivePath);
|
|
58
|
+
}
|
|
59
|
+
|
|
37
60
|
export function isConfigArchivePath(archivePath: string): boolean {
|
|
38
61
|
return CONFIG_ARCHIVE_PATHS.has(archivePath);
|
|
39
62
|
}
|