@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
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Route handlers for conversation listing, detail, and seen/unread state.
|
|
3
3
|
*
|
|
4
|
-
* GET /v1/conversations
|
|
5
|
-
* POST /v1/conversations/seen
|
|
6
|
-
* POST /v1/conversations/
|
|
7
|
-
*
|
|
4
|
+
* GET /v1/conversations — paginated conversation list
|
|
5
|
+
* POST /v1/conversations/seen — record a seen signal (single)
|
|
6
|
+
* POST /v1/conversations/seen/bulk — record seen signals (batch)
|
|
7
|
+
* POST /v1/conversations/unread — mark a conversation unread
|
|
8
|
+
* GET /v1/conversations/:id — conversation detail
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { z } from "zod";
|
|
@@ -37,7 +38,10 @@ import {
|
|
|
37
38
|
buildConversationDetailResponse,
|
|
38
39
|
serializeConversationSummary,
|
|
39
40
|
} from "../services/conversation-serializer.js";
|
|
40
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
publishConversationListAndMetadataChanged,
|
|
43
|
+
publishConversationListChanged,
|
|
44
|
+
} from "../sync/resource-sync-events.js";
|
|
41
45
|
import {
|
|
42
46
|
BadRequestError,
|
|
43
47
|
InternalError,
|
|
@@ -73,6 +77,7 @@ const assistantAttentionSchema = z.object({
|
|
|
73
77
|
"macos_notification_view",
|
|
74
78
|
"macos_conversation_opened",
|
|
75
79
|
"ios_conversation_opened",
|
|
80
|
+
"web_bulk_mark_read",
|
|
76
81
|
"telegram_inbound_message",
|
|
77
82
|
"telegram_callback",
|
|
78
83
|
"slack_inbound_message",
|
|
@@ -318,6 +323,55 @@ function handleRecordSeen({ body = {}, headers }: RouteHandlerArgs) {
|
|
|
318
323
|
}
|
|
319
324
|
}
|
|
320
325
|
|
|
326
|
+
function handleRecordSeenBulk({ body = {}, headers }: RouteHandlerArgs) {
|
|
327
|
+
const rawIds = body.conversationIds as string[] | undefined;
|
|
328
|
+
if (!Array.isArray(rawIds) || rawIds.length === 0) {
|
|
329
|
+
throw new BadRequestError("conversationIds must be a non-empty array");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const originClientId = headers?.["x-vellum-client-id"]?.trim() || undefined;
|
|
333
|
+
const changedIds: string[] = [];
|
|
334
|
+
|
|
335
|
+
for (const rawId of rawIds) {
|
|
336
|
+
try {
|
|
337
|
+
const conversationId = resolveOrThrow(rawId);
|
|
338
|
+
const priorState = getAttentionStateByConversationIds([
|
|
339
|
+
conversationId,
|
|
340
|
+
]).get(conversationId);
|
|
341
|
+
const wasUnseen =
|
|
342
|
+
priorState != null &&
|
|
343
|
+
priorState.latestAssistantMessageAt != null &&
|
|
344
|
+
(priorState.lastSeenAssistantMessageAt == null ||
|
|
345
|
+
priorState.lastSeenAssistantMessageAt <
|
|
346
|
+
priorState.latestAssistantMessageAt);
|
|
347
|
+
|
|
348
|
+
recordConversationSeenSignal({
|
|
349
|
+
conversationId,
|
|
350
|
+
sourceChannel: "vellum",
|
|
351
|
+
signalType: "web_bulk_mark_read",
|
|
352
|
+
confidence: "explicit",
|
|
353
|
+
source: "http-api",
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
if (wasUnseen) {
|
|
357
|
+
changedIds.push(conversationId);
|
|
358
|
+
}
|
|
359
|
+
} catch (err) {
|
|
360
|
+
log.error(
|
|
361
|
+
{ err, conversationId: rawId },
|
|
362
|
+
"POST /v1/conversations/seen/bulk: failed for conversation",
|
|
363
|
+
);
|
|
364
|
+
// Best-effort: continue with remaining conversations.
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (changedIds.length > 0) {
|
|
369
|
+
publishConversationListChanged("seen_changed", originClientId);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { ok: true, updated: changedIds.length };
|
|
373
|
+
}
|
|
374
|
+
|
|
321
375
|
function handleMarkUnread({ body = {}, headers }: RouteHandlerArgs) {
|
|
322
376
|
const rawConversationId = body.conversationId as string | undefined;
|
|
323
377
|
if (!rawConversationId) {
|
|
@@ -426,6 +480,24 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
426
480
|
responseBody: z.object({ ok: z.boolean() }),
|
|
427
481
|
handler: handleRecordSeen,
|
|
428
482
|
},
|
|
483
|
+
{
|
|
484
|
+
operationId: "recordConversationSeenBulk",
|
|
485
|
+
endpoint: "conversations/seen/bulk",
|
|
486
|
+
method: "POST",
|
|
487
|
+
policy: {
|
|
488
|
+
requiredScopes: ["chat.write"],
|
|
489
|
+
allowedPrincipalTypes: ACTOR_PRINCIPALS,
|
|
490
|
+
},
|
|
491
|
+
summary: "Bulk mark conversations as seen",
|
|
492
|
+
description:
|
|
493
|
+
"Mark multiple conversations as seen in one request. Emits a single sync invalidation for the entire batch instead of per-conversation events.",
|
|
494
|
+
tags: ["conversations"],
|
|
495
|
+
requestBody: z.object({
|
|
496
|
+
conversationIds: z.array(z.string()).min(1),
|
|
497
|
+
}),
|
|
498
|
+
responseBody: z.object({ ok: z.boolean(), updated: z.number() }),
|
|
499
|
+
handler: handleRecordSeenBulk,
|
|
500
|
+
},
|
|
429
501
|
{
|
|
430
502
|
operationId: "markConversationUnread",
|
|
431
503
|
endpoint: "conversations/unread",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* DELETE /v1/conversations/:id — delete a single conversation
|
|
12
12
|
* POST /v1/conversations/:id/archive — archive a conversation
|
|
13
13
|
* POST /v1/conversations/:id/unarchive — restore an archived conversation
|
|
14
|
+
* POST /v1/conversations/archive/bulk — archive multiple conversations
|
|
14
15
|
* POST /v1/conversations/:id/surface — promote to / demote from Recents
|
|
15
16
|
* POST /v1/conversations/:id/cancel — cancel generation
|
|
16
17
|
* POST /v1/conversations/:id/undo — undo last message
|
|
@@ -362,6 +363,41 @@ function handleUnarchiveConversation({
|
|
|
362
363
|
return { ok: true, conversationId: resolvedId };
|
|
363
364
|
}
|
|
364
365
|
|
|
366
|
+
function handleArchiveConversationsBulk({
|
|
367
|
+
body = {},
|
|
368
|
+
headers,
|
|
369
|
+
}: RouteHandlerArgs) {
|
|
370
|
+
const rawIds = body.conversationIds as string[] | undefined;
|
|
371
|
+
if (!Array.isArray(rawIds) || rawIds.length === 0) {
|
|
372
|
+
throw new BadRequestError("conversationIds must be a non-empty array");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const originClientId = headers?.["x-vellum-client-id"]?.trim() || undefined;
|
|
376
|
+
const archivedIds: string[] = [];
|
|
377
|
+
|
|
378
|
+
for (const rawId of rawIds) {
|
|
379
|
+
try {
|
|
380
|
+
const conversationId = resolveOrThrow(rawId);
|
|
381
|
+
const archived = archiveConversation(conversationId);
|
|
382
|
+
if (archived) {
|
|
383
|
+
archivedIds.push(conversationId);
|
|
384
|
+
}
|
|
385
|
+
} catch (err) {
|
|
386
|
+
log.error(
|
|
387
|
+
{ err, conversationId: rawId },
|
|
388
|
+
"POST /v1/conversations/archive/bulk: failed for conversation",
|
|
389
|
+
);
|
|
390
|
+
// Best-effort: continue with remaining conversations.
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (archivedIds.length > 0) {
|
|
395
|
+
publishConversationListChanged("reordered", originClientId);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return { ok: true, archived: archivedIds.length };
|
|
399
|
+
}
|
|
400
|
+
|
|
365
401
|
/**
|
|
366
402
|
* Set or clear the `surfacedAt` promotion marker on a conversation.
|
|
367
403
|
*
|
|
@@ -695,6 +731,24 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
695
731
|
}),
|
|
696
732
|
handler: handleUnarchiveConversation,
|
|
697
733
|
},
|
|
734
|
+
{
|
|
735
|
+
operationId: "archiveConversationsBulk",
|
|
736
|
+
endpoint: "conversations/archive/bulk",
|
|
737
|
+
method: "POST",
|
|
738
|
+
policy: {
|
|
739
|
+
requiredScopes: ["chat.write"],
|
|
740
|
+
allowedPrincipalTypes: ACTOR_PRINCIPALS,
|
|
741
|
+
},
|
|
742
|
+
summary: "Bulk archive conversations",
|
|
743
|
+
description:
|
|
744
|
+
"Archive multiple conversations in one request. Emits a single sync invalidation for the entire batch.",
|
|
745
|
+
tags: ["conversations"],
|
|
746
|
+
requestBody: z.object({
|
|
747
|
+
conversationIds: z.array(z.string()).min(1),
|
|
748
|
+
}),
|
|
749
|
+
responseBody: z.object({ ok: z.boolean(), archived: z.number() }),
|
|
750
|
+
handler: handleArchiveConversationsBulk,
|
|
751
|
+
},
|
|
698
752
|
{
|
|
699
753
|
operationId: "surfaceConversation",
|
|
700
754
|
endpoint: "conversations/:id/surface",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* GET /v1/messages/:id/content — full message content
|
|
15
15
|
* GET /v1/messages/:id/llm-context — LLM request logs for a message
|
|
16
16
|
* GET /v1/llm-request-logs/:id/payload — raw payload for a single log
|
|
17
|
+
* GET /v1/llm-request-logs/:id/context — normalized context for a single log
|
|
17
18
|
* DELETE /v1/messages/queued/:id — delete queued message
|
|
18
19
|
* POST /v1/messages/queued/:id/steer — steer to a queued message
|
|
19
20
|
*/
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
import { z } from "zod";
|
|
22
23
|
|
|
23
24
|
import { LlmContextResponseSchema } from "../../api/responses/llm-context-response.js";
|
|
25
|
+
import { LLMRequestLogEntrySchema } from "../../api/responses/llm-request-log-entry.js";
|
|
24
26
|
import {
|
|
25
27
|
deepMergeOverwrite,
|
|
26
28
|
fillContextDefaultsForMissingKeys,
|
|
@@ -236,7 +238,26 @@ function applyStoredProviderToLlmContextResult(
|
|
|
236
238
|
return { ...normalized, summary };
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
|
|
241
|
+
/**
|
|
242
|
+
* `full` returns the complete normalized entry including request/response
|
|
243
|
+
* sections; `summary` omits the sections so list responses stay small —
|
|
244
|
+
* sections for a single call are fetched lazily through
|
|
245
|
+
* `/v1/llm-request-logs/{logId}/context`.
|
|
246
|
+
*/
|
|
247
|
+
type LlmContextView = "full" | "summary";
|
|
248
|
+
|
|
249
|
+
function resolveLlmContextView(view: string | undefined): LlmContextView {
|
|
250
|
+
if (view === undefined || view === "full") return "full";
|
|
251
|
+
if (view === "summary") return "summary";
|
|
252
|
+
throw new BadRequestError(
|
|
253
|
+
`Invalid view parameter: ${view}. Expected "full" or "summary".`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeLlmContextLog(
|
|
258
|
+
log: LogRow,
|
|
259
|
+
view: LlmContextView = "full",
|
|
260
|
+
): LlmContextRouteResult & {
|
|
240
261
|
id: string;
|
|
241
262
|
requestPayload: null;
|
|
242
263
|
responsePayload: null;
|
|
@@ -283,6 +304,9 @@ function normalizeLlmContextLog(log: LogRow): LlmContextRouteResult & {
|
|
|
283
304
|
// existing `agent_loop_exit_reason` column tells it WHICH error fired.
|
|
284
305
|
callSite: log.callSite ?? null,
|
|
285
306
|
...result,
|
|
307
|
+
...(view === "summary"
|
|
308
|
+
? { requestSections: undefined, responseSections: undefined }
|
|
309
|
+
: {}),
|
|
286
310
|
};
|
|
287
311
|
}
|
|
288
312
|
|
|
@@ -898,11 +922,15 @@ function resolveConversationKind(
|
|
|
898
922
|
return "user";
|
|
899
923
|
}
|
|
900
924
|
|
|
901
|
-
async function handleGetLlmContext({
|
|
925
|
+
async function handleGetLlmContext({
|
|
926
|
+
pathParams = {},
|
|
927
|
+
queryParams = {},
|
|
928
|
+
}: RouteHandlerArgs) {
|
|
902
929
|
const messageId = pathParams.id;
|
|
903
930
|
if (!messageId) {
|
|
904
931
|
throw new BadRequestError("message id is required");
|
|
905
932
|
}
|
|
933
|
+
const view = resolveLlmContextView(queryParams.view);
|
|
906
934
|
const source = await getLlmRequestLogSource();
|
|
907
935
|
const logs = await source.getRequestLogsByMessageId(messageId);
|
|
908
936
|
const turnMessageIds = getAssistantMessageIdsInTurn(messageId);
|
|
@@ -932,7 +960,7 @@ async function handleGetLlmContext({ pathParams = {} }: RouteHandlerArgs) {
|
|
|
932
960
|
messageId,
|
|
933
961
|
conversationKind,
|
|
934
962
|
conversationTotalEstimatedCostUsd,
|
|
935
|
-
logs: logs.map(normalizeLlmContextLog),
|
|
963
|
+
logs: logs.map((log) => normalizeLlmContextLog(log, view)),
|
|
936
964
|
memoryRecall: memoryRecallLog ?? null,
|
|
937
965
|
memoryV2Activation: memoryV2Activation ?? null,
|
|
938
966
|
memoryV3Selection,
|
|
@@ -944,6 +972,7 @@ async function handleGetConversationLlmContext({
|
|
|
944
972
|
}: RouteHandlerArgs) {
|
|
945
973
|
const conversationKey = queryParams.conversationKey;
|
|
946
974
|
const requestedConversationId = queryParams.conversationId;
|
|
975
|
+
const view = resolveLlmContextView(queryParams.view);
|
|
947
976
|
|
|
948
977
|
let conversationId: string | undefined = requestedConversationId;
|
|
949
978
|
if (!conversationId && conversationKey) {
|
|
@@ -1000,7 +1029,7 @@ async function handleGetConversationLlmContext({
|
|
|
1000
1029
|
conversationId: conversation.id,
|
|
1001
1030
|
conversationKind,
|
|
1002
1031
|
conversationTotalEstimatedCostUsd,
|
|
1003
|
-
logs: logs.map(normalizeLlmContextLog),
|
|
1032
|
+
logs: logs.map((log) => normalizeLlmContextLog(log, view)),
|
|
1004
1033
|
memoryRecall: null,
|
|
1005
1034
|
memoryV2Activation: null,
|
|
1006
1035
|
memoryV3Selection: null,
|
|
@@ -1034,6 +1063,21 @@ async function handleGetLlmRequestLogPayload({
|
|
|
1034
1063
|
return { id: log.id, requestPayload, responsePayload };
|
|
1035
1064
|
}
|
|
1036
1065
|
|
|
1066
|
+
async function handleGetLlmRequestLogContext({
|
|
1067
|
+
pathParams = {},
|
|
1068
|
+
}: RouteHandlerArgs) {
|
|
1069
|
+
const logId = pathParams.id;
|
|
1070
|
+
if (!logId) {
|
|
1071
|
+
throw new BadRequestError("log id is required");
|
|
1072
|
+
}
|
|
1073
|
+
const source = await getLlmRequestLogSource();
|
|
1074
|
+
const log = await source.getRequestLogById(logId);
|
|
1075
|
+
if (!log) {
|
|
1076
|
+
throw new NotFoundError("log not found");
|
|
1077
|
+
}
|
|
1078
|
+
return normalizeLlmContextLog(log);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1037
1081
|
function handleDeleteQueuedMessage({
|
|
1038
1082
|
queryParams = {},
|
|
1039
1083
|
pathParams = {},
|
|
@@ -1322,6 +1366,13 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
1322
1366
|
schema: { type: "string" },
|
|
1323
1367
|
description: "Internal conversation identifier.",
|
|
1324
1368
|
},
|
|
1369
|
+
{
|
|
1370
|
+
name: "view",
|
|
1371
|
+
required: false,
|
|
1372
|
+
schema: { type: "string", enum: ["full", "summary"] },
|
|
1373
|
+
description:
|
|
1374
|
+
"Response shape. 'summary' omits per-log request/response sections; defaults to 'full'.",
|
|
1375
|
+
},
|
|
1325
1376
|
],
|
|
1326
1377
|
responseBody: LlmContextResponseSchema,
|
|
1327
1378
|
handler: handleGetConversationLlmContext,
|
|
@@ -1338,6 +1389,15 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
1338
1389
|
description:
|
|
1339
1390
|
"Return request/response logs and memory recall data for a specific message.",
|
|
1340
1391
|
tags: ["messages"],
|
|
1392
|
+
queryParams: [
|
|
1393
|
+
{
|
|
1394
|
+
name: "view",
|
|
1395
|
+
required: false,
|
|
1396
|
+
schema: { type: "string", enum: ["full", "summary"] },
|
|
1397
|
+
description:
|
|
1398
|
+
"Response shape. 'summary' omits per-log request/response sections; defaults to 'full'.",
|
|
1399
|
+
},
|
|
1400
|
+
],
|
|
1341
1401
|
responseBody: LlmContextResponseSchema,
|
|
1342
1402
|
handler: handleGetLlmContext,
|
|
1343
1403
|
},
|
|
@@ -1360,6 +1420,21 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
1360
1420
|
}),
|
|
1361
1421
|
handler: handleGetLlmRequestLogPayload,
|
|
1362
1422
|
},
|
|
1423
|
+
{
|
|
1424
|
+
operationId: "llm_request_logs_context_get",
|
|
1425
|
+
endpoint: "llm-request-logs/:id/context",
|
|
1426
|
+
method: "GET",
|
|
1427
|
+
policy: {
|
|
1428
|
+
requiredScopes: ["chat.read"],
|
|
1429
|
+
allowedPrincipalTypes: ACTOR_PRINCIPALS,
|
|
1430
|
+
},
|
|
1431
|
+
summary: "Get normalized context for a single LLM request log",
|
|
1432
|
+
description:
|
|
1433
|
+
"Return the normalized summary and request/response sections for a specific log entry.",
|
|
1434
|
+
tags: ["messages"],
|
|
1435
|
+
responseBody: LLMRequestLogEntrySchema,
|
|
1436
|
+
handler: handleGetLlmRequestLogContext,
|
|
1437
|
+
},
|
|
1363
1438
|
{
|
|
1364
1439
|
operationId: "messages_queued_delete",
|
|
1365
1440
|
endpoint: "messages/queued/:id",
|
|
@@ -1,80 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Gateway log tail route — gateway
|
|
2
|
+
* Gateway log tail route — gateway IPC proxy.
|
|
3
3
|
*
|
|
4
|
-
* The handler
|
|
5
|
-
*
|
|
4
|
+
* The handler calls the gateway over the local IPC socket so the assistant
|
|
5
|
+
* does not need gateway signing material.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type GatewayLogsTailIpcResponse,
|
|
9
|
+
GatewayLogsTailIpcResponseSchema,
|
|
10
|
+
GatewayLogsTailRouteParamsSchema,
|
|
11
|
+
} from "@vellumai/gateway-client/gateway-ipc-contracts";
|
|
8
12
|
|
|
9
|
-
import {
|
|
13
|
+
import { ipcCallPersistent } from "../../ipc/gateway-client.js";
|
|
10
14
|
import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
|
|
11
15
|
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
12
16
|
|
|
13
|
-
// ── Shared helper ───────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
async function gatewayFetch(
|
|
16
|
-
path: string,
|
|
17
|
-
init?: RequestInit,
|
|
18
|
-
): Promise<unknown> {
|
|
19
|
-
const base = getGatewayInternalBaseUrl();
|
|
20
|
-
const res = await fetch(`${base}${path}`, init);
|
|
21
|
-
if (!res.ok) {
|
|
22
|
-
let message = `Gateway request failed (${res.status})`;
|
|
23
|
-
try {
|
|
24
|
-
const body = (await res.json()) as { error?: unknown };
|
|
25
|
-
if (typeof body.error === "string") {
|
|
26
|
-
message = body.error;
|
|
27
|
-
}
|
|
28
|
-
} catch {
|
|
29
|
-
// ignore JSON parse failures
|
|
30
|
-
}
|
|
31
|
-
throw new Error(message);
|
|
32
|
-
}
|
|
33
|
-
return res.json();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ── Schemas ─────────────────────────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
const LEVEL_NAMES = [
|
|
39
|
-
"trace",
|
|
40
|
-
"debug",
|
|
41
|
-
"info",
|
|
42
|
-
"warn",
|
|
43
|
-
"error",
|
|
44
|
-
"fatal",
|
|
45
|
-
] as const;
|
|
46
|
-
|
|
47
|
-
const GatewayLogsTailParams = z
|
|
48
|
-
.object({
|
|
49
|
-
n: z.coerce.number().int().min(1).max(1000).optional(),
|
|
50
|
-
level: z.enum(LEVEL_NAMES).optional(),
|
|
51
|
-
module: z.string().optional(),
|
|
52
|
-
})
|
|
53
|
-
.strict();
|
|
54
|
-
|
|
55
|
-
const GatewayLogsTailResponseSchema = z.object({
|
|
56
|
-
lines: z.array(z.record(z.string(), z.unknown())),
|
|
57
|
-
truncated: z.boolean(),
|
|
58
|
-
});
|
|
59
|
-
type GatewayLogsTailResponse = z.infer<typeof GatewayLogsTailResponseSchema>;
|
|
60
|
-
|
|
61
17
|
// ── Handlers ────────────────────────────────────────────────────────────
|
|
62
18
|
|
|
63
19
|
async function handleGatewayLogsTail({
|
|
64
20
|
queryParams = {},
|
|
65
21
|
body = {},
|
|
66
|
-
}: RouteHandlerArgs): Promise<
|
|
22
|
+
}: RouteHandlerArgs): Promise<GatewayLogsTailIpcResponse> {
|
|
67
23
|
// HTTP GET delivers filters via queryParams; CLI IPC puts them in body.
|
|
68
24
|
const source = Object.keys(queryParams).length > 0 ? queryParams : body;
|
|
69
|
-
const p =
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
if (p.level !== undefined) qs.set("level", p.level);
|
|
73
|
-
if (p.module !== undefined) qs.set("module", p.module);
|
|
74
|
-
const query = qs.toString();
|
|
75
|
-
return gatewayFetch(
|
|
76
|
-
`/v1/logs/tail${query ? `?${query}` : ""}`,
|
|
77
|
-
) as Promise<GatewayLogsTailResponse>;
|
|
25
|
+
const p = GatewayLogsTailRouteParamsSchema.parse(source);
|
|
26
|
+
const result = await ipcCallPersistent("gateway_logs_tail", p);
|
|
27
|
+
return GatewayLogsTailIpcResponseSchema.parse(result);
|
|
78
28
|
}
|
|
79
29
|
|
|
80
30
|
// ── Route definitions ───────────────────────────────────────────────────
|
|
@@ -93,7 +43,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
93
43
|
description:
|
|
94
44
|
"Return the last N structured log entries from the gateway log files.",
|
|
95
45
|
tags: ["gateway-logs"],
|
|
96
|
-
responseBody:
|
|
46
|
+
responseBody: GatewayLogsTailIpcResponseSchema,
|
|
97
47
|
queryParams: [
|
|
98
48
|
{
|
|
99
49
|
name: "n",
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
HomeFeedResponseSchema,
|
|
30
30
|
} from "../../api/responses/home.js";
|
|
31
31
|
import { patchFeedItemStatus, readHomeFeed } from "../../home/feed-writer.js";
|
|
32
|
+
import { revalidateHomeContentInBackground } from "../../home/home-content-refresh.js";
|
|
32
33
|
import { getPersonalizedGreeting } from "../../home/home-greeting.js";
|
|
33
34
|
import { getSuggestedPrompts } from "../../home/suggested-prompts.js";
|
|
34
35
|
import {
|
|
@@ -139,6 +140,15 @@ export async function handleGetHomeFeed({
|
|
|
139
140
|
|
|
140
141
|
const now = new Date();
|
|
141
142
|
|
|
143
|
+
// Stale-while-revalidate: serve whatever is cached right now and kick
|
|
144
|
+
// off a bounded background regeneration of any stale LLM content. The
|
|
145
|
+
// refresh publishes `home_feed_updated` when fresh content lands, so
|
|
146
|
+
// connected clients refetch and the personalized content swaps in.
|
|
147
|
+
// This is the accepted exception to GET-handler idempotency documented
|
|
148
|
+
// in `src/runtime/AGENTS.md` — the handler itself stays read-only and
|
|
149
|
+
// returns immediately with cached/fallback copy.
|
|
150
|
+
revalidateHomeContentInBackground();
|
|
151
|
+
|
|
142
152
|
const personalizedGreeting = getPersonalizedGreeting();
|
|
143
153
|
const suggestedPrompts = await getSuggestedPrompts();
|
|
144
154
|
|
|
@@ -25,7 +25,7 @@ import { getWorkspacePromptPath } from "../../util/platform.js";
|
|
|
25
25
|
// Constants
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
27
|
|
|
28
|
-
const CACHE_TTL_MS =
|
|
28
|
+
const CACHE_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
29
29
|
|
|
30
30
|
const CHECKPOINT_KEY_GREETINGS = "identity:intro:greetings";
|
|
31
31
|
const CHECKPOINT_KEY_TIMESTAMP = "identity:intro:cached_at";
|
|
@@ -7,7 +7,6 @@ import { availableParallelism, cpus, totalmem } from "node:os";
|
|
|
7
7
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
|
|
10
|
-
import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
|
|
11
10
|
import { getCpuLimit, getIsPlatform } from "../../config/env-registry.js";
|
|
12
11
|
import { resolveCallSiteConfig } from "../../config/llm-resolver.js";
|
|
13
12
|
import { getConfig } from "../../config/loader.js";
|
|
@@ -36,7 +35,7 @@ import {
|
|
|
36
35
|
readWorkspaceIdentityIntro,
|
|
37
36
|
setCachedIntro,
|
|
38
37
|
} from "./identity-intro-cache.js";
|
|
39
|
-
import type { RouteDefinition } from "./types.js";
|
|
38
|
+
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
40
39
|
|
|
41
40
|
interface MemoryInfo {
|
|
42
41
|
currentMb: number;
|
|
@@ -413,12 +412,12 @@ const FALLBACK_GREETINGS = [
|
|
|
413
412
|
"Ready when you are.",
|
|
414
413
|
];
|
|
415
414
|
|
|
416
|
-
const GENERATED_GREETING_LIMIT =
|
|
415
|
+
const GENERATED_GREETING_LIMIT = 5;
|
|
417
416
|
const GREETING_GENERATION_TIMEOUT_MS = 10_000;
|
|
418
417
|
const GREETING_GENERATION_FAILURE_COOLDOWN_MS = 60_000;
|
|
419
418
|
const EMPTY_STATE_GREETING_CALLSITE = "emptyStateGreeting" as const;
|
|
420
|
-
const
|
|
421
|
-
|
|
419
|
+
const EXPLICIT_TIME_OF_DAY_PATTERN =
|
|
420
|
+
/\b(?:morning|afternoon|evening|tonight|midnight|noon|sunrise|sunset)\b/i;
|
|
422
421
|
|
|
423
422
|
type IdentityIntroSource = "workspace" | "cache" | "fallback";
|
|
424
423
|
|
|
@@ -445,21 +444,18 @@ function identityIntroResponse(
|
|
|
445
444
|
};
|
|
446
445
|
}
|
|
447
446
|
|
|
448
|
-
function getIdentityIntro(
|
|
447
|
+
function getIdentityIntro({
|
|
448
|
+
queryParams = {},
|
|
449
|
+
}: RouteHandlerArgs = {}): IdentityIntroResponse {
|
|
450
|
+
const localTimeContext = buildLocalTimeContext(queryParams);
|
|
451
|
+
|
|
449
452
|
// 1. User-defined greetings from SOUL.md `## Greetings`
|
|
450
453
|
const workspaceGreetings = readWorkspaceGreetings();
|
|
451
454
|
if (workspaceGreetings) {
|
|
452
455
|
return identityIntroResponse(workspaceGreetings, "workspace");
|
|
453
456
|
}
|
|
454
457
|
|
|
455
|
-
|
|
456
|
-
if (
|
|
457
|
-
!isAssistantFeatureFlagEnabled(EMPTY_STATE_DYNAMIC_GREETINGS_FLAG, config)
|
|
458
|
-
) {
|
|
459
|
-
return identityIntroResponse(FALLBACK_GREETINGS, "fallback");
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// 2. Cached LLM-generated greetings (populated by background refresh)
|
|
458
|
+
// 2. Cached LLM-generated greetings
|
|
463
459
|
const cached = getCachedIntro();
|
|
464
460
|
if (cached) {
|
|
465
461
|
return identityIntroResponse(cached.greetings, "cache");
|
|
@@ -471,7 +467,7 @@ function getIdentityIntro(): IdentityIntroResponse {
|
|
|
471
467
|
if (identityIntro) {
|
|
472
468
|
// Still trigger background generation so the next request gets
|
|
473
469
|
// LLM-generated greetings instead of the static tagline.
|
|
474
|
-
const refreshing = triggerEmptyStateGreetingGeneration();
|
|
470
|
+
const refreshing = triggerEmptyStateGreetingGeneration(localTimeContext);
|
|
475
471
|
return identityIntroResponse(
|
|
476
472
|
[identityIntro, ...FALLBACK_GREETINGS],
|
|
477
473
|
"workspace",
|
|
@@ -480,13 +476,50 @@ function getIdentityIntro(): IdentityIntroResponse {
|
|
|
480
476
|
}
|
|
481
477
|
|
|
482
478
|
// 4. Trigger fresh generation without blocking the empty-state UI.
|
|
483
|
-
const refreshing = triggerEmptyStateGreetingGeneration();
|
|
479
|
+
const refreshing = triggerEmptyStateGreetingGeneration(localTimeContext);
|
|
484
480
|
|
|
485
481
|
// 5. Generic fallback only when generation is unavailable.
|
|
486
482
|
return identityIntroResponse(FALLBACK_GREETINGS, "fallback", refreshing);
|
|
487
483
|
}
|
|
488
484
|
|
|
489
|
-
function
|
|
485
|
+
function buildLocalTimeContext(
|
|
486
|
+
queryParams: Record<string, string>,
|
|
487
|
+
): string | null {
|
|
488
|
+
const rawHour = queryParams.localHour;
|
|
489
|
+
const rawMinute = queryParams.localMinute;
|
|
490
|
+
if (rawHour === undefined || rawMinute === undefined) {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const hour = Number(rawHour);
|
|
495
|
+
const minute = Number(rawMinute);
|
|
496
|
+
if (
|
|
497
|
+
!Number.isInteger(hour) ||
|
|
498
|
+
!Number.isInteger(minute) ||
|
|
499
|
+
hour < 0 ||
|
|
500
|
+
hour > 23 ||
|
|
501
|
+
minute < 0 ||
|
|
502
|
+
minute > 59
|
|
503
|
+
) {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const period =
|
|
508
|
+
hour >= 5 && hour < 12
|
|
509
|
+
? "morning"
|
|
510
|
+
: hour >= 12 && hour < 17
|
|
511
|
+
? "afternoon"
|
|
512
|
+
: hour >= 17 && hour < 21
|
|
513
|
+
? "evening"
|
|
514
|
+
: "late night";
|
|
515
|
+
const paddedHour = String(hour).padStart(2, "0");
|
|
516
|
+
const paddedMinute = String(minute).padStart(2, "0");
|
|
517
|
+
return `${period} (${paddedHour}:${paddedMinute})`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function triggerEmptyStateGreetingGeneration(
|
|
521
|
+
localTimeContext: string | null,
|
|
522
|
+
): boolean {
|
|
490
523
|
if (greetingGenerationInFlight) {
|
|
491
524
|
return true;
|
|
492
525
|
}
|
|
@@ -501,7 +534,7 @@ function triggerEmptyStateGreetingGeneration(): boolean {
|
|
|
501
534
|
|
|
502
535
|
greetingGenerationInFlight = new Promise<void>((resolve) => {
|
|
503
536
|
queueMicrotask(() => {
|
|
504
|
-
void generateEmptyStateGreetings()
|
|
537
|
+
void generateEmptyStateGreetings(localTimeContext)
|
|
505
538
|
.then((greetings) => {
|
|
506
539
|
lastGreetingGenerationFailureAt = greetings === null ? Date.now() : 0;
|
|
507
540
|
})
|
|
@@ -515,7 +548,9 @@ function triggerEmptyStateGreetingGeneration(): boolean {
|
|
|
515
548
|
return true;
|
|
516
549
|
}
|
|
517
550
|
|
|
518
|
-
async function generateEmptyStateGreetings(
|
|
551
|
+
async function generateEmptyStateGreetings(
|
|
552
|
+
localTimeContext: string | null,
|
|
553
|
+
): Promise<string[] | null> {
|
|
519
554
|
try {
|
|
520
555
|
const provider = await getConfiguredProvider(EMPTY_STATE_GREETING_CALLSITE);
|
|
521
556
|
if (!provider) {
|
|
@@ -530,12 +565,18 @@ async function generateEmptyStateGreetings(): Promise<string[] | null> {
|
|
|
530
565
|
excludeBootstrap: true,
|
|
531
566
|
excludeCustomPrefix: true,
|
|
532
567
|
});
|
|
568
|
+
const localTimeInstruction = localTimeContext
|
|
569
|
+
? ` Current user-local time for subtle tone only: ${localTimeContext}.`
|
|
570
|
+
: "";
|
|
533
571
|
const result = await runBtwSidechain({
|
|
534
572
|
content:
|
|
535
573
|
`Generate ${GENERATED_GREETING_LIMIT} short first-person greeting options for the empty new-chat screen. ` +
|
|
536
574
|
"Use the assistant identity, voice, and relationship guidance from IDENTITY.md and SOUL.md. " +
|
|
537
575
|
"Each greeting should feel personal and inviting, not like a generic assistant introduction. " +
|
|
538
|
-
"Return only a JSON array of strings. No markdown, keys, or explanation."
|
|
576
|
+
"Return only a JSON array of strings. No markdown, keys, or explanation. " +
|
|
577
|
+
"Generated greetings are cached for 4 hours, so do not mention the current time " +
|
|
578
|
+
"or use explicit time-of-day words like morning, afternoon, evening, or tonight." +
|
|
579
|
+
localTimeInstruction,
|
|
539
580
|
provider,
|
|
540
581
|
systemPrompt,
|
|
541
582
|
messages: [],
|
|
@@ -601,6 +642,7 @@ function normalizeGeneratedGreetings(values: unknown[]): string[] {
|
|
|
601
642
|
.replace(/^["'`]+|["'`]+$/g, "")
|
|
602
643
|
.trim();
|
|
603
644
|
if (!greeting) continue;
|
|
645
|
+
if (EXPLICIT_TIME_OF_DAY_PATTERN.test(greeting)) continue;
|
|
604
646
|
|
|
605
647
|
const key = greeting.toLowerCase();
|
|
606
648
|
if (seen.has(key)) continue;
|
|
@@ -758,6 +800,20 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
758
800
|
description:
|
|
759
801
|
"Returns greetings sourced from SOUL.md, the generated cache, or generic fallbacks while background generation refreshes the cache.",
|
|
760
802
|
tags: ["identity"],
|
|
803
|
+
queryParams: [
|
|
804
|
+
{
|
|
805
|
+
name: "localHour",
|
|
806
|
+
schema: { type: "integer", minimum: 0, maximum: 23 },
|
|
807
|
+
description:
|
|
808
|
+
"Optional client-local hour of day used only when refreshing generated greetings.",
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
name: "localMinute",
|
|
812
|
+
schema: { type: "integer", minimum: 0, maximum: 59 },
|
|
813
|
+
description:
|
|
814
|
+
"Optional client-local minute used only when refreshing generated greetings.",
|
|
815
|
+
},
|
|
816
|
+
],
|
|
761
817
|
responseBody: z.object({
|
|
762
818
|
greetings: z.array(z.string()),
|
|
763
819
|
text: z.string(),
|