codekanban 0.29.0 → 0.31.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.
|
@@ -1,35 +1,39 @@
|
|
|
1
|
-
import path from
|
|
1
|
+
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import { CodeKanbanValidationError } from
|
|
4
|
-
import {
|
|
3
|
+
import { CodeKanbanValidationError } from "./errors.js";
|
|
4
|
+
import {
|
|
5
|
+
ensureArrayOfStrings,
|
|
6
|
+
ensureOptionalString,
|
|
7
|
+
ensureString,
|
|
8
|
+
} from "./utils.js";
|
|
5
9
|
|
|
6
10
|
export const WEB_SESSION_PROTOCOL_VERSION = 1;
|
|
7
|
-
export const WEB_SESSION_COMMAND_WS_PATH =
|
|
8
|
-
export const WEB_SESSION_EVENTS_WS_PATH =
|
|
9
|
-
export const WEB_SESSION_HEARTBEAT_KIND =
|
|
11
|
+
export const WEB_SESSION_COMMAND_WS_PATH = "/api/v1/web-sessions/ws";
|
|
12
|
+
export const WEB_SESSION_EVENTS_WS_PATH = "/api/v1/web-sessions/events";
|
|
13
|
+
export const WEB_SESSION_HEARTBEAT_KIND = "hb";
|
|
10
14
|
|
|
11
15
|
const IMAGE_MIME_BY_EXT = new Map([
|
|
12
|
-
[
|
|
13
|
-
[
|
|
14
|
-
[
|
|
15
|
-
[
|
|
16
|
-
[
|
|
17
|
-
[
|
|
18
|
-
[
|
|
19
|
-
[
|
|
20
|
-
[
|
|
21
|
-
[
|
|
22
|
-
[
|
|
23
|
-
[
|
|
24
|
-
[
|
|
16
|
+
[".apng", "image/apng"],
|
|
17
|
+
[".bmp", "image/bmp"],
|
|
18
|
+
[".gif", "image/gif"],
|
|
19
|
+
[".heic", "image/heic"],
|
|
20
|
+
[".heif", "image/heif"],
|
|
21
|
+
[".jpeg", "image/jpeg"],
|
|
22
|
+
[".jpg", "image/jpeg"],
|
|
23
|
+
[".png", "image/png"],
|
|
24
|
+
[".svg", "image/svg+xml"],
|
|
25
|
+
[".svgz", "image/svg+xml"],
|
|
26
|
+
[".tif", "image/tiff"],
|
|
27
|
+
[".tiff", "image/tiff"],
|
|
28
|
+
[".webp", "image/webp"],
|
|
25
29
|
]);
|
|
26
30
|
|
|
27
31
|
function stringValue(value) {
|
|
28
|
-
if (typeof value ===
|
|
32
|
+
if (typeof value === "string") {
|
|
29
33
|
return value;
|
|
30
34
|
}
|
|
31
35
|
if (value == null) {
|
|
32
|
-
return
|
|
36
|
+
return "";
|
|
33
37
|
}
|
|
34
38
|
return String(value);
|
|
35
39
|
}
|
|
@@ -95,7 +99,7 @@ function normalizeHistoryToolCommandGroup(value) {
|
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
function normalizeHistoryTool(value) {
|
|
98
|
-
if (!value || typeof value !==
|
|
102
|
+
if (!value || typeof value !== "object") {
|
|
99
103
|
return null;
|
|
100
104
|
}
|
|
101
105
|
return {
|
|
@@ -104,8 +108,8 @@ function normalizeHistoryTool(value) {
|
|
|
104
108
|
kind: trimmedString(value.kind) || null,
|
|
105
109
|
input: value.in,
|
|
106
110
|
output: trimmedString(value.out) || null,
|
|
107
|
-
status: trimmedString(value.st) ||
|
|
108
|
-
meta: value.meta && typeof value.meta ===
|
|
111
|
+
status: trimmedString(value.st) || "running",
|
|
112
|
+
meta: value.meta && typeof value.meta === "object" ? value.meta : null,
|
|
109
113
|
commandGroup: normalizeHistoryToolCommandGroup(value.cg),
|
|
110
114
|
};
|
|
111
115
|
}
|
|
@@ -124,7 +128,9 @@ function normalizeUserInputQuestion(value) {
|
|
|
124
128
|
question: trimmedString(value?.question),
|
|
125
129
|
isOther: booleanValue(value?.isOther),
|
|
126
130
|
isSecret: booleanValue(value?.isSecret),
|
|
127
|
-
options: Array.isArray(value?.options)
|
|
131
|
+
options: Array.isArray(value?.options)
|
|
132
|
+
? value.options.map(normalizeUserInputOption)
|
|
133
|
+
: [],
|
|
128
134
|
};
|
|
129
135
|
}
|
|
130
136
|
|
|
@@ -132,49 +138,83 @@ function normalizeHistoryAnswerEntry(value) {
|
|
|
132
138
|
return {
|
|
133
139
|
id: trimmedString(value?.id),
|
|
134
140
|
label: trimmedString(value?.label),
|
|
135
|
-
values: ensureArrayOfStrings(value?.values,
|
|
141
|
+
values: ensureArrayOfStrings(value?.values, "values"),
|
|
136
142
|
masked: booleanValue(value?.masked),
|
|
137
143
|
};
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
function normalizeHistoryDetail(value) {
|
|
141
|
-
if (!value || typeof value !==
|
|
147
|
+
if (!value || typeof value !== "object") {
|
|
142
148
|
return null;
|
|
143
149
|
}
|
|
144
150
|
return {
|
|
145
151
|
type: trimmedString(value.type),
|
|
146
152
|
prompt: trimmedString(value.prompt) || null,
|
|
147
|
-
questions: Array.isArray(value.questions)
|
|
148
|
-
|
|
153
|
+
questions: Array.isArray(value.questions)
|
|
154
|
+
? value.questions.map(normalizeUserInputQuestion)
|
|
155
|
+
: [],
|
|
156
|
+
answers: Array.isArray(value.answers)
|
|
157
|
+
? value.answers.map(normalizeHistoryAnswerEntry)
|
|
158
|
+
: [],
|
|
149
159
|
action: trimmedString(value.action) || null,
|
|
150
160
|
};
|
|
151
161
|
}
|
|
152
162
|
|
|
163
|
+
function normalizePendingInput(value) {
|
|
164
|
+
const id = trimmedString(value?.id);
|
|
165
|
+
if (!id) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const mode = trimmedString(value?.m || value?.mode);
|
|
169
|
+
if (mode !== "redirect" && mode !== "queue") {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
id,
|
|
174
|
+
mode,
|
|
175
|
+
text: stringValue(value?.txt ?? value?.text),
|
|
176
|
+
attachmentIds: ensureArrayOfStrings(
|
|
177
|
+
value?.atts ?? value?.attachmentIds,
|
|
178
|
+
"attachmentIds",
|
|
179
|
+
),
|
|
180
|
+
createdAt:
|
|
181
|
+
isoFromUnixMilli(value?.ca) ||
|
|
182
|
+
(typeof value?.createdAt === "string"
|
|
183
|
+
? trimmedString(value.createdAt) || null
|
|
184
|
+
: null),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
153
188
|
export function normalizeWebSessionHistoryItem(value) {
|
|
154
|
-
const updatedTimestamp =
|
|
189
|
+
const updatedTimestamp =
|
|
190
|
+
isoFromUnixMilli(value?.ts2) || isoFromUnixMilli(value?.obs);
|
|
155
191
|
return {
|
|
156
192
|
id: trimmedString(value?.id),
|
|
157
193
|
sourceTurnId: trimmedString(value?.stid) || null,
|
|
158
194
|
sourceItemId: trimmedString(value?.siid) || null,
|
|
159
195
|
orderIndex: numberValue(value?.oi, 0),
|
|
160
|
-
kind: trimmedString(value?.kd) ||
|
|
196
|
+
kind: trimmedString(value?.kd) || "system",
|
|
161
197
|
itemType: trimmedString(value?.tp),
|
|
162
198
|
text: stringValue(value?.txt),
|
|
163
199
|
timestamp: isoFromUnixMilli(value?.ts2),
|
|
164
200
|
observedAt: isoFromUnixMilli(value?.obs),
|
|
165
|
-
attachments: Array.isArray(value?.atts)
|
|
201
|
+
attachments: Array.isArray(value?.atts)
|
|
202
|
+
? value.atts.map(normalizeHistoryAttachment)
|
|
203
|
+
: [],
|
|
166
204
|
tool: normalizeHistoryTool(value?.tl),
|
|
167
205
|
level: trimmedString(value?.lvl) || null,
|
|
168
206
|
done: booleanValue(value?.dn),
|
|
169
207
|
detail: normalizeHistoryDetail(value?.dt),
|
|
170
|
-
payload: value?.pl && typeof value.pl ===
|
|
208
|
+
payload: value?.pl && typeof value.pl === "object" ? value.pl : null,
|
|
171
209
|
updatedAt: updatedTimestamp,
|
|
172
210
|
};
|
|
173
211
|
}
|
|
174
212
|
|
|
175
213
|
export function normalizeWebSessionHistoryWindow(value) {
|
|
176
214
|
return {
|
|
177
|
-
items: Array.isArray(value?.its)
|
|
215
|
+
items: Array.isArray(value?.its)
|
|
216
|
+
? value.its.map(normalizeWebSessionHistoryItem)
|
|
217
|
+
: [],
|
|
178
218
|
hasMore: booleanValue(value?.hm),
|
|
179
219
|
beforeCursor: trimmedString(value?.bc) || null,
|
|
180
220
|
total: numberValue(value?.tot, 0),
|
|
@@ -188,23 +228,24 @@ export function normalizeWebSessionSummaryFromWire(value) {
|
|
|
188
228
|
projectId: trimmedString(value?.pid),
|
|
189
229
|
worktreeId: trimmedString(value?.wid) || null,
|
|
190
230
|
orderIndex: numberValue(value?.oi, 0),
|
|
191
|
-
agent: trimmedString(value?.ag) ||
|
|
231
|
+
agent: trimmedString(value?.ag) || "codex",
|
|
192
232
|
title: trimmedString(value?.ttl),
|
|
193
233
|
model: trimmedString(value?.md),
|
|
194
|
-
reasoningEffort: trimmedString(value?.re) ||
|
|
195
|
-
workflowMode: trimmedString(value?.wm) ||
|
|
196
|
-
permissionLevel: trimmedString(value?.pl) ||
|
|
234
|
+
reasoningEffort: trimmedString(value?.re) || "default",
|
|
235
|
+
workflowMode: trimmedString(value?.wm) || "default",
|
|
236
|
+
permissionLevel: trimmedString(value?.pl) || "elevated",
|
|
197
237
|
cwd: trimmedString(value?.cwd),
|
|
198
238
|
nativeSessionId: trimmedString(value?.nsid) || null,
|
|
199
|
-
status: trimmedString(value?.st) ||
|
|
239
|
+
status: trimmedString(value?.st) || "idle",
|
|
200
240
|
assistantState: trimmedString(value?.ast) || null,
|
|
201
241
|
hasUnread: booleanValue(value?.unr),
|
|
202
242
|
archivedAt: isoFromUnixMilli(value?.aa),
|
|
203
|
-
activityAt:
|
|
243
|
+
activityAt:
|
|
244
|
+
isoFromUnixMilli(value?.act) || new Date(updatedAtMs).toISOString(),
|
|
204
245
|
lastMessageAt: isoFromUnixMilli(value?.lma),
|
|
205
246
|
assistantStateUpdatedAt: isoFromUnixMilli(value?.asu),
|
|
206
|
-
sourceKind: trimmedString(value?.sk) ||
|
|
207
|
-
syncState: trimmedString(value?.ss) ||
|
|
247
|
+
sourceKind: trimmedString(value?.sk) || "codex_app_server",
|
|
248
|
+
syncState: trimmedString(value?.ss) || "missing",
|
|
208
249
|
lastSyncMode: trimmedString(value?.lsm) || null,
|
|
209
250
|
sourceCreatedAt: isoFromUnixMilli(value?.sca),
|
|
210
251
|
sourceUpdatedAt: isoFromUnixMilli(value?.sua),
|
|
@@ -214,14 +255,15 @@ export function normalizeWebSessionSummaryFromWire(value) {
|
|
|
214
255
|
turnCount: numberValue(value?.tc, 0),
|
|
215
256
|
itemCount: numberValue(value?.ic, 0),
|
|
216
257
|
syncError: trimmedString(value?.se) || null,
|
|
217
|
-
createdAt:
|
|
258
|
+
createdAt:
|
|
259
|
+
isoFromUnixMilli(value?.ca) || new Date(updatedAtMs).toISOString(),
|
|
218
260
|
updatedAt: new Date(updatedAtMs).toISOString(),
|
|
219
261
|
usage: {
|
|
220
262
|
...normalizeUsage(value?.usa),
|
|
221
263
|
cost: numberValue(value?.cost, 0),
|
|
222
264
|
},
|
|
223
265
|
contextWindowTokens: nullableNumberValue(value?.cwt),
|
|
224
|
-
contextWindowSource: trimmedString(value?.cws) ||
|
|
266
|
+
contextWindowSource: trimmedString(value?.cws) || "unavailable",
|
|
225
267
|
};
|
|
226
268
|
}
|
|
227
269
|
|
|
@@ -229,11 +271,15 @@ export function normalizeWebSessionSnapshotFromWire(frame) {
|
|
|
229
271
|
return {
|
|
230
272
|
session: normalizeWebSessionSummaryFromWire(frame?.s),
|
|
231
273
|
history: normalizeWebSessionHistoryWindow(frame?.h),
|
|
274
|
+
pendingInputs: Array.isArray(frame?.pi)
|
|
275
|
+
? frame.pi.map(normalizePendingInput).filter(Boolean)
|
|
276
|
+
: [],
|
|
232
277
|
};
|
|
233
278
|
}
|
|
234
279
|
|
|
235
|
-
const PROCESS_RESTART_REASON =
|
|
236
|
-
const DEFAULT_RECOVERY_MESSAGE =
|
|
280
|
+
const PROCESS_RESTART_REASON = "process_restart";
|
|
281
|
+
const DEFAULT_RECOVERY_MESSAGE =
|
|
282
|
+
"Session runtime was interrupted. Send a new message to continue.";
|
|
237
283
|
|
|
238
284
|
function isProcessRestartPayload(payload) {
|
|
239
285
|
return trimmedString(payload?.reason) === PROCESS_RESTART_REASON;
|
|
@@ -244,26 +290,35 @@ function getRecoveryMessage(payload) {
|
|
|
244
290
|
}
|
|
245
291
|
|
|
246
292
|
function normalizeChoiceText(value) {
|
|
247
|
-
return trimmedString(value)
|
|
248
|
-
.toLowerCase()
|
|
249
|
-
.replace(/\s+/g, ' ');
|
|
293
|
+
return trimmedString(value).toLowerCase().replace(/\s+/g, " ");
|
|
250
294
|
}
|
|
251
295
|
|
|
252
296
|
function isExecutePlanOption(option) {
|
|
253
|
-
const text = normalizeChoiceText(
|
|
297
|
+
const text = normalizeChoiceText(
|
|
298
|
+
`${option?.label || ""} ${option?.description || ""}`,
|
|
299
|
+
);
|
|
254
300
|
const mentionsPlan = /计划|plan/.test(text);
|
|
255
|
-
const mentionsExecute =
|
|
301
|
+
const mentionsExecute =
|
|
302
|
+
/开始|执行|实现|实施|继续|start|execute|implement|proceed/.test(text);
|
|
256
303
|
const mentionsCancel = /取消|暂不|稍后|later|cancel|dismiss|hold/.test(text);
|
|
257
304
|
return mentionsExecute && (mentionsPlan || !mentionsCancel);
|
|
258
305
|
}
|
|
259
306
|
|
|
260
307
|
function isCancelPlanOption(option) {
|
|
261
|
-
const text = normalizeChoiceText(
|
|
262
|
-
|
|
308
|
+
const text = normalizeChoiceText(
|
|
309
|
+
`${option?.label || ""} ${option?.description || ""}`,
|
|
310
|
+
);
|
|
311
|
+
return /取消|暂不|稍后|later|cancel|dismiss|hold|keep planning|stay in plan/.test(
|
|
312
|
+
text,
|
|
313
|
+
);
|
|
263
314
|
}
|
|
264
315
|
|
|
265
316
|
function isPlanChoiceQuestion(question) {
|
|
266
|
-
if (
|
|
317
|
+
if (
|
|
318
|
+
!question ||
|
|
319
|
+
!Array.isArray(question.options) ||
|
|
320
|
+
question.options.length !== 2
|
|
321
|
+
) {
|
|
267
322
|
return false;
|
|
268
323
|
}
|
|
269
324
|
const hasExecute = question.options.some(isExecutePlanOption);
|
|
@@ -274,21 +329,25 @@ function isPlanChoiceQuestion(question) {
|
|
|
274
329
|
function findPendingApproval(items) {
|
|
275
330
|
let pending = null;
|
|
276
331
|
for (const item of items) {
|
|
277
|
-
if (item?.detail?.type ===
|
|
332
|
+
if (item?.detail?.type === "approval_request") {
|
|
278
333
|
pending = {
|
|
279
334
|
id: item.id,
|
|
280
335
|
itemId: item.sourceItemId || item.id,
|
|
281
|
-
prompt: item.detail.prompt || item.text ||
|
|
336
|
+
prompt: item.detail.prompt || item.text || "",
|
|
282
337
|
requestedAt: item.timestamp || item.observedAt || null,
|
|
283
338
|
stale: false,
|
|
284
339
|
};
|
|
285
340
|
continue;
|
|
286
341
|
}
|
|
287
|
-
if (item?.detail?.type ===
|
|
342
|
+
if (item?.detail?.type === "approval_response" || item?.kind === "user") {
|
|
288
343
|
pending = null;
|
|
289
344
|
continue;
|
|
290
345
|
}
|
|
291
|
-
if (
|
|
346
|
+
if (
|
|
347
|
+
item?.itemType === "run_abort" &&
|
|
348
|
+
pending &&
|
|
349
|
+
isProcessRestartPayload(item.payload || undefined)
|
|
350
|
+
) {
|
|
292
351
|
pending = {
|
|
293
352
|
...pending,
|
|
294
353
|
stale: true,
|
|
@@ -297,7 +356,7 @@ function findPendingApproval(items) {
|
|
|
297
356
|
};
|
|
298
357
|
continue;
|
|
299
358
|
}
|
|
300
|
-
if (item?.itemType ===
|
|
359
|
+
if (item?.itemType === "run_abort" || item?.itemType === "run_fail") {
|
|
301
360
|
pending = null;
|
|
302
361
|
}
|
|
303
362
|
}
|
|
@@ -307,8 +366,10 @@ function findPendingApproval(items) {
|
|
|
307
366
|
function findPendingUserInput(items) {
|
|
308
367
|
let pending = null;
|
|
309
368
|
for (const item of items) {
|
|
310
|
-
if (item?.detail?.type ===
|
|
311
|
-
const questions = Array.isArray(item.detail.questions)
|
|
369
|
+
if (item?.detail?.type === "user_input_request") {
|
|
370
|
+
const questions = Array.isArray(item.detail.questions)
|
|
371
|
+
? item.detail.questions
|
|
372
|
+
: [];
|
|
312
373
|
const question = questions[0];
|
|
313
374
|
const executeOption =
|
|
314
375
|
questions.length === 1 && isPlanChoiceQuestion(question)
|
|
@@ -316,22 +377,29 @@ function findPendingUserInput(items) {
|
|
|
316
377
|
: null;
|
|
317
378
|
pending = {
|
|
318
379
|
id: item.id,
|
|
319
|
-
itemId:
|
|
320
|
-
|
|
380
|
+
itemId:
|
|
381
|
+
item.sourceItemId || trimmedString(item.payload?.iid) || item.id,
|
|
382
|
+
prompt: item.detail.prompt || item.text || "",
|
|
321
383
|
questions,
|
|
322
384
|
requestedAt: item.timestamp || item.observedAt || null,
|
|
323
385
|
stale: false,
|
|
324
386
|
isPlanChoice: Boolean(executeOption),
|
|
325
387
|
questionId: executeOption ? trimmedString(question?.id) || null : null,
|
|
326
|
-
executeOptionLabel: executeOption
|
|
388
|
+
executeOptionLabel: executeOption
|
|
389
|
+
? trimmedString(executeOption.label) || null
|
|
390
|
+
: null,
|
|
327
391
|
};
|
|
328
392
|
continue;
|
|
329
393
|
}
|
|
330
|
-
if (item?.detail?.type ===
|
|
394
|
+
if (item?.detail?.type === "user_input_response" || item?.kind === "user") {
|
|
331
395
|
pending = null;
|
|
332
396
|
continue;
|
|
333
397
|
}
|
|
334
|
-
if (
|
|
398
|
+
if (
|
|
399
|
+
item?.itemType === "run_abort" &&
|
|
400
|
+
pending &&
|
|
401
|
+
isProcessRestartPayload(item.payload || undefined)
|
|
402
|
+
) {
|
|
335
403
|
pending = {
|
|
336
404
|
...pending,
|
|
337
405
|
stale: true,
|
|
@@ -340,7 +408,7 @@ function findPendingUserInput(items) {
|
|
|
340
408
|
};
|
|
341
409
|
continue;
|
|
342
410
|
}
|
|
343
|
-
if (item?.itemType ===
|
|
411
|
+
if (item?.itemType === "run_abort" || item?.itemType === "run_fail") {
|
|
344
412
|
pending = null;
|
|
345
413
|
}
|
|
346
414
|
}
|
|
@@ -350,18 +418,20 @@ function findPendingUserInput(items) {
|
|
|
350
418
|
function findLatestPlan(items) {
|
|
351
419
|
const latestPlan = [...items]
|
|
352
420
|
.reverse()
|
|
353
|
-
.find(item => item?.kind ===
|
|
421
|
+
.find((item) => item?.kind === "tool" && item?.tool?.kind === "plan");
|
|
354
422
|
if (!latestPlan) {
|
|
355
423
|
return null;
|
|
356
424
|
}
|
|
357
425
|
const hasUserMessageAfter = items.some(
|
|
358
|
-
|
|
426
|
+
(item) =>
|
|
427
|
+
item?.kind === "user" &&
|
|
428
|
+
numberValue(item.orderIndex, 0) > numberValue(latestPlan.orderIndex, 0),
|
|
359
429
|
);
|
|
360
430
|
return {
|
|
361
431
|
id: latestPlan.id,
|
|
362
432
|
itemId: latestPlan.id,
|
|
363
433
|
toolId: latestPlan.tool.id || latestPlan.id,
|
|
364
|
-
output: latestPlan.tool.output || latestPlan.text ||
|
|
434
|
+
output: latestPlan.tool.output || latestPlan.text || "",
|
|
365
435
|
timestamp: latestPlan.timestamp || latestPlan.observedAt || null,
|
|
366
436
|
orderIndex: latestPlan.orderIndex,
|
|
367
437
|
hasUserMessageAfter,
|
|
@@ -370,14 +440,16 @@ function findLatestPlan(items) {
|
|
|
370
440
|
}
|
|
371
441
|
|
|
372
442
|
function findLastAssistantMessage(items) {
|
|
373
|
-
const lastAssistant = [...items]
|
|
443
|
+
const lastAssistant = [...items]
|
|
444
|
+
.reverse()
|
|
445
|
+
.find((item) => item?.kind === "assistant");
|
|
374
446
|
if (!lastAssistant) {
|
|
375
447
|
return null;
|
|
376
448
|
}
|
|
377
449
|
return {
|
|
378
450
|
id: lastAssistant.id,
|
|
379
451
|
itemId: lastAssistant.id,
|
|
380
|
-
text: lastAssistant.text ||
|
|
452
|
+
text: lastAssistant.text || "",
|
|
381
453
|
timestamp: lastAssistant.timestamp || lastAssistant.observedAt || null,
|
|
382
454
|
orderIndex: lastAssistant.orderIndex,
|
|
383
455
|
};
|
|
@@ -385,40 +457,48 @@ function findLastAssistantMessage(items) {
|
|
|
385
457
|
|
|
386
458
|
export function analyzeWebSession(snapshot) {
|
|
387
459
|
const session = snapshot?.session || null;
|
|
388
|
-
const history = snapshot?.history || {
|
|
460
|
+
const history = snapshot?.history || {
|
|
461
|
+
items: [],
|
|
462
|
+
hasMore: false,
|
|
463
|
+
beforeCursor: null,
|
|
464
|
+
total: 0,
|
|
465
|
+
};
|
|
466
|
+
const pendingInputs = Array.isArray(snapshot?.pendingInputs)
|
|
467
|
+
? snapshot.pendingInputs.filter(Boolean)
|
|
468
|
+
: [];
|
|
389
469
|
const items = Array.isArray(history.items) ? history.items : [];
|
|
390
470
|
const pendingApproval = findPendingApproval(items);
|
|
391
471
|
const pendingUserInput = findPendingUserInput(items);
|
|
392
472
|
const latestPlan = findLatestPlan(items);
|
|
393
473
|
const lastAssistantMessage = findLastAssistantMessage(items);
|
|
394
474
|
|
|
395
|
-
let phase =
|
|
396
|
-
if (session?.assistantState ===
|
|
397
|
-
phase =
|
|
398
|
-
} else if (session?.assistantState ===
|
|
399
|
-
phase =
|
|
400
|
-
} else if (session?.assistantState ===
|
|
401
|
-
phase =
|
|
402
|
-
} else if (session?.status ===
|
|
403
|
-
phase =
|
|
404
|
-
} else if (session?.status ===
|
|
405
|
-
phase =
|
|
406
|
-
} else if (session?.status ===
|
|
407
|
-
phase =
|
|
408
|
-
} else if (session?.status ===
|
|
409
|
-
phase =
|
|
475
|
+
let phase = "idle";
|
|
476
|
+
if (session?.assistantState === "waiting_input") {
|
|
477
|
+
phase = "waiting_input";
|
|
478
|
+
} else if (session?.assistantState === "waiting_approval") {
|
|
479
|
+
phase = "waiting_approval";
|
|
480
|
+
} else if (session?.assistantState === "waiting_plan_approval") {
|
|
481
|
+
phase = "waiting_plan_approval";
|
|
482
|
+
} else if (session?.status === "running") {
|
|
483
|
+
phase = "running";
|
|
484
|
+
} else if (session?.status === "done") {
|
|
485
|
+
phase = "done";
|
|
486
|
+
} else if (session?.status === "err") {
|
|
487
|
+
phase = "error";
|
|
488
|
+
} else if (session?.status === "aborting") {
|
|
489
|
+
phase = "aborting";
|
|
410
490
|
}
|
|
411
491
|
|
|
412
492
|
let nextAction = null;
|
|
413
493
|
if (pendingApproval) {
|
|
414
494
|
nextAction = {
|
|
415
|
-
type:
|
|
495
|
+
type: "approval",
|
|
416
496
|
prompt: pendingApproval.prompt,
|
|
417
497
|
requestedAt: pendingApproval.requestedAt,
|
|
418
498
|
};
|
|
419
499
|
} else if (pendingUserInput?.isPlanChoice && latestPlan?.awaitingExecution) {
|
|
420
500
|
nextAction = {
|
|
421
|
-
type:
|
|
501
|
+
type: "execute_plan",
|
|
422
502
|
itemId: pendingUserInput.itemId,
|
|
423
503
|
questionId: pendingUserInput.questionId,
|
|
424
504
|
executeOptionLabel: pendingUserInput.executeOptionLabel,
|
|
@@ -426,24 +506,27 @@ export function analyzeWebSession(snapshot) {
|
|
|
426
506
|
};
|
|
427
507
|
} else if (pendingUserInput) {
|
|
428
508
|
nextAction = {
|
|
429
|
-
type:
|
|
509
|
+
type: "answer_user_input",
|
|
430
510
|
itemId: pendingUserInput.itemId,
|
|
431
511
|
prompt: pendingUserInput.prompt,
|
|
432
512
|
questions: pendingUserInput.questions,
|
|
433
513
|
requestedAt: pendingUserInput.requestedAt,
|
|
434
514
|
};
|
|
435
|
-
} else if (
|
|
515
|
+
} else if (
|
|
516
|
+
phase === "waiting_plan_approval" &&
|
|
517
|
+
latestPlan?.awaitingExecution
|
|
518
|
+
) {
|
|
436
519
|
nextAction = {
|
|
437
|
-
type:
|
|
520
|
+
type: "execute_plan",
|
|
438
521
|
latestPlan,
|
|
439
522
|
};
|
|
440
523
|
}
|
|
441
524
|
|
|
442
525
|
const canSend =
|
|
443
|
-
phase ===
|
|
444
|
-
phase ===
|
|
445
|
-
phase ===
|
|
446
|
-
phase ===
|
|
526
|
+
phase === "idle" ||
|
|
527
|
+
phase === "done" ||
|
|
528
|
+
phase === "error" ||
|
|
529
|
+
phase === "waiting_plan_approval";
|
|
447
530
|
|
|
448
531
|
return {
|
|
449
532
|
phase,
|
|
@@ -452,27 +535,31 @@ export function analyzeWebSession(snapshot) {
|
|
|
452
535
|
nextAction,
|
|
453
536
|
pendingApproval,
|
|
454
537
|
pendingUserInput,
|
|
538
|
+
pendingInputs,
|
|
455
539
|
latestPlan,
|
|
456
540
|
lastAssistantMessage,
|
|
457
541
|
session,
|
|
458
542
|
snapshot: {
|
|
459
543
|
session,
|
|
460
544
|
history,
|
|
545
|
+
pendingInputs,
|
|
461
546
|
},
|
|
462
547
|
};
|
|
463
548
|
}
|
|
464
549
|
|
|
465
550
|
export function decodeWebSessionSocketMessage(raw) {
|
|
466
|
-
if (typeof raw ===
|
|
551
|
+
if (typeof raw === "string") {
|
|
467
552
|
return JSON.parse(raw);
|
|
468
553
|
}
|
|
469
554
|
if (raw instanceof ArrayBuffer) {
|
|
470
|
-
return JSON.parse(Buffer.from(raw).toString(
|
|
555
|
+
return JSON.parse(Buffer.from(raw).toString("utf8"));
|
|
471
556
|
}
|
|
472
557
|
if (ArrayBuffer.isView(raw)) {
|
|
473
|
-
return JSON.parse(
|
|
558
|
+
return JSON.parse(
|
|
559
|
+
Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString("utf8"),
|
|
560
|
+
);
|
|
474
561
|
}
|
|
475
|
-
return JSON.parse(String(raw ??
|
|
562
|
+
return JSON.parse(String(raw ?? ""));
|
|
476
563
|
}
|
|
477
564
|
|
|
478
565
|
export function normalizeWebSessionFrame(rawFrame) {
|
|
@@ -486,21 +573,21 @@ export function normalizeWebSessionFrame(rawFrame) {
|
|
|
486
573
|
raw: rawFrame,
|
|
487
574
|
};
|
|
488
575
|
|
|
489
|
-
if (rawFrame?.k ===
|
|
576
|
+
if (rawFrame?.k === "ack") {
|
|
490
577
|
return {
|
|
491
578
|
...base,
|
|
492
|
-
type:
|
|
579
|
+
type: "ack",
|
|
493
580
|
ok: rawFrame?.ok === 1 || rawFrame?.ok === true,
|
|
494
581
|
payload: rawFrame?.p ?? null,
|
|
495
582
|
};
|
|
496
583
|
}
|
|
497
584
|
|
|
498
|
-
if (rawFrame?.k ===
|
|
585
|
+
if (rawFrame?.k === "err") {
|
|
499
586
|
return {
|
|
500
587
|
...base,
|
|
501
|
-
type:
|
|
502
|
-
code: trimmedString(rawFrame?.code) ||
|
|
503
|
-
message: trimmedString(rawFrame?.msg) ||
|
|
588
|
+
type: "error",
|
|
589
|
+
code: trimmedString(rawFrame?.code) || "unknown_error",
|
|
590
|
+
message: trimmedString(rawFrame?.msg) || "Unknown websocket error",
|
|
504
591
|
retry: booleanValue(rawFrame?.retry),
|
|
505
592
|
};
|
|
506
593
|
}
|
|
@@ -508,63 +595,79 @@ export function normalizeWebSessionFrame(rawFrame) {
|
|
|
508
595
|
if (rawFrame?.k === WEB_SESSION_HEARTBEAT_KIND) {
|
|
509
596
|
return {
|
|
510
597
|
...base,
|
|
511
|
-
type:
|
|
598
|
+
type: "heartbeat",
|
|
512
599
|
heartbeatType: trimmedString(rawFrame?.op) || null,
|
|
513
600
|
};
|
|
514
601
|
}
|
|
515
602
|
|
|
516
|
-
if (rawFrame?.k ===
|
|
603
|
+
if (rawFrame?.k === "snap") {
|
|
517
604
|
return {
|
|
518
605
|
...base,
|
|
519
|
-
type:
|
|
606
|
+
type: "snapshot",
|
|
520
607
|
snapshot: normalizeWebSessionSnapshotFromWire(rawFrame),
|
|
521
608
|
};
|
|
522
609
|
}
|
|
523
610
|
|
|
524
|
-
if (rawFrame?.k ===
|
|
525
|
-
if (rawFrame?.op ===
|
|
611
|
+
if (rawFrame?.k === "evt") {
|
|
612
|
+
if (rawFrame?.op === "session" && rawFrame?.s) {
|
|
526
613
|
return {
|
|
527
614
|
...base,
|
|
528
|
-
type:
|
|
615
|
+
type: "session",
|
|
529
616
|
session: normalizeWebSessionSummaryFromWire(rawFrame.s),
|
|
530
617
|
};
|
|
531
618
|
}
|
|
532
|
-
if (rawFrame?.op ===
|
|
619
|
+
if (rawFrame?.op === "hist_page" && rawFrame?.h) {
|
|
533
620
|
return {
|
|
534
621
|
...base,
|
|
535
|
-
type:
|
|
622
|
+
type: "historyPage",
|
|
536
623
|
history: normalizeWebSessionHistoryWindow(rawFrame.h),
|
|
537
624
|
};
|
|
538
625
|
}
|
|
539
|
-
if (rawFrame?.op ===
|
|
626
|
+
if (rawFrame?.op === "hist_item" && rawFrame?.i) {
|
|
540
627
|
return {
|
|
541
628
|
...base,
|
|
542
|
-
type:
|
|
629
|
+
type: "historyItem",
|
|
543
630
|
item: normalizeWebSessionHistoryItem(rawFrame.i),
|
|
544
|
-
session: rawFrame?.s
|
|
631
|
+
session: rawFrame?.s
|
|
632
|
+
? normalizeWebSessionSummaryFromWire(rawFrame.s)
|
|
633
|
+
: null,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
if (rawFrame?.op === "pending") {
|
|
637
|
+
return {
|
|
638
|
+
...base,
|
|
639
|
+
type: "pending",
|
|
640
|
+
pendingInputs: Array.isArray(rawFrame?.pi)
|
|
641
|
+
? rawFrame.pi.map(normalizePendingInput).filter(Boolean)
|
|
642
|
+
: [],
|
|
545
643
|
};
|
|
546
644
|
}
|
|
547
645
|
return {
|
|
548
646
|
...base,
|
|
549
|
-
type:
|
|
647
|
+
type: "event",
|
|
550
648
|
payload: rawFrame?.p ?? null,
|
|
551
649
|
};
|
|
552
650
|
}
|
|
553
651
|
|
|
554
652
|
return {
|
|
555
653
|
...base,
|
|
556
|
-
type:
|
|
654
|
+
type: "unknown",
|
|
557
655
|
payload: rawFrame?.p ?? null,
|
|
558
656
|
};
|
|
559
657
|
}
|
|
560
658
|
|
|
561
|
-
export function buildWebSessionCommandFrame({
|
|
659
|
+
export function buildWebSessionCommandFrame({
|
|
660
|
+
requestId,
|
|
661
|
+
sessionId,
|
|
662
|
+
operation,
|
|
663
|
+
payload,
|
|
664
|
+
}) {
|
|
562
665
|
return {
|
|
563
666
|
v: WEB_SESSION_PROTOCOL_VERSION,
|
|
564
|
-
k:
|
|
565
|
-
rid: ensureString(requestId,
|
|
667
|
+
k: "cmd",
|
|
668
|
+
rid: ensureString(requestId, "requestId"),
|
|
566
669
|
sid: ensureOptionalString(sessionId) || undefined,
|
|
567
|
-
op: ensureString(operation,
|
|
670
|
+
op: ensureString(operation, "operation"),
|
|
568
671
|
p: payload ?? {},
|
|
569
672
|
};
|
|
570
673
|
}
|
|
@@ -574,7 +677,7 @@ export function buildWebSessionHeartbeatFrame(operation) {
|
|
|
574
677
|
v: WEB_SESSION_PROTOCOL_VERSION,
|
|
575
678
|
k: WEB_SESSION_HEARTBEAT_KIND,
|
|
576
679
|
ts: Date.now(),
|
|
577
|
-
op: ensureString(operation,
|
|
680
|
+
op: ensureString(operation, "operation"),
|
|
578
681
|
};
|
|
579
682
|
}
|
|
580
683
|
|
|
@@ -583,7 +686,7 @@ export function isWebSessionHeartbeatFrame(rawFrame) {
|
|
|
583
686
|
}
|
|
584
687
|
|
|
585
688
|
export function normalizeWebSessionAttachment(value) {
|
|
586
|
-
if (!value || typeof value !==
|
|
689
|
+
if (!value || typeof value !== "object") {
|
|
587
690
|
return null;
|
|
588
691
|
}
|
|
589
692
|
return {
|
|
@@ -592,21 +695,23 @@ export function normalizeWebSessionAttachment(value) {
|
|
|
592
695
|
mime: trimmedString(value.mime),
|
|
593
696
|
size: numberValue(value.size, 0),
|
|
594
697
|
path: trimmedString(value.path),
|
|
595
|
-
createdAt: stringValue(value.createdAt ||
|
|
698
|
+
createdAt: stringValue(value.createdAt || ""),
|
|
596
699
|
};
|
|
597
700
|
}
|
|
598
701
|
|
|
599
702
|
export function inferWebSessionAttachmentMimeType(fileName) {
|
|
600
703
|
const extension = path.extname(trimmedString(fileName)).toLowerCase();
|
|
601
|
-
return IMAGE_MIME_BY_EXT.get(extension) ||
|
|
704
|
+
return IMAGE_MIME_BY_EXT.get(extension) || "";
|
|
602
705
|
}
|
|
603
706
|
|
|
604
707
|
export function ensureImageMimeType(value, fileName) {
|
|
605
708
|
const explicit = ensureOptionalString(value);
|
|
606
709
|
const inferred = inferWebSessionAttachmentMimeType(fileName);
|
|
607
710
|
const mimeType = explicit || inferred;
|
|
608
|
-
if (!mimeType || !mimeType.startsWith(
|
|
609
|
-
throw new CodeKanbanValidationError(
|
|
711
|
+
if (!mimeType || !mimeType.startsWith("image/")) {
|
|
712
|
+
throw new CodeKanbanValidationError(
|
|
713
|
+
"web session attachments must be image files",
|
|
714
|
+
);
|
|
610
715
|
}
|
|
611
716
|
return mimeType;
|
|
612
717
|
}
|
|
@@ -614,15 +719,19 @@ export function ensureImageMimeType(value, fileName) {
|
|
|
614
719
|
export function shouldEmitWebSessionFrame(frame, sessionIdFilter) {
|
|
615
720
|
const normalizedFilter = ensureOptionalString(sessionIdFilter);
|
|
616
721
|
if (!normalizedFilter) {
|
|
617
|
-
return frame?.type !==
|
|
722
|
+
return frame?.type !== "heartbeat";
|
|
618
723
|
}
|
|
619
|
-
if (!frame || typeof frame !==
|
|
724
|
+
if (!frame || typeof frame !== "object") {
|
|
620
725
|
return false;
|
|
621
726
|
}
|
|
622
|
-
if (
|
|
727
|
+
if (
|
|
728
|
+
frame.type === "open" ||
|
|
729
|
+
frame.type === "close" ||
|
|
730
|
+
frame.type === "error"
|
|
731
|
+
) {
|
|
623
732
|
return true;
|
|
624
733
|
}
|
|
625
|
-
if (frame.type ===
|
|
734
|
+
if (frame.type === "heartbeat") {
|
|
626
735
|
return false;
|
|
627
736
|
}
|
|
628
737
|
return ensureOptionalString(frame.sessionId) === normalizedFilter;
|