@k2b/cloud 0.37.0 → 0.39.0

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.
Files changed (149) hide show
  1. package/package.json +8 -3
  2. package/scripts/build-canvas-workers.ts +109 -29
  3. package/scripts/build.ts +3 -5
  4. package/src/_internal/app-path.ts +25 -0
  5. package/src/_internal/app-presentation.ts +4 -0
  6. package/src/_internal/canvas-worker.ts +5 -3
  7. package/src/_internal/define-app.ts +28 -2
  8. package/src/_internal/help-catalog.ts +123 -4
  9. package/src/_internal/help.ts +27 -6
  10. package/src/_internal/panel-cookie.ts +39 -0
  11. package/src/_internal/registry-validation.ts +21 -26
  12. package/src/_internal/runtime-context.ts +1 -0
  13. package/src/access/PermissionEditor.browser-harness.tsx +57 -38
  14. package/src/access/PermissionEditor.tsx +30 -5
  15. package/src/access/ResourceApiKeys.tsx +5 -1
  16. package/src/account/Avatar.tsx +20 -2
  17. package/src/account/ui.ts +1 -1
  18. package/src/ai/approval-routes.ts +5 -2
  19. package/src/ai/approvals.ts +139 -28
  20. package/src/ai/browser.ts +11 -0
  21. package/src/ai/chart-block.ts +44 -13
  22. package/src/ai/chat/action-sentences.ts +1 -44
  23. package/src/ai/chat/chart-block.tsx +2 -11
  24. package/src/ai/chat/messages.ts +10 -0
  25. package/src/ai/chat/turn-view.tsx +35 -3
  26. package/src/ai/chat/view-block.tsx +7 -2
  27. package/src/ai/chat/web-tools.tsx +23 -6
  28. package/src/ai/chat-task-contracts.ts +28 -3
  29. package/src/ai/chat-task-reasons-migrate.ts +68 -0
  30. package/src/ai/chat-task-reasons.ts +92 -0
  31. package/src/ai/chat-tasks.ts +460 -66
  32. package/src/ai/default-tool-contracts.ts +30 -13
  33. package/src/ai/fetch-file-tool.ts +37 -3
  34. package/src/ai/index.ts +12 -0
  35. package/src/ai/migrate.ts +46 -3
  36. package/src/ai/readable-html.ts +1057 -0
  37. package/src/ai/request-context.ts +7 -1
  38. package/src/ai/routes.ts +27 -1
  39. package/src/ai/runtime.ts +12 -27
  40. package/src/ai/store.ts +5 -3
  41. package/src/ai/timeline.ts +9 -1
  42. package/src/ai/tool-wording.ts +45 -0
  43. package/src/ai/turn-abort.ts +33 -0
  44. package/src/ai/types.ts +12 -1
  45. package/src/api/admin-core-settings.ts +190 -139
  46. package/src/api/admin-outgoing-mail.ts +4 -2
  47. package/src/api/app-approval.ts +9 -2
  48. package/src/api/approvals.ts +1 -1
  49. package/src/api/help.ts +23 -4
  50. package/src/api/index.ts +2 -1
  51. package/src/api/mcp.ts +8 -2
  52. package/src/browser/CloudResourceSearch.tsx +10 -6
  53. package/src/browser/FileChooser.tsx +11 -3
  54. package/src/browser/app-approval.ts +7 -2
  55. package/src/browser/choose-files.browser-harness.tsx +4 -1
  56. package/src/browser/composer-catalog.ts +103 -0
  57. package/src/browser/composer-insert-messages.ts +31 -0
  58. package/src/browser/composer-insert.ts +150 -0
  59. package/src/browser/file-chooser-messages.ts +4 -0
  60. package/src/browser/file-providers.ts +14 -1
  61. package/src/browser/panel-messages.ts +24 -0
  62. package/src/browser/panel-slot.ts +354 -0
  63. package/src/browser/panel.ts +28 -0
  64. package/src/browser/provider-folder.tsx +58 -5
  65. package/src/browser/resource-picker.tsx +17 -2
  66. package/src/cli/admin/workflows.ts +1 -0
  67. package/src/config/env.ts +14 -0
  68. package/src/contracts/app-approval.ts +49 -9
  69. package/src/contracts/app.ts +14 -0
  70. package/src/contracts/file-provider.ts +48 -1
  71. package/src/contracts/outgoing-mail.ts +8 -1
  72. package/src/contracts/registry.ts +3 -1
  73. package/src/contracts/shared.ts +2 -0
  74. package/src/server/help.ts +136 -10
  75. package/src/server/services/access.ts +5 -1
  76. package/src/server/services/geo.ts +21 -4
  77. package/src/services/accounts/posix.ts +2 -1
  78. package/src/services/allowed-network-fetch.ts +189 -0
  79. package/src/services/app-approval.ts +158 -38
  80. package/src/services/approvals/device.ts +2 -0
  81. package/src/services/help/index.ts +46 -5
  82. package/src/services/help/store.ts +26 -5
  83. package/src/services/help/types.ts +10 -2
  84. package/src/services/index.ts +22 -3
  85. package/src/services/notifications/browser.ts +16 -5
  86. package/src/services/outgoing-mail/dispatcher.ts +7 -2
  87. package/src/services/outgoing-mail/resolve-host.ts +39 -0
  88. package/src/services/outgoing-mail/test-send.ts +18 -3
  89. package/src/services/outgoing-mail/transport.ts +24 -13
  90. package/src/services/pdf/image-protocol.ts +135 -0
  91. package/src/services/pdf/image-variants.ts +124 -0
  92. package/src/services/pdf/image-worker.ts +31 -0
  93. package/src/services/pdf/image.ts +109 -0
  94. package/src/services/pdf/index.ts +9 -0
  95. package/src/services/settings/api.ts +11 -6
  96. package/src/services/settings/app.ts +5 -4
  97. package/src/services/settings/core-settings.ts +4 -3
  98. package/src/services/settings/index.ts +67 -43
  99. package/src/services/settings/namespace.ts +2 -1
  100. package/src/services/settings/store.ts +83 -11
  101. package/src/shared/ai-platform-prompt.ts +1 -1
  102. package/src/shared/app-presentation.ts +1 -0
  103. package/src/shared/help.ts +41 -0
  104. package/src/shared/index.ts +9 -1
  105. package/src/shared/markdown/extensions/guided-help.ts +208 -64
  106. package/src/shared/markdown/formula.ts +8 -8
  107. package/src/shared/markdown/index.ts +72 -11
  108. package/src/shared/redirect.ts +12 -0
  109. package/src/ssr/CloudShell.island.tsx +86 -9
  110. package/src/ssr/Layout.tsx +62 -11
  111. package/src/ssr/LayoutHeader.tsx +15 -9
  112. package/src/ssr/LayoutHelp.browser-harness.tsx +130 -27
  113. package/src/ssr/LayoutHelp.tsx +569 -410
  114. package/src/ssr/LayoutRail.tsx +2 -2
  115. package/src/ssr/LayoutTabBar.tsx +29 -0
  116. package/src/ssr/PanelLayout.tsx +27 -0
  117. package/src/ssr/PanelRuntime.island.tsx +87 -0
  118. package/src/ssr/PanelSlot.tsx +108 -0
  119. package/src/ssr/PanelTrigger.tsx +49 -0
  120. package/src/ssr/ShellRuntime.island.tsx +72 -25
  121. package/src/ssr/app-appearance.ts +3 -2
  122. package/src/ssr/app-color.ts +100 -0
  123. package/src/ssr/app-launchpad.tsx +18 -1
  124. package/src/ssr/app-navigation.ts +15 -0
  125. package/src/ssr/compact-shell.baseline.json +5270 -0
  126. package/src/ssr/compact-shell.browser-harness.ts +431 -0
  127. package/src/ssr/help-messages.ts +60 -38
  128. package/src/ssr/help.ts +3 -0
  129. package/src/ssr/index.ts +3 -0
  130. package/src/ssr/layout-help-article.ts +41 -0
  131. package/src/ssr/layout-help-navigation.ts +0 -12
  132. package/src/ssr/layout-help-registry.tsx +4 -11
  133. package/src/ssr/layout-help-search.ts +9 -12
  134. package/src/ssr/layout-help-start.ts +68 -0
  135. package/src/ssr/panel-slot.browser-harness.tsx +110 -0
  136. package/src/ssr/platform-messages.ts +34 -0
  137. package/src/ssr/rail-navigation.ts +44 -0
  138. package/src/ssr/shell-data.tsx +11 -0
  139. package/src/ssr/shell-shortcuts.ts +19 -0
  140. package/src/styles/effects.css +689 -185
  141. package/src/styles/global.css +269 -55
  142. package/src/styles/panel.css +373 -0
  143. package/src/styles/resource-search.css +25 -0
  144. package/src/styles/tokens.css +23 -6
  145. package/src/styles/utilities-navigation.css +5 -21
  146. package/src/workflows/store/events.ts +24 -0
  147. package/src/workflows/store/index.ts +8 -0
  148. package/src/workflows/store/observability.ts +6 -1
  149. package/src/workflows/store/retention.ts +157 -0
@@ -3,7 +3,7 @@ import { userFromActor } from "../server/actor";
3
3
  import { type ApiErrorResponse, respond } from "../server/api";
4
4
  import type { AuthContext, RequestActor } from "../server/middleware/auth";
5
5
  import { err, fail } from "../server/services";
6
- import type { AiToolApprovalContext } from "./approvals";
6
+ import type { AiToolApprovalChangeContext, AiToolApprovalContext } from "./approvals";
7
7
  import { personalAiModelPolicy } from "./personal-agent";
8
8
  import type { AiModelPolicy } from "./types";
9
9
 
@@ -13,6 +13,7 @@ export type AiChatRequestContext = {
13
13
  ownerUserId: string;
14
14
  modelPolicy: AiModelPolicy;
15
15
  toolApprovalContext: AiToolApprovalContext;
16
+ toolApprovalChangeContext: AiToolApprovalChangeContext;
16
17
  /** A person in a signed-in browser session, not `cld`, an API key, or another delegated credential. */
17
18
  signedInSession: boolean;
18
19
  };
@@ -28,6 +29,11 @@ export const resolveAiChatRequestContext = async <T extends AuthContext>(
28
29
  ownerUserId: user.id,
29
30
  modelPolicy: personalAiModelPolicy,
30
31
  toolApprovalContext: { actorUserId: user.id },
32
+ toolApprovalChangeContext: {
33
+ actorUserId: user.id,
34
+ actor,
35
+ requestId: c.req.header("x-request-id") ?? null,
36
+ },
31
37
  signedInSession: actor.kind === "user" && !actor.delegation && c.get("credentialKind") === "session",
32
38
  };
33
39
  };
package/src/ai/routes.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  respond,
18
18
  v,
19
19
  } from "../server";
20
+ import { isAppSessionActor } from "../server/actor";
20
21
  import { isRequestCredentialCurrent } from "../server/middleware/auth";
21
22
  import { approvalDecisionChannel } from "../services/approvals";
22
23
  import { ImageMetadataError } from "../services/image-metadata";
@@ -28,6 +29,7 @@ import { AI_AUDIO_MAX_BYTES } from "./audio-format";
28
29
  import { buildAiCapabilityCatalog } from "./capabilities";
29
30
  import { aiChatMessages } from "./chat/messages";
30
31
  import { getAiChatQuotas } from "./chat-quotas";
32
+ import { aiChatTasks } from "./chat-tasks";
31
33
  import { AI_CLIENT_TOOL_ACTION_MAX_BYTES, aiClientToolOutputSchemas } from "./client-tool-results";
32
34
  import { createCloudAiLocalBashTool, createCloudAiLocalTools, createConfiguredDefaultCloudAiTools } from "./default-tools";
33
35
  import { enqueueAiDictation } from "./dictation-runtime";
@@ -784,6 +786,13 @@ export const aiRoutes = (() => {
784
786
  ownerUserId: ctx.ownerUserId,
785
787
  });
786
788
  if (!archived) return notFound(c);
789
+ // The chat is archived either way; admission pauses a routine this step missed before its next run.
790
+ await aiChatTasks.pauseForArchivedChat({ conversationId: conversation.id, userId: ctx.ownerUserId }).catch((error) =>
791
+ logger("ai:chat-tasks").warn("Could not pause the routines of an archived chat", {
792
+ conversationId: conversation.id,
793
+ error: error instanceof Error ? error.message : String(error),
794
+ }),
795
+ );
787
796
  return respond(c, ok({ ok: true }));
788
797
  })
789
798
  .post("/conversations/:conversationId/restore", async (c) => {
@@ -791,10 +800,27 @@ export const aiRoutes = (() => {
791
800
  if (ctx instanceof Response) return ctx;
792
801
  const conversation = await loadConversation(c, ctx, true);
793
802
  if (!conversation) return notFound(c);
803
+ await aiChatTasks.markRestoreStarted({ conversationId: conversation.id, userId: ctx.ownerUserId });
794
804
  const restored = await aiConversations.restoreConversation({
795
805
  conversationId: conversation.id,
796
806
  ownerUserId: ctx.ownerUserId,
797
807
  });
808
+ // A restored chat must not leave archive reasons on routines that stay paused.
809
+ // The phone app may restore the chat and relabel pauses, but may not resume background authority.
810
+ // If this step fails, the routine recovery relabels them after a grace period; it never resumes.
811
+ if (restored)
812
+ await aiChatTasks
813
+ .resumeForRestoredChat({
814
+ conversationId: conversation.id,
815
+ userId: ctx.ownerUserId,
816
+ resume: !isAppSessionActor(c.get("actor")),
817
+ })
818
+ .catch((error) =>
819
+ logger("ai:chat-tasks").warn("Could not resume the routines of a restored chat", {
820
+ conversationId: conversation.id,
821
+ error: error instanceof Error ? error.message : String(error),
822
+ }),
823
+ );
798
824
  return restored ? respond(c, ok(await publicConversationFor(restored, c.get("accessSubject")))) : notFound(c);
799
825
  })
800
826
  .post("/conversations/:conversationId/viewed", async (c) => {
@@ -1168,7 +1194,7 @@ export const aiRoutes = (() => {
1168
1194
  client: c.req.header(APPROVAL_CLIENT_HEADER),
1169
1195
  }),
1170
1196
  },
1171
- toolApprovalContext: ctx.toolApprovalContext,
1197
+ toolApprovalContext: ctx.toolApprovalChangeContext,
1172
1198
  signedInSession: ctx.signedInSession,
1173
1199
  });
1174
1200
  if (!result.ok) return toAiActionFailureResponse(c, result);
package/src/ai/runtime.ts CHANGED
@@ -8,7 +8,13 @@ import { type ApprovalDecisionChannel, approvals } from "../services/approvals";
8
8
  import { logger } from "../services/logging";
9
9
  import { superviseRuntimeTask } from "../services/runtime-lifecycle";
10
10
  import { coreSettings } from "../services/settings/api";
11
- import { AI_WEBSITE_APPROVAL_TOOL, type AiToolApprovalContext, aiTurnAllowsRememberedApprovals, rememberAiToolApproval } from "./approvals";
11
+ import {
12
+ AI_WEBSITE_APPROVAL_TOOL,
13
+ type AiToolApprovalChangeContext,
14
+ type AiToolApprovalContext,
15
+ aiTurnAllowsRememberedApprovals,
16
+ rememberAiToolApproval,
17
+ } from "./approvals";
12
18
  import { aiChatAccessSubject, selectAssistantAiModelId } from "./assistant-models";
13
19
  import { startAiDictationRuntime } from "./dictation-runtime";
14
20
  import { AiTurnExecutor, type ExecutorConfig } from "./executor";
@@ -20,8 +26,9 @@ import { aiQuotas } from "./quotas";
20
26
  import { parseAiResourceMarker } from "./resource-markers";
21
27
  import { isAiVisionModelConfigured } from "./settings";
22
28
  import { aiConversations } from "./store";
23
- import { publishAiTurnAbort, publishAiWireEvent } from "./stream";
29
+ import { publishAiWireEvent } from "./stream";
24
30
  import { aiToolAudit } from "./tool-audit";
31
+ import { finishAiTurnAbort } from "./turn-abort";
25
32
  import type {
26
33
  AiChatTurnRunConfig,
27
34
  AiClientToolId,
@@ -273,32 +280,10 @@ export const submitAiCompaction = async (input: SubmitAiCompactionInput): Promis
273
280
  // ---------------------------------------------------------------------------
274
281
 
275
282
  export const abortAiTurn = async (input: { conversationId: string; turnId: string }): Promise<{ ok: true }> => {
276
- // Capture the wire coordinates before finalization so we can emit turn_finished.
283
+ // Capture wire coordinates before the cancellation flag can wake its owner.
277
284
  const active = await aiConversations.getActiveTurn({ conversationId: input.conversationId }).catch(() => null);
278
285
  const request = await aiConversations.requestTurnAbort({ ...input, reason: "user" });
279
- if (!request.found) return { ok: true };
280
-
281
- // Tell any live owner to stop (its heartbeat also detects the cancel flag).
282
- await publishAiTurnAbort(input).catch(() => undefined);
283
-
284
- if (request.ownerless) {
285
- const finalized = await aiConversations.completeTurn({ ...input, status: "aborted", error: null });
286
- const config = await aiConversations.getTurnRunConfig(input);
287
- if (finalized === "completed" && !(config?.kind === "chat" && config.background)) {
288
- const attempt = active?.turn.id === input.turnId ? active.turn.attempt : 1;
289
- const seq = (active?.turn.id === input.turnId ? active.liveSeq : 0) + 1;
290
- await publishAiWireEvent({
291
- v: 1,
292
- conversationId: input.conversationId,
293
- turnId: input.turnId,
294
- attempt,
295
- seq,
296
- type: "turn_finished",
297
- status: "aborted",
298
- error: null,
299
- }).catch(() => undefined);
300
- }
301
- }
286
+ await finishAiTurnAbort(input, request, active);
302
287
  return { ok: true };
303
288
  };
304
289
 
@@ -331,7 +316,7 @@ export const submitAiTurnAction = async (input: {
331
316
  callId: string;
332
317
  action: AiTurnActionInput;
333
318
  decidedBy: { userId: string; via: ApprovalDecisionChannel; deviceId?: string };
334
- toolApprovalContext?: AiToolApprovalContext;
319
+ toolApprovalContext?: AiToolApprovalChangeContext;
335
320
  /** Whether a person decides in a signed-in browser session; only such a decision may remember a website. */
336
321
  signedInSession?: boolean;
337
322
  }): Promise<{ ok: true } | { ok: false; status: 400 | 403 | 404 | 409; message: string }> => {
package/src/ai/store.ts CHANGED
@@ -2992,12 +2992,14 @@ export const aiConversations: AiConversationService = {
2992
2992
  return Boolean(rows[0]);
2993
2993
  },
2994
2994
 
2995
- requestTurnAbort: async (input) => {
2995
+ requestTurnAbort: async (input, options) => {
2996
2996
  const reason = input.reason ?? "user";
2997
- const rows = await sql<TurnRow[]>`
2997
+ const rows = await (options?.db ?? sql)<TurnRow[]>`
2998
2998
  UPDATE ai.turns
2999
2999
  SET cancel_requested_at = COALESCE(cancel_requested_at, now()),
3000
- cancellation_reason = COALESCE(cancellation_reason, ${reason})
3000
+ cancellation_reason = CASE WHEN ${reason} = 'routine_deleted' THEN ${reason}
3001
+ WHEN ${reason} = 'routine_paused' AND cancellation_reason IS DISTINCT FROM 'routine_deleted' THEN ${reason}
3002
+ ELSE COALESCE(cancellation_reason, ${reason}) END
3001
3003
  WHERE id = ${input.turnId}
3002
3004
  AND conversation_id = ${input.conversationId}
3003
3005
  AND status IN ('queued', 'running', 'waiting_for_action')
@@ -36,6 +36,9 @@ const isAssistantPart = (entry: AiStoredMessage): boolean =>
36
36
  entry.kind === "message" && (entry.message.role === "assistant" || entry.message.role === "tool_result");
37
37
 
38
38
  const sameLoop = (entry: AiStoredMessage, loopId: string | null): boolean => (loopId ? entry.loopId === loopId : entry.loopId === null);
39
+ /** Each delivered routine run is its own answer, with its own run link and system line, also when runs follow each other. */
40
+ const sameRoutineRun = (entry: AiStoredMessage, first: AiStoredMessage): boolean =>
41
+ (entry.meta?.scheduledTask?.occurrenceId ?? null) === (first.meta?.scheduledTask?.occurrenceId ?? null);
39
42
 
40
43
  const assistantActionEntry = (entries: AiStoredMessage[]): AiStoredMessage | null => {
41
44
  const assistantEntries = entries.filter((entry) => entry.kind === "message" && entry.message.role === "assistant");
@@ -107,7 +110,12 @@ export const buildAiMessageTimeline = (messages: AiStoredMessage[]): AiMessageTi
107
110
  pendingSteers = [];
108
111
  const entries = [entry];
109
112
  index += 1;
110
- while (index < messages.length && isAssistantPart(messages[index]!) && sameLoop(messages[index]!, loopId)) {
113
+ while (
114
+ index < messages.length &&
115
+ isAssistantPart(messages[index]!) &&
116
+ sameLoop(messages[index]!, loopId) &&
117
+ (loopId !== null || sameRoutineRun(messages[index]!, entry))
118
+ ) {
111
119
  entries.push(messages[index]!);
112
120
  index += 1;
113
121
  }
@@ -0,0 +1,45 @@
1
+ import {
2
+ type CapabilityActionWording,
3
+ capabilityActionOutcome,
4
+ capabilityActionSubject,
5
+ resolveCapabilityActionSentences,
6
+ } from "../_internal/capability-sentences";
7
+ import { displayToolName, isRecord } from "./chat/message-utils";
8
+ import type { AiTurnBlock } from "./protocol";
9
+ import { CLOUD_AI_TOOL_PRESENTATION } from "./tool-sentences";
10
+
11
+ type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
12
+
13
+ /**
14
+ * How the chat words one call that acts on something: an app Action through its saved presentation, a
15
+ * Cloud tool through Cloud's own catalog of the same shape. `null` for calls that only read.
16
+ */
17
+ export const toolWording = (block: ToolBlock, locale: string): CapabilityActionWording | null => {
18
+ const presentation = block.presentation;
19
+ if (presentation?.kind === "capability") {
20
+ if (presentation.capabilityKind !== "action") return null;
21
+ return {
22
+ title: presentation.title,
23
+ ...(presentation.sentences ? { sentences: presentation.sentences } : {}),
24
+ ...(presentation.fields ? { fields: presentation.fields } : {}),
25
+ };
26
+ }
27
+ const sentences = resolveCapabilityActionSentences(block.name, CLOUD_AI_TOOL_PRESENTATION, locale);
28
+ return sentences ? { title: displayToolName(block.name, locale), sentences } : null;
29
+ };
30
+
31
+ /** What the call does or would do, as one sentence; `null` for calls that only read. */
32
+ export const toolSubject = (block: ToolBlock, locale: string, timeZone: string): string | null => {
33
+ const wording = toolWording(block, locale);
34
+ return wording ? capabilityActionSubject(wording, block.args, { locale, timeZone }) : null;
35
+ };
36
+
37
+ /** The sentence for one outcome, or `null` when the tool has none that renders for this call. */
38
+ export const toolOutcome = (block: ToolBlock, key: "done" | "rejected" | "notRun", locale: string, timeZone: string): string | null => {
39
+ const wording = toolWording(block, locale);
40
+ if (!wording) return null;
41
+ const result = isRecord(block.result) ? block.result : undefined;
42
+ // App Actions return their data inside the result envelope; Cloud tools return the data itself.
43
+ const data = block.presentation?.kind === "capability" ? result?.data : block.result;
44
+ return capabilityActionOutcome(wording, key, { input: block.args, data }, { locale, timeZone });
45
+ };
@@ -0,0 +1,33 @@
1
+ import { aiConversations } from "./store";
2
+ import { publishAiTurnAbort, publishAiWireEvent } from "./stream";
3
+ import type { AiTurnAbortRequest } from "./types";
4
+
5
+ /** Finish a committed cancellation request; shared by interactive and routine cancellation. */
6
+ export const finishAiTurnAbort = async (
7
+ input: { conversationId: string; turnId: string },
8
+ request: AiTurnAbortRequest,
9
+ active: Awaited<ReturnType<typeof aiConversations.getActiveTurn>> = null,
10
+ ): Promise<void> => {
11
+ if (!request.found) return;
12
+ // Tell any live owner to stop (its heartbeat also detects the cancel flag).
13
+ await publishAiTurnAbort(input).catch(() => undefined);
14
+
15
+ if (request.ownerless) {
16
+ const finalized = await aiConversations.completeTurn({ ...input, status: "aborted", error: null });
17
+ const config = await aiConversations.getTurnRunConfig(input);
18
+ if (finalized === "completed" && !(config?.kind === "chat" && config.background)) {
19
+ const attempt = active?.turn.id === input.turnId ? active.turn.attempt : 1;
20
+ const seq = (active?.turn.id === input.turnId ? active.liveSeq : 0) + 1;
21
+ await publishAiWireEvent({
22
+ v: 1,
23
+ conversationId: input.conversationId,
24
+ turnId: input.turnId,
25
+ attempt,
26
+ seq,
27
+ type: "turn_finished",
28
+ status: "aborted",
29
+ error: null,
30
+ }).catch(() => undefined);
31
+ }
32
+ }
33
+ };
package/src/ai/types.ts CHANGED
@@ -298,6 +298,10 @@ export type AiStoredMessage = {
298
298
  sourceHref?: string;
299
299
  };
300
300
  scheduledTask?: {
301
+ reason?: import("./chat-task-reasons").ChatTaskReason;
302
+ /** The message text is only the reason sentence; the chat words the reason itself instead. */
303
+ reasonOnly?: true;
304
+ stop?: { cause: "paused" | "deleted"; done: string[] };
301
305
  taskId: string;
302
306
  occurrenceId: string;
303
307
  scheduledFor: string;
@@ -613,6 +617,10 @@ export type AiConversationFileSnapshot = {
613
617
  };
614
618
 
615
619
  export type AiBackgroundRunContext = {
620
+ scheduledFor?: string;
621
+ trigger?: "scheduled" | "manual";
622
+ timezone?: string;
623
+ stopDeliveredAt?: string;
616
624
  taskId: string;
617
625
  occurrenceId: string;
618
626
  context: Array<{ seq: number; kind: "message" | "summary"; message: Message }>;
@@ -977,7 +985,10 @@ export type AiConversationService = {
977
985
  seq: number;
978
986
  }): Promise<boolean>;
979
987
  /** Record an abort wish. The caller finalizes ownerless turns itself. */
980
- requestTurnAbort(input: { conversationId: string; turnId: string; reason?: string }): Promise<AiTurnAbortRequest>;
988
+ requestTurnAbort(
989
+ input: { conversationId: string; turnId: string; reason?: string },
990
+ options?: { db: import("bun").SQL },
991
+ ): Promise<AiTurnAbortRequest>;
981
992
  completeTurn(input: {
982
993
  conversationId: string;
983
994
  turnId: string;