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.
- package/README.md +60 -3
- package/dist/client/assets/index-9XMv2BvD.js +38 -0
- package/dist/client/assets/{index-CPQBJDsg.css → index-Bxv5Z2w4.css} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/client/features/chat/activity-summary.js +71 -0
- package/dist/server/client/features/chat/chat-projection.js +722 -0
- package/dist/server/client/features/chat/message-store.js +30 -0
- package/dist/server/server/app.js +1 -1
- package/dist/server/server/cli.js +8 -0
- package/dist/server/server/composition.contracts.js +158 -6
- package/dist/server/server/composition.js +266 -56
- package/dist/server/server/features/autopilot/register-routes.js +2 -2
- package/dist/server/server/features/autopilot/toggle/endpoint.js +5 -1
- package/dist/server/server/features/catalog/get-bootstrap/use-case.js +5 -3
- package/dist/server/server/features/sessions/get-session/endpoint.js +1 -1
- package/dist/server/server/features/sessions/list-models/endpoint.js +16 -0
- package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +8 -3
- package/dist/server/server/features/sessions/list-sessions/endpoint.js +1 -1
- package/dist/server/server/features/sessions/model/relay-session.js +2 -0
- package/dist/server/server/features/sessions/promote-recent-thread/endpoint.js +6 -2
- package/dist/server/server/features/sessions/promote-recent-thread/use-case.js +7 -2
- package/dist/server/server/features/sessions/register-routes.js +8 -3
- package/dist/server/server/features/sessions/respond-interaction/endpoint.js +1 -1
- package/dist/server/server/features/sessions/select-model/endpoint.js +1 -1
- package/dist/server/server/features/sessions/start-session/request.js +1 -0
- package/dist/server/server/features/sessions/start-session/use-case.js +21 -7
- package/dist/server/server/features/skills/list-available/endpoint.js +4 -2
- package/dist/server/server/platform/catalog/provider-model-catalog.js +15 -0
- package/dist/server/server/platform/kimi/kimi-activity-facts.js +88 -0
- package/dist/server/server/platform/kimi/kimi-errors.js +30 -0
- package/dist/server/server/platform/kimi/kimi-model-catalog.js +63 -0
- package/dist/server/server/platform/kimi/kimi-normalizer.js +196 -0
- package/dist/server/server/platform/kimi/kimi-recent-session-lister.js +68 -0
- package/dist/server/server/platform/kimi/kimi-server-request.js +143 -0
- package/dist/server/server/platform/kimi/kimi-session-runtime.js +653 -0
- package/dist/server/server/platform/kimi/kimi-share-dir.js +65 -0
- package/dist/server/server/platform/kimi/kimi-skill-catalog.js +79 -0
- package/dist/server/server/platform/kimi/kimi-skills-materializer.js +41 -0
- package/dist/server/server/platform/kimi/kimi-web-client.js +63 -0
- package/dist/server/server/platform/kimi/kimi-web-server-manager.js +215 -0
- package/dist/server/server/platform/kimi/kimi-ws-client.js +154 -0
- package/dist/server/server/platform/persistence/migrate.js +2 -0
- package/dist/server/server/platform/persistence/sqlite-session-repository.js +3 -2
- package/dist/server/server/platform/skills/cached-skill-catalog.js +0 -0
- package/dist/server/shared/contracts/llm-provider.js +6 -0
- package/package.json +4 -3
- 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
|
+
}
|