@k2b/cloud 0.23.0 → 0.24.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.
- package/package.json +2 -2
- package/src/_internal/capabilities.ts +230 -75
- package/src/_internal/define-app.ts +12 -3
- package/src/_internal/fixtures/filesv2-manifest-cloud-v0.29.0.json +887 -0
- package/src/_internal/page-responses.ts +1 -1
- package/src/_internal/registry.ts +78 -3
- package/src/ai/chat/blocks.tsx +2 -1
- package/src/ai/chat/messages.ts +6 -0
- package/src/ai/client/controller.ts +65 -20
- package/src/ai/client/transport.ts +5 -5
- package/src/ai/code-mode-skill.ts +2 -2
- package/src/ai/index.ts +0 -7
- package/src/ai/live-events.ts +1 -262
- package/src/ai/live.ts +31 -2
- package/src/ai/migrate.ts +35 -16
- package/src/ai/runtime.ts +1 -6
- package/src/ai/solid.ts +0 -5
- package/src/api/help.ts +2 -2
- package/src/api/pwa-phone.ts +10 -3
- package/src/api/search/schemas.ts +34 -0
- package/src/api/search.ts +118 -50
- package/src/browser/CloudResourceSearch.browser-harness.tsx +11 -0
- package/src/browser/CloudResourceSearch.tsx +208 -80
- package/src/browser/resource-search-messages.ts +15 -2
- package/src/browser/search-stream.ts +151 -0
- package/src/contracts/capabilities.ts +89 -24
- package/src/contracts/capability-compatibility.ts +28 -14
- package/src/contracts/file-provider.ts +158 -0
- package/src/contracts/index.ts +1 -0
- package/src/events/live-engine.ts +11 -7
- package/src/events/live.ts +32 -32
- package/src/server/help.ts +3 -3
- package/src/services/index.ts +1 -1
- package/src/services/outbox.ts +29 -115
- package/src/services/pdf/markdown.ts +22 -2
- package/src/services/pwa-devices.ts +74 -8
- package/src/shared/markdown/formula.ts +122 -26
- package/src/shared/markdown/index.ts +27 -14
- package/src/ssr/PwaLayout.tsx +2 -1
- package/src/styles/resource-search.css +19 -0
- package/src/ai/client/live-connection.ts +0 -141
- package/src/ai/live-messages.ts +0 -45
- package/src/ai/live-outbox.ts +0 -102
- package/src/ai/live-routes.ts +0 -412
- package/src/shared/markdown/extensions/info-blocks.ts +0 -79
package/src/ai/live-events.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import type { AiStreamEvent } from "./protocol";
|
|
3
2
|
|
|
4
3
|
export const AI_INVALIDATION_DOMAINS = [
|
|
5
4
|
"conversation-list",
|
|
@@ -17,8 +16,8 @@ export const AiInvalidationDomainSchema = z.enum(AI_INVALIDATION_DOMAINS);
|
|
|
17
16
|
export type AiInvalidationDomain = z.infer<typeof AiInvalidationDomainSchema>;
|
|
18
17
|
|
|
19
18
|
export const AiResourceIdSchema = z.string().regex(/^[0-9A-Za-z]{6}$/);
|
|
20
|
-
export const AiLiveCursorSchema = z.string().regex(/^s6t\.[a-zA-Z0-9_-]+\.\d+$/);
|
|
21
19
|
|
|
20
|
+
/** The data of one AI live update on the `user` channel of `/api/ai/live`: which views of the user's AI data changed. */
|
|
22
21
|
export const AiInvalidationSchema = z
|
|
23
22
|
.object({
|
|
24
23
|
type: z.literal("ai.invalidated"),
|
|
@@ -31,263 +30,3 @@ export const AiInvalidationSchema = z
|
|
|
31
30
|
.strict();
|
|
32
31
|
|
|
33
32
|
export type AiInvalidation = z.infer<typeof AiInvalidationSchema>;
|
|
34
|
-
|
|
35
|
-
export const AI_LIVE_WS_TYPE = {
|
|
36
|
-
subscribe: "ai.live.subscribe",
|
|
37
|
-
ready: "ai.live.ready",
|
|
38
|
-
event: "ai.live.event",
|
|
39
|
-
scopeChanged: "ai.live.scope_changed",
|
|
40
|
-
revoked: "ai.live.revoked",
|
|
41
|
-
error: "ai.live.error",
|
|
42
|
-
turnSubscribe: "ai.turn.subscribe",
|
|
43
|
-
turnUnsubscribe: "ai.turn.unsubscribe",
|
|
44
|
-
turnEvent: "ai.turn.event",
|
|
45
|
-
turnError: "ai.turn.error",
|
|
46
|
-
} as const;
|
|
47
|
-
|
|
48
|
-
const AiLiveSubscribeMessageSchema = z
|
|
49
|
-
.object({
|
|
50
|
-
type: z.literal(AI_LIVE_WS_TYPE.subscribe),
|
|
51
|
-
payload: z.object({ fromCursor: AiLiveCursorSchema.nullable(), recover: z.boolean() }).strict(),
|
|
52
|
-
})
|
|
53
|
-
.strict();
|
|
54
|
-
|
|
55
|
-
const AiTurnSubscribeMessageSchema = z
|
|
56
|
-
.object({
|
|
57
|
-
type: z.literal(AI_LIVE_WS_TYPE.turnSubscribe),
|
|
58
|
-
payload: z.object({ conversationId: AiResourceIdSchema }).strict(),
|
|
59
|
-
})
|
|
60
|
-
.strict();
|
|
61
|
-
|
|
62
|
-
const AiTurnUnsubscribeMessageSchema = z
|
|
63
|
-
.object({
|
|
64
|
-
type: z.literal(AI_LIVE_WS_TYPE.turnUnsubscribe),
|
|
65
|
-
payload: z.object({ conversationId: AiResourceIdSchema }).strict(),
|
|
66
|
-
})
|
|
67
|
-
.strict();
|
|
68
|
-
|
|
69
|
-
export const AiLiveClientMessageSchema = z.discriminatedUnion("type", [
|
|
70
|
-
AiLiveSubscribeMessageSchema,
|
|
71
|
-
AiTurnSubscribeMessageSchema,
|
|
72
|
-
AiTurnUnsubscribeMessageSchema,
|
|
73
|
-
]);
|
|
74
|
-
export type AiLiveClientMessage = z.infer<typeof AiLiveClientMessageSchema>;
|
|
75
|
-
|
|
76
|
-
const AiLiveRevocationCodeSchema = z.enum(["login_required", "access_denied"]);
|
|
77
|
-
const AiLiveErrorCodeSchema = z.enum(["invalid_json", "invalid_message", "backpressure", "internal_error", "stream_failed"]);
|
|
78
|
-
const AiTurnErrorCodeSchema = z.enum(["not_found", "access_denied", "stream_failed"]);
|
|
79
|
-
|
|
80
|
-
export type AiLiveRevocationCode = z.infer<typeof AiLiveRevocationCodeSchema>;
|
|
81
|
-
export type AiLiveErrorCode = z.infer<typeof AiLiveErrorCodeSchema>;
|
|
82
|
-
export type AiTurnErrorCode = z.infer<typeof AiTurnErrorCodeSchema>;
|
|
83
|
-
|
|
84
|
-
const AiTurnBlockSchema = z.discriminatedUnion("kind", [
|
|
85
|
-
z.object({ id: z.string(), kind: z.literal("text"), text: z.string() }).strict(),
|
|
86
|
-
z.object({ id: z.string(), kind: z.literal("thinking"), text: z.string() }).strict(),
|
|
87
|
-
z
|
|
88
|
-
.object({
|
|
89
|
-
id: z.string(),
|
|
90
|
-
kind: z.literal("steer_message"),
|
|
91
|
-
steerId: z.string(),
|
|
92
|
-
text: z.string(),
|
|
93
|
-
status: z.enum(["pending", "consumed", "failed"]),
|
|
94
|
-
})
|
|
95
|
-
.strict(),
|
|
96
|
-
z.object({ id: z.string(), kind: z.literal("steer_applied"), steerId: z.string() }).strict(),
|
|
97
|
-
z
|
|
98
|
-
.object({
|
|
99
|
-
id: z.string(),
|
|
100
|
-
kind: z.literal("tool"),
|
|
101
|
-
callId: z.string(),
|
|
102
|
-
name: z.string(),
|
|
103
|
-
args: z.unknown().optional(),
|
|
104
|
-
status: z.enum(["running", "awaiting_approval", "awaiting_client", "completed", "failed", "rejected"]),
|
|
105
|
-
result: z.unknown().optional(),
|
|
106
|
-
isError: z.boolean().optional(),
|
|
107
|
-
approval: z.unknown().optional(),
|
|
108
|
-
frontendMode: z.enum(["client", "client_view", "client_interaction"]).optional(),
|
|
109
|
-
presentation: z.unknown().optional(),
|
|
110
|
-
})
|
|
111
|
-
.strict(),
|
|
112
|
-
z
|
|
113
|
-
.object({
|
|
114
|
-
id: z.string(),
|
|
115
|
-
kind: z.literal("compaction"),
|
|
116
|
-
status: z.enum(["running", "completed", "skipped", "failed"]),
|
|
117
|
-
result: z.unknown().optional(),
|
|
118
|
-
})
|
|
119
|
-
.strict(),
|
|
120
|
-
]);
|
|
121
|
-
|
|
122
|
-
const AiConversationSchema = z
|
|
123
|
-
.object({
|
|
124
|
-
id: AiResourceIdSchema,
|
|
125
|
-
shortId: AiResourceIdSchema,
|
|
126
|
-
title: z.string(),
|
|
127
|
-
titleSource: z.enum(["default", "auto", "user"]),
|
|
128
|
-
description: z.string(),
|
|
129
|
-
descriptionSource: z.enum(["default", "auto", "user"]),
|
|
130
|
-
keywords: z.array(z.string()),
|
|
131
|
-
pinnedAt: z.string().nullable(),
|
|
132
|
-
archivedAt: z.string().nullable(),
|
|
133
|
-
done: z.boolean().nullable(),
|
|
134
|
-
isDone: z.boolean(),
|
|
135
|
-
lastUsedAt: z.string(),
|
|
136
|
-
runStatus: z.enum(["idle", "queued", "running", "needs_attention", "waiting_for_browser", "failed"]),
|
|
137
|
-
runError: z.string().nullable(),
|
|
138
|
-
unreadCompletion: z.boolean(),
|
|
139
|
-
projectId: z.string().nullable(),
|
|
140
|
-
draft: z.object({ content: z.array(z.unknown()), revision: z.number().int(), updatedAt: z.string().nullable() }).strict(),
|
|
141
|
-
createdByUserId: z.string().nullable(),
|
|
142
|
-
createdAt: z.string(),
|
|
143
|
-
updatedAt: z.string(),
|
|
144
|
-
})
|
|
145
|
-
.strict();
|
|
146
|
-
|
|
147
|
-
const AiStoredMessageSchema = z
|
|
148
|
-
.object({
|
|
149
|
-
id: AiResourceIdSchema,
|
|
150
|
-
shortId: AiResourceIdSchema,
|
|
151
|
-
conversationId: AiResourceIdSchema,
|
|
152
|
-
seq: z.number().int().nonnegative(),
|
|
153
|
-
kind: z.enum(["message", "summary"]),
|
|
154
|
-
message: z.unknown(),
|
|
155
|
-
loopId: AiResourceIdSchema.nullable(),
|
|
156
|
-
modelProfileId: z.string().nullable(),
|
|
157
|
-
providerModel: z.string().nullable(),
|
|
158
|
-
usage: z.unknown().nullable(),
|
|
159
|
-
stopReason: z.string().nullable(),
|
|
160
|
-
loopAggregate: z.unknown().nullable(),
|
|
161
|
-
loopDoneReason: z.string().nullable(),
|
|
162
|
-
compactedAt: z.string().nullable(),
|
|
163
|
-
meta: z.unknown().nullable(),
|
|
164
|
-
createdAt: z.string(),
|
|
165
|
-
})
|
|
166
|
-
.strict();
|
|
167
|
-
|
|
168
|
-
const AiTurnSnapshotSchema = z
|
|
169
|
-
.object({
|
|
170
|
-
turnId: AiResourceIdSchema,
|
|
171
|
-
attempt: z.number().int().nonnegative(),
|
|
172
|
-
status: z.enum(["queued", "running", "waiting_for_action", "completed", "failed", "aborted"]),
|
|
173
|
-
seq: z.number().int().nonnegative(),
|
|
174
|
-
blocks: z.array(AiTurnBlockSchema),
|
|
175
|
-
modelProfileId: z.string().nullable(),
|
|
176
|
-
createdAt: z.string(),
|
|
177
|
-
})
|
|
178
|
-
.strict();
|
|
179
|
-
|
|
180
|
-
const AiWireEventBaseSchema = {
|
|
181
|
-
v: z.literal(1),
|
|
182
|
-
conversationId: AiResourceIdSchema,
|
|
183
|
-
turnId: AiResourceIdSchema,
|
|
184
|
-
attempt: z.number().int().nonnegative(),
|
|
185
|
-
seq: z.number().int().nonnegative(),
|
|
186
|
-
} as const;
|
|
187
|
-
|
|
188
|
-
const AiStreamEventWireSchema = z.union([
|
|
189
|
-
z
|
|
190
|
-
.object({
|
|
191
|
-
type: z.literal("state"),
|
|
192
|
-
conversation: AiConversationSchema,
|
|
193
|
-
messages: z.array(AiStoredMessageSchema),
|
|
194
|
-
hasMoreMessages: z.boolean().optional(),
|
|
195
|
-
activeTurn: AiTurnSnapshotSchema.nullable(),
|
|
196
|
-
})
|
|
197
|
-
.strict(),
|
|
198
|
-
z
|
|
199
|
-
.object({
|
|
200
|
-
...AiWireEventBaseSchema,
|
|
201
|
-
type: z.literal("turn_started"),
|
|
202
|
-
modelProfileId: z.string(),
|
|
203
|
-
providerModel: z.string(),
|
|
204
|
-
blocks: z.array(AiTurnBlockSchema).optional(),
|
|
205
|
-
})
|
|
206
|
-
.strict(),
|
|
207
|
-
z.object({ ...AiWireEventBaseSchema, type: z.literal("block_set"), block: AiTurnBlockSchema }).strict(),
|
|
208
|
-
z
|
|
209
|
-
.object({
|
|
210
|
-
...AiWireEventBaseSchema,
|
|
211
|
-
type: z.literal("block_delta"),
|
|
212
|
-
blockId: z.string(),
|
|
213
|
-
blockKind: z.enum(["text", "thinking"]),
|
|
214
|
-
delta: z.string(),
|
|
215
|
-
})
|
|
216
|
-
.strict(),
|
|
217
|
-
z
|
|
218
|
-
.object({
|
|
219
|
-
...AiWireEventBaseSchema,
|
|
220
|
-
type: z.literal("turn_finished"),
|
|
221
|
-
status: z.enum(["completed", "failed", "aborted"]),
|
|
222
|
-
error: z.string().nullable(),
|
|
223
|
-
messages: z.array(AiStoredMessageSchema).optional(),
|
|
224
|
-
})
|
|
225
|
-
.strict(),
|
|
226
|
-
]);
|
|
227
|
-
|
|
228
|
-
export const AiStreamEventSchema = z.custom<AiStreamEvent>((value) => AiStreamEventWireSchema.safeParse(value).success);
|
|
229
|
-
|
|
230
|
-
export const AiLiveServerMessageSchema = z.discriminatedUnion("type", [
|
|
231
|
-
z
|
|
232
|
-
.object({
|
|
233
|
-
type: z.literal(AI_LIVE_WS_TYPE.ready),
|
|
234
|
-
payload: z.object({ cursor: AiLiveCursorSchema, recovered: z.boolean() }).strict(),
|
|
235
|
-
})
|
|
236
|
-
.strict(),
|
|
237
|
-
z
|
|
238
|
-
.object({
|
|
239
|
-
type: z.literal(AI_LIVE_WS_TYPE.event),
|
|
240
|
-
payload: z.object({ cursor: AiLiveCursorSchema, event: AiInvalidationSchema }).strict(),
|
|
241
|
-
})
|
|
242
|
-
.strict(),
|
|
243
|
-
z
|
|
244
|
-
.object({
|
|
245
|
-
type: z.literal(AI_LIVE_WS_TYPE.scopeChanged),
|
|
246
|
-
payload: z.object({ at: z.string().datetime() }).strict(),
|
|
247
|
-
})
|
|
248
|
-
.strict(),
|
|
249
|
-
z
|
|
250
|
-
.object({
|
|
251
|
-
type: z.literal(AI_LIVE_WS_TYPE.revoked),
|
|
252
|
-
payload: z.object({ code: AiLiveRevocationCodeSchema, message: z.string().min(1).max(500) }).strict(),
|
|
253
|
-
})
|
|
254
|
-
.strict(),
|
|
255
|
-
z
|
|
256
|
-
.object({
|
|
257
|
-
type: z.literal(AI_LIVE_WS_TYPE.error),
|
|
258
|
-
payload: z.object({ code: AiLiveErrorCodeSchema, message: z.string().min(1).max(500) }).strict(),
|
|
259
|
-
})
|
|
260
|
-
.strict(),
|
|
261
|
-
z
|
|
262
|
-
.object({
|
|
263
|
-
type: z.literal(AI_LIVE_WS_TYPE.turnEvent),
|
|
264
|
-
payload: z.object({ conversationId: AiResourceIdSchema, event: AiStreamEventSchema }).strict(),
|
|
265
|
-
})
|
|
266
|
-
.strict(),
|
|
267
|
-
z
|
|
268
|
-
.object({
|
|
269
|
-
type: z.literal(AI_LIVE_WS_TYPE.turnError),
|
|
270
|
-
payload: z.object({ conversationId: AiResourceIdSchema, code: AiTurnErrorCodeSchema, message: z.string().min(1).max(500) }).strict(),
|
|
271
|
-
})
|
|
272
|
-
.strict(),
|
|
273
|
-
]);
|
|
274
|
-
|
|
275
|
-
export type AiLiveServerMessage = z.infer<typeof AiLiveServerMessageSchema>;
|
|
276
|
-
|
|
277
|
-
export const aiTurnEventMessage = (conversationId: string, event: AiStreamEvent): AiLiveServerMessage => ({
|
|
278
|
-
type: AI_LIVE_WS_TYPE.turnEvent,
|
|
279
|
-
payload: { conversationId, event },
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
export const parseAiLiveServerMessage = (raw: string): AiLiveServerMessage | null => {
|
|
283
|
-
try {
|
|
284
|
-
const parsed = AiLiveServerMessageSchema.safeParse(JSON.parse(raw));
|
|
285
|
-
if (!parsed.success) return null;
|
|
286
|
-
if (parsed.data.type !== AI_LIVE_WS_TYPE.turnEvent) return parsed.data;
|
|
287
|
-
const eventConversationId =
|
|
288
|
-
parsed.data.payload.event.type === "state" ? parsed.data.payload.event.conversation.id : parsed.data.payload.event.conversationId;
|
|
289
|
-
return eventConversationId === parsed.data.payload.conversationId ? parsed.data : null;
|
|
290
|
-
} catch {
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
};
|
package/src/ai/live.ts
CHANGED
|
@@ -1,2 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineLive, type LiveViewer } from "../events/live";
|
|
3
|
+
import { AiInvalidationSchema } from "./live-events";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Live updates of a user's AI data. Core publishes and serves them at
|
|
7
|
+
* `/api/ai/live`; the AI's database triggers write them, keyed `u:<user ID>`,
|
|
8
|
+
* in the transaction of every change. Other applications, such as Assistant,
|
|
9
|
+
* read `cursor()` before their snapshot.
|
|
10
|
+
*/
|
|
11
|
+
export const aiLive = defineLive({ appId: "core", event: AiInvalidationSchema });
|
|
12
|
+
|
|
13
|
+
/** The user an AI request acts for: the user, or the user a credential is delegated by. */
|
|
14
|
+
const ownerOf = (viewer: LiveViewer): string | null => (viewer.accessSubject.type === "user" ? viewer.accessSubject.userId : null);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `user` follows the viewer's own AI data: conversations, their files, tasks and
|
|
18
|
+
* dictations, and the Projects the viewer can read. Its single key admits only
|
|
19
|
+
* that user; who receives a Project change was decided when it was written.
|
|
20
|
+
*/
|
|
21
|
+
export const aiLiveChannels = {
|
|
22
|
+
user: {
|
|
23
|
+
scope: z.object({}).strict(),
|
|
24
|
+
keys: async (_scope: unknown, viewer: LiveViewer) => {
|
|
25
|
+
const owner = ownerOf(viewer);
|
|
26
|
+
return owner ? [`u:${owner}`] : null;
|
|
27
|
+
},
|
|
28
|
+
authorize: async (key: string, viewers: readonly LiveViewer[]) =>
|
|
29
|
+
new Set(viewers.filter((viewer) => `u:${ownerOf(viewer)}` === key).map((viewer) => viewer.id)),
|
|
30
|
+
},
|
|
31
|
+
};
|
package/src/ai/migrate.ts
CHANGED
|
@@ -13,6 +13,14 @@ const backfillAiShortIds = async (
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export const migrateCloudAi = async (): Promise<void> => {
|
|
16
|
+
// AI writes its live updates to Core's platform outbox, in the transaction of every
|
|
17
|
+
// AI write: without events.enqueue(), every one of them would fail.
|
|
18
|
+
const [outbox] = await sql<{ ready: boolean }[]>`
|
|
19
|
+
SELECT to_regprocedure('events.enqueue(uuid,text,text,text,jsonb,text)') IS NOT NULL AS ready
|
|
20
|
+
`;
|
|
21
|
+
if (!outbox?.ready) {
|
|
22
|
+
throw new Error("AI live updates write to events.outbox, which does not exist. Run Core's events migration before the AI migration.");
|
|
23
|
+
}
|
|
16
24
|
await sql`CREATE SCHEMA IF NOT EXISTS ai`.simple();
|
|
17
25
|
console.log(" ✓ ai schema");
|
|
18
26
|
|
|
@@ -1722,10 +1730,12 @@ export const migrateCloudAi = async (): Promise<void> => {
|
|
|
1722
1730
|
WHERE project_id IS NOT NULL AND archived_at IS NULL
|
|
1723
1731
|
`.simple();
|
|
1724
1732
|
|
|
1725
|
-
//
|
|
1726
|
-
//
|
|
1727
|
-
//
|
|
1728
|
-
|
|
1733
|
+
// Live updates. Triggers live at the persistence seam so route, worker, tool,
|
|
1734
|
+
// enrichment, and scheduler writes cannot bypass them. They write to Core's
|
|
1735
|
+
// platform outbox in the transaction of the domain write (see the start of this migration).
|
|
1736
|
+
|
|
1737
|
+
// The previous outbox. Nothing writes it anymore; an older Core that still runs during
|
|
1738
|
+
// an update finds it empty. The cleanup release drops it with the topic cloud-ai-invalidations.
|
|
1729
1739
|
await sql`
|
|
1730
1740
|
CREATE TABLE IF NOT EXISTS ai.live_invalidation_outbox (
|
|
1731
1741
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
@@ -1786,6 +1796,10 @@ export const migrateCloudAi = async (): Promise<void> => {
|
|
|
1786
1796
|
DROP FUNCTION IF EXISTS ai.enqueue_live_for_user(UUID, TEXT, UUID, TEXT, TEXT, TEXT[])
|
|
1787
1797
|
`.simple();
|
|
1788
1798
|
|
|
1799
|
+
// Every AI live update goes through this function: one update for one user,
|
|
1800
|
+
// keyed `u:<user>`, on Core's live topic. Its data is an `AiInvalidation`.
|
|
1801
|
+
// No coalesce key: the last payload would win and drop the domains of earlier ones.
|
|
1802
|
+
// Gateway health reports a body without events.enqueue: an older Core restored its own.
|
|
1789
1803
|
await sql`
|
|
1790
1804
|
CREATE OR REPLACE FUNCTION ai.enqueue_live_for_user(
|
|
1791
1805
|
p_change_id UUID,
|
|
@@ -1796,11 +1810,23 @@ export const migrateCloudAi = async (): Promise<void> => {
|
|
|
1796
1810
|
) RETURNS void AS $$
|
|
1797
1811
|
BEGIN
|
|
1798
1812
|
IF p_user_id IS NULL OR cardinality(p_domains) = 0 THEN RETURN; END IF;
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1813
|
+
PERFORM events.enqueue(
|
|
1814
|
+
gen_random_uuid(),
|
|
1815
|
+
'core',
|
|
1816
|
+
'live',
|
|
1817
|
+
'u:' || p_user_id::text,
|
|
1818
|
+
jsonb_build_object(
|
|
1819
|
+
'v', 1,
|
|
1820
|
+
'k', 'u:' || p_user_id::text,
|
|
1821
|
+
'd', jsonb_build_object(
|
|
1822
|
+
'type', 'ai.invalidated',
|
|
1823
|
+
'changeId', p_change_id,
|
|
1824
|
+
'conversationId', p_conversation_short_id,
|
|
1825
|
+
'projectId', p_project_short_id,
|
|
1826
|
+
'domains', to_jsonb(ARRAY(SELECT DISTINCT domain FROM unnest(p_domains) domain ORDER BY domain)),
|
|
1827
|
+
'at', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')
|
|
1828
|
+
)
|
|
1829
|
+
)
|
|
1804
1830
|
);
|
|
1805
1831
|
END
|
|
1806
1832
|
$$ LANGUAGE plpgsql
|
|
@@ -2213,13 +2239,6 @@ export const migrateCloudAi = async (): Promise<void> => {
|
|
|
2213
2239
|
)`.simple();
|
|
2214
2240
|
await sql`CREATE INDEX IF NOT EXISTS ai_dictations_pending ON ai.dictations(conversation_id, created_at, id) WHERE disposition = 'pending'`.simple();
|
|
2215
2241
|
await sql`CREATE INDEX IF NOT EXISTS ai_dictations_recovery ON ai.dictations(status, next_attempt_at, lease_until) WHERE status IN ('queued', 'running')`.simple();
|
|
2216
|
-
await sql.begin(async (tx) => {
|
|
2217
|
-
await tx`ALTER TABLE ai.live_invalidation_outbox DROP CONSTRAINT IF EXISTS ai_live_invalidation_domains_check`;
|
|
2218
|
-
await tx`ALTER TABLE ai.live_invalidation_outbox ADD CONSTRAINT ai_live_invalidation_domains_check CHECK (
|
|
2219
|
-
domains <@ ARRAY['conversation-list', 'conversation-detail', 'conversation-sources', 'conversation-files', 'conversation-tasks',
|
|
2220
|
-
'conversation-dictations', 'project-list', 'project-detail', 'project-context']::text[] AND cardinality(domains) > 0
|
|
2221
|
-
)`;
|
|
2222
|
-
});
|
|
2223
2242
|
await sql`DROP TRIGGER IF EXISTS ai_live_dictations_changed ON ai.dictations`.simple();
|
|
2224
2243
|
await sql`DROP TRIGGER IF EXISTS ai_live_dictations_state_changed ON ai.dictations`.simple();
|
|
2225
2244
|
await sql`CREATE TRIGGER ai_live_dictations_changed AFTER INSERT OR DELETE ON ai.dictations
|
package/src/ai/runtime.ts
CHANGED
|
@@ -12,7 +12,6 @@ import { aiChatAccessSubject, selectAssistantAiModelId } from "./assistant-model
|
|
|
12
12
|
import { startAiDictationRuntime } from "./dictation-runtime";
|
|
13
13
|
import { AiTurnExecutor } from "./executor";
|
|
14
14
|
import { canonicalizeAiConversationAttachments, snapshotAiConversationFiles } from "./file-context";
|
|
15
|
-
import { startAiInvalidationRuntime, stopAiInvalidationRuntime } from "./live-outbox";
|
|
16
15
|
import { drainQueuedMessages } from "./message-queue";
|
|
17
16
|
import { aiQuotas } from "./quotas";
|
|
18
17
|
import { parseAiResourceMarker } from "./resource-markers";
|
|
@@ -577,7 +576,6 @@ export const startAiRuntime = (
|
|
|
577
576
|
},
|
|
578
577
|
};
|
|
579
578
|
running = state;
|
|
580
|
-
startAiInvalidationRuntime();
|
|
581
579
|
log.info("AI runtime started", { workerId: AI_WORKER_ID, concurrency });
|
|
582
580
|
return releaseAiRuntime(listener);
|
|
583
581
|
};
|
|
@@ -591,10 +589,7 @@ const releaseAiRuntime = (listener: ((event: AiTurnFinalizedEvent) => Promise<vo
|
|
|
591
589
|
if (!state) return;
|
|
592
590
|
if (listener) state.listeners.delete(listener);
|
|
593
591
|
refCount = Math.max(0, refCount - 1);
|
|
594
|
-
if (refCount === 0)
|
|
595
|
-
state.stop();
|
|
596
|
-
void stopAiInvalidationRuntime();
|
|
597
|
-
}
|
|
592
|
+
if (refCount === 0) state.stop();
|
|
598
593
|
};
|
|
599
594
|
};
|
|
600
595
|
|
package/src/ai/solid.ts
CHANGED
|
@@ -7,11 +7,6 @@ export {
|
|
|
7
7
|
createAiChatController,
|
|
8
8
|
} from "./client/controller";
|
|
9
9
|
export { conversationFileSource } from "./client/file-source";
|
|
10
|
-
export {
|
|
11
|
-
type AiLiveConnection,
|
|
12
|
-
type CreateAiLiveConnectionOptions,
|
|
13
|
-
createAiLiveConnection,
|
|
14
|
-
} from "./client/live-connection";
|
|
15
10
|
export {
|
|
16
11
|
type AiActiveTurn,
|
|
17
12
|
type AiChatProjection,
|
package/src/api/help.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { helpApiMessages } from "./help-messages";
|
|
|
8
8
|
export type HelpRouteDependencies = {
|
|
9
9
|
help?: HelpReaderFactory;
|
|
10
10
|
authenticate?: MiddlewareHandler<AuthContext>;
|
|
11
|
-
renderMarkdown?: (markdown: string) => string;
|
|
11
|
+
renderMarkdown?: (markdown: string, locale: string) => string;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
export const createHelpRoutes = (dependencies: HelpRouteDependencies = {}) => {
|
|
@@ -34,7 +34,7 @@ export const createHelpRoutes = (dependencies: HelpRouteDependencies = {}) => {
|
|
|
34
34
|
id: document.documentId,
|
|
35
35
|
title: document.title,
|
|
36
36
|
markdown: document.markdown,
|
|
37
|
-
html: (dependencies.renderMarkdown ?? renderHelpMarkdown)(document.markdown),
|
|
37
|
+
html: (dependencies.renderMarkdown ?? renderHelpMarkdown)(document.markdown, document.locale),
|
|
38
38
|
};
|
|
39
39
|
return c.json(payload);
|
|
40
40
|
});
|
package/src/api/pwa-phone.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { type Context, Hono } from "hono";
|
|
|
2
2
|
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
3
3
|
import { env } from "../config/env";
|
|
4
4
|
import { PWA_AUTH_PATH, PWA_COOKIES, PWA_LIMITS, PWA_SCOPE, PwaClaimSchema, type PwaLaunchState, PwaRenameSchema } from "../contracts/pwa";
|
|
5
|
-
import { type AuthContext, auth, rateLimit, v } from "../server";
|
|
6
|
-
import { type PwaCredentials, PwaError, pwaDevices } from "../services/pwa-devices";
|
|
5
|
+
import { type AuthContext, auth, getLocale, rateLimit, v } from "../server";
|
|
6
|
+
import { type PwaCredentials, type PwaDeviceNotifier, PwaError, pwaDevices } from "../services/pwa-devices";
|
|
7
7
|
import { defaultShellAvailable, handlePwaError, type PwaRouteOptions, pwaErrorResponse, pwaInvalidRequest, pwaTransport } from "./pwa";
|
|
8
8
|
|
|
9
9
|
const PAIRING_COOKIE_PATH = `${PWA_AUTH_PATH}/pairings`;
|
|
@@ -58,12 +58,17 @@ export const launchTarget = (to: string | undefined): string => {
|
|
|
58
58
|
|
|
59
59
|
const stateUrl = (state: PwaLaunchState) => `${PWA_SCOPE}?pwa=${state}`;
|
|
60
60
|
|
|
61
|
+
export type PwaPhoneRouteOptions = PwaRouteOptions & {
|
|
62
|
+
/** Sends the "new phone paired" notice; completion awaits it before the phone gets its credentials. */
|
|
63
|
+
notify: PwaDeviceNotifier;
|
|
64
|
+
};
|
|
65
|
+
|
|
61
66
|
/**
|
|
62
67
|
* Phone side of the mobile app (preview), mounted by Core at `/pwa/_auth` before its page
|
|
63
68
|
* catch-all. The device key and the completion secret use cookie paths below `/pwa/_auth`,
|
|
64
69
|
* which only Core serves; other applications see only the 24-hour app session.
|
|
65
70
|
*/
|
|
66
|
-
export const createPwaPhoneRoutes = (options:
|
|
71
|
+
export const createPwaPhoneRoutes = (options: PwaPhoneRouteOptions) => {
|
|
67
72
|
const service = options.service ?? pwaDevices;
|
|
68
73
|
const shellAvailable = options.shellAvailable ?? defaultShellAvailable;
|
|
69
74
|
const limit = rateLimit({ keyBy: "ip" });
|
|
@@ -102,6 +107,8 @@ export const createPwaPhoneRoutes = (options: PwaRouteOptions = {}) => {
|
|
|
102
107
|
deviceKey: getCookie(c, PWA_COOKIES.device),
|
|
103
108
|
appSession: await appSession(c),
|
|
104
109
|
webUserId: web?.user.id ?? null,
|
|
110
|
+
locale: getLocale(c),
|
|
111
|
+
notify: options.notify,
|
|
105
112
|
});
|
|
106
113
|
if (result.state === "waiting") return c.json(result, 202);
|
|
107
114
|
if (result.credentials) setAppCredentials(c, result.credentials);
|
|
@@ -85,6 +85,40 @@ export const SearchResponseSchema = z.object({
|
|
|
85
85
|
failedApps: z.array(z.string()).optional(),
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
/** Media type a client sends in `Accept` to receive one NDJSON line per app as soon as it finishes. */
|
|
89
|
+
export const SEARCH_STREAM_CONTENT_TYPE = "application/x-ndjson";
|
|
90
|
+
|
|
91
|
+
export const SearchProviderStatusSchema = z.enum(["ok", "empty", "timeout", "error"]);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* One line of a streamed search: `start` names the apps that search, one `provider` line follows per app in the
|
|
95
|
+
* order the apps finish, and `done` closes the search. Apps without a line before `done` do not exist: every app
|
|
96
|
+
* named in `start` gets exactly one line.
|
|
97
|
+
*/
|
|
98
|
+
export const SearchStreamLineSchema = z.discriminatedUnion("type", [
|
|
99
|
+
z.object({
|
|
100
|
+
type: z.literal("start"),
|
|
101
|
+
query: z.string(),
|
|
102
|
+
apps: z.array(SearchAppSchema),
|
|
103
|
+
providers: z.array(z.string()),
|
|
104
|
+
unsupportedTags: z.array(z.string()).optional(),
|
|
105
|
+
}),
|
|
106
|
+
z.object({
|
|
107
|
+
type: z.literal("provider"),
|
|
108
|
+
provider: z.string(),
|
|
109
|
+
status: SearchProviderStatusSchema,
|
|
110
|
+
results: z.array(SearchItemSchema),
|
|
111
|
+
ms: z.number().int().nonnegative(),
|
|
112
|
+
}),
|
|
113
|
+
z.object({
|
|
114
|
+
type: z.literal("done"),
|
|
115
|
+
status: z.enum(["complete", "partial"]),
|
|
116
|
+
count: z.number().int().nonnegative(),
|
|
117
|
+
}),
|
|
118
|
+
]);
|
|
119
|
+
|
|
88
120
|
export type SearchApp = z.infer<typeof SearchAppSchema>;
|
|
89
121
|
export type SearchItem = z.infer<typeof SearchItemSchema>;
|
|
90
122
|
export type SearchResponse = z.infer<typeof SearchResponseSchema>;
|
|
123
|
+
export type SearchProviderStatus = z.infer<typeof SearchProviderStatusSchema>;
|
|
124
|
+
export type SearchStreamLine = z.infer<typeof SearchStreamLineSchema>;
|