@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,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",
|
|
@@ -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(),
|
|
@@ -48,10 +48,6 @@ import {
|
|
|
48
48
|
updateMessageContent,
|
|
49
49
|
updateMessageMetadata,
|
|
50
50
|
} from "../../memory/conversation-crud.js";
|
|
51
|
-
import {
|
|
52
|
-
clearPendingVerificationReply,
|
|
53
|
-
getPendingVerificationReply,
|
|
54
|
-
} from "../../memory/delivery-channels.js";
|
|
55
51
|
import {
|
|
56
52
|
clearPayload,
|
|
57
53
|
findMessageBySourceId,
|
|
@@ -629,38 +625,6 @@ export async function handleChannelInbound({
|
|
|
629
625
|
|
|
630
626
|
const replyCallbackUrl = body.replyCallbackUrl;
|
|
631
627
|
|
|
632
|
-
// ── Retry pending verification reply on duplicate ──
|
|
633
|
-
// If a previous verification delivery failed and stored a pending reply,
|
|
634
|
-
// gateway retries (duplicates) re-attempt delivery here. On success the
|
|
635
|
-
// pending marker is cleared so further duplicates short-circuit normally.
|
|
636
|
-
if (result.duplicate && replyCallbackUrl) {
|
|
637
|
-
const pendingReply = getPendingVerificationReply(result.eventId);
|
|
638
|
-
if (pendingReply) {
|
|
639
|
-
try {
|
|
640
|
-
await deliverChannelReply(replyCallbackUrl, {
|
|
641
|
-
chatId: pendingReply.chatId,
|
|
642
|
-
text: pendingReply.text,
|
|
643
|
-
assistantId: pendingReply.assistantId,
|
|
644
|
-
});
|
|
645
|
-
clearPendingVerificationReply(result.eventId);
|
|
646
|
-
log.info(
|
|
647
|
-
{ eventId: result.eventId },
|
|
648
|
-
"Retried pending verification reply: delivered",
|
|
649
|
-
);
|
|
650
|
-
} catch (retryErr) {
|
|
651
|
-
log.error(
|
|
652
|
-
{ err: retryErr, eventId: result.eventId },
|
|
653
|
-
"Retry of pending verification reply failed; will retry on next duplicate",
|
|
654
|
-
);
|
|
655
|
-
}
|
|
656
|
-
return {
|
|
657
|
-
accepted: true,
|
|
658
|
-
duplicate: true,
|
|
659
|
-
eventId: result.eventId,
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
628
|
// external_conversation_bindings is assistant-agnostic. Restrict writes to
|
|
665
629
|
// self so assistant-scoped legacy routes do not overwrite each other's
|
|
666
630
|
// channel binding metadata for the same chat.
|
|
@@ -191,6 +191,27 @@ const pluginDetailsResponseSchema = z.object({
|
|
|
191
191
|
ref: z
|
|
192
192
|
.string()
|
|
193
193
|
.describe("Git ref the catalog metadata / README were resolved at."),
|
|
194
|
+
artifact: z
|
|
195
|
+
.object({
|
|
196
|
+
url: z
|
|
197
|
+
.string()
|
|
198
|
+
.describe("HTTPS URL the prebuilt client artifact is downloaded from."),
|
|
199
|
+
sha256: z
|
|
200
|
+
.string()
|
|
201
|
+
.describe(
|
|
202
|
+
"Lowercase 64-char hex SHA-256 the download is verified against.",
|
|
203
|
+
),
|
|
204
|
+
label: z
|
|
205
|
+
.string()
|
|
206
|
+
.optional()
|
|
207
|
+
.describe(
|
|
208
|
+
'Optional human label for the download (e.g. "Download for macOS"); absent when the plugin doesn\'t name it.',
|
|
209
|
+
),
|
|
210
|
+
})
|
|
211
|
+
.nullable()
|
|
212
|
+
.describe(
|
|
213
|
+
"Prebuilt client artifact from `package.json` `vellum.artifact`, or null when the plugin ships none or its descriptor is incomplete (e.g. a placeholder sha256).",
|
|
214
|
+
),
|
|
194
215
|
});
|
|
195
216
|
|
|
196
217
|
const pluginInstallRequestSchema = z.object({
|
|
@@ -12,6 +12,10 @@ import { INTERNAL_GUARDIAN_TRUST_CONTEXT } from "../../daemon/trust-context.js";
|
|
|
12
12
|
import { bootstrapConversation } from "../../memory/conversation-bootstrap.js";
|
|
13
13
|
import { getConversation } from "../../memory/conversation-crud.js";
|
|
14
14
|
import { getUsageCostForConversationWindow } from "../../memory/llm-usage-store.js";
|
|
15
|
+
import {
|
|
16
|
+
describeRRuleExpression,
|
|
17
|
+
isSingleFireRRule,
|
|
18
|
+
} from "../../schedule/recurrence-engine.js";
|
|
15
19
|
import { normalizeScheduleSyntax } from "../../schedule/recurrence-types.js";
|
|
16
20
|
import {
|
|
17
21
|
runScript,
|
|
@@ -136,7 +140,20 @@ function getCadenceDescription(
|
|
|
136
140
|
if (job.syntax === "cron") {
|
|
137
141
|
return describeCronExpression(job.cronExpression);
|
|
138
142
|
}
|
|
139
|
-
return job.
|
|
143
|
+
return describeRRuleExpression(job.cronExpression);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Presentation-layer one-shot flag. A COUNT=1 rrule fires exactly once and
|
|
148
|
+
* should read as one-time in clients, even though the scheduler internally
|
|
149
|
+
* treats expression-backed jobs as recurring (retry policy, conversation
|
|
150
|
+
* reuse). Do not feed this back into scheduler logic.
|
|
151
|
+
*/
|
|
152
|
+
function isOneShotForDisplay(
|
|
153
|
+
job: Pick<ScheduleJob, "syntax" | "cronExpression">,
|
|
154
|
+
): boolean {
|
|
155
|
+
if (job.cronExpression == null) return true;
|
|
156
|
+
return job.syntax === "rrule" && isSingleFireRRule(job.cronExpression);
|
|
140
157
|
}
|
|
141
158
|
|
|
142
159
|
function handleListSchedules(queryParams: Record<string, string>) {
|
|
@@ -182,7 +199,7 @@ function handleListSchedules(queryParams: Record<string, string>) {
|
|
|
182
199
|
routingIntent: j.routingIntent,
|
|
183
200
|
reuseConversation: j.reuseConversation,
|
|
184
201
|
wakeConversationId: j.wakeConversationId,
|
|
185
|
-
isOneShot: j
|
|
202
|
+
isOneShot: isOneShotForDisplay(j),
|
|
186
203
|
};
|
|
187
204
|
}),
|
|
188
205
|
};
|