diff --git a/apps/web/src/domains/chat/api/event-parser.test.ts b/apps/web/src/domains/chat/api/event-parser.test.ts index 8b84874159a..0286ab1a69a 100644 --- a/apps/web/src/domains/chat/api/event-parser.test.ts +++ b/apps/web/src/domains/chat/api/event-parser.test.ts @@ -715,6 +715,149 @@ describe("parseAssistantEvent", () => { }); }); + // --------------------------------------------------------------------- + // compaction_circuit_open / compaction_circuit_closed (schema-validated) + // --------------------------------------------------------------------- + + test("parses compaction_circuit_open with all required fields", () => { + const event = parseAssistantEvent({ + type: "compaction_circuit_open", + conversationId: "conv-1", + reason: "3_consecutive_failures", + openUntil: 1_700_000_000_000, + }); + expect(event).toEqual({ + type: "compaction_circuit_open", + conversationId: "conv-1", + reason: "3_consecutive_failures", + openUntil: 1_700_000_000_000, + }); + }); + + test("returns unknown compaction_circuit_open when reason is not the recognized literal", () => { + const data = { + type: "compaction_circuit_open", + conversationId: "conv-1", + reason: "unexplained", + openUntil: 1_700_000_000_000, + }; + expect(parseAssistantEvent(data)).toEqual({ + type: "unknown", + rawType: "compaction_circuit_open", + data, + conversationId: "conv-1", + }); + }); + + test("returns unknown compaction_circuit_open when openUntil is missing", () => { + const data = { + type: "compaction_circuit_open", + conversationId: "conv-1", + reason: "3_consecutive_failures", + }; + expect(parseAssistantEvent(data)).toEqual({ + type: "unknown", + rawType: "compaction_circuit_open", + data, + conversationId: "conv-1", + }); + }); + + test("parses compaction_circuit_closed with required fields", () => { + const event = parseAssistantEvent({ + type: "compaction_circuit_closed", + conversationId: "conv-1", + }); + expect(event).toEqual({ + type: "compaction_circuit_closed", + conversationId: "conv-1", + }); + }); + + test("returns unknown compaction_circuit_closed when conversationId is missing", () => { + const data = { type: "compaction_circuit_closed" }; + expect(parseAssistantEvent(data)).toEqual({ + type: "unknown", + rawType: "compaction_circuit_closed", + data, + }); + }); + + // --------------------------------------------------------------------- + // home_feed_updated (schema-validated) + // --------------------------------------------------------------------- + + test("parses home_feed_updated with all required fields", () => { + const event = parseAssistantEvent({ + type: "home_feed_updated", + updatedAt: "2026-05-29T15:00:00.000Z", + newItemCount: 3, + }); + expect(event).toEqual({ + type: "home_feed_updated", + updatedAt: "2026-05-29T15:00:00.000Z", + newItemCount: 3, + }); + }); + + test("returns unknown home_feed_updated when updatedAt is missing", () => { + const data = { type: "home_feed_updated", newItemCount: 3 }; + expect(parseAssistantEvent(data)).toEqual({ + type: "unknown", + rawType: "home_feed_updated", + data, + }); + }); + + test("returns unknown home_feed_updated when newItemCount has wrong type", () => { + const data = { + type: "home_feed_updated", + updatedAt: "2026-05-29T15:00:00.000Z", + newItemCount: "many", + }; + expect(parseAssistantEvent(data)).toEqual({ + type: "unknown", + rawType: "home_feed_updated", + data, + }); + }); + + // --------------------------------------------------------------------- + // interaction_resolved (schema-validated; legacy tests above still cover + // happy path + invalid state + missing requestId) + // --------------------------------------------------------------------- + + test("returns unknown interaction_resolved when conversationId is missing", () => { + const data = { + type: "interaction_resolved", + requestId: "req-1", + state: "approved", + kind: "confirmation", + }; + expect(parseAssistantEvent(data)).toEqual({ + type: "unknown", + rawType: "interaction_resolved", + data, + }); + }); + + test("returns unknown interaction_resolved when an unknown field is present", () => { + const data = { + type: "interaction_resolved", + requestId: "req-1", + conversationId: "conv-1", + state: "approved", + kind: "confirmation", + legacyField: "x", + }; + expect(parseAssistantEvent(data)).toEqual({ + type: "unknown", + rawType: "interaction_resolved", + data, + conversationId: "conv-1", + }); + }); + test("parses error with code and message", () => { const event = parseAssistantEvent({ type: "error", diff --git a/apps/web/src/domains/chat/api/event-parser.ts b/apps/web/src/domains/chat/api/event-parser.ts index f2805c92290..ea3b749a7c7 100644 --- a/apps/web/src/domains/chat/api/event-parser.ts +++ b/apps/web/src/domains/chat/api/event-parser.ts @@ -20,7 +20,6 @@ import type { AssistantEvent, ConversationListInvalidatedReason, DirectoryScopeOption, - InteractionKind, QuestionEntry, QuestionOption, ScopeOption, @@ -650,22 +649,6 @@ function parseLegacyEvent(data: Record): AssistantEvent { : undefined, }; - case "compaction_circuit_open": - return { - type: "compaction_circuit_open", - conversationId: - typeof data.conversationId === "string" ? data.conversationId : "", - reason: typeof data.reason === "string" ? data.reason : "", - openUntil: typeof data.openUntil === "number" ? data.openUntil : 0, - }; - - case "compaction_circuit_closed": - return { - type: "compaction_circuit_closed", - conversationId: - typeof data.conversationId === "string" ? data.conversationId : "", - }; - case "disk_pressure_status_changed": return { type: "disk_pressure_status_changed", @@ -680,14 +663,6 @@ function parseLegacyEvent(data: Record): AssistantEvent { : undefined, }; - case "home_feed_updated": - return { - type: "home_feed_updated", - updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "", - newItemCount: - typeof data.newItemCount === "number" ? data.newItemCount : 0, - }; - case "subagent_spawned": { const subagentId = typeof data.subagentId === "string" ? data.subagentId : ""; @@ -776,37 +751,6 @@ function parseLegacyEvent(data: Record): AssistantEvent { }; } - case "interaction_resolved": { - const requestId = - typeof data.requestId === "string" ? data.requestId : ""; - const stateRaw = typeof data.state === "string" ? data.state : ""; - const validStates = new Set([ - "approved", - "rejected", - "answered", - "cancelled", - "superseded", - ]); - if (!requestId || !validStates.has(stateRaw)) { - return unknownEvent(rawType, data); - } - const conversationId = - typeof data.conversationId === "string" ? data.conversationId : ""; - const kind = typeof data.kind === "string" ? data.kind : ""; - return { - type: "interaction_resolved", - requestId, - conversationId, - state: stateRaw as - | "approved" - | "rejected" - | "answered" - | "cancelled" - | "superseded", - kind: kind as InteractionKind, - }; - } - case "document_editor_update": { const surfaceId = typeof data.surfaceId === "string" ? data.surfaceId : ""; diff --git a/apps/web/src/domains/chat/api/event-types.ts b/apps/web/src/domains/chat/api/event-types.ts index 0bcc3050c05..0b5555810c2 100644 --- a/apps/web/src/domains/chat/api/event-types.ts +++ b/apps/web/src/domains/chat/api/event-types.ts @@ -379,13 +379,6 @@ export interface NavigateSettingsEvent { conversationId?: string; } -export interface HomeFeedUpdatedEvent { - type: "home_feed_updated"; - updatedAt: string; - newItemCount: number; - conversationId?: string; -} - // --------------------------------------------------------------------------- // Subagent event types // --------------------------------------------------------------------------- @@ -536,18 +529,6 @@ export interface ConversationErrorEvent { errorCategory?: string; } -export interface CompactionCircuitOpenEvent { - type: "compaction_circuit_open"; - conversationId: string; - reason: string; - openUntil: number; -} - -export interface CompactionCircuitClosedEvent { - type: "compaction_circuit_closed"; - conversationId: string; -} - export interface DiskPressureStatusChangedEvent { type: "disk_pressure_status_changed"; status: DiskPressureStatus | null; @@ -558,17 +539,6 @@ export interface AssistantSyncChangedEvent extends SyncChangedEvent { conversationId?: string; } -/** - * Lifecycle outcome reported alongside `interaction_resolved`. Mirrors the - * daemon-side `InteractionResolutionState` union. - */ -export type InteractionResolutionState = - | "approved" - | "rejected" - | "answered" - | "cancelled" - | "superseded"; - /** * Mirrors the daemon's `PendingInteraction["kind"]` union * (`assistant/src/runtime/pending-interactions.ts`). Split into user-facing @@ -611,21 +581,6 @@ export const USER_FACING_INTERACTION_KINDS: ReadonlySet = "acp_confirmation", ]); -/** - * Emitted when a daemon-side pending interaction (confirmation, secret, - * question, host-proxy request) transitions to a resolved state. Drives - * push-based attention reconciliation in the sidebar. - */ -export interface InteractionResolvedEvent { - type: "interaction_resolved"; - requestId: string; - /** Conversation id the resolved interaction was registered against. */ - conversationId: string; - state: InteractionResolutionState; - /** Kind of the resolved interaction (e.g. `"confirmation"`, `"secret"`). */ - kind: InteractionKind; -} - /** * Every event the chat SSE stream might emit. Schema-validated events * are covered by `APIAssistantEvent` (the inferred union from @@ -656,15 +611,11 @@ export type AssistantEvent = | IdentityChangedEvent | AvatarUpdatedEvent | ConversationErrorEvent - | CompactionCircuitOpenEvent - | CompactionCircuitClosedEvent | DiskPressureStatusChangedEvent | AssistantSyncChangedEvent - | HomeFeedUpdatedEvent | SubagentSpawnedEvent | SubagentStatusChangedEvent | SubagentEventWrapperEvent | DocumentEditorUpdateEvent | TurnProfileAutoRoutedEvent - | InteractionResolvedEvent | UnknownEvent; diff --git a/apps/web/src/domains/chat/utils/stream-handlers/home-handlers.ts b/apps/web/src/domains/chat/utils/stream-handlers/home-handlers.ts index 28d4b23fcb9..9fecf1d0de5 100644 --- a/apps/web/src/domains/chat/utils/stream-handlers/home-handlers.ts +++ b/apps/web/src/domains/chat/utils/stream-handlers/home-handlers.ts @@ -1,6 +1,8 @@ import type { QueryClient } from "@tanstack/react-query"; -import type { RelationshipStateUpdatedEvent } from "@vellumai/assistant-api"; -import type { HomeFeedUpdatedEvent } from "@/domains/chat/api/event-types"; +import type { + HomeFeedUpdatedEvent, + RelationshipStateUpdatedEvent, +} from "@vellumai/assistant-api"; import { HOME_FEED_QUERY_KEY_PREFIX } from "@/lib/sync/query-tags"; export function handleHomeFeedUpdated( diff --git a/apps/web/src/domains/chat/utils/stream-handlers/metadata-handlers.test.ts b/apps/web/src/domains/chat/utils/stream-handlers/metadata-handlers.test.ts index 744d92d497a..d11bf9b8fa1 100644 --- a/apps/web/src/domains/chat/utils/stream-handlers/metadata-handlers.test.ts +++ b/apps/web/src/domains/chat/utils/stream-handlers/metadata-handlers.test.ts @@ -45,10 +45,7 @@ describe("handleUsageUpdate", () => { it("sets fillRatio to null when maxTokens is missing", () => { const ctx = makeCtx(); - handleUsageUpdate( - { type: "usage_update", contextWindowTokens: 5000 }, - ctx, - ); + handleUsageUpdate({ type: "usage_update", contextWindowTokens: 5000 }, ctx); expect( ctx.contextWindowUsageByConversationRef.current.get("conv-1"), ).toEqual({ @@ -69,8 +66,7 @@ describe("handleUsageUpdate", () => { ctx, ); expect( - ctx.contextWindowUsageByConversationRef.current.get("conv-1") - ?.fillRatio, + ctx.contextWindowUsageByConversationRef.current.get("conv-1")?.fillRatio, ).toBe(1); }); }); @@ -80,9 +76,7 @@ describe("handleConversationTitleUpdated", () => { const ctx = makeCtx(); ctx.queryClient.setQueryData( conversationsQueryKey(ctx.assistantIdRef.current), - [ - { conversationId: "conv-1", title: "Old Title" } as Conversation, - ], + [{ conversationId: "conv-1", title: "Old Title" } as Conversation], ); handleConversationTitleUpdated( @@ -97,9 +91,7 @@ describe("handleConversationTitleUpdated", () => { const cached = ctx.queryClient.getQueryData( conversationsQueryKey(ctx.assistantIdRef.current), ); - const conv = cached?.find( - (c) => c.conversationId === "conv-1", - ); + const conv = cached?.find((c) => c.conversationId === "conv-1"); expect(conv?.title).toBe("New Title"); }); }); @@ -111,7 +103,7 @@ describe("handleCompactionCircuitOpen", () => { { type: "compaction_circuit_open", conversationId: "conv-1", - reason: "test", + reason: "3_consecutive_failures", openUntil: Date.now() + 60000, }, ctx, diff --git a/apps/web/src/domains/chat/utils/stream-handlers/metadata-handlers.ts b/apps/web/src/domains/chat/utils/stream-handlers/metadata-handlers.ts index e4367629e1f..001ee628cf1 100644 --- a/apps/web/src/domains/chat/utils/stream-handlers/metadata-handlers.ts +++ b/apps/web/src/domains/chat/utils/stream-handlers/metadata-handlers.ts @@ -7,7 +7,19 @@ import { } from "@/runtime/notifications"; import { patchConversation } from "@/domains/conversations/conversation-queries"; import type { StreamHandlerContext } from "@/domains/chat/utils/stream-handlers/types"; -import type { AvatarUpdatedEvent, CompactionCircuitClosedEvent, CompactionCircuitOpenEvent, ConversationTitleUpdatedEvent, DiskPressureStatusChangedEvent, IdentityChangedEvent, NotificationIntentEvent, TurnProfileAutoRoutedEvent, UsageUpdateEvent } from "@/domains/chat/api/event-types"; +import type { + CompactionCircuitClosedEvent, + CompactionCircuitOpenEvent, +} from "@vellumai/assistant-api"; +import type { + AvatarUpdatedEvent, + ConversationTitleUpdatedEvent, + DiskPressureStatusChangedEvent, + IdentityChangedEvent, + NotificationIntentEvent, + TurnProfileAutoRoutedEvent, + UsageUpdateEvent, +} from "@/domains/chat/api/event-types"; import { useConversationStore } from "@/stores/conversation-store"; export function handleUsageUpdate( @@ -19,15 +31,11 @@ export function handleUsageUpdate( if (typeof tokens !== "number" || !Number.isFinite(tokens)) return; const resolvedMax = - typeof maxTokens === "number" && - Number.isFinite(maxTokens) && - maxTokens > 0 + typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0 ? maxTokens : null; const fillRatio = - resolvedMax != null - ? Math.min(1, Math.max(0, tokens / resolvedMax)) - : null; + resolvedMax != null ? Math.min(1, Math.max(0, tokens / resolvedMax)) : null; const usage: ContextWindowUsage = { tokens, maxTokens: resolvedMax, @@ -74,28 +82,19 @@ export function handleNotificationIntent( if (event.targetGuardianPrincipalId) { if (ackAssistantId && event.deliveryId) { - void sendNotificationIntentAck( - ackAssistantId, - event.deliveryId, - true, - ); + void sendNotificationIntentAck(ackAssistantId, event.deliveryId, true); } return; } - const metadataConversationId = extractConversationId( - event.deepLinkMetadata, - ); + const metadataConversationId = extractConversationId(event.deepLinkMetadata); if ( metadataConversationId && - metadataConversationId === useConversationStore.getState().activeConversationId + metadataConversationId === + useConversationStore.getState().activeConversationId ) { if (ackAssistantId && event.deliveryId) { - void sendNotificationIntentAck( - ackAssistantId, - event.deliveryId, - true, - ); + void sendNotificationIntentAck(ackAssistantId, event.deliveryId, true); } return; } diff --git a/assistant/src/api/events/compaction-circuit-closed.ts b/assistant/src/api/events/compaction-circuit-closed.ts new file mode 100644 index 00000000000..c830de28f57 --- /dev/null +++ b/assistant/src/api/events/compaction-circuit-closed.ts @@ -0,0 +1,28 @@ +/** + * `compaction_circuit_closed` SSE event. + * + * Emitted when the per-conversation auto-compaction circuit breaker + * transitions from open → closed because a successful compaction reset + * `ctx.compactionCircuitOpenUntil`. Clients clear the "auto-compaction + * paused" banner so it dismisses immediately instead of lingering until + * the original `openUntil` deadline. + * + * Only fires on the open → closed transition — successful compactions + * while the breaker was already closed would be noise. + * + * Canonical wire-contract source. Daemon code imports the type directly + * from this file; external consumers import via `@vellumai/assistant-api`. + */ + +import { z } from "zod"; + +export const CompactionCircuitClosedEventSchema = z + .object({ + type: z.literal("compaction_circuit_closed"), + conversationId: z.string(), + }) + .strict(); + +export type CompactionCircuitClosedEvent = z.infer< + typeof CompactionCircuitClosedEventSchema +>; diff --git a/assistant/src/api/events/compaction-circuit-open.ts b/assistant/src/api/events/compaction-circuit-open.ts new file mode 100644 index 00000000000..6516b4f1cd0 --- /dev/null +++ b/assistant/src/api/events/compaction-circuit-open.ts @@ -0,0 +1,30 @@ +/** + * `compaction_circuit_open` SSE event. + * + * Emitted when the per-conversation auto-compaction circuit breaker trips + * (3 consecutive failures). The Swift / web client surfaces a banner + * indicating auto-compaction is paused until `openUntil` (ms epoch). + * + * `reason` is narrowed to the only string the daemon emits today + * (`"3_consecutive_failures"`). Strict by design — any future trip + * reason must be added here and on the daemon side together. + * + * Canonical wire-contract source. Daemon code imports the type directly + * from this file; external consumers import via `@vellumai/assistant-api`. + */ + +import { z } from "zod"; + +export const CompactionCircuitOpenEventSchema = z + .object({ + type: z.literal("compaction_circuit_open"), + conversationId: z.string(), + reason: z.literal("3_consecutive_failures"), + /** Timestamp (ms since epoch) when the breaker will allow auto-compaction again. */ + openUntil: z.number(), + }) + .strict(); + +export type CompactionCircuitOpenEvent = z.infer< + typeof CompactionCircuitOpenEventSchema +>; diff --git a/assistant/src/api/events/home-feed-updated.ts b/assistant/src/api/events/home-feed-updated.ts new file mode 100644 index 00000000000..bc08f44b5ce --- /dev/null +++ b/assistant/src/api/events/home-feed-updated.ts @@ -0,0 +1,28 @@ +/** + * `home_feed_updated` SSE event. + * + * Emitted after a successful write to the home feed journal — clients + * invalidate their cached feed view and refetch. Skipped when the + * underlying write fails. + * + * Global event (no `conversationId`): the home feed is per-user, not + * per-conversation, and the daemon fans this out across every active + * client of the user. + * + * Canonical wire-contract source. Daemon code imports the type directly + * from this file; external consumers import via `@vellumai/assistant-api`. + */ + +import { z } from "zod"; + +export const HomeFeedUpdatedEventSchema = z + .object({ + type: z.literal("home_feed_updated"), + /** ISO-8601 timestamp of when the feed was written. */ + updatedAt: z.string(), + /** Count of items with `status === "new"` after this write. */ + newItemCount: z.number(), + }) + .strict(); + +export type HomeFeedUpdatedEvent = z.infer; diff --git a/assistant/src/api/events/interaction-resolved.ts b/assistant/src/api/events/interaction-resolved.ts new file mode 100644 index 00000000000..c79c2f70f26 --- /dev/null +++ b/assistant/src/api/events/interaction-resolved.ts @@ -0,0 +1,52 @@ +/** + * `interaction_resolved` SSE event. + * + * Broadcast when a pending interaction (confirmation, secret, question, + * host-proxy request) transitions to a resolved state. Clients use this + * to drop attention / processing indicators without polling. + * + * `state` is the lifecycle outcome: + * - `approved` / `rejected` — user-facing confirmation outcome + * - `answered` — question / secret responded to + * - `cancelled` — runtime-side termination (timeout, abort, dispose, + * prompter shutdown) + * - `superseded` — invalidated by a newer event (auto-deny on enqueue, + * fresh user message arriving while a confirmation was outstanding). + * + * `kind` is the interaction category (`"confirmation"`, `"secret"`, + * `"question"`, `"host_bash"`, …) — kept loose (`string`) on the wire + * because the daemon emits it directly from the pending-interaction + * map without enum narrowing. Web-side narrowing happens at the + * attention-tracking allowlist boundary. + * + * Canonical wire-contract source. Daemon code imports the type directly + * from this file; external consumers import via `@vellumai/assistant-api`. + */ + +import { z } from "zod"; + +export const InteractionResolutionStateSchema = z.enum([ + "approved", + "rejected", + "answered", + "cancelled", + "superseded", +]); + +export type InteractionResolutionState = z.infer< + typeof InteractionResolutionStateSchema +>; + +export const InteractionResolvedEventSchema = z + .object({ + type: z.literal("interaction_resolved"), + requestId: z.string(), + conversationId: z.string(), + state: InteractionResolutionStateSchema, + kind: z.string(), + }) + .strict(); + +export type InteractionResolvedEvent = z.infer< + typeof InteractionResolvedEventSchema +>; diff --git a/assistant/src/api/index.ts b/assistant/src/api/index.ts index b02eb82f428..5a927e97811 100644 --- a/assistant/src/api/index.ts +++ b/assistant/src/api/index.ts @@ -2,12 +2,16 @@ import { z } from "zod"; import { AssistantTextDeltaEventSchema } from "./events/assistant-text-delta.js"; import { AssistantTurnStartEventSchema } from "./events/assistant-turn-start.js"; +import { CompactionCircuitClosedEventSchema } from "./events/compaction-circuit-closed.js"; +import { CompactionCircuitOpenEventSchema } from "./events/compaction-circuit-open.js"; import { DocumentCommentCreatedEventSchema } from "./events/document-comment-created.js"; import { DocumentCommentDeletedEventSchema } from "./events/document-comment-deleted.js"; import { DocumentCommentReopenedEventSchema } from "./events/document-comment-reopened.js"; import { DocumentCommentResolvedEventSchema } from "./events/document-comment-resolved.js"; import { GenerationCancelledEventSchema } from "./events/generation-cancelled.js"; import { GenerationHandoffEventSchema } from "./events/generation-handoff.js"; +import { HomeFeedUpdatedEventSchema } from "./events/home-feed-updated.js"; +import { InteractionResolvedEventSchema } from "./events/interaction-resolved.js"; import { MessageCompleteEventSchema } from "./events/message-complete.js"; import { MessageDequeuedEventSchema } from "./events/message-dequeued.js"; import { MessageQueuedEventSchema } from "./events/message-queued.js"; @@ -30,6 +34,14 @@ export { type AssistantTurnStartEvent, AssistantTurnStartEventSchema, } from "./events/assistant-turn-start.js"; +export { + type CompactionCircuitClosedEvent, + CompactionCircuitClosedEventSchema, +} from "./events/compaction-circuit-closed.js"; +export { + type CompactionCircuitOpenEvent, + CompactionCircuitOpenEventSchema, +} from "./events/compaction-circuit-open.js"; export { type DocumentCommentCreatedEvent, DocumentCommentCreatedEventSchema, @@ -54,6 +66,16 @@ export { type GenerationHandoffEvent, GenerationHandoffEventSchema, } from "./events/generation-handoff.js"; +export { + type HomeFeedUpdatedEvent, + HomeFeedUpdatedEventSchema, +} from "./events/home-feed-updated.js"; +export { + type InteractionResolutionState, + InteractionResolutionStateSchema, + type InteractionResolvedEvent, + InteractionResolvedEventSchema, +} from "./events/interaction-resolved.js"; export { type MessageCompleteEvent, MessageCompleteEventSchema, @@ -128,12 +150,16 @@ export { export const AssistantEventSchema = z.discriminatedUnion("type", [ AssistantTextDeltaEventSchema, AssistantTurnStartEventSchema, + CompactionCircuitClosedEventSchema, + CompactionCircuitOpenEventSchema, DocumentCommentCreatedEventSchema, DocumentCommentDeletedEventSchema, DocumentCommentReopenedEventSchema, DocumentCommentResolvedEventSchema, GenerationCancelledEventSchema, GenerationHandoffEventSchema, + HomeFeedUpdatedEventSchema, + InteractionResolvedEventSchema, MessageCompleteEventSchema, MessageDequeuedEventSchema, MessageQueuedEventSchema, diff --git a/assistant/src/daemon/message-types/conversations.ts b/assistant/src/daemon/message-types/conversations.ts index 6e3b93b3b11..14bdbe537eb 100644 --- a/assistant/src/daemon/message-types/conversations.ts +++ b/assistant/src/daemon/message-types/conversations.ts @@ -1,5 +1,7 @@ // Conversation lifecycle, auth, model config, and history types. +import type { CompactionCircuitClosedEvent } from "../../api/events/compaction-circuit-closed.js"; +import type { CompactionCircuitOpenEvent } from "../../api/events/compaction-circuit-open.js"; import type { GenerationCancelledEvent } from "../../api/events/generation-cancelled.js"; import type { GenerationHandoffEvent } from "../../api/events/generation-handoff.js"; import type { @@ -509,30 +511,9 @@ export interface ContextCompacted { * conversation would set the "auto-compaction paused" banner on every open * `ChatViewModel`. */ -export interface CompactionCircuitOpen { - type: "compaction_circuit_open"; - conversationId: string; - reason: "3_consecutive_failures"; - /** Timestamp (ms since epoch) when the breaker will allow auto-compaction again. */ - openUntil: number; -} +export type CompactionCircuitOpen = CompactionCircuitOpenEvent; -/** - * Emitted when the compaction circuit breaker transitions from open → closed - * because a successful compaction reset - * `ctx.compactionCircuitOpenUntil`. The Swift client clears its banner state - * on receipt so the "auto-compaction paused" indicator dismisses immediately - * instead of lingering until the original `openUntil` deadline (up to 1h). - * - * Only fires on the open→closed transition — successful compactions while - * the breaker was already closed would be noise. - * - * Scoped per-conversation — see `CompactionCircuitOpen` doc for why. - */ -export interface CompactionCircuitClosed { - type: "compaction_circuit_closed"; - conversationId: string; -} +export type CompactionCircuitClosed = CompactionCircuitClosedEvent; export type ConversationErrorCode = | "PROVIDER_NETWORK" diff --git a/assistant/src/daemon/message-types/home.ts b/assistant/src/daemon/message-types/home.ts index 56d525d4fdf..ca03a0d8955 100644 --- a/assistant/src/daemon/message-types/home.ts +++ b/assistant/src/daemon/message-types/home.ts @@ -7,22 +7,11 @@ * just enough metadata to invalidate a cache and trigger a refetch. */ +import type { HomeFeedUpdatedEvent } from "../../api/events/home-feed-updated.js"; import type { RelationshipStateUpdatedEvent } from "../../api/events/relationship-state-updated.js"; -/** - * Broadcast after the daemon successfully writes a fresh home activity - * feed snapshot. Subscribers (e.g. `HomeFeedStore` on the client) should - * refetch the authoritative feed from its HTTP route. - * - * Only emitted on the success branch of the feed writer — if the - * underlying write fails, this event is NOT published. - */ -export interface HomeFeedUpdated { - type: "home_feed_updated"; - /** ISO-8601 timestamp of when the feed was written. */ - updatedAt: string; - /** Count of items with `status === "new"` after this write. */ - newItemCount: number; -} +export type HomeFeedUpdated = HomeFeedUpdatedEvent; -export type _HomeServerMessages = RelationshipStateUpdatedEvent | HomeFeedUpdated; +export type _HomeServerMessages = + | RelationshipStateUpdatedEvent + | HomeFeedUpdated; diff --git a/assistant/src/daemon/message-types/messages.ts b/assistant/src/daemon/message-types/messages.ts index 985dd414223..e6ac0e8fed0 100644 --- a/assistant/src/daemon/message-types/messages.ts +++ b/assistant/src/daemon/message-types/messages.ts @@ -2,6 +2,10 @@ import type { AssistantTextDeltaEvent } from "../../api/events/assistant-text-delta.js"; import type { AssistantTurnStartEvent } from "../../api/events/assistant-turn-start.js"; +import type { + InteractionResolutionState as CanonicalInteractionResolutionState, + InteractionResolvedEvent, +} from "../../api/events/interaction-resolved.js"; import type { MessageCompleteEvent } from "../../api/events/message-complete.js"; import type { MessageDequeuedEvent } from "../../api/events/message-dequeued.js"; import type { MessageQueuedEvent } from "../../api/events/message-queued.js"; @@ -371,27 +375,9 @@ export interface ConfirmationStateChanged { * - `"superseded"` — invalidated by a newer event (auto-deny on enqueue, a * fresh user message arriving while a confirmation was outstanding). */ -export type InteractionResolutionState = - | "approved" - | "rejected" - | "answered" - | "cancelled" - | "superseded"; +export type InteractionResolutionState = CanonicalInteractionResolutionState; -/** - * Broadcast when a pending interaction (confirmation, secret, question, - * host-proxy request) transitions to a resolved state. Clients use this to - * drop attention/processing indicators without polling. - */ -export interface InteractionResolved { - type: "interaction_resolved"; - requestId: string; - /** Conversation id the interaction belongs to. */ - conversationId: string; - state: InteractionResolutionState; - /** Kind of the resolved interaction (e.g. "confirmation", "secret", "host_bash"). */ - kind: string; -} +export type InteractionResolved = InteractionResolvedEvent; /** * Server-side assistant activity lifecycle for thinking indicator placement.