@vellumai/assistant 0.8.11 → 0.8.12-staging.2
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 +633 -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-catalog-parity.test.ts +16 -0
- package/src/__tests__/llm-usage-store.test.ts +11 -0
- package/src/__tests__/log-export-workspace.test.ts +468 -3
- 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__/secret-fixtures.ts +20 -0
- 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-approval-handler.test.ts +85 -0
- package/src/__tests__/tool-audit-listener.test.ts +86 -0
- 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-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
- package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
- package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
- package/src/acp/__tests__/agent-process.test.ts +315 -2
- package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
- package/src/acp/agent-process.ts +163 -34
- package/src/acp/prepare-agent-env.ts +55 -15
- 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/bundler/app-compiler.ts +8 -0
- 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 +307 -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 +225 -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/config/seed-inference-profiles.ts +4 -8
- 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/events/tool-audit-listener.ts +40 -9
- 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/__tests__/unparseable-tool-args.test.ts +53 -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-catalog.ts +28 -0
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/chat-completions-provider.ts +4 -2
- package/src/providers/openai/responses-provider.ts +7 -2
- package/src/providers/retry.ts +8 -0
- package/src/providers/types.ts +11 -0
- package/src/providers/unparseable-tool-args.ts +56 -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__/conversation-query-routes.test.ts +132 -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 +348 -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/conversation-query-routes.ts +79 -4
- 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/log-export-routes.ts +143 -96
- package/src/runtime/routes/plugins-routes.ts +380 -0
- package/src/runtime/routes/redact-staged-export.ts +259 -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/security/redact-json.ts +61 -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/tools/tool-approval-handler.ts +31 -0
- 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/100-upgrade-quality-profile-to-fable-5.ts +86 -0
- package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
- package/src/workspace/migrations/registry.ts +8 -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
|
@@ -16,11 +16,12 @@ import type {
|
|
|
16
16
|
} from "../channels/types.js";
|
|
17
17
|
import { getConfig } from "../config/loader.js";
|
|
18
18
|
import { recordEstimate } from "../context/estimator-calibration.js";
|
|
19
|
+
import { stripInjectionsForCompaction } from "../context/strip-injections.js";
|
|
19
20
|
import { getCalibrationProviderKey } from "../context/token-estimator.js";
|
|
20
21
|
import {
|
|
21
22
|
recordCompactionEndBestEffort,
|
|
22
23
|
recordCompactionStartBestEffort,
|
|
23
|
-
} from "../memory/compaction-log-
|
|
24
|
+
} from "../memory/compaction-log-store-clickhouse.js";
|
|
24
25
|
import { projectAssistantMessage } from "../memory/conversation-attention-store.js";
|
|
25
26
|
import {
|
|
26
27
|
deleteMessageById,
|
|
@@ -286,6 +287,14 @@ export interface EventHandlerState {
|
|
|
286
287
|
* never claims content a flush has not yet written.
|
|
287
288
|
*/
|
|
288
289
|
lastPersistedContentSeq: number | undefined;
|
|
290
|
+
/**
|
|
291
|
+
* Pre-compaction history buffered from `context_compacting` start events,
|
|
292
|
+
* keyed by `compactionId`. The paired `compaction_completed` event no
|
|
293
|
+
* longer carries the pre-compaction history, so the dispatcher re-derives
|
|
294
|
+
* the stripped durable base from the buffered start messages. Entries are
|
|
295
|
+
* consumed (deleted) when the end event dispatches.
|
|
296
|
+
*/
|
|
297
|
+
readonly compactionStartMessages: Map<string, Message[]>;
|
|
289
298
|
}
|
|
290
299
|
|
|
291
300
|
/** Immutable context shared across event handlers within a single agent loop run. */
|
|
@@ -301,8 +310,8 @@ export interface EventHandlerDeps {
|
|
|
301
310
|
readonly turnInterfaceContext: TurnInterfaceContext;
|
|
302
311
|
/**
|
|
303
312
|
* Commit a successful inline compaction to durable state. Invoked from the
|
|
304
|
-
* `compaction_completed` dispatch case (when `
|
|
305
|
-
* loop's compaction result and the stripped pre-compaction
|
|
313
|
+
* `compaction_completed` dispatch case (when `compacted`) with the
|
|
314
|
+
* loop's compaction result and the stripped pre-compaction history. Supplied
|
|
306
315
|
* by the orchestrator because the body writes Conversation DB-record fields,
|
|
307
316
|
* projects Slack provenance, and emits transport the loop is intentionally
|
|
308
317
|
* blind to.
|
|
@@ -361,6 +370,7 @@ export function createEventHandlerState(): EventHandlerState {
|
|
|
361
370
|
currentMessageContent: [],
|
|
362
371
|
currentThinkingTimestamps: [],
|
|
363
372
|
lastPersistedContentSeq: undefined,
|
|
373
|
+
compactionStartMessages: new Map(),
|
|
364
374
|
};
|
|
365
375
|
}
|
|
366
376
|
|
|
@@ -2316,6 +2326,9 @@ export async function dispatchAgentEvent(
|
|
|
2316
2326
|
}
|
|
2317
2327
|
case "context_compacting":
|
|
2318
2328
|
recordCompactionStartBestEffort(deps.ctx.conversationId, event);
|
|
2329
|
+
// Buffer the pre-compaction history so the paired end event can
|
|
2330
|
+
// re-derive the stripped durable base.
|
|
2331
|
+
state.compactionStartMessages.set(event.compactionId, event.messages);
|
|
2319
2332
|
deps.ctx.emitActivityState("thinking", "context_compacting", {
|
|
2320
2333
|
requestId: deps.reqId,
|
|
2321
2334
|
statusText: "Compacting context",
|
|
@@ -2329,22 +2342,34 @@ export async function dispatchAgentEvent(
|
|
|
2329
2342
|
// banner.
|
|
2330
2343
|
deps.onEvent(event);
|
|
2331
2344
|
break;
|
|
2332
|
-
case "compaction_completed":
|
|
2333
|
-
// Always commit the
|
|
2334
|
-
// so re-injection re-applies onto the stripped history
|
|
2335
|
-
// pipeline ran but did not compact.
|
|
2336
|
-
//
|
|
2337
|
-
//
|
|
2338
|
-
//
|
|
2339
|
-
//
|
|
2340
|
-
//
|
|
2341
|
-
//
|
|
2345
|
+
case "compaction_completed": {
|
|
2346
|
+
// Always commit the stripped pre-compaction history as the durable
|
|
2347
|
+
// message base so re-injection re-applies onto the stripped history
|
|
2348
|
+
// even when the pipeline ran but did not compact. The base is
|
|
2349
|
+
// re-derived from the buffered start event's messages (the end event
|
|
2350
|
+
// carries only the pipeline's output). When the pipeline did compact,
|
|
2351
|
+
// commit the durable result (DB-record fields, Slack provenance,
|
|
2352
|
+
// SSE) — which overwrites `ctx.messages` with the compacted history.
|
|
2353
|
+
// This runs before the loop's `reinject` hook (the loop awaits this
|
|
2354
|
+
// dispatch), so the committed history is in place in time. A failed
|
|
2355
|
+
// durable commit re-throws below to abort the turn rather than
|
|
2356
|
+
// re-injecting against half-applied state.
|
|
2342
2357
|
recordCompactionEndBestEffort(deps.ctx.conversationId, event);
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2358
|
+
const startMessages = state.compactionStartMessages.get(
|
|
2359
|
+
event.compactionId,
|
|
2360
|
+
);
|
|
2361
|
+
state.compactionStartMessages.delete(event.compactionId);
|
|
2362
|
+
// Fall back to the pipeline's output when the start event was never
|
|
2363
|
+
// buffered — on the no-compaction path it is the stripped input.
|
|
2364
|
+
const strippedBase = startMessages
|
|
2365
|
+
? stripInjectionsForCompaction(startMessages)
|
|
2366
|
+
: event.messages;
|
|
2367
|
+
deps.ctx.messages = strippedBase;
|
|
2368
|
+
if (event.compacted) {
|
|
2369
|
+
await deps.applyCompaction(event, strippedBase);
|
|
2346
2370
|
}
|
|
2347
2371
|
break;
|
|
2372
|
+
}
|
|
2348
2373
|
case "history_stripped":
|
|
2349
2374
|
// Record the history-stripped DB marker right after the loop strips
|
|
2350
2375
|
// injections (before the pipeline). Best-effort: a transient marker
|
|
@@ -255,7 +255,7 @@ export function classifyConversationError(
|
|
|
255
255
|
return {
|
|
256
256
|
code: "PROVIDER_NOT_CONFIGURED",
|
|
257
257
|
userMessage:
|
|
258
|
-
"No compatible provider connection found for this profile. Check your provider connections in Settings.",
|
|
258
|
+
"No compatible provider connection found for this profile. Check your provider connections in Settings → Models & Services.",
|
|
259
259
|
retryable: true,
|
|
260
260
|
debugDetails,
|
|
261
261
|
errorCategory: "provider_not_configured",
|
|
@@ -334,15 +334,6 @@ function classifyCore(
|
|
|
334
334
|
}
|
|
335
335
|
return invalidApiKeyClassification(attribution);
|
|
336
336
|
}
|
|
337
|
-
if (error.statusCode === 401) {
|
|
338
|
-
return {
|
|
339
|
-
code: "PROVIDER_INVALID_KEY",
|
|
340
|
-
userMessage:
|
|
341
|
-
"Your API key is invalid or expired. Update it in Settings or switch to managed mode.",
|
|
342
|
-
retryable: false,
|
|
343
|
-
errorCategory: "provider_invalid_key",
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
337
|
if (error.statusCode === 402) {
|
|
347
338
|
if (isManagedBalanceError(error)) {
|
|
348
339
|
return managedBalanceClassification();
|
|
@@ -543,7 +534,7 @@ function providerBillingClassification(): Omit<
|
|
|
543
534
|
return {
|
|
544
535
|
code: "PROVIDER_BILLING",
|
|
545
536
|
userMessage:
|
|
546
|
-
"Your API provider account or key needs credits. Add funds with the provider or update the key in Settings.",
|
|
537
|
+
"Your API provider account or key needs credits. Add funds with the provider or update the key in Settings → Models & Services.",
|
|
547
538
|
retryable: false,
|
|
548
539
|
errorCategory: "provider_billing",
|
|
549
540
|
};
|
|
@@ -583,8 +574,8 @@ function invalidApiKeyClassification(
|
|
|
583
574
|
return {
|
|
584
575
|
code: "PROVIDER_INVALID_KEY",
|
|
585
576
|
userMessage: target
|
|
586
|
-
? `The API key${target} was rejected by the provider. Update it in Settings.`
|
|
587
|
-
: "Your API key was rejected by the provider. Update it in Settings.",
|
|
577
|
+
? `The API key${target} was rejected by the provider. Update it in Settings → Models & Services.`
|
|
578
|
+
: "Your API key was rejected by the provider. Update it in Settings → Models & Services.",
|
|
588
579
|
retryable: false,
|
|
589
580
|
errorCategory: "provider_invalid_key",
|
|
590
581
|
...(attribution?.connectionName
|
|
@@ -610,8 +601,8 @@ function providerNotConfiguredClassification(
|
|
|
610
601
|
return {
|
|
611
602
|
code: "PROVIDER_NOT_CONFIGURED",
|
|
612
603
|
userMessage: target
|
|
613
|
-
? `No API key configured${target}. Add one in Settings to start chatting.`
|
|
614
|
-
: "No API key configured for inference. Add one in Settings to start chatting.",
|
|
604
|
+
? `No API key configured${target}. Add one in Settings → Models & Services to start chatting.`
|
|
605
|
+
: "No API key configured for inference. Add one in Settings → Models & Services to start chatting.",
|
|
615
606
|
retryable: true,
|
|
616
607
|
errorCategory: "provider_not_configured",
|
|
617
608
|
...(attribution?.connectionName
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
resolveOverrideProfile,
|
|
52
52
|
setConversationHistoryStrippedAt,
|
|
53
53
|
} from "../memory/conversation-crud.js";
|
|
54
|
+
import { getResolvedConversationDirPath } from "../memory/conversation-directories.js";
|
|
54
55
|
import { ConversationGraphMemory } from "../memory/graph/conversation-graph-memory.js";
|
|
55
56
|
import { unwrapMemoryBlock, wrapMemoryBlock } from "../memory/memory-marker.js";
|
|
56
57
|
import { PermissionPrompter } from "../permissions/prompter.js";
|
|
@@ -638,6 +639,14 @@ export class Conversation {
|
|
|
638
639
|
toolExecutor: toolDefs.length > 0 ? toolExecutor : undefined,
|
|
639
640
|
resolveTools,
|
|
640
641
|
resolveSystemPrompt: resolveSystemPromptCallback,
|
|
642
|
+
resolveConversationDir: () => {
|
|
643
|
+
const conv = getConversation(this.conversationId);
|
|
644
|
+
if (!conv) return null;
|
|
645
|
+
return getResolvedConversationDirPath(
|
|
646
|
+
this.conversationId,
|
|
647
|
+
conv.createdAt,
|
|
648
|
+
);
|
|
649
|
+
},
|
|
641
650
|
});
|
|
642
651
|
createContextWindowManager({
|
|
643
652
|
provider,
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -35,7 +35,6 @@ import {
|
|
|
35
35
|
} from "../credential-execution/startup-timeout.js";
|
|
36
36
|
import { FilingService } from "../filing/filing-service.js";
|
|
37
37
|
import { HeartbeatService } from "../heartbeat/heartbeat-service.js";
|
|
38
|
-
import { startHomeContentRefresh } from "../home/home-content-refresh.js";
|
|
39
38
|
import { backfillRelationshipStateIfMissing } from "../home/relationship-state-writer.js";
|
|
40
39
|
import { closeSentry, initSentry, setSentryDeviceId } from "../instrument.js";
|
|
41
40
|
import {
|
|
@@ -98,8 +97,8 @@ import {
|
|
|
98
97
|
listWorkItems,
|
|
99
98
|
updateWorkItem,
|
|
100
99
|
} from "../work-items/work-item-store.js";
|
|
100
|
+
import { repairAdaptiveThinkingOnManagedProfiles } from "../workspace/adaptive-thinking-repair.js";
|
|
101
101
|
import { WorkspaceHeartbeatService } from "../workspace/heartbeat-service.js";
|
|
102
|
-
import { repairAdaptiveThinkingOnManagedProfiles } from "../workspace/migrations/097-enable-adaptive-thinking-managed-profiles.js";
|
|
103
102
|
import { WORKSPACE_MIGRATIONS } from "../workspace/migrations/registry.js";
|
|
104
103
|
import { runWorkspaceMigrations } from "../workspace/migrations/runner.js";
|
|
105
104
|
import {
|
|
@@ -480,10 +479,6 @@ export async function runDaemon(): Promise<void> {
|
|
|
480
479
|
);
|
|
481
480
|
});
|
|
482
481
|
|
|
483
|
-
// Pre-warm LLM-generated home page content (greeting + suggestion
|
|
484
|
-
// prompts) so the GET handler never triggers LLM calls.
|
|
485
|
-
startHomeContentRefresh();
|
|
486
|
-
|
|
487
482
|
// Backfill injection templates on Slack bot token credentials so the
|
|
488
483
|
// credential proxy can inject Authorization headers. Safe on every startup.
|
|
489
484
|
try {
|
|
@@ -780,20 +775,6 @@ export async function runDaemon(): Promise<void> {
|
|
|
780
775
|
startDiskPressureGuardForLifecycle();
|
|
781
776
|
startOrphanReaper();
|
|
782
777
|
|
|
783
|
-
// Kick off the update bulletin background job AFTER `server.start()`
|
|
784
|
-
// resolves. The conversation store must be initialized before wake
|
|
785
|
-
// calls can resolve targets.
|
|
786
|
-
//
|
|
787
|
-
// Kept fire-and-forget (`void import(...).then(...).catch(...)`) so the
|
|
788
|
-
// daemon never blocks startup on it.
|
|
789
|
-
if (dbReady) {
|
|
790
|
-
void import("../prompts/update-bulletin-job.js")
|
|
791
|
-
.then((m) => m.runUpdateBulletinJobIfNeeded())
|
|
792
|
-
.catch((err) =>
|
|
793
|
-
log.warn({ err }, "Update bulletin job failed — continuing startup"),
|
|
794
|
-
);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
778
|
// Mutable refs for Qdrant and memory worker so background
|
|
798
779
|
// init can assign them and the shutdown handler always sees the latest value.
|
|
799
780
|
const bgRefs: {
|
|
@@ -7,6 +7,7 @@ import type { ConversationListInvalidatedEvent } from "../../api/events/conversa
|
|
|
7
7
|
import type { ConversationTitleUpdatedEvent } from "../../api/events/conversation-title-updated.js";
|
|
8
8
|
import type { GenerationCancelledEvent } from "../../api/events/generation-cancelled.js";
|
|
9
9
|
import type { GenerationHandoffEvent } from "../../api/events/generation-handoff.js";
|
|
10
|
+
import type { UsageProgressEvent } from "../../api/events/usage-progress.js";
|
|
10
11
|
import type { UsageUpdateEvent } from "../../api/events/usage-update.js";
|
|
11
12
|
import type {
|
|
12
13
|
ChannelId,
|
|
@@ -425,20 +426,6 @@ export interface UndoComplete {
|
|
|
425
426
|
conversationId?: string;
|
|
426
427
|
}
|
|
427
428
|
|
|
428
|
-
/**
|
|
429
|
-
* Emitted after each LLM call with per-call token deltas and estimated cost.
|
|
430
|
-
* Clients accumulate these additively for live-updating usage metrics.
|
|
431
|
-
* This is a UI-only hint — it does not persist to DB or affect billing.
|
|
432
|
-
*/
|
|
433
|
-
export interface UsageProgress {
|
|
434
|
-
type: "usage_progress";
|
|
435
|
-
conversationId: string;
|
|
436
|
-
inputTokens: number;
|
|
437
|
-
outputTokens: number;
|
|
438
|
-
estimatedCost: number;
|
|
439
|
-
model: string;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
429
|
export interface UsageResponse {
|
|
443
430
|
type: "usage_response";
|
|
444
431
|
totalInputTokens: number;
|
|
@@ -550,7 +537,7 @@ export type _ConversationsServerMessages =
|
|
|
550
537
|
| HistoryResponse
|
|
551
538
|
| UndoComplete
|
|
552
539
|
| UsageUpdateEvent
|
|
553
|
-
|
|
|
540
|
+
| UsageProgressEvent
|
|
554
541
|
| UsageResponse
|
|
555
542
|
| ContextCompacted
|
|
556
543
|
| CompactionCircuitOpenEvent
|
|
@@ -42,7 +42,7 @@ export interface TrustContext {
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Trust context used by internal background jobs (memory consolidation,
|
|
45
|
-
*
|
|
45
|
+
* scheduled tasks) when invoking the agent loop without
|
|
46
46
|
* an inbound actor identity. The assistant is the guardian over its own
|
|
47
47
|
* internal state, so self-maintenance flows clear the side-effect
|
|
48
48
|
* approval gate. Inbound message conversations resolve trust per-actor
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
recordToolInvocation,
|
|
4
4
|
type ToolInvocationRecord,
|
|
5
5
|
} from "../memory/tool-usage-store.js";
|
|
6
|
+
import { redactJsonStringLeaves } from "../security/redact-json.js";
|
|
6
7
|
import { redactSecrets } from "../security/secret-scanner.js";
|
|
7
8
|
import {
|
|
8
9
|
stringifyToolInput,
|
|
@@ -43,11 +44,14 @@ function toInvocationRecord(
|
|
|
43
44
|
): ToolInvocationRecord | null {
|
|
44
45
|
switch (event.type) {
|
|
45
46
|
case "executed": {
|
|
46
|
-
const
|
|
47
|
+
const rawInput = stringifyToolInput(event.input);
|
|
47
48
|
return {
|
|
48
49
|
conversationId: event.conversationId,
|
|
49
50
|
toolName: event.toolName,
|
|
50
|
-
|
|
51
|
+
// Inputs can carry secrets the model typed verbatim (e.g.
|
|
52
|
+
// `export OPENAI_API_KEY=...` in a bash command) — redact before
|
|
53
|
+
// the row reaches the audit store, like results below.
|
|
54
|
+
input: redactToolInput(event.input, rawInput),
|
|
51
55
|
result: redactSecrets(event.result.content).slice(
|
|
52
56
|
0,
|
|
53
57
|
RESULT_PREVIEW_LIMIT,
|
|
@@ -63,18 +67,18 @@ function toInvocationRecord(
|
|
|
63
67
|
// don't stamp.
|
|
64
68
|
...telemetryColumns(
|
|
65
69
|
event,
|
|
66
|
-
|
|
70
|
+
rawInput,
|
|
67
71
|
event.resultBytes ?? Buffer.byteLength(event.result.content, "utf8"),
|
|
68
72
|
),
|
|
69
73
|
};
|
|
70
74
|
}
|
|
71
75
|
case "error": {
|
|
72
|
-
const
|
|
76
|
+
const rawInput = stringifyToolInput(event.input);
|
|
73
77
|
const result = `error: ${event.errorMessage}`;
|
|
74
78
|
return {
|
|
75
79
|
conversationId: event.conversationId,
|
|
76
80
|
toolName: event.toolName,
|
|
77
|
-
input,
|
|
81
|
+
input: redactToolInput(event.input, rawInput),
|
|
78
82
|
result,
|
|
79
83
|
decision: "error",
|
|
80
84
|
riskLevel: event.riskLevel,
|
|
@@ -83,7 +87,7 @@ function toInvocationRecord(
|
|
|
83
87
|
// The error result string is built right here and never goes
|
|
84
88
|
// through sensitive-output sanitization, so sizing it directly is
|
|
85
89
|
// already raw — no executor stamp exists or is needed.
|
|
86
|
-
...telemetryColumns(event,
|
|
90
|
+
...telemetryColumns(event, rawInput, Buffer.byteLength(result, "utf8")),
|
|
87
91
|
};
|
|
88
92
|
}
|
|
89
93
|
case "permission_denied":
|
|
@@ -92,7 +96,7 @@ function toInvocationRecord(
|
|
|
92
96
|
return {
|
|
93
97
|
conversationId: event.conversationId,
|
|
94
98
|
toolName: event.toolName,
|
|
95
|
-
input: stringifyToolInput(event.input),
|
|
99
|
+
input: redactToolInput(event.input, stringifyToolInput(event.input)),
|
|
96
100
|
result: formatDeniedResult(event.reason),
|
|
97
101
|
decision: "denied",
|
|
98
102
|
riskLevel: event.riskLevel,
|
|
@@ -105,6 +109,33 @@ function toInvocationRecord(
|
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Redact secrets from a tool input while keeping the stored audit string
|
|
114
|
+
* parseable JSON. The redaction marker (`<redacted type="..." />`) contains
|
|
115
|
+
* double quotes, so redacting the serialized string would corrupt it —
|
|
116
|
+
* instead, walk the input's string leaves BEFORE stringification so the
|
|
117
|
+
* marker lands inside a JSON string value (with its quotes escaped).
|
|
118
|
+
*
|
|
119
|
+
* `rawInput` is the canonical pre-redaction serialization (also used for
|
|
120
|
+
* the `argBytes` telemetry fallback — byte sizes must reflect the full
|
|
121
|
+
* payload before truncation and redaction). It is returned untouched when
|
|
122
|
+
* nothing matched, keeping benign inputs byte-identical, and is redacted as
|
|
123
|
+
* plain text if the input can't be walked or re-serialized (e.g. cyclic
|
|
124
|
+
* structures).
|
|
125
|
+
*/
|
|
126
|
+
function redactToolInput(
|
|
127
|
+
input: Record<string, unknown>,
|
|
128
|
+
rawInput: string,
|
|
129
|
+
): string {
|
|
130
|
+
try {
|
|
131
|
+
const { value, changed } = redactJsonStringLeaves(input);
|
|
132
|
+
if (!changed) return rawInput;
|
|
133
|
+
return JSON.stringify(value);
|
|
134
|
+
} catch {
|
|
135
|
+
return redactSecrets(rawInput);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
108
139
|
type TelemetryColumns = Pick<ToolInvocationRecord, "argBytes" | "resultBytes"> &
|
|
109
140
|
UsageAttributionColumns;
|
|
110
141
|
|
|
@@ -134,12 +165,12 @@ const NULL_TELEMETRY_COLUMNS: TelemetryColumns = {
|
|
|
134
165
|
*/
|
|
135
166
|
function telemetryColumns(
|
|
136
167
|
event: Extract<ToolLifecycleEvent, { type: "executed" | "error" }>,
|
|
137
|
-
|
|
168
|
+
rawInput: string,
|
|
138
169
|
resultBytes: number,
|
|
139
170
|
): TelemetryColumns {
|
|
140
171
|
if (!getConfig().collectUsageData) return NULL_TELEMETRY_COLUMNS;
|
|
141
172
|
return {
|
|
142
|
-
argBytes: event.inputBytes ?? Buffer.byteLength(
|
|
173
|
+
argBytes: event.inputBytes ?? Buffer.byteLength(rawInput, "utf8"),
|
|
143
174
|
resultBytes,
|
|
144
175
|
...toAttributionColumns(event.attribution),
|
|
145
176
|
};
|
|
@@ -114,7 +114,7 @@ mock.module("../../config/loader.js", () => ({
|
|
|
114
114
|
getNestedValue: () => undefined,
|
|
115
115
|
setNestedValue: () => {},
|
|
116
116
|
API_KEY_PROVIDERS: [],
|
|
117
|
-
|
|
117
|
+
_writeQuarantineNotice: () => {},
|
|
118
118
|
}));
|
|
119
119
|
|
|
120
120
|
// Stub prompt helpers.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// ─── Mocks ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
let greetingRefreshed = false;
|
|
6
|
+
let promptsRefreshed = false;
|
|
7
|
+
let greetingCalls = 0;
|
|
8
|
+
let promptsCalls = 0;
|
|
9
|
+
let greetingGate: Promise<void> = Promise.resolve();
|
|
10
|
+
|
|
11
|
+
mock.module("../home-greeting.js", () => ({
|
|
12
|
+
refreshPersonalizedGreeting: async () => {
|
|
13
|
+
greetingCalls++;
|
|
14
|
+
await greetingGate;
|
|
15
|
+
return greetingRefreshed;
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
mock.module("../suggested-prompts.js", () => ({
|
|
20
|
+
refreshAssistantSuggestedPrompts: async () => {
|
|
21
|
+
promptsCalls++;
|
|
22
|
+
return promptsRefreshed;
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
const publishSpy = mock<(event: unknown) => Promise<void>>(async () => {});
|
|
27
|
+
mock.module("../../runtime/assistant-event-hub.js", () => ({
|
|
28
|
+
assistantEventHub: { publish: publishSpy },
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
mock.module("../../runtime/assistant-event.js", () => ({
|
|
32
|
+
buildAssistantEvent: (e: unknown) => e,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
mock.module("../../util/logger.js", () => ({
|
|
36
|
+
getLogger: () =>
|
|
37
|
+
new Proxy({} as Record<string, unknown>, {
|
|
38
|
+
get: () => () => {},
|
|
39
|
+
}),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const { revalidateHomeContentInBackground } =
|
|
43
|
+
await import("../home-content-refresh.js");
|
|
44
|
+
|
|
45
|
+
async function settle(): Promise<void> {
|
|
46
|
+
// Let the fire-and-forget revalidation chain run to completion.
|
|
47
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
48
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Tests ─────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
describe("revalidateHomeContentInBackground", () => {
|
|
54
|
+
test("publishes home_feed_updated when content was refreshed", async () => {
|
|
55
|
+
publishSpy.mockClear();
|
|
56
|
+
greetingRefreshed = true;
|
|
57
|
+
promptsRefreshed = false;
|
|
58
|
+
|
|
59
|
+
revalidateHomeContentInBackground();
|
|
60
|
+
await settle();
|
|
61
|
+
|
|
62
|
+
expect(publishSpy).toHaveBeenCalledTimes(1);
|
|
63
|
+
expect(publishSpy.mock.calls[0]?.[0]).toMatchObject({
|
|
64
|
+
type: "home_feed_updated",
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("does not publish when both caches were fresh", async () => {
|
|
69
|
+
publishSpy.mockClear();
|
|
70
|
+
greetingRefreshed = false;
|
|
71
|
+
promptsRefreshed = false;
|
|
72
|
+
|
|
73
|
+
revalidateHomeContentInBackground();
|
|
74
|
+
await settle();
|
|
75
|
+
|
|
76
|
+
expect(publishSpy).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("concurrent calls share a single in-flight revalidation", async () => {
|
|
80
|
+
greetingCalls = 0;
|
|
81
|
+
promptsCalls = 0;
|
|
82
|
+
greetingRefreshed = false;
|
|
83
|
+
promptsRefreshed = false;
|
|
84
|
+
|
|
85
|
+
let release!: () => void;
|
|
86
|
+
greetingGate = new Promise((resolve) => {
|
|
87
|
+
release = resolve;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
revalidateHomeContentInBackground();
|
|
91
|
+
revalidateHomeContentInBackground();
|
|
92
|
+
revalidateHomeContentInBackground();
|
|
93
|
+
|
|
94
|
+
release();
|
|
95
|
+
greetingGate = Promise.resolve();
|
|
96
|
+
await settle();
|
|
97
|
+
|
|
98
|
+
expect(greetingCalls).toBe(1);
|
|
99
|
+
expect(promptsCalls).toBe(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("a completed run allows a later revalidation", async () => {
|
|
103
|
+
greetingCalls = 0;
|
|
104
|
+
greetingRefreshed = false;
|
|
105
|
+
promptsRefreshed = false;
|
|
106
|
+
|
|
107
|
+
revalidateHomeContentInBackground();
|
|
108
|
+
await settle();
|
|
109
|
+
revalidateHomeContentInBackground();
|
|
110
|
+
await settle();
|
|
111
|
+
|
|
112
|
+
expect(greetingCalls).toBe(2);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -4,6 +4,25 @@ import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
|
4
4
|
|
|
5
5
|
const mockIntegrationSummary = "Gmail ✓ | Slack ✓ | Twilio ✗ | Telegram ✗";
|
|
6
6
|
let mockSidechainText = "";
|
|
7
|
+
let sidechainCalls = 0;
|
|
8
|
+
|
|
9
|
+
// In-memory stand-in for the memory_checkpoints table so the cache
|
|
10
|
+
// round-trips without a real database.
|
|
11
|
+
const checkpointStore = new Map<string, string>();
|
|
12
|
+
let checkpointWritesShouldThrow = false;
|
|
13
|
+
|
|
14
|
+
mock.module("../../memory/checkpoints.js", () => ({
|
|
15
|
+
getMemoryCheckpoint: (key: string) => checkpointStore.get(key) ?? null,
|
|
16
|
+
setMemoryCheckpoint: (key: string, value: string) => {
|
|
17
|
+
if (checkpointWritesShouldThrow) {
|
|
18
|
+
throw new Error("synthetic checkpoint write failure");
|
|
19
|
+
}
|
|
20
|
+
checkpointStore.set(key, value);
|
|
21
|
+
},
|
|
22
|
+
deleteMemoryCheckpoint: (key: string) => {
|
|
23
|
+
checkpointStore.delete(key);
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
7
26
|
|
|
8
27
|
mock.module("../../schedule/integration-status.js", () => ({
|
|
9
28
|
formatIntegrationSummary: async () => mockIntegrationSummary,
|
|
@@ -41,7 +60,10 @@ mock.module("../../prompts/system-prompt.js", () => ({
|
|
|
41
60
|
}));
|
|
42
61
|
|
|
43
62
|
mock.module("../../runtime/btw-sidechain.js", () => ({
|
|
44
|
-
runBtwSidechain: async () =>
|
|
63
|
+
runBtwSidechain: async () => {
|
|
64
|
+
sidechainCalls++;
|
|
65
|
+
return { text: mockSidechainText };
|
|
66
|
+
},
|
|
45
67
|
}));
|
|
46
68
|
|
|
47
69
|
mock.module("../../runtime/assistant-event.js", () => ({
|
|
@@ -62,8 +84,10 @@ const {
|
|
|
62
84
|
|
|
63
85
|
describe("getSuggestedPrompts", () => {
|
|
64
86
|
afterEach(() => {
|
|
87
|
+
checkpointWritesShouldThrow = false;
|
|
65
88
|
invalidateAssistantSuggestedPromptsCache();
|
|
66
89
|
mockSidechainText = "";
|
|
90
|
+
sidechainCalls = 0;
|
|
67
91
|
});
|
|
68
92
|
|
|
69
93
|
test("returns empty array before cache is populated", async () => {
|
|
@@ -98,12 +122,17 @@ describe("getSuggestedPrompts", () => {
|
|
|
98
122
|
expect(await getSuggestedPrompts()).toEqual([]);
|
|
99
123
|
});
|
|
100
124
|
|
|
101
|
-
test("handles empty LLM response gracefully", async () => {
|
|
125
|
+
test("handles empty LLM response gracefully and caches the empty result", async () => {
|
|
102
126
|
mockSidechainText = "";
|
|
103
127
|
|
|
104
|
-
await refreshAssistantSuggestedPrompts();
|
|
105
|
-
|
|
106
|
-
|
|
128
|
+
expect(await refreshAssistantSuggestedPrompts()).toBe(false);
|
|
129
|
+
expect(await getSuggestedPrompts()).toEqual([]);
|
|
130
|
+
|
|
131
|
+
// The empty result is cached: the next refresh within the TTL must
|
|
132
|
+
// not hit the LLM again (prevents a generation loop on every Home
|
|
133
|
+
// feed GET while the model returns nothing usable).
|
|
134
|
+
expect(await refreshAssistantSuggestedPrompts()).toBe(false);
|
|
135
|
+
expect(sidechainCalls).toBe(1);
|
|
107
136
|
});
|
|
108
137
|
|
|
109
138
|
test("handles malformed LLM response gracefully", async () => {
|
|
@@ -113,4 +142,56 @@ describe("getSuggestedPrompts", () => {
|
|
|
113
142
|
const prompts = await getSuggestedPrompts();
|
|
114
143
|
expect(prompts).toEqual([]);
|
|
115
144
|
});
|
|
145
|
+
|
|
146
|
+
test("refresh reports whether new content was generated", async () => {
|
|
147
|
+
mockSidechainText = JSON.stringify([
|
|
148
|
+
{ label: "Do stuff", prompt: "Do stuff for me" },
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
expect(await refreshAssistantSuggestedPrompts()).toBe(true);
|
|
152
|
+
// Cache is fresh — second refresh must not hit the LLM again.
|
|
153
|
+
expect(await refreshAssistantSuggestedPrompts()).toBe(false);
|
|
154
|
+
expect(sidechainCalls).toBe(1);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("refresh reports false when the cache write fails", async () => {
|
|
158
|
+
mockSidechainText = JSON.stringify([
|
|
159
|
+
{ label: "Do stuff", prompt: "Do stuff for me" },
|
|
160
|
+
]);
|
|
161
|
+
checkpointWritesShouldThrow = true;
|
|
162
|
+
|
|
163
|
+
// The generation succeeded but nothing was cached, so the caller must
|
|
164
|
+
// not announce fresh content (clients would refetch into a cache miss
|
|
165
|
+
// and re-trigger generation on every Home load).
|
|
166
|
+
expect(await refreshAssistantSuggestedPrompts()).toBe(false);
|
|
167
|
+
expect(await getSuggestedPrompts()).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("refresh regenerates after invalidation", async () => {
|
|
171
|
+
mockSidechainText = JSON.stringify([
|
|
172
|
+
{ label: "Do stuff", prompt: "Do stuff for me" },
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
await refreshAssistantSuggestedPrompts();
|
|
176
|
+
invalidateAssistantSuggestedPromptsCache();
|
|
177
|
+
|
|
178
|
+
expect(await refreshAssistantSuggestedPrompts()).toBe(true);
|
|
179
|
+
expect(sidechainCalls).toBe(2);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("cache survives across module state (checkpoint-backed)", async () => {
|
|
183
|
+
mockSidechainText = JSON.stringify([
|
|
184
|
+
{ label: "Persisted", prompt: "I came from the checkpoint store" },
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
await refreshAssistantSuggestedPrompts();
|
|
188
|
+
// Simulate the read path a fresh daemon would take: the prompts come
|
|
189
|
+
// back from the checkpoint store, not module-local memory.
|
|
190
|
+
expect(checkpointStore.get("home:suggested_prompts:json")).toContain(
|
|
191
|
+
"Persisted",
|
|
192
|
+
);
|
|
193
|
+
const prompts = await getSuggestedPrompts();
|
|
194
|
+
expect(prompts).toHaveLength(1);
|
|
195
|
+
expect(prompts[0]!.label).toBe("Persisted");
|
|
196
|
+
});
|
|
116
197
|
});
|