@vellumai/assistant 0.8.2 → 0.8.3
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 +11 -12
- package/docker-entrypoint.sh +13 -1
- package/docker-init-apt-root.sh +79 -6
- package/openapi.yaml +336 -21
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
- package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
- package/src/__tests__/config-get-vision-flag.test.ts +136 -0
- package/src/__tests__/config-loader-backfill.test.ts +115 -18
- package/src/__tests__/context-token-estimator.test.ts +30 -65
- package/src/__tests__/conversation-agent-loop.test.ts +57 -1
- package/src/__tests__/conversation-media-retry.test.ts +19 -8
- package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
- package/src/__tests__/date-context.test.ts +45 -0
- package/src/__tests__/external-plugin-loader.test.ts +91 -19
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +24 -164
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/host-app-control-proxy.test.ts +241 -0
- package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
- package/src/__tests__/injector-background-turn.test.ts +153 -0
- package/src/__tests__/injector-chain.test.ts +5 -0
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
- package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +3 -0
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +255 -2
- package/src/__tests__/managed-profile-guard.test.ts +10 -0
- package/src/__tests__/notification-decision-fallback.test.ts +0 -91
- package/src/__tests__/notification-decision-strategy.test.ts +14 -31
- package/src/__tests__/notification-deep-link.test.ts +15 -0
- package/src/__tests__/notification-guardian-path.test.ts +1 -2
- package/src/__tests__/notification-platform-adapter.test.ts +5 -4
- package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
- package/src/__tests__/openai-provider.test.ts +218 -3
- package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
- package/src/__tests__/openrouter-provider-only.test.ts +51 -3
- package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
- package/src/__tests__/platform-proxy-context.test.ts +6 -1
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
- package/src/__tests__/plugin-types.test.ts +2 -2
- package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +6 -73
- package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
- package/src/a2a/__tests__/agent-card.test.ts +98 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
- package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
- package/src/a2a/__tests__/task-store.test.ts +246 -0
- package/src/a2a/agent-card.ts +58 -0
- package/src/a2a/feature-gate.ts +8 -0
- package/src/a2a/protocol-constants.ts +21 -0
- package/src/a2a/protocol-errors.ts +50 -0
- package/src/a2a/protocol-types.ts +162 -0
- package/src/a2a/task-store.ts +168 -0
- package/src/agent/loop.ts +167 -18
- package/src/channels/config.ts +9 -0
- package/src/channels/types.ts +14 -0
- package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
- package/src/cli/commands/__tests__/schedules.test.ts +469 -0
- package/src/cli/commands/notifications.ts +65 -35
- package/src/cli/commands/plugins.ts +67 -0
- package/src/cli/commands/schedules.ts +297 -5
- package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
- package/src/cli/lib/install-from-github.ts +8 -9
- package/src/cli/lib/search-plugins.ts +163 -0
- package/src/cli/program.ts +14 -0
- package/src/config/assistant-feature-flags.ts +24 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/call-site-defaults.ts +105 -0
- package/src/config/feature-flag-registry.json +21 -29
- package/src/config/llm-resolver.ts +52 -1
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
- package/src/config/schemas/channels.ts +9 -0
- package/src/config/schemas/conversations.ts +10 -0
- package/src/config/schemas/heartbeat.ts +14 -0
- package/src/config/schemas/llm.ts +1 -3
- package/src/config/schemas/memory-retrospective.ts +1 -1
- package/src/config/schemas/memory-v2.ts +4 -4
- package/src/config/schemas/memory.ts +3 -1
- package/src/config/seed-inference-profiles.ts +99 -29
- package/src/context/compactor.ts +72 -12
- package/src/context/token-estimator.ts +32 -34
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
- package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
- package/src/daemon/conversation-agent-loop.ts +29 -2
- package/src/daemon/conversation-runtime-assembly.ts +9 -0
- package/src/daemon/conversation.ts +0 -7
- package/src/daemon/date-context.ts +40 -0
- package/src/daemon/guardian-action-generators.ts +1 -125
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
- package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
- package/src/daemon/handlers/config-a2a.ts +289 -0
- package/src/daemon/handlers/conversations.ts +1 -0
- package/src/daemon/host-app-control-proxy.ts +69 -18
- package/src/daemon/host-proxy-preactivation.ts +85 -18
- package/src/daemon/lifecycle.ts +49 -61
- package/src/daemon/memory-v2-startup.ts +49 -13
- package/src/daemon/message-types/notifications.ts +21 -0
- package/src/daemon/pkb-reminder-builder.test.ts +10 -53
- package/src/daemon/pkb-reminder-builder.ts +4 -19
- package/src/daemon/process-message.ts +3 -0
- package/src/daemon/skill-memory-refresh.ts +5 -1
- package/src/daemon/wake-target-adapter.ts +2 -0
- package/src/export/__tests__/transcript-formatter.test.ts +121 -0
- package/src/export/transcript-formatter.ts +54 -20
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
- package/src/heartbeat/heartbeat-service.ts +34 -191
- package/src/home/__tests__/feed-types.test.ts +40 -0
- package/src/home/feed-types.ts +14 -2
- package/src/ipc/cli-client.ts +147 -45
- package/src/memory/__tests__/conversation-queries.test.ts +220 -0
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
- package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
- package/src/memory/conversation-queries.ts +87 -1
- package/src/memory/conversation-title-service.ts +26 -4
- package/src/memory/db-init.ts +6 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
- package/src/memory/graph/conversation-graph-memory.ts +18 -6
- package/src/memory/graph/tools.ts +6 -37
- package/src/memory/invite-store.ts +53 -0
- package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
- package/src/memory/llm-request-log-store.ts +92 -1
- package/src/memory/memory-retrospective-enqueue.ts +1 -20
- package/src/memory/memory-retrospective-job.ts +33 -6
- package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
- package/src/memory/migrations/251-a2a-tasks.ts +49 -0
- package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/a2a.ts +15 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/inference.ts +2 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
- package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
- package/src/memory/v2/__tests__/injection.test.ts +190 -3
- package/src/memory/v2/__tests__/static-context.test.ts +12 -1
- package/src/memory/v2/activation-store.ts +14 -16
- package/src/memory/v2/cli-command-content.ts +19 -0
- package/src/memory/v2/cli-command-store.ts +304 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -1
- package/src/memory/v2/injection.ts +49 -20
- package/src/memory/v2/page-index.ts +38 -13
- package/src/memory/v2/static-context.ts +4 -4
- package/src/memory/v2/types.ts +23 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
- package/src/messaging/providers/a2a/deliver.ts +156 -0
- package/src/messaging/providers/gmail/client.ts +9 -2
- package/src/messaging/providers/index.ts +11 -2
- package/src/notifications/__tests__/broadcaster.test.ts +203 -0
- package/src/notifications/__tests__/decision-engine.test.ts +283 -0
- package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
- package/src/notifications/adapters/macos.ts +12 -2
- package/src/notifications/broadcaster.ts +29 -4
- package/src/notifications/copy-composer.ts +17 -64
- package/src/notifications/decision-engine.ts +111 -44
- package/src/notifications/deterministic-checks.ts +96 -0
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/home-feed-side-effect.ts +85 -6
- package/src/notifications/signal.ts +0 -4
- package/src/notifications/types.ts +8 -0
- package/src/oauth/platform-connection.test.ts +43 -3
- package/src/oauth/platform-connection.ts +13 -4
- package/src/plugins/defaults/injectors.ts +38 -19
- package/src/plugins/external-plugin-loader.ts +82 -10
- package/src/plugins/types.ts +16 -7
- package/src/prompts/__tests__/system-prompt.test.ts +6 -51
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
- package/src/prompts/system-prompt.ts +0 -8
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/system-sections.ts +0 -9
- package/src/providers/__tests__/inference.test.ts +2 -0
- package/src/providers/call-site-routing.ts +24 -6
- package/src/providers/connection-resolution.ts +63 -13
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
- package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
- package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
- package/src/providers/inference/adapter-factory.ts +9 -20
- package/src/providers/inference/auth.ts +12 -0
- package/src/providers/inference/backfill.ts +14 -1
- package/src/providers/inference/connections.ts +85 -5
- package/src/providers/inference/resolve-auth.ts +2 -0
- package/src/providers/model-catalog.ts +199 -244
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
- package/src/providers/openai/chat-completions-provider.ts +159 -6
- package/src/providers/openrouter/client.ts +42 -4
- package/src/providers/platform-proxy/constants.ts +3 -4
- package/src/providers/provider-catalog-visibility.ts +3 -1
- package/src/providers/provider-send-message.ts +27 -12
- package/src/providers/registry.ts +30 -1
- package/src/runtime/agent-wake.ts +61 -1
- package/src/runtime/auth/route-policy.ts +13 -0
- package/src/runtime/http-server.ts +7 -16
- package/src/runtime/http-types.ts +0 -47
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
- package/src/runtime/routes/channel-availability-routes.ts +5 -0
- package/src/runtime/routes/consolidation-routes.ts +100 -0
- package/src/runtime/routes/conversation-query-routes.ts +70 -11
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
- package/src/runtime/routes/integrations/a2a.ts +235 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
- package/src/runtime/routes/subagents-routes.ts +41 -0
- package/src/subagent/manager.ts +2 -0
- package/src/tools/memory/register.ts +1 -9
- package/src/tools/registry.ts +2 -2
- package/src/tools/types.ts +37 -2
- package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
- package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
- package/src/runtime/guardian-action-conversation-turn.ts +0 -99
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* subagent conversation sections when present in message metadata.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { formatLocalTimestamp } from "../daemon/date-context.js";
|
|
8
9
|
import {
|
|
9
10
|
getConversation,
|
|
10
11
|
getMessages,
|
|
@@ -23,10 +24,6 @@ interface ContentBlock {
|
|
|
23
24
|
source?: { media_type?: string; filename?: string };
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
function formatTimestamp(ms: number): string {
|
|
27
|
-
return new Date(ms).toISOString().replace("T", " ").slice(0, 19);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
27
|
function extractAnalysisText(blocks: ContentBlock[]): string {
|
|
31
28
|
const parts: string[] = [];
|
|
32
29
|
for (const block of blocks) {
|
|
@@ -67,15 +64,36 @@ function extractAnalysisText(blocks: ContentBlock[]): string {
|
|
|
67
64
|
return parts.join("\n");
|
|
68
65
|
}
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
export interface TranscriptFormatOptions {
|
|
68
|
+
timeZone?: string;
|
|
69
|
+
assistantName?: string | null;
|
|
70
|
+
userName?: string | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveName(
|
|
74
|
+
name: string | null | undefined,
|
|
75
|
+
fallback: string,
|
|
76
|
+
): string {
|
|
77
|
+
return name && name.length > 0 ? name : fallback;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatRole(
|
|
81
|
+
role: string,
|
|
82
|
+
options: TranscriptFormatOptions = {},
|
|
83
|
+
): string {
|
|
84
|
+
return role === "user"
|
|
85
|
+
? resolveName(options.userName, "User")
|
|
86
|
+
: resolveName(options.assistantName, "Assistant");
|
|
72
87
|
}
|
|
73
88
|
|
|
74
|
-
function formatSubagentMessages(
|
|
89
|
+
function formatSubagentMessages(
|
|
90
|
+
msgs: ReturnType<typeof getMessages>,
|
|
91
|
+
options: TranscriptFormatOptions = {},
|
|
92
|
+
): string {
|
|
75
93
|
const lines: string[] = [];
|
|
76
94
|
for (const msg of msgs) {
|
|
77
|
-
const role = formatRole(msg.role);
|
|
78
|
-
const time =
|
|
95
|
+
const role = formatRole(msg.role, options);
|
|
96
|
+
const time = formatLocalTimestamp(msg.createdAt, options.timeZone);
|
|
79
97
|
const content = parseContent(msg.content);
|
|
80
98
|
const text = extractAnalysisText(content);
|
|
81
99
|
if (text) {
|
|
@@ -103,24 +121,33 @@ type TranscriptMessage = ReturnType<typeof getMessages>[number];
|
|
|
103
121
|
* Format a slice of messages as a transcript body (no top-of-conversation
|
|
104
122
|
* header). Used by background jobs that process incremental slices — the
|
|
105
123
|
* memory-retrospective job re-renders only the messages added since its
|
|
106
|
-
* last successful run rather than the whole conversation. The
|
|
107
|
-
* matches `buildAnalysisTranscript`
|
|
108
|
-
*
|
|
109
|
-
*
|
|
124
|
+
* last successful run rather than the whole conversation. The per-message
|
|
125
|
+
* structural shape matches `buildAnalysisTranscript` (header line, body,
|
|
126
|
+
* optional subagent block) so downstream agents see consistent framing.
|
|
127
|
+
* The participant *labels*, however, intentionally diverge: this function
|
|
128
|
+
* honors `TranscriptFormatOptions` so the memory-retrospective prompt can
|
|
129
|
+
* render the conversation under the assistant and user display names,
|
|
130
|
+
* while `buildAnalysisTranscript` always uses generic "User"/"Assistant"
|
|
131
|
+
* labels for the analyze-conversation flow.
|
|
110
132
|
*/
|
|
111
133
|
export function formatMessageSliceForTranscript(
|
|
112
134
|
messages: TranscriptMessage[],
|
|
135
|
+
options: TranscriptFormatOptions = {},
|
|
113
136
|
): string {
|
|
114
137
|
const lines: string[] = [];
|
|
115
138
|
for (const msg of messages) {
|
|
116
|
-
appendMessageBlock(lines, msg);
|
|
139
|
+
appendMessageBlock(lines, msg, options);
|
|
117
140
|
}
|
|
118
141
|
return lines.join("\n");
|
|
119
142
|
}
|
|
120
143
|
|
|
121
|
-
function appendMessageBlock(
|
|
122
|
-
|
|
123
|
-
|
|
144
|
+
function appendMessageBlock(
|
|
145
|
+
lines: string[],
|
|
146
|
+
msg: TranscriptMessage,
|
|
147
|
+
options: TranscriptFormatOptions = {},
|
|
148
|
+
): void {
|
|
149
|
+
const role = formatRole(msg.role, options);
|
|
150
|
+
const time = formatLocalTimestamp(msg.createdAt, options.timeZone);
|
|
124
151
|
const content = parseContent(msg.content);
|
|
125
152
|
const text = extractAnalysisText(content);
|
|
126
153
|
|
|
@@ -142,7 +169,14 @@ function appendMessageBlock(lines: string[], msg: TranscriptMessage): void {
|
|
|
142
169
|
const subMessages = getMessages(notif.conversationId);
|
|
143
170
|
lines.push(`### Subagent: ${notif.label} (${notif.status})`);
|
|
144
171
|
lines.push("");
|
|
145
|
-
|
|
172
|
+
// Subagent conversations persist the parent assistant's objective
|
|
173
|
+
// as a `user` message (see subagent/manager.ts), so reusing the
|
|
174
|
+
// parent's display-name options would render the assistant's
|
|
175
|
+
// tasking text under the human user's name. Keep child transcripts
|
|
176
|
+
// on generic role labels — and only pass through the time zone.
|
|
177
|
+
lines.push(
|
|
178
|
+
formatSubagentMessages(subMessages, { timeZone: options.timeZone }),
|
|
179
|
+
);
|
|
146
180
|
lines.push("");
|
|
147
181
|
}
|
|
148
182
|
}
|
|
@@ -163,12 +197,12 @@ export function buildAnalysisTranscript(conversationId: string): string {
|
|
|
163
197
|
const lines: string[] = [];
|
|
164
198
|
|
|
165
199
|
lines.push(`# Conversation: ${title}`);
|
|
166
|
-
lines.push(`Created: ${
|
|
200
|
+
lines.push(`Created: ${formatLocalTimestamp(conversation.createdAt)}`);
|
|
167
201
|
lines.push("");
|
|
168
202
|
|
|
169
203
|
for (const msg of allMessages) {
|
|
170
204
|
const role = formatRole(msg.role);
|
|
171
|
-
const time =
|
|
205
|
+
const time = formatLocalTimestamp(msg.createdAt);
|
|
172
206
|
const content = parseContent(msg.content);
|
|
173
207
|
const text = extractAnalysisText(content);
|
|
174
208
|
|
|
@@ -55,6 +55,7 @@ const stubConfig: {
|
|
|
55
55
|
activeHoursStart: number | null;
|
|
56
56
|
activeHoursEnd: number | null;
|
|
57
57
|
maxConsecutiveRuns: number | null;
|
|
58
|
+
disposition: string;
|
|
58
59
|
};
|
|
59
60
|
} = {
|
|
60
61
|
heartbeat: {
|
|
@@ -63,6 +64,7 @@ const stubConfig: {
|
|
|
63
64
|
activeHoursStart: null,
|
|
64
65
|
activeHoursEnd: null,
|
|
65
66
|
maxConsecutiveRuns: null,
|
|
67
|
+
disposition: "Default disposition text.",
|
|
66
68
|
},
|
|
67
69
|
};
|
|
68
70
|
mock.module("../../config/loader.js", () => ({
|
|
@@ -395,6 +397,48 @@ describe("HeartbeatService", () => {
|
|
|
395
397
|
}
|
|
396
398
|
});
|
|
397
399
|
|
|
400
|
+
test("resetTimer() during an in-flight run is not undone by that run's increment", async () => {
|
|
401
|
+
// Regression: if a guardian message arrives mid-run, `resetTimer()`
|
|
402
|
+
// zeroes the counter but the in-flight run's `finally` block used to
|
|
403
|
+
// unconditionally `_consecutiveRuns++`, leaving the counter at 1 and
|
|
404
|
+
// tripping the cap-at-1 path one auto run too early.
|
|
405
|
+
stubConfig.heartbeat.maxConsecutiveRuns = 1;
|
|
406
|
+
|
|
407
|
+
let releaseInflight: () => void = () => {};
|
|
408
|
+
const inflight = new Promise<void>((resolve) => {
|
|
409
|
+
releaseInflight = resolve;
|
|
410
|
+
});
|
|
411
|
+
runBackgroundJobImpl = async () => {
|
|
412
|
+
await inflight;
|
|
413
|
+
return { conversationId: STUB_CONVERSATION_ID, ok: true };
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const service = new HeartbeatService({ alerter: () => {} });
|
|
417
|
+
service.start();
|
|
418
|
+
try {
|
|
419
|
+
const runPromise = service.runOnce({ force: false });
|
|
420
|
+
// Guardian message arrives while the run is still executing.
|
|
421
|
+
service.resetTimer();
|
|
422
|
+
releaseInflight();
|
|
423
|
+
expect(await runPromise).toBe(true);
|
|
424
|
+
|
|
425
|
+
// The reset during the in-flight run must survive: the next auto run
|
|
426
|
+
// should proceed because the counter is still 0, not 1.
|
|
427
|
+
runBackgroundJobImpl = async () => ({
|
|
428
|
+
conversationId: STUB_CONVERSATION_ID,
|
|
429
|
+
ok: true,
|
|
430
|
+
});
|
|
431
|
+
expect(await service.runOnce({ force: false })).toBe(true);
|
|
432
|
+
expect(
|
|
433
|
+
skipHeartbeatRunCalls.some(
|
|
434
|
+
(c) => c.reason === "max_consecutive_runs",
|
|
435
|
+
),
|
|
436
|
+
).toBe(false);
|
|
437
|
+
} finally {
|
|
438
|
+
await service.stop();
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
398
442
|
test("null disables the cap entirely", async () => {
|
|
399
443
|
stubConfig.heartbeat.maxConsecutiveRuns = null;
|
|
400
444
|
const service = new HeartbeatService({ alerter: () => {} });
|
|
@@ -9,8 +9,6 @@ import {
|
|
|
9
9
|
shouldLogDiskPressureBackgroundSkip,
|
|
10
10
|
} from "../daemon/disk-pressure-background-gate.js";
|
|
11
11
|
import type { HeartbeatAlert } from "../daemon/message-protocol.js";
|
|
12
|
-
import { getConversation, getMessages } from "../memory/conversation-crud.js";
|
|
13
|
-
import { GENERATING_TITLE } from "../memory/conversation-title-service.js";
|
|
14
12
|
import { emitNotificationSignal } from "../notifications/emit-signal.js";
|
|
15
13
|
import {
|
|
16
14
|
GUARDIAN_PERSONA_TEMPLATE,
|
|
@@ -47,9 +45,6 @@ const DEFAULT_CHECKLIST = `- Check in with yourself. Read NOW.md. Is it still ac
|
|
|
47
45
|
const EARLY_HEARTBEAT_THRESHOLD = 3;
|
|
48
46
|
const REENGAGEMENT_COOLDOWN_MS = 18 * 60 * 60 * 1000; // 18 hours
|
|
49
47
|
const HEARTBEAT_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
50
|
-
const HEARTBEAT_ALERT_MARKER = "HEARTBEAT_ALERT";
|
|
51
|
-
const HEARTBEAT_OK_MARKER = "HEARTBEAT_OK";
|
|
52
|
-
const HEARTBEAT_ALERT_SUMMARY_MAX_CHARS = 700;
|
|
53
48
|
|
|
54
49
|
// Stripped-comment form of the guardian persona scaffold. Computed
|
|
55
50
|
// once at module load because stripping comment lines is deterministic
|
|
@@ -102,69 +97,6 @@ function recordReengagementTimestamp(): void {
|
|
|
102
97
|
}
|
|
103
98
|
}
|
|
104
99
|
|
|
105
|
-
type HeartbeatDisposition = "alert" | "ok" | "unknown";
|
|
106
|
-
|
|
107
|
-
function parseHeartbeatDisposition(text: string | null): HeartbeatDisposition {
|
|
108
|
-
if (!text) return "unknown";
|
|
109
|
-
const lines = text
|
|
110
|
-
.trim()
|
|
111
|
-
.split(/\r?\n/)
|
|
112
|
-
.map((line) => line.trim())
|
|
113
|
-
.filter((line) => line.length > 0);
|
|
114
|
-
const lastLine = lines.at(-1);
|
|
115
|
-
if (lastLine === HEARTBEAT_ALERT_MARKER) return "alert";
|
|
116
|
-
if (lastLine === HEARTBEAT_OK_MARKER) return "ok";
|
|
117
|
-
return "unknown";
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function stripHeartbeatDispositionMarkers(text: string): string {
|
|
121
|
-
return text
|
|
122
|
-
.replace(
|
|
123
|
-
new RegExp(
|
|
124
|
-
`(?:\\r?\\n)?\\s*(?:${HEARTBEAT_ALERT_MARKER}|${HEARTBEAT_OK_MARKER})\\s*$`,
|
|
125
|
-
),
|
|
126
|
-
"",
|
|
127
|
-
)
|
|
128
|
-
.trim();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function truncateSummary(text: string, maxChars: number): string {
|
|
132
|
-
if (text.length <= maxChars) return text;
|
|
133
|
-
return `${text.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function buildHeartbeatAlertSummary(text: string | null): string {
|
|
137
|
-
const summary = text ? stripHeartbeatDispositionMarkers(text) : "";
|
|
138
|
-
return truncateSummary(
|
|
139
|
-
summary || "Your assistant found something worth your attention.",
|
|
140
|
-
HEARTBEAT_ALERT_SUMMARY_MAX_CHARS,
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function extractVisibleTextFromStoredMessageContent(raw: string): string {
|
|
145
|
-
try {
|
|
146
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
147
|
-
if (typeof parsed === "string") return parsed;
|
|
148
|
-
if (!Array.isArray(parsed)) return "";
|
|
149
|
-
const texts: string[] = [];
|
|
150
|
-
for (const block of parsed) {
|
|
151
|
-
if (
|
|
152
|
-
block != null &&
|
|
153
|
-
typeof block === "object" &&
|
|
154
|
-
"type" in block &&
|
|
155
|
-
block.type === "text" &&
|
|
156
|
-
"text" in block &&
|
|
157
|
-
typeof block.text === "string"
|
|
158
|
-
) {
|
|
159
|
-
texts.push(block.text);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return texts.join("\n").trim();
|
|
163
|
-
} catch {
|
|
164
|
-
return raw;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
100
|
export interface HeartbeatDeps {
|
|
169
101
|
alerter: (alert: HeartbeatAlert) => void;
|
|
170
102
|
onConversationCreated?: (info: {
|
|
@@ -202,6 +134,9 @@ export class HeartbeatService {
|
|
|
202
134
|
// Reset by resetTimer (guardian message), reconfigure, and stop. Force runs
|
|
203
135
|
// bypass the cap and do not increment.
|
|
204
136
|
private _consecutiveRuns = 0;
|
|
137
|
+
// Bumped every time the counter is reset so an in-flight run that finishes
|
|
138
|
+
// after a guardian message can detect the reset and skip its increment.
|
|
139
|
+
private _resetGeneration = 0;
|
|
205
140
|
|
|
206
141
|
constructor(deps: HeartbeatDeps) {
|
|
207
142
|
this.deps = deps;
|
|
@@ -360,6 +295,7 @@ export class HeartbeatService {
|
|
|
360
295
|
/** Restart the timer with the latest config (e.g. after settings change). */
|
|
361
296
|
reconfigure(): void {
|
|
362
297
|
this._consecutiveRuns = 0;
|
|
298
|
+
this._resetGeneration++;
|
|
363
299
|
this.configEpoch++;
|
|
364
300
|
if (this._pendingRunId) {
|
|
365
301
|
supersedePendingRun(this._pendingRunId);
|
|
@@ -384,6 +320,7 @@ export class HeartbeatService {
|
|
|
384
320
|
// Counter resets even when the timer is null so a guardian message during
|
|
385
321
|
// a stopped window still clears the count.
|
|
386
322
|
this._consecutiveRuns = 0;
|
|
323
|
+
this._resetGeneration++;
|
|
387
324
|
if (!this.timer) return;
|
|
388
325
|
if (this.cronMode) {
|
|
389
326
|
clearTimeout(this.timer as ReturnType<typeof setTimeout>);
|
|
@@ -404,6 +341,7 @@ export class HeartbeatService {
|
|
|
404
341
|
|
|
405
342
|
async stop(): Promise<void> {
|
|
406
343
|
this._consecutiveRuns = 0;
|
|
344
|
+
this._resetGeneration++;
|
|
407
345
|
this.stopped = true;
|
|
408
346
|
if (this.timer) {
|
|
409
347
|
clearTimeout(this.timer as ReturnType<typeof setTimeout>);
|
|
@@ -552,6 +490,11 @@ export class HeartbeatService {
|
|
|
552
490
|
}
|
|
553
491
|
const run = this.executeRun(runId, scheduledFor);
|
|
554
492
|
this.activeRun = run;
|
|
493
|
+
// Snapshot the reset generation so we can detect whether a reset (guardian
|
|
494
|
+
// message, reconfigure, stop) happened while this run was in flight. If it
|
|
495
|
+
// did, the counter was already zeroed and we must not undo that reset by
|
|
496
|
+
// incrementing in `finally`.
|
|
497
|
+
const startGeneration = this._resetGeneration;
|
|
555
498
|
try {
|
|
556
499
|
await run;
|
|
557
500
|
} catch (err) {
|
|
@@ -561,7 +504,7 @@ export class HeartbeatService {
|
|
|
561
504
|
this.activeRun = null;
|
|
562
505
|
}
|
|
563
506
|
this._lastRunAt = Date.now();
|
|
564
|
-
if (!force) {
|
|
507
|
+
if (!force && this._resetGeneration === startGeneration) {
|
|
565
508
|
this._consecutiveRuns++;
|
|
566
509
|
}
|
|
567
510
|
if (!this.cronMode) {
|
|
@@ -699,66 +642,6 @@ export class HeartbeatService {
|
|
|
699
642
|
}
|
|
700
643
|
}
|
|
701
644
|
|
|
702
|
-
private getLatestAssistantMessage(
|
|
703
|
-
conversationId: string,
|
|
704
|
-
): { id: string; text: string } | null {
|
|
705
|
-
try {
|
|
706
|
-
const messages = getMessages(conversationId);
|
|
707
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
708
|
-
const message = messages[i]!;
|
|
709
|
-
if (message.role !== "assistant") continue;
|
|
710
|
-
return {
|
|
711
|
-
id: message.id,
|
|
712
|
-
text: extractVisibleTextFromStoredMessageContent(message.content),
|
|
713
|
-
};
|
|
714
|
-
}
|
|
715
|
-
} catch (err) {
|
|
716
|
-
log.warn(
|
|
717
|
-
{ err, conversationId },
|
|
718
|
-
"Failed to read heartbeat assistant message",
|
|
719
|
-
);
|
|
720
|
-
}
|
|
721
|
-
return null;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
private async emitHeartbeatAlertNotification(params: {
|
|
725
|
-
runId: string;
|
|
726
|
-
conversationId: string;
|
|
727
|
-
messageId?: string;
|
|
728
|
-
conversationTitle: string;
|
|
729
|
-
summary: string;
|
|
730
|
-
}): Promise<void> {
|
|
731
|
-
const { emitNotificationSignal } =
|
|
732
|
-
await import("../notifications/emit-signal.js");
|
|
733
|
-
|
|
734
|
-
await emitNotificationSignal({
|
|
735
|
-
sourceEventName: "heartbeat.alert",
|
|
736
|
-
sourceChannel: "watcher",
|
|
737
|
-
sourceContextId: params.runId,
|
|
738
|
-
dedupeKey: `heartbeat:alert:${params.runId}`,
|
|
739
|
-
attentionHints: {
|
|
740
|
-
requiresAction: true,
|
|
741
|
-
urgency: "medium",
|
|
742
|
-
isAsyncBackground: true,
|
|
743
|
-
visibleInSourceNow: false,
|
|
744
|
-
},
|
|
745
|
-
contextPayload: {
|
|
746
|
-
title: "Heartbeat Alert",
|
|
747
|
-
summary: params.summary,
|
|
748
|
-
conversationTitle: params.conversationTitle,
|
|
749
|
-
conversationId: params.conversationId,
|
|
750
|
-
messageId: params.messageId,
|
|
751
|
-
},
|
|
752
|
-
routingIntent: "single_channel",
|
|
753
|
-
conversationAffinityHint: { vellum: params.conversationId },
|
|
754
|
-
conversationMetadata: {
|
|
755
|
-
source: "heartbeat",
|
|
756
|
-
groupId: "system:background",
|
|
757
|
-
conversationType: "background",
|
|
758
|
-
},
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
|
|
762
645
|
private async executeRun(runId: string, scheduledFor: number): Promise<void> {
|
|
763
646
|
log.info("Running heartbeat");
|
|
764
647
|
|
|
@@ -787,9 +670,9 @@ export class HeartbeatService {
|
|
|
787
670
|
// The runner fires `onConversationCreated` synchronously after
|
|
788
671
|
// bootstrap so the macOS sidebar gets the new conversation
|
|
789
672
|
// immediately rather than waiting up to HEARTBEAT_TIMEOUT_MS for
|
|
790
|
-
// the LLM turn to finish.
|
|
791
|
-
//
|
|
792
|
-
//
|
|
673
|
+
// the LLM turn to finish. If the model judges the run worth
|
|
674
|
+
// surfacing to the guardian, it calls the `notifications` skill
|
|
675
|
+
// directly — no in-band marker.
|
|
793
676
|
let conversationId: string | undefined;
|
|
794
677
|
const result = await runBackgroundJob({
|
|
795
678
|
jobName: "heartbeat",
|
|
@@ -821,62 +704,26 @@ export class HeartbeatService {
|
|
|
821
704
|
"Heartbeat completed",
|
|
822
705
|
);
|
|
823
706
|
|
|
824
|
-
// Mark the run record as ok
|
|
825
|
-
// alert the
|
|
826
|
-
//
|
|
827
|
-
//
|
|
828
|
-
// contents.
|
|
707
|
+
// Mark the run record as ok. The runner owns failure emission via
|
|
708
|
+
// `activity.failed`; any user-facing alert the model decided to
|
|
709
|
+
// raise was emitted in-band via the `notifications` skill during
|
|
710
|
+
// the turn itself.
|
|
829
711
|
const transitioned = completeHeartbeatRun(runId, {
|
|
830
712
|
status: "ok",
|
|
831
713
|
conversationId: result.conversationId,
|
|
832
714
|
});
|
|
833
715
|
|
|
834
|
-
if (transitioned) {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
} catch {
|
|
842
|
-
// Best-effort; fall back to generic title.
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
const assistantMessage = this.getLatestAssistantMessage(
|
|
846
|
-
result.conversationId,
|
|
847
|
-
);
|
|
848
|
-
const disposition = parseHeartbeatDisposition(
|
|
849
|
-
assistantMessage?.text ?? null,
|
|
850
|
-
);
|
|
851
|
-
if (disposition === "alert") {
|
|
852
|
-
// Conversation was already surfaced via the runner's bootstrap
|
|
853
|
-
// callback above; alert just needs to emit the notification.
|
|
854
|
-
void this.emitHeartbeatAlertNotification({
|
|
716
|
+
if (transitioned && latenessMs > LATE_THRESHOLD_MS) {
|
|
717
|
+
const lateMinutes = Math.round(latenessMs / 60_000);
|
|
718
|
+
log.warn(
|
|
719
|
+
{
|
|
720
|
+
latenessMs,
|
|
721
|
+
lateMinutes,
|
|
722
|
+
scheduledFor,
|
|
855
723
|
runId,
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
summary: buildHeartbeatAlertSummary(assistantMessage?.text ?? null),
|
|
860
|
-
}).catch((err) => {
|
|
861
|
-
log.warn(
|
|
862
|
-
{ err, conversationId: result.conversationId },
|
|
863
|
-
"Failed to emit heartbeat alert notification",
|
|
864
|
-
);
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
if (latenessMs > LATE_THRESHOLD_MS) {
|
|
869
|
-
const lateMinutes = Math.round(latenessMs / 60_000);
|
|
870
|
-
log.warn(
|
|
871
|
-
{
|
|
872
|
-
latenessMs,
|
|
873
|
-
lateMinutes,
|
|
874
|
-
scheduledFor,
|
|
875
|
-
runId,
|
|
876
|
-
},
|
|
877
|
-
"Heartbeat ran late",
|
|
878
|
-
);
|
|
879
|
-
}
|
|
724
|
+
},
|
|
725
|
+
"Heartbeat ran late",
|
|
726
|
+
);
|
|
880
727
|
}
|
|
881
728
|
return;
|
|
882
729
|
}
|
|
@@ -941,18 +788,14 @@ Do NOT attempt to use tools for these providers — they will fail. Skip any che
|
|
|
941
788
|
</credential-status>`;
|
|
942
789
|
}
|
|
943
790
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
After completing your review, end your response with one of:
|
|
949
|
-
- HEARTBEAT_OK — if everything looks good, no action needed
|
|
950
|
-
- HEARTBEAT_ALERT — if you found issues that need attention (describe them before this marker)
|
|
951
|
-
</heartbeat-disposition>`;
|
|
791
|
+
const disposition = getConfig().heartbeat.disposition;
|
|
792
|
+
if (disposition) {
|
|
793
|
+
prompt += `\n\n<heartbeat-disposition>\n${disposition}\n</heartbeat-disposition>`;
|
|
794
|
+
}
|
|
952
795
|
|
|
953
796
|
if (completedRunCount < EARLY_HEARTBEAT_THRESHOLD) {
|
|
954
797
|
prompt += `\n\n<early-heartbeat>
|
|
955
|
-
This is one of your first heartbeats. Your user hasn't heard from you yet and may not know you're here. Find something genuinely useful to share — a follow-up from a recent conversation, something you noticed, or a quick check-in. Lean toward
|
|
798
|
+
This is one of your first heartbeats. Your user hasn't heard from you yet and may not know you're here. Find something genuinely useful to share — a follow-up from a recent conversation, something you noticed, or a quick check-in. Lean toward surfacing it via the notifications skill this time. First impressions matter.
|
|
956
799
|
</early-heartbeat>`;
|
|
957
800
|
}
|
|
958
801
|
|
|
@@ -116,6 +116,28 @@ describe("feedItemSchema — valid minimal items", () => {
|
|
|
116
116
|
expect(parsed.category).toBeUndefined();
|
|
117
117
|
expect(parsed.metadata).toBeUndefined();
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
test("noteworthy field passes through when present", () => {
|
|
121
|
+
const parsed = feedItemSchema.parse({
|
|
122
|
+
...minimalNotification(),
|
|
123
|
+
noteworthy: true,
|
|
124
|
+
});
|
|
125
|
+
expect(parsed.noteworthy).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("items without noteworthy field still parse (backward compat)", () => {
|
|
129
|
+
const parsed = feedItemSchema.parse(minimalNotification());
|
|
130
|
+
expect(parsed.noteworthy).toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("title is optional and may be omitted", () => {
|
|
134
|
+
const { title: _omitted, ...rest } = minimalNotification();
|
|
135
|
+
const parsed = feedItemSchema.parse(rest);
|
|
136
|
+
expect(parsed.title).toBeUndefined();
|
|
137
|
+
expect(parsed.summary).toBe(
|
|
138
|
+
"You mentioned wanting to review the onboarding designs.",
|
|
139
|
+
);
|
|
140
|
+
});
|
|
119
141
|
});
|
|
120
142
|
|
|
121
143
|
// ---------------------------------------------------------------------------
|
|
@@ -244,4 +266,22 @@ describe("parseFeedFile", () => {
|
|
|
244
266
|
}),
|
|
245
267
|
).toThrow();
|
|
246
268
|
});
|
|
269
|
+
|
|
270
|
+
test("accepts a file with a noteworthy item", () => {
|
|
271
|
+
const parsed = parseFeedFile({
|
|
272
|
+
version: 2,
|
|
273
|
+
items: [{ ...minimalNotification(), noteworthy: true }],
|
|
274
|
+
updatedAt: NOW_ISO,
|
|
275
|
+
});
|
|
276
|
+
expect(parsed.items[0]?.noteworthy).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("accepts a file whose items omit noteworthy (backward compat)", () => {
|
|
280
|
+
const parsed = parseFeedFile({
|
|
281
|
+
version: 2,
|
|
282
|
+
items: [minimalNotification()],
|
|
283
|
+
updatedAt: NOW_ISO,
|
|
284
|
+
});
|
|
285
|
+
expect(parsed.items[0]?.noteworthy).toBeUndefined();
|
|
286
|
+
});
|
|
247
287
|
});
|
package/src/home/feed-types.ts
CHANGED
|
@@ -89,7 +89,13 @@ export interface FeedItem {
|
|
|
89
89
|
type: FeedItemType;
|
|
90
90
|
/** Integer in [0, 100]; higher values sort earlier. */
|
|
91
91
|
priority: number;
|
|
92
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Optional short header. Omit when the source did not supply one — the
|
|
94
|
+
* notification pipeline never manufactures a title from rendered copy
|
|
95
|
+
* (LLM-echoed bodies stutter against `summary`). Clients fall back to
|
|
96
|
+
* `summary` when rendering a row.
|
|
97
|
+
*/
|
|
98
|
+
title?: string;
|
|
93
99
|
summary: string;
|
|
94
100
|
/** Event time (ISO-8601). */
|
|
95
101
|
timestamp: string;
|
|
@@ -106,6 +112,10 @@ export interface FeedItem {
|
|
|
106
112
|
detailPanel?: FeedItemDetailPanel;
|
|
107
113
|
/** Broad category for grouping and filtering feed items. */
|
|
108
114
|
category?: FeedItemCategory;
|
|
115
|
+
/** True when this item represents an assistant-initiated share or a high-importance system event. Used by clients to split inbox vs activity surfaces. */
|
|
116
|
+
noteworthy?: boolean;
|
|
117
|
+
/** True when the assistant herself emitted this item (e.g. via the `notifications send` skill). Drives clients to swap the row's leading icon for the persona avatar; system-generated items keep the category icon. */
|
|
118
|
+
fromAssistant?: boolean;
|
|
109
119
|
/** Arbitrary structured data the detail panel or other consumers can use. */
|
|
110
120
|
metadata?: Record<string, unknown>;
|
|
111
121
|
/** Internal: ISO-8601 writer-record time, used for ordering + TTL. */
|
|
@@ -198,7 +208,7 @@ export const feedItemSchema = z.object({
|
|
|
198
208
|
id: z.string(),
|
|
199
209
|
type: feedItemTypeSchema,
|
|
200
210
|
priority: z.number().int().min(0).max(100),
|
|
201
|
-
title: z.string(),
|
|
211
|
+
title: z.string().optional(),
|
|
202
212
|
summary: z.string(),
|
|
203
213
|
timestamp: z.string(),
|
|
204
214
|
status: feedItemStatusSchema.default("new"),
|
|
@@ -208,6 +218,8 @@ export const feedItemSchema = z.object({
|
|
|
208
218
|
conversationId: z.string().optional(),
|
|
209
219
|
detailPanel: feedItemDetailPanelSchema.optional(),
|
|
210
220
|
category: feedItemCategorySchema.optional(),
|
|
221
|
+
noteworthy: z.boolean().optional(),
|
|
222
|
+
fromAssistant: z.boolean().optional(),
|
|
211
223
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
212
224
|
createdAt: z.string(),
|
|
213
225
|
});
|