@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
@@ -0,0 +1,157 @@
1
+ import { type SQL, sql } from "bun";
2
+ import { logger } from "../../services/logging";
3
+ import { toPgUuidArray } from "../../services/postgres";
4
+ import { withTransaction } from "./transaction";
5
+
6
+ /**
7
+ * Events are cause records; runs keep their own inputs and context. After 30
8
+ * days only the slim cause row remains: Mail backfill and Grids idempotency
9
+ * still need its dedupe key. This window is fixed, not configurable.
10
+ */
11
+ export const WORKFLOW_EVENT_RETENTION_DAYS = 30;
12
+
13
+ const DEFAULT_BATCH_SIZE = 1000;
14
+ const log = logger("workflows:retention");
15
+
16
+ const retentionIndexes = [
17
+ {
18
+ name: "idx_workflows_event_retention",
19
+ table: "workflows.event",
20
+ definition:
21
+ "((COALESCE(dispatched_at, dispatch_failed_at)), id) WHERE payload_expired_at IS NULL AND (dispatched_at IS NOT NULL OR dispatch_failed_at IS NOT NULL)",
22
+ },
23
+ {
24
+ name: "idx_workflows_run_event",
25
+ table: "workflows.run",
26
+ definition: "(event_id) WHERE event_id IS NOT NULL",
27
+ },
28
+ ] as const;
29
+
30
+ const readRetentionIndex = async (db: SQL, index: (typeof retentionIndexes)[number]) => {
31
+ const name = `workflows.${index.name}`;
32
+ const [state] = await db<{ present: boolean; valid: boolean; building: boolean }[]>`
33
+ SELECT to_regclass(${name}) IS NOT NULL AS present,
34
+ EXISTS (SELECT 1 FROM pg_catalog.pg_index WHERE indexrelid = to_regclass(${name}) AND indisvalid AND indisready AND indislive) AS valid,
35
+ EXISTS (SELECT 1 FROM pg_catalog.pg_stat_progress_create_index progress
36
+ WHERE progress.datname = pg_catalog.current_database()
37
+ AND (progress.relid = to_regclass(${index.table}) OR progress.relid IS NULL)) AS building
38
+ `;
39
+ return state ?? { present: false, valid: false, building: false };
40
+ };
41
+
42
+ const dropRetentionIndex = (db: SQL, name: string) => db.unsafe(`DROP INDEX CONCURRENTLY IF EXISTS workflows.${name}`).simple();
43
+
44
+ /**
45
+ * Build after Core setup without blocking event or run writes. Each statement
46
+ * runs outside a transaction, including through transaction poolers. Interrupted
47
+ * builds are replaced; active builds are left for the next sweep. Never throws.
48
+ */
49
+ export const ensureWorkflowEventRetentionIndexes = async (db: SQL = sql): Promise<boolean> => {
50
+ try {
51
+ for (const index of retentionIndexes) {
52
+ const before = await readRetentionIndex(db, index);
53
+ if (before.valid) continue;
54
+ if (before.building) return false;
55
+ if (before.present) await dropRetentionIndex(db, index.name);
56
+ try {
57
+ await db.unsafe(`CREATE INDEX CONCURRENTLY IF NOT EXISTS ${index.name} ON ${index.table} ${index.definition}`).simple();
58
+ } catch (error) {
59
+ const after = await readRetentionIndex(db, index);
60
+ log.warn("Workflow event retention index build failed; retrying on the next sweep", {
61
+ index: index.name,
62
+ error: error instanceof Error ? error.message : String(error),
63
+ });
64
+ if (after.present && !after.valid && !after.building) await dropRetentionIndex(db, index.name);
65
+ return false;
66
+ }
67
+ if (!(await readRetentionIndex(db, index)).valid) return false;
68
+ }
69
+ return true;
70
+ } catch (error) {
71
+ log.warn("Workflow event retention indexes are unavailable; retrying on the next sweep", {
72
+ error: error instanceof Error ? error.message : String(error),
73
+ });
74
+ return false;
75
+ }
76
+ };
77
+
78
+ /** Expire one bounded batch atomically; concurrent Core replicas skip locked rows. */
79
+ export const expireWorkflowEventPayloadsBatch = async (options: { olderThan: Date; limit?: number; db?: SQL }): Promise<number> => {
80
+ const limit = options.limit ?? DEFAULT_BATCH_SIZE;
81
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new RangeError("Workflow event retention batch size must be a positive integer.");
82
+ return withTransaction(options.db, async (tx) => {
83
+ const rows = await tx<{ id: string }[]>`
84
+ SELECT e.id FROM workflows.event e
85
+ WHERE e.payload_expired_at IS NULL
86
+ AND (e.dispatched_at IS NOT NULL OR e.dispatch_failed_at IS NOT NULL)
87
+ AND COALESCE(e.dispatched_at, e.dispatch_failed_at) < ${options.olderThan}
88
+ AND NOT EXISTS (
89
+ SELECT 1 FROM workflows.run r
90
+ WHERE r.event_id = e.id AND r.state NOT IN ('succeeded', 'failed', 'canceled')
91
+ )
92
+ ORDER BY COALESCE(e.dispatched_at, e.dispatch_failed_at), e.id
93
+ LIMIT ${limit}
94
+ FOR UPDATE SKIP LOCKED
95
+ `;
96
+ if (rows.length === 0) return 0;
97
+ const ids = toPgUuidArray(rows.map((row) => row.id));
98
+ await tx`DELETE FROM workflows.event_delivery WHERE event_id = ANY(${ids}::uuid[])`;
99
+ await tx`
100
+ UPDATE workflows.event
101
+ SET data = '{}'::jsonb, context = '{}'::jsonb, authorization_snapshot = '{}'::jsonb,
102
+ payload_expired_at = now()
103
+ WHERE id = ANY(${ids}::uuid[])
104
+ `;
105
+ return rows.length;
106
+ });
107
+ };
108
+
109
+ /** Sweep for at most five minutes, stopping between batches on cancellation. */
110
+ export const expireWorkflowEventPayloads = async (
111
+ options: { now?: Date; signal?: AbortSignal; db?: SQL; batchSize?: number } = {},
112
+ ): Promise<number> => {
113
+ const olderThan = new Date((options.now ?? new Date()).getTime() - WORKFLOW_EVENT_RETENTION_DAYS * 86_400_000);
114
+ const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
115
+ const deadline = Date.now() + 5 * 60_000;
116
+ let expired = 0;
117
+ while (!options.signal?.aborted && Date.now() < deadline) {
118
+ const count = await expireWorkflowEventPayloadsBatch({ olderThan, limit: batchSize, db: options.db });
119
+ expired += count;
120
+ if (count < batchSize) break;
121
+ }
122
+ return expired;
123
+ };
124
+
125
+ const RETENTION_INTERVAL_MS = 6 * 60 * 60 * 1000;
126
+ let retentionTimer: ReturnType<typeof setInterval> | null = null;
127
+ let retentionRunning = false;
128
+
129
+ /** Core owns this scheduler; SKIP LOCKED makes multiple replicas safe. */
130
+ export const startWorkflowEventRetention = (): (() => void) => {
131
+ if (retentionTimer) return () => undefined;
132
+ const abort = new AbortController();
133
+ const expire = async (): Promise<void> => {
134
+ if (retentionRunning) return;
135
+ retentionRunning = true;
136
+ try {
137
+ if (!(await ensureWorkflowEventRetentionIndexes())) {
138
+ log.info("Skipped workflow event retention sweep until its indexes are valid");
139
+ return;
140
+ }
141
+ const expired = await expireWorkflowEventPayloads({ signal: abort.signal });
142
+ if (expired > 0) log.info("Expired workflow event payloads", { expired });
143
+ } catch (error) {
144
+ log.error("Workflow event retention sweep failed", { error: error instanceof Error ? error.message : String(error) });
145
+ } finally {
146
+ retentionRunning = false;
147
+ }
148
+ };
149
+ void expire();
150
+ retentionTimer = setInterval(() => void expire(), RETENTION_INTERVAL_MS);
151
+ retentionTimer.unref?.();
152
+ return () => {
153
+ abort.abort();
154
+ if (retentionTimer) clearInterval(retentionTimer);
155
+ retentionTimer = null;
156
+ };
157
+ };