gestalt-mobile 0.35.5 → 0.36.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 (47) hide show
  1. package/README.md +60 -3
  2. package/dist/client/assets/index-9XMv2BvD.js +38 -0
  3. package/dist/client/assets/{index-CPQBJDsg.css → index-Bxv5Z2w4.css} +1 -1
  4. package/dist/client/index.html +2 -2
  5. package/dist/server/client/features/chat/activity-summary.js +71 -0
  6. package/dist/server/client/features/chat/chat-projection.js +722 -0
  7. package/dist/server/client/features/chat/message-store.js +30 -0
  8. package/dist/server/server/app.js +1 -1
  9. package/dist/server/server/cli.js +8 -0
  10. package/dist/server/server/composition.contracts.js +158 -6
  11. package/dist/server/server/composition.js +266 -56
  12. package/dist/server/server/features/autopilot/register-routes.js +2 -2
  13. package/dist/server/server/features/autopilot/toggle/endpoint.js +5 -1
  14. package/dist/server/server/features/catalog/get-bootstrap/use-case.js +5 -3
  15. package/dist/server/server/features/sessions/get-session/endpoint.js +1 -1
  16. package/dist/server/server/features/sessions/list-models/endpoint.js +16 -0
  17. package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +8 -3
  18. package/dist/server/server/features/sessions/list-sessions/endpoint.js +1 -1
  19. package/dist/server/server/features/sessions/model/relay-session.js +2 -0
  20. package/dist/server/server/features/sessions/promote-recent-thread/endpoint.js +6 -2
  21. package/dist/server/server/features/sessions/promote-recent-thread/use-case.js +7 -2
  22. package/dist/server/server/features/sessions/register-routes.js +8 -3
  23. package/dist/server/server/features/sessions/respond-interaction/endpoint.js +1 -1
  24. package/dist/server/server/features/sessions/select-model/endpoint.js +1 -1
  25. package/dist/server/server/features/sessions/start-session/request.js +1 -0
  26. package/dist/server/server/features/sessions/start-session/use-case.js +21 -7
  27. package/dist/server/server/features/skills/list-available/endpoint.js +4 -2
  28. package/dist/server/server/platform/catalog/provider-model-catalog.js +15 -0
  29. package/dist/server/server/platform/kimi/kimi-activity-facts.js +88 -0
  30. package/dist/server/server/platform/kimi/kimi-errors.js +30 -0
  31. package/dist/server/server/platform/kimi/kimi-model-catalog.js +63 -0
  32. package/dist/server/server/platform/kimi/kimi-normalizer.js +196 -0
  33. package/dist/server/server/platform/kimi/kimi-recent-session-lister.js +68 -0
  34. package/dist/server/server/platform/kimi/kimi-server-request.js +143 -0
  35. package/dist/server/server/platform/kimi/kimi-session-runtime.js +653 -0
  36. package/dist/server/server/platform/kimi/kimi-share-dir.js +65 -0
  37. package/dist/server/server/platform/kimi/kimi-skill-catalog.js +79 -0
  38. package/dist/server/server/platform/kimi/kimi-skills-materializer.js +41 -0
  39. package/dist/server/server/platform/kimi/kimi-web-client.js +63 -0
  40. package/dist/server/server/platform/kimi/kimi-web-server-manager.js +215 -0
  41. package/dist/server/server/platform/kimi/kimi-ws-client.js +154 -0
  42. package/dist/server/server/platform/persistence/migrate.js +2 -0
  43. package/dist/server/server/platform/persistence/sqlite-session-repository.js +3 -2
  44. package/dist/server/server/platform/skills/cached-skill-catalog.js +0 -0
  45. package/dist/server/shared/contracts/llm-provider.js +6 -0
  46. package/package.json +4 -3
  47. package/dist/client/assets/index-SZM7lpix.js +0 -38
@@ -0,0 +1,722 @@
1
+ /*
2
+ * Copyright (C) 2026 Dyne.org foundation
3
+ * Designed by Denis Roio <jaromil@dyne.org>
4
+ * SPDX-License-Identifier: AGPL-3.0-or-later
5
+ */
6
+ import { autopilotAuditLabel } from '../../../shared/contracts/autopilot-audit.js';
7
+ import { toActivity } from './activity-summary.js';
8
+ const empty = () => ({
9
+ sessionId: null,
10
+ cursor: 0,
11
+ snapshotting: false,
12
+ lifecycle: 'finished',
13
+ activeTurnId: null,
14
+ messages: [],
15
+ activities: [],
16
+ prompts: [],
17
+ interactions: [],
18
+ autopilotAuditTruncated: false,
19
+ buffered: new Map(),
20
+ });
21
+ export const createChatProjection = (sessionId = null) => ({
22
+ ...empty(),
23
+ sessionId,
24
+ });
25
+ function messageFromItem(item) {
26
+ if (item.kind === 'autopilot')
27
+ return {
28
+ id: `item:${item.id}`,
29
+ role: 'audit',
30
+ text: 'Continued execution automatically',
31
+ ...(typeof item.occurredAt === 'number' && Number.isFinite(item.occurredAt)
32
+ ? { occurredAt: item.occurredAt }
33
+ : {}),
34
+ ...(typeof item.controlId === 'string' ? { controlId: item.controlId } : {}),
35
+ complete: true,
36
+ };
37
+ if ((item.kind !== 'user' && item.kind !== 'agent') || typeof item.text !== 'string')
38
+ return null;
39
+ return {
40
+ id: `item:${item.id}`,
41
+ role: item.kind === 'user' ? 'user' : 'assistant',
42
+ text: item.text,
43
+ ...(item.phase === 'commentary' || item.phase === 'final_answer' ? { phase: item.phase } : {}),
44
+ ...(typeof item.occurredAt === 'number' && Number.isFinite(item.occurredAt)
45
+ ? { occurredAt: item.occurredAt }
46
+ : {}),
47
+ ...(typeof item.turnId === 'string' ? { turnId: item.turnId } : {}),
48
+ complete: true,
49
+ };
50
+ }
51
+ function auditMessage(item) {
52
+ return {
53
+ id: item.id,
54
+ role: 'audit',
55
+ text: item.label,
56
+ occurredAt: item.occurredAt,
57
+ ...(item.controlId ? { controlId: item.controlId } : {}),
58
+ complete: true,
59
+ };
60
+ }
61
+ function auditEventMessage(event) {
62
+ const occurredAt = parseOccurredAt(event.occurredAt);
63
+ const label = autopilotAuditLabel(event.type, event.payload);
64
+ const controlId = event.payload &&
65
+ typeof event.payload === 'object' &&
66
+ typeof event.payload.controlId === 'string'
67
+ ? event.payload.controlId
68
+ : undefined;
69
+ return label && occurredAt !== undefined
70
+ ? auditMessage({
71
+ id: `audit:${event.sequence}`,
72
+ label,
73
+ occurredAt,
74
+ ...(controlId ? { controlId } : {}),
75
+ })
76
+ : null;
77
+ }
78
+ function parseOccurredAt(value) {
79
+ if (typeof value !== 'string')
80
+ return undefined;
81
+ const timestamp = Date.parse(value);
82
+ return Number.isFinite(timestamp) ? timestamp : undefined;
83
+ }
84
+ function interaction(snapshot) {
85
+ const occurredAt = parseOccurredAt(snapshot.requestedAt);
86
+ return {
87
+ requestId: snapshot.requestId,
88
+ key: `interaction:${snapshot.requestId}`,
89
+ kind: snapshot.kind,
90
+ turnId: snapshot.turnId,
91
+ payload: 'payload' in snapshot ? snapshot.payload : null,
92
+ state: snapshot.resolvedAt
93
+ ? snapshot.outcome === 'failed'
94
+ ? 'failed'
95
+ : 'resolved'
96
+ : 'pending',
97
+ ...(snapshot.resolvedAt ? { attemptedOutcome: snapshot.outcome } : {}),
98
+ ...(occurredAt !== undefined ? { occurredAt } : {}),
99
+ };
100
+ }
101
+ function safeOutcome(interaction, attemptedOutcome) {
102
+ if (attemptedOutcome === 'approved' ||
103
+ attemptedOutcome === 'denied' ||
104
+ attemptedOutcome === 'answered' ||
105
+ attemptedOutcome === 'dismissed' ||
106
+ attemptedOutcome === 'failed')
107
+ return attemptedOutcome;
108
+ if (interaction?.kind === 'quiz' || interaction?.kind === 'userInput')
109
+ return 'answered';
110
+ if (typeof attemptedOutcome === 'object' &&
111
+ attemptedOutcome !== null &&
112
+ attemptedOutcome.decision === 'decline')
113
+ return 'denied';
114
+ return interaction ? 'approved' : undefined;
115
+ }
116
+ function lifecycle(activeTurnId, previous) {
117
+ if (activeTurnId)
118
+ return previous === 'finished' || previous === 'interrupted' ? previous : 'working';
119
+ return previous === 'working' || previous === 'starting' || previous === 'recoverable'
120
+ ? 'finished'
121
+ : previous;
122
+ }
123
+ function unique(items) {
124
+ return [...new Map(items.map((item) => [item.key ?? item.id ?? '', item])).values()];
125
+ }
126
+ function upsertActivity(activities, activity) {
127
+ return activities.some((item) => item.id === activity.id)
128
+ ? activities.map((item) => item.id === activity.id
129
+ ? {
130
+ ...activity,
131
+ ...(activity.occurredAt === undefined && item.occurredAt !== undefined
132
+ ? { occurredAt: item.occurredAt }
133
+ : {}),
134
+ }
135
+ : item)
136
+ : [...activities, activity];
137
+ }
138
+ function mergeMessages(items, prompts, previous) {
139
+ const old = new Map(previous.map((message) => [message.id, message]));
140
+ const previousIndex = new Map(previous.map((message, index) => [message.id, index]));
141
+ const unmatched = new Set(prompts.filter((prompt) => prompt.state !== 'canonical').map((prompt) => prompt.operationId));
142
+ const retained = new Set();
143
+ const canonical = items.flatMap((item) => {
144
+ const message = messageFromItem(item);
145
+ if (!message)
146
+ return [];
147
+ if (item.kind === 'user') {
148
+ const direct = prompts.find((prompt) => (typeof item.operationId === 'string' && item.operationId === prompt.operationId) ||
149
+ (typeof item.turnId === 'string' && item.turnId === prompt.turnId));
150
+ const fallback = !direct &&
151
+ prompts.filter((prompt) => prompt.state !== 'canonical' && prompt.text === item.text)
152
+ .length === 1
153
+ ? prompts.find((prompt) => prompt.state !== 'canonical' && prompt.text === item.text)
154
+ : undefined;
155
+ const prompt = direct ?? fallback;
156
+ if (prompt) {
157
+ unmatched.delete(prompt.operationId);
158
+ const existing = old.get(prompt.key);
159
+ const reconciled = {
160
+ ...message,
161
+ id: prompt.key,
162
+ turnId: prompt.turnId ?? message.turnId,
163
+ ...(message.occurredAt === undefined && existing?.occurredAt !== undefined
164
+ ? { occurredAt: existing.occurredAt }
165
+ : {}),
166
+ };
167
+ retained.add(reconciled.id);
168
+ return [
169
+ existing &&
170
+ existing.text === reconciled.text &&
171
+ existing.turnId === reconciled.turnId &&
172
+ existing.complete === reconciled.complete &&
173
+ existing.occurredAt === reconciled.occurredAt
174
+ ? existing
175
+ : reconciled,
176
+ ];
177
+ }
178
+ }
179
+ if (item.kind === 'agent' && typeof item.turnId === 'string') {
180
+ const live = old.get(`assistant:${item.id}`) ??
181
+ previous.find((candidate) => Boolean(candidate.role === 'assistant' &&
182
+ candidate.turnId === item.turnId &&
183
+ candidate.text === message.text &&
184
+ (candidate.phase === message.phase || !candidate.complete)));
185
+ if (live) {
186
+ retained.add(live.id);
187
+ const reconciled = {
188
+ ...message,
189
+ id: live.id,
190
+ turnId: message.turnId ?? live.turnId,
191
+ ...(message.occurredAt === undefined && live.occurredAt !== undefined
192
+ ? { occurredAt: live.occurredAt }
193
+ : {}),
194
+ };
195
+ return [
196
+ live.text === reconciled.text &&
197
+ live.phase === reconciled.phase &&
198
+ live.occurredAt === reconciled.occurredAt
199
+ ? live
200
+ : reconciled,
201
+ ];
202
+ }
203
+ }
204
+ retained.add(message.id);
205
+ return [message];
206
+ });
207
+ const optimistic = prompts
208
+ .filter((prompt) => unmatched.has(prompt.operationId))
209
+ .map((prompt) => old.get(prompt.key) ?? {
210
+ id: prompt.key,
211
+ role: 'user',
212
+ text: prompt.text,
213
+ complete: prompt.state === 'failed',
214
+ ...(prompt.turnId ? { turnId: prompt.turnId } : {}),
215
+ });
216
+ const live = previous.filter((message) => message.role === 'assistant' &&
217
+ message.id.startsWith('assistant:') &&
218
+ !retained.has(message.id));
219
+ const extras = unique([...optimistic, ...live]);
220
+ const canonicalIndex = new Map(canonical.map((message, index) => [message.id, index]));
221
+ const slots = new Map();
222
+ for (const message of extras) {
223
+ let after = null;
224
+ if (message.turnId) {
225
+ for (let index = canonical.length - 1; index >= 0; index -= 1) {
226
+ if (canonical[index]?.turnId === message.turnId) {
227
+ after = index;
228
+ break;
229
+ }
230
+ }
231
+ }
232
+ if (after === null) {
233
+ const index = previousIndex.get(message.id);
234
+ if (index !== undefined) {
235
+ for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
236
+ const anchor = canonicalIndex.get(previous[cursor].id);
237
+ if (anchor !== undefined) {
238
+ after = anchor;
239
+ break;
240
+ }
241
+ }
242
+ if (after === null) {
243
+ for (let cursor = index + 1; cursor < previous.length; cursor += 1) {
244
+ const anchor = canonicalIndex.get(previous[cursor].id);
245
+ if (anchor !== undefined) {
246
+ after = anchor - 1;
247
+ break;
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+ const slot = after ?? canonical.length - 1;
254
+ slots.set(slot, [...(slots.get(slot) ?? []), message]);
255
+ }
256
+ return unique([
257
+ ...(slots.get(-1) ?? []),
258
+ ...canonical.flatMap((message, index) => [message, ...(slots.get(index) ?? [])]),
259
+ ]);
260
+ }
261
+ /** Cache input is deliberately treated as untrusted: it is only a local rendering hint. */
262
+ export function hydrateCache(sessionId, cached) {
263
+ if (!cached || typeof cached !== 'object')
264
+ return createChatProjection(sessionId);
265
+ const candidate = cached;
266
+ if (!Number.isInteger(candidate.cursor) ||
267
+ candidate.cursor < 0 ||
268
+ !Array.isArray(candidate.messages) ||
269
+ !Array.isArray(candidate.prompts) ||
270
+ !Array.isArray(candidate.interactions))
271
+ return createChatProjection(sessionId);
272
+ const promptStates = ['submitting', 'accepted', 'failed', 'canonical'];
273
+ const interactionStates = ['pending', 'submitting', 'resolved', 'failed'];
274
+ const lifecycles = [
275
+ 'starting',
276
+ 'working',
277
+ 'finished',
278
+ 'interrupted',
279
+ 'recoverable',
280
+ ];
281
+ return {
282
+ ...createChatProjection(sessionId),
283
+ sessionId,
284
+ cursor: candidate.cursor,
285
+ lifecycle: lifecycles.includes(candidate.lifecycle)
286
+ ? candidate.lifecycle
287
+ : 'finished',
288
+ activeTurnId: typeof candidate.activeTurnId === 'string' || candidate.activeTurnId === null
289
+ ? candidate.activeTurnId
290
+ : null,
291
+ snapshotting: false,
292
+ buffered: new Map(),
293
+ messages: candidate.messages
294
+ .filter((message) => Boolean(message &&
295
+ typeof message.id === 'string' &&
296
+ (message.role === 'user' || message.role === 'assistant' || message.role === 'audit') &&
297
+ typeof message.text === 'string' &&
298
+ (message.occurredAt === undefined ||
299
+ (typeof message.occurredAt === 'number' && Number.isFinite(message.occurredAt)))))
300
+ .slice(-200),
301
+ activities: Array.isArray(candidate.activities)
302
+ ? candidate.activities
303
+ .filter((activity) => Boolean(activity &&
304
+ typeof activity.id === 'string' &&
305
+ typeof activity.label === 'string' &&
306
+ typeof activity.detail === 'string' &&
307
+ (activity.changes === undefined ||
308
+ (Array.isArray(activity.changes) &&
309
+ activity.changes.every((change) => {
310
+ if (!change || typeof change !== 'object')
311
+ return false;
312
+ const value = change;
313
+ return (typeof value.path === 'string' &&
314
+ Number.isInteger(value.additions) &&
315
+ value.additions >= 0 &&
316
+ Number.isInteger(value.deletions) &&
317
+ value.deletions >= 0);
318
+ }))) &&
319
+ (activity.turnId === undefined || typeof activity.turnId === 'string')))
320
+ .slice(-200)
321
+ : [],
322
+ prompts: candidate.prompts
323
+ .filter((prompt) => Boolean(prompt &&
324
+ typeof prompt.operationId === 'string' &&
325
+ typeof prompt.key === 'string' &&
326
+ typeof prompt.text === 'string' &&
327
+ promptStates.includes(prompt.state) &&
328
+ (prompt.occurredAt === undefined ||
329
+ (typeof prompt.occurredAt === 'number' && Number.isFinite(prompt.occurredAt)))))
330
+ .slice(-200),
331
+ interactions: candidate.interactions
332
+ .filter((item) => Boolean(item &&
333
+ typeof item.requestId === 'string' &&
334
+ typeof item.key === 'string' &&
335
+ typeof item.kind === 'string' &&
336
+ (item.turnId === undefined || item.turnId === null || typeof item.turnId === 'string') &&
337
+ interactionStates.includes(item.state) &&
338
+ (item.occurredAt === undefined ||
339
+ (typeof item.occurredAt === 'number' && Number.isFinite(item.occurredAt)))))
340
+ .slice(-200),
341
+ };
342
+ }
343
+ /** Accepts authoritative history without discarding a still-pending local operation. */
344
+ export function acceptSnapshot(current, snapshot) {
345
+ if (!Number.isInteger(snapshot.baseSequence) || snapshot.baseSequence < 0)
346
+ return current;
347
+ // The projection has already applied state newer than this snapshot cut. Rebuilding
348
+ // from the stale snapshot would roll those sequenced events back, so retain the
349
+ // current projection and only drain events buffered while the request was in flight.
350
+ if (snapshot.baseSequence < current.cursor)
351
+ return replayBuffered({ ...current, snapshotting: false });
352
+ const canonicalUsers = snapshot.items.filter((item) => item.kind === 'user');
353
+ const prompts = current.prompts.map((prompt) => {
354
+ const correlated = canonicalUsers.some((item) => (typeof item.turnId === 'string' && item.turnId === prompt.turnId) ||
355
+ (typeof item.operationId === 'string' && item.operationId === prompt.operationId));
356
+ const sameText = canonicalUsers.filter((item) => item.text === prompt.text);
357
+ // Text is only a safe fallback when it identifies a single outstanding prompt and item.
358
+ const unambiguousFallback = !prompt.turnId &&
359
+ sameText.length === 1 &&
360
+ current.prompts.filter((item) => item.state !== 'canonical' && item.text === prompt.text)
361
+ .length === 1;
362
+ return correlated || unambiguousFallback ? { ...prompt, state: 'canonical' } : prompt;
363
+ });
364
+ const canonicalInteractions = snapshot.interactions.map(interaction);
365
+ const priorById = new Map(current.interactions.map((item) => [item.requestId, item]));
366
+ const interactions = unique(canonicalInteractions.map((next) => {
367
+ const prior = priorById.get(next.requestId);
368
+ if (prior?.state === 'resolved')
369
+ return prior;
370
+ return {
371
+ ...next,
372
+ ...(next.occurredAt === undefined && prior?.occurredAt !== undefined
373
+ ? { occurredAt: prior.occurredAt }
374
+ : {}),
375
+ };
376
+ }));
377
+ const canonicalActivities = unique(snapshot.items
378
+ .flatMap((item) => {
379
+ const activity = toActivity(item);
380
+ return activity ? [{ ...activity, key: `activity:${activity.id}` }] : [];
381
+ })
382
+ .concat((snapshot.activities ?? []).map((activity) => ({
383
+ ...activity,
384
+ key: `activity:${activity.id}`,
385
+ })))).map((item) => ({
386
+ id: item.id,
387
+ label: item.label,
388
+ detail: item.detail,
389
+ ...(item.turnId ? { turnId: item.turnId } : {}),
390
+ ...(item.actorTurnId ? { actorTurnId: item.actorTurnId } : {}),
391
+ ...(item.occurredAt !== undefined ? { occurredAt: item.occurredAt } : {}),
392
+ ...(item.changes ? { changes: item.changes } : {}),
393
+ }));
394
+ const activities = canonicalActivities.reduce(upsertActivity, [...current.activities]);
395
+ const turnTimes = new Map(snapshot.turns.map((turn) => [turn.id, turn]));
396
+ const items = snapshot.items.map((item) => {
397
+ if (typeof item.occurredAt === 'number' && Number.isFinite(item.occurredAt))
398
+ return item;
399
+ const turn = typeof item.turnId === 'string' ? turnTimes.get(item.turnId) : undefined;
400
+ const occurredAt = item.kind === 'agent'
401
+ ? (turn?.completedAt ?? turn?.startedAt)
402
+ : (turn?.startedAt ?? turn?.completedAt);
403
+ return typeof occurredAt === 'number' && Number.isFinite(occurredAt)
404
+ ? { ...item, occurredAt }
405
+ : item;
406
+ });
407
+ const auditItems = (snapshot.autopilotAudit ?? []).filter((item) => typeof item?.id === 'string' &&
408
+ typeof item.label === 'string' &&
409
+ typeof item.occurredAt === 'number' &&
410
+ Number.isFinite(item.occurredAt));
411
+ const projected = {
412
+ ...current,
413
+ cursor: Math.max(current.cursor, snapshot.baseSequence),
414
+ snapshotting: false,
415
+ activeTurnId: snapshot.activeTurnId,
416
+ lifecycle: lifecycle(snapshot.activeTurnId, current.cursor === 0 && current.lifecycle === 'finished' ? 'working' : current.lifecycle),
417
+ prompts,
418
+ messages: unique(mergeMessages(items, prompts, current.messages).concat(auditItems.map(auditMessage))).sort((left, right) => (left.occurredAt ?? Number.MAX_SAFE_INTEGER) -
419
+ (right.occurredAt ?? Number.MAX_SAFE_INTEGER)),
420
+ activities,
421
+ interactions,
422
+ autopilotAuditTruncated: snapshot.autopilotAuditTruncated === true,
423
+ buffered: new Map([...current.buffered].filter(([sequence]) => sequence > snapshot.baseSequence)),
424
+ };
425
+ return replayBuffered({
426
+ ...projected,
427
+ messages: canonicalAutopilotAudit(projected.messages),
428
+ });
429
+ }
430
+ /** History items and turn-started journal records describe the same coordinator action. */
431
+ function canonicalAutopilotAudit(messages) {
432
+ const seenControls = new Set();
433
+ return messages.filter((message) => {
434
+ if (message.role !== 'audit' || !message.controlId)
435
+ return true;
436
+ if (seenControls.has(message.controlId))
437
+ return false;
438
+ seenControls.add(message.controlId);
439
+ return true;
440
+ });
441
+ }
442
+ export function beginSnapshot(current) {
443
+ return { ...current, snapshotting: true };
444
+ }
445
+ export function queuePrompt(current, operationId, text, occurredAt) {
446
+ if (!text.trim() || current.prompts.some((prompt) => prompt.operationId === operationId))
447
+ return current;
448
+ const prompt = {
449
+ operationId,
450
+ key: `prompt:${operationId}`,
451
+ text,
452
+ state: 'submitting',
453
+ turnId: null,
454
+ ...(typeof occurredAt === 'number' && Number.isFinite(occurredAt) ? { occurredAt } : {}),
455
+ };
456
+ return {
457
+ ...current,
458
+ lifecycle: 'starting',
459
+ prompts: [...current.prompts, prompt],
460
+ messages: [
461
+ ...current.messages,
462
+ {
463
+ id: prompt.key,
464
+ role: 'user',
465
+ text,
466
+ ...(prompt.occurredAt !== undefined ? { occurredAt: prompt.occurredAt } : {}),
467
+ complete: false,
468
+ },
469
+ ],
470
+ };
471
+ }
472
+ export function queueActivePrompt(current, operationId, text, occurredAt) {
473
+ const queued = queuePrompt(current, operationId, text, occurredAt);
474
+ return queued === current ? current : { ...queued, lifecycle: 'working' };
475
+ }
476
+ export function promotePrompt(current, operationId, turnId) {
477
+ return {
478
+ ...current,
479
+ activeTurnId: turnId ?? current.activeTurnId,
480
+ lifecycle: turnId ? 'working' : current.lifecycle,
481
+ prompts: current.prompts.map((prompt) => prompt.operationId === operationId ? { ...prompt, state: 'accepted', turnId } : prompt),
482
+ messages: current.messages.map((message) => message.id === `prompt:${operationId}`
483
+ ? { ...message, complete: true, ...(turnId ? { turnId } : {}) }
484
+ : message),
485
+ };
486
+ }
487
+ export function failPrompt(current, operationId) {
488
+ return {
489
+ ...current,
490
+ lifecycle: 'recoverable',
491
+ prompts: current.prompts.map((prompt) => prompt.operationId === operationId ? { ...prompt, state: 'failed' } : prompt),
492
+ messages: current.messages.map((message) => message.id === `prompt:${operationId}` ? { ...message, complete: true } : message),
493
+ };
494
+ }
495
+ export function beginInteraction(current, requestId, operationId) {
496
+ return {
497
+ ...current,
498
+ interactions: current.interactions.map((item) => item.requestId === requestId && (item.state === 'pending' || item.state === 'failed')
499
+ ? { ...item, state: 'submitting', ...(operationId ? { operationId } : {}) }
500
+ : item),
501
+ };
502
+ }
503
+ export function failInteraction(current, requestId, attemptedOutcome) {
504
+ return {
505
+ ...current,
506
+ interactions: current.interactions.map((item) => item.requestId === requestId && item.state !== 'resolved'
507
+ ? { ...item, state: 'failed', attemptedOutcome }
508
+ : item),
509
+ };
510
+ }
511
+ export function resolveInteraction(current, requestId, attemptedOutcome) {
512
+ return {
513
+ ...current,
514
+ interactions: current.interactions.map((item) => item.requestId === requestId
515
+ ? {
516
+ ...item,
517
+ state: 'resolved',
518
+ ...(safeOutcome(item, attemptedOutcome)
519
+ ? { attemptedOutcome: safeOutcome(item, attemptedOutcome) }
520
+ : {}),
521
+ }
522
+ : item),
523
+ };
524
+ }
525
+ export function applyProjectionEvent(current, event) {
526
+ if (!Number.isInteger(event.sequence) || event.sequence <= current.cursor)
527
+ return current;
528
+ if (current.snapshotting || event.sequence > current.cursor + 1) {
529
+ const buffered = new Map(current.buffered);
530
+ buffered.set(event.sequence, event);
531
+ return { ...current, snapshotting: true, buffered };
532
+ }
533
+ let next = { ...current, cursor: event.sequence };
534
+ const payload = event.payload;
535
+ const occurredAt = parseOccurredAt(event.occurredAt);
536
+ if (event.type === 'agentMessageDelta' && typeof payload?.text === 'string') {
537
+ const owner = typeof payload.turnId === 'string' ? payload.turnId : (next.activeTurnId ?? undefined);
538
+ const itemId = typeof payload.itemId === 'string' ? payload.itemId : (owner ?? 'live');
539
+ const id = `assistant:${itemId}`;
540
+ const existing = next.messages.find((message) => message.id === id);
541
+ next = {
542
+ ...next,
543
+ lifecycle: next.activeTurnId ? 'working' : next.lifecycle,
544
+ messages: existing
545
+ ? next.messages.map((message) => message.id === id
546
+ ? {
547
+ ...message,
548
+ text: message.text + payload.text,
549
+ ...(payload.phase === 'commentary' || payload.phase === 'final_answer'
550
+ ? { phase: payload.phase }
551
+ : {}),
552
+ complete: payload.phase === 'final_answer' ? true : message.complete,
553
+ ...(message.occurredAt === undefined && occurredAt !== undefined
554
+ ? { occurredAt }
555
+ : {}),
556
+ }
557
+ : message)
558
+ : [
559
+ ...next.messages,
560
+ {
561
+ id,
562
+ role: 'assistant',
563
+ ...(payload.phase === 'commentary' || payload.phase === 'final_answer'
564
+ ? { phase: payload.phase }
565
+ : {}),
566
+ text: payload.text,
567
+ ...(owner ? { turnId: owner } : {}),
568
+ ...(occurredAt !== undefined ? { occurredAt } : {}),
569
+ complete: payload.phase === 'final_answer',
570
+ },
571
+ ],
572
+ };
573
+ }
574
+ else if ((event.type === 'agentMessageStarted' || event.type === 'agentMessageCompleted') &&
575
+ typeof payload.text === 'string') {
576
+ const itemId = typeof payload.itemId === 'string' ? payload.itemId : (next.activeTurnId ?? 'live');
577
+ const id = `assistant:${itemId}`;
578
+ const existing = next.messages.find((message) => message.id === id);
579
+ const complete = event.type === 'agentMessageCompleted';
580
+ const message = {
581
+ id,
582
+ role: 'assistant',
583
+ text: complete && payload.text ? payload.text : (existing?.text ?? payload.text),
584
+ complete,
585
+ ...(payload.phase === 'commentary' || payload.phase === 'final_answer'
586
+ ? { phase: payload.phase }
587
+ : existing?.phase
588
+ ? { phase: existing.phase }
589
+ : {}),
590
+ ...(typeof payload.turnId === 'string'
591
+ ? { turnId: payload.turnId }
592
+ : existing?.turnId
593
+ ? { turnId: existing.turnId }
594
+ : next.activeTurnId
595
+ ? { turnId: next.activeTurnId }
596
+ : {}),
597
+ ...(existing?.occurredAt !== undefined
598
+ ? { occurredAt: existing.occurredAt }
599
+ : occurredAt !== undefined
600
+ ? { occurredAt }
601
+ : {}),
602
+ };
603
+ next = {
604
+ ...next,
605
+ lifecycle: next.activeTurnId ? 'working' : next.lifecycle,
606
+ messages: existing
607
+ ? next.messages.map((item) => (item.id === id ? message : item))
608
+ : [...next.messages, message],
609
+ };
610
+ }
611
+ else if (event.type === 'turnCompleted' || event.type === 'turnInterrupted') {
612
+ next = {
613
+ ...next,
614
+ activeTurnId: null,
615
+ lifecycle: event.type === 'turnCompleted' || next.lifecycle === 'finished'
616
+ ? 'finished'
617
+ : 'interrupted',
618
+ messages: next.messages.map((message) => message.role === 'assistant' ? { ...message, complete: true } : message),
619
+ };
620
+ }
621
+ else if (event.type === 'interaction.requested' &&
622
+ typeof payload?.requestId === 'string' &&
623
+ typeof payload.kind === 'string') {
624
+ const existing = next.interactions.find((item) => item.requestId === payload.requestId);
625
+ if (!existing || existing.state === 'resolved') {
626
+ const requested = {
627
+ requestId: payload.requestId,
628
+ key: `interaction:${payload.requestId}`,
629
+ kind: payload.kind,
630
+ turnId: typeof payload.turnId === 'string' ? payload.turnId : next.activeTurnId,
631
+ payload: payload.payload,
632
+ state: 'pending',
633
+ ...(occurredAt !== undefined ? { occurredAt } : {}),
634
+ };
635
+ next = {
636
+ ...next,
637
+ interactions: existing
638
+ ? next.interactions.map((item) => item.requestId === payload.requestId ? requested : item)
639
+ : [...next.interactions, requested],
640
+ };
641
+ }
642
+ }
643
+ else if (event.type === 'interaction.resolved' && typeof payload?.requestId === 'string')
644
+ next = resolveInteraction(next, payload.requestId, payload.outcome);
645
+ else if (event.type === 'activity.updated' &&
646
+ typeof payload?.id === 'string' &&
647
+ typeof payload.label === 'string' &&
648
+ typeof payload.detail === 'string') {
649
+ const changes = safeFileChanges(payload.changes);
650
+ next = {
651
+ ...next,
652
+ activities: upsertActivity(next.activities, {
653
+ id: payload.id,
654
+ label: payload.label,
655
+ detail: payload.detail,
656
+ ...(occurredAt !== undefined ? { occurredAt } : {}),
657
+ ...(changes.length ? { changes } : {}),
658
+ ...(typeof payload.turnId === 'string'
659
+ ? { turnId: payload.turnId }
660
+ : next.activeTurnId
661
+ ? { turnId: next.activeTurnId }
662
+ : {}),
663
+ ...(typeof payload.actorTurnId === 'string' ? { actorTurnId: payload.actorTurnId } : {}),
664
+ }),
665
+ };
666
+ }
667
+ else if (event.type === 'session.updated' && typeof payload?.activeTurnId !== 'undefined') {
668
+ const id = typeof payload.activeTurnId === 'string' ? payload.activeTurnId : null;
669
+ next = { ...next, activeTurnId: id, lifecycle: id ? 'working' : lifecycle(id, next.lifecycle) };
670
+ }
671
+ const audit = auditEventMessage(event);
672
+ if (audit && !next.messages.some((message) => message.id === audit.id))
673
+ next = { ...next, messages: canonicalAutopilotAudit([...next.messages, audit]) };
674
+ return next;
675
+ }
676
+ function safeFileChanges(value) {
677
+ if (!Array.isArray(value) || value.length > 1_000)
678
+ return [];
679
+ return value.flatMap((change) => {
680
+ if (!change || typeof change !== 'object')
681
+ return [];
682
+ const item = change;
683
+ return typeof item.path === 'string' &&
684
+ Number.isInteger(item.additions) &&
685
+ item.additions >= 0 &&
686
+ Number.isInteger(item.deletions) &&
687
+ item.deletions >= 0
688
+ ? [
689
+ {
690
+ path: item.path,
691
+ additions: item.additions,
692
+ deletions: item.deletions,
693
+ },
694
+ ]
695
+ : [];
696
+ });
697
+ }
698
+ export function replayBuffered(current) {
699
+ let buffer = new Map(current.buffered);
700
+ let next = { ...current, snapshotting: false, buffered: buffer };
701
+ for (;;) {
702
+ const event = buffer.get(next.cursor + 1);
703
+ if (!event)
704
+ return { ...next, buffered: buffer, snapshotting: buffer.size > 0 };
705
+ buffer.delete(event.sequence);
706
+ next = applyProjectionEvent({ ...next, buffered: buffer }, event);
707
+ buffer = new Map(next.buffered);
708
+ }
709
+ }
710
+ export function deriveStatus(projection) {
711
+ if (!projection.sessionId)
712
+ return 'Choose a workspace and start a session.';
713
+ if (projection.snapshotting || projection.lifecycle === 'recoverable')
714
+ return 'Connection interrupted. Reconnecting…';
715
+ if (projection.lifecycle === 'starting')
716
+ return 'Starting Codex turn…';
717
+ if (projection.lifecycle === 'working')
718
+ return 'Codex is working…';
719
+ if (projection.lifecycle === 'interrupted')
720
+ return 'Codex turn interrupted.';
721
+ return 'Ready.';
722
+ }