@tea-agent/loop-agent 0.33.3 → 0.33.5
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/CHANGELOG.md +39 -0
- package/dist/executors/dag-pi-executor.js +34 -12
- package/dist/executors/shell-executor.js +26 -66
- package/dist/task/config-types.js +2 -2
- package/dist/worker/console/chat/chat-event-store.js +57 -18
- package/dist/worker/console/chat/routes.js +850 -170
- package/dist/worker/console/static/assets/{index-PzYzcuFG.js → index-CteJFFL2.js} +17 -17
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +33 -8
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +80 -2
- package/dist/workflows/dag/backend-test-markdown-workflow.js +17 -0
- package/dist/workflows/dag/backend-test-module-stem.js +21 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +44 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +39 -31
- package/dist/workflows/dag/backend-test-writer-completeness.js +130 -45
- package/dist/workflows/dag/init-hybrid.js +29 -160
- package/docs/templates/backend-test-dag.json +3 -3
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +2 -2
- package/docs/templates/frontend-test-dag.json +9 -9
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +7 -7
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/playwright-cli/SKILL.md +2 -3
|
@@ -24,14 +24,14 @@ import { isSensitivePath, scrubSecrets } from "./explore-tools.js";
|
|
|
24
24
|
import { listRepoDirectory, readRepoPreview, RepoBrowserError, } from "./repo-browser.js";
|
|
25
25
|
import { projectRuntimeContext } from "./runtime-context.js";
|
|
26
26
|
import { patchModelsConfig, readModelsConfig, readPackageInventory, readSkillPreferences, writeSkillPreferences, } from "./pi-console-config.js";
|
|
27
|
-
import { ChatInterviewAdapter, projectChatInterviewState } from "./interview-adapter.js";
|
|
27
|
+
import { ChatInterviewAdapter, projectChatInterviewState, } from "./interview-adapter.js";
|
|
28
28
|
import { classifyChatCompactionFailure } from "./compaction-errors.js";
|
|
29
|
-
import { evaluateMutationGate, isMutationMethod
|
|
29
|
+
import { evaluateMutationGate, isMutationMethod } from "../security.js";
|
|
30
30
|
import { dispatchOperatorAction } from "../operator-actions.js";
|
|
31
|
-
import { makeHumanGateCard } from "./human-gate-card.js";
|
|
31
|
+
import { makeHumanGateCard, } from "./human-gate-card.js";
|
|
32
32
|
import { ContractApplyReceiptStore } from "./contract-apply-receipt-store.js";
|
|
33
33
|
import { projectTaskContext } from "./context-panel.js";
|
|
34
|
-
import { contractApplyPayloadHash, issueHumanGateToken, verifyHumanGateToken } from "../human-gate-token.js";
|
|
34
|
+
import { contractApplyPayloadHash, issueHumanGateToken, verifyHumanGateToken, } from "../human-gate-token.js";
|
|
35
35
|
import { sendJson } from "../routes.js";
|
|
36
36
|
import { openSseResponse, parseLastEventId, writeSseEvent, } from "../operation-sse.js";
|
|
37
37
|
function writeChatSse(res, event) {
|
|
@@ -51,7 +51,10 @@ function turnEventToStorePartial(event) {
|
|
|
51
51
|
case "agent_start":
|
|
52
52
|
return { kind: "agent_start", data: {} };
|
|
53
53
|
case "message_update":
|
|
54
|
-
return {
|
|
54
|
+
return {
|
|
55
|
+
kind: "message_update",
|
|
56
|
+
data: { text: String(event.text ?? "") },
|
|
57
|
+
};
|
|
55
58
|
case "message_end":
|
|
56
59
|
return { kind: "message_end", data: { text: String(event.text ?? "") } };
|
|
57
60
|
case "usage":
|
|
@@ -90,6 +93,22 @@ function turnEventToStorePartial(event) {
|
|
|
90
93
|
return undefined;
|
|
91
94
|
}
|
|
92
95
|
}
|
|
96
|
+
/** Guarantee a durable terminal event for a turn that ended without the SDK
|
|
97
|
+
* emitting agent_end (client abort, pre-prompt persistence failure, runtime
|
|
98
|
+
* error). Without it, a reconnect replay stops at agent_start and the UI stays
|
|
99
|
+
* streaming forever. Idempotent: normal settled turns already carry the SDK's
|
|
100
|
+
* agent_end in the ring, so they are skipped. */
|
|
101
|
+
function ensureAgentEndEvent(deps, sessionId, turnId) {
|
|
102
|
+
const already = deps.events
|
|
103
|
+
.snapshot(sessionId)
|
|
104
|
+
.some((event) => event.turnId === turnId && event.kind === "agent_end");
|
|
105
|
+
if (!already) {
|
|
106
|
+
deps.events.append(sessionId, turnId, {
|
|
107
|
+
kind: "agent_end",
|
|
108
|
+
data: { willRetry: false },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
93
112
|
async function readJsonBody(req, maxBytes = 2 * 1024 * 1024) {
|
|
94
113
|
const chunks = [];
|
|
95
114
|
let total = 0;
|
|
@@ -106,9 +125,22 @@ async function readJsonBody(req, maxBytes = 2 * 1024 * 1024) {
|
|
|
106
125
|
const raw = Buffer.concat(chunks).toString("utf8");
|
|
107
126
|
if (!raw.trim())
|
|
108
127
|
return {};
|
|
109
|
-
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(raw);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
// Preserve throw semantics: callers map parse failures to
|
|
133
|
+
// INVALID_INPUT / 400. The explicit catch keeps malformed JSON from
|
|
134
|
+
// surfacing as an unhandled SyntaxError.
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
110
137
|
}
|
|
111
|
-
const IMAGE_MIME_TYPES = new Set([
|
|
138
|
+
const IMAGE_MIME_TYPES = new Set([
|
|
139
|
+
"image/png",
|
|
140
|
+
"image/jpeg",
|
|
141
|
+
"image/gif",
|
|
142
|
+
"image/webp",
|
|
143
|
+
]);
|
|
112
144
|
const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
113
145
|
const MAX_TOTAL_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
114
146
|
const MAX_IMAGES = 4;
|
|
@@ -122,12 +154,19 @@ export function parseChatImages(value) {
|
|
|
122
154
|
if (!item || typeof item !== "object")
|
|
123
155
|
throw new Error(`images[${index}] is invalid`);
|
|
124
156
|
const record = item;
|
|
125
|
-
if (record.type !== "image" ||
|
|
157
|
+
if (record.type !== "image" ||
|
|
158
|
+
typeof record.mimeType !== "string" ||
|
|
159
|
+
!IMAGE_MIME_TYPES.has(record.mimeType))
|
|
126
160
|
throw new Error(`images[${index}] must be a supported raster image`);
|
|
127
|
-
if (typeof record.data !== "string" ||
|
|
161
|
+
if (typeof record.data !== "string" ||
|
|
162
|
+
!record.data ||
|
|
163
|
+
!/^[A-Za-z0-9+/]+={0,2}$/.test(record.data) ||
|
|
164
|
+
record.data.length % 4 !== 0)
|
|
128
165
|
throw new Error(`images[${index}] data must be valid base64`);
|
|
129
166
|
const bytes = Buffer.from(record.data, "base64");
|
|
130
|
-
const textProbe = bytes
|
|
167
|
+
const textProbe = bytes
|
|
168
|
+
.subarray(0, Math.min(bytes.length, 64 * 1024))
|
|
169
|
+
.toString("utf8");
|
|
131
170
|
if (scrubSecrets(textProbe).redactions.length > 0)
|
|
132
171
|
throw new Error(`images[${index}] contains secret-shaped content`);
|
|
133
172
|
if (bytes.length > MAX_IMAGE_BYTES)
|
|
@@ -135,7 +174,11 @@ export function parseChatImages(value) {
|
|
|
135
174
|
total += bytes.length;
|
|
136
175
|
if (total > MAX_TOTAL_IMAGE_BYTES)
|
|
137
176
|
throw new Error(`images exceed ${MAX_TOTAL_IMAGE_BYTES} total bytes`);
|
|
138
|
-
return {
|
|
177
|
+
return {
|
|
178
|
+
type: "image",
|
|
179
|
+
mimeType: record.mimeType,
|
|
180
|
+
data: bytes.toString("base64"),
|
|
181
|
+
};
|
|
139
182
|
});
|
|
140
183
|
}
|
|
141
184
|
function gateMutation(req, deps) {
|
|
@@ -174,7 +217,8 @@ export async function handleChatCapabilities(_req, res, deps) {
|
|
|
174
217
|
})),
|
|
175
218
|
failures: skills.failures,
|
|
176
219
|
skipped: skills.skipped,
|
|
177
|
-
promptFragmentCharCount: composeInstructionSkillsPrompt(skills.loaded)
|
|
220
|
+
promptFragmentCharCount: composeInstructionSkillsPrompt(skills.loaded)
|
|
221
|
+
.length,
|
|
178
222
|
},
|
|
179
223
|
// ADR 0011: full Pi tools including bash are default-active.
|
|
180
224
|
hasBash: true,
|
|
@@ -204,22 +248,45 @@ export async function handleCreateChatSession(req, res, deps) {
|
|
|
204
248
|
return;
|
|
205
249
|
}
|
|
206
250
|
if ("clientRequestId" in body || "requestIndex" in body) {
|
|
207
|
-
sendJson(res, 400, {
|
|
251
|
+
sendJson(res, 400, {
|
|
252
|
+
ok: false,
|
|
253
|
+
error: {
|
|
254
|
+
code: "INVALID_INPUT",
|
|
255
|
+
message: "operation idempotency ownership cannot be copied",
|
|
256
|
+
},
|
|
257
|
+
});
|
|
208
258
|
return;
|
|
209
259
|
}
|
|
210
260
|
const provider = typeof body.modelProvider === "string" ? body.modelProvider : undefined;
|
|
211
261
|
const modelId = typeof body.modelId === "string" ? body.modelId : undefined;
|
|
212
262
|
const snapshotInput = body.snapshot;
|
|
213
|
-
if (snapshotInput !== undefined &&
|
|
214
|
-
|
|
263
|
+
if (snapshotInput !== undefined &&
|
|
264
|
+
(!snapshotInput ||
|
|
265
|
+
typeof snapshotInput !== "object" ||
|
|
266
|
+
snapshotInput.schemaVersion !== 1 ||
|
|
267
|
+
typeof snapshotInput.summary !== "string" ||
|
|
268
|
+
!snapshotInput.summary)) {
|
|
269
|
+
sendJson(res, 400, {
|
|
270
|
+
ok: false,
|
|
271
|
+
error: {
|
|
272
|
+
code: "INVALID_SNAPSHOT",
|
|
273
|
+
message: "valid OperatorContextSnapshotV1 is required",
|
|
274
|
+
},
|
|
275
|
+
});
|
|
215
276
|
return;
|
|
216
277
|
}
|
|
217
278
|
try {
|
|
218
279
|
deps.runtime.setDisabledInstructionSkills(await readSkillPreferences(deps.appData));
|
|
219
|
-
const snapshot = snapshotInput === undefined
|
|
280
|
+
const snapshot = snapshotInput === undefined
|
|
281
|
+
? undefined
|
|
282
|
+
: projectCompactSnapshot(snapshotInput);
|
|
220
283
|
const handle = await deps.runtime.createSession({
|
|
221
284
|
...(provider && modelId ? { model: { provider, modelId } } : {}),
|
|
222
|
-
...(snapshot
|
|
285
|
+
...(snapshot
|
|
286
|
+
? {
|
|
287
|
+
systemPromptSuffix: `Read-only initial Operator context snapshot; this is not runtime rollback.\n${snapshot.summary}`,
|
|
288
|
+
}
|
|
289
|
+
: {}),
|
|
223
290
|
});
|
|
224
291
|
await deps.store.create({
|
|
225
292
|
sessionId: handle.sessionId,
|
|
@@ -228,7 +295,10 @@ export async function handleCreateChatSession(req, res, deps) {
|
|
|
228
295
|
modelId: handle.model?.modelId,
|
|
229
296
|
});
|
|
230
297
|
if (snapshot)
|
|
231
|
-
deps.events.append(handle.sessionId, `${handle.sessionId}:snapshot`, {
|
|
298
|
+
deps.events.append(handle.sessionId, `${handle.sessionId}:snapshot`, {
|
|
299
|
+
kind: "compact",
|
|
300
|
+
data: snapshot,
|
|
301
|
+
});
|
|
232
302
|
sendJson(res, 201, {
|
|
233
303
|
ok: true,
|
|
234
304
|
sessionId: handle.sessionId,
|
|
@@ -249,47 +319,102 @@ export async function handleCreateChatSession(req, res, deps) {
|
|
|
249
319
|
export async function handleForkChatSession(req, res, deps, sourceSessionId) {
|
|
250
320
|
const gate = gateMutation(req, deps);
|
|
251
321
|
if (!gate.ok) {
|
|
252
|
-
sendJson(res, gate.status, {
|
|
322
|
+
sendJson(res, gate.status, {
|
|
323
|
+
ok: false,
|
|
324
|
+
error: { code: gate.code, message: gate.message },
|
|
325
|
+
});
|
|
253
326
|
return;
|
|
254
327
|
}
|
|
255
328
|
if (deps.events.getActiveTurn(sourceSessionId)) {
|
|
256
|
-
sendJson(res, 409, {
|
|
329
|
+
sendJson(res, 409, {
|
|
330
|
+
ok: false,
|
|
331
|
+
error: { code: "TURN_ACTIVE", message: "cannot fork an active turn" },
|
|
332
|
+
});
|
|
257
333
|
return;
|
|
258
334
|
}
|
|
259
335
|
const source = await deps.store.get(sourceSessionId);
|
|
260
336
|
if (!source) {
|
|
261
|
-
sendJson(res, 404, {
|
|
337
|
+
sendJson(res, 404, {
|
|
338
|
+
ok: false,
|
|
339
|
+
error: {
|
|
340
|
+
code: "NOT_FOUND",
|
|
341
|
+
message: `chat session not found: ${sourceSessionId}`,
|
|
342
|
+
},
|
|
343
|
+
});
|
|
262
344
|
return;
|
|
263
345
|
}
|
|
264
346
|
try {
|
|
265
|
-
const handle = await deps.runtime.forkSession({
|
|
266
|
-
|
|
267
|
-
|
|
347
|
+
const handle = await deps.runtime.forkSession({
|
|
348
|
+
sourceSessionFile: source.sessionFile ?? "",
|
|
349
|
+
...(source.modelProvider && source.modelId
|
|
350
|
+
? { model: { provider: source.modelProvider, modelId: source.modelId } }
|
|
351
|
+
: {}),
|
|
352
|
+
});
|
|
353
|
+
await deps.store.forkFrom({
|
|
354
|
+
sourceSessionId,
|
|
355
|
+
targetSessionId: handle.sessionId,
|
|
356
|
+
targetSessionFile: handle.sessionFile,
|
|
357
|
+
});
|
|
358
|
+
const copied = deps.events.forkContextRefs({
|
|
359
|
+
sourceSessionId,
|
|
360
|
+
targetSessionId: handle.sessionId,
|
|
361
|
+
targetTurnId: `${handle.sessionId}:fork`,
|
|
362
|
+
});
|
|
268
363
|
await deps.operationLinker.recoverSession(handle.sessionId);
|
|
269
|
-
sendJson(res, 201, {
|
|
364
|
+
sendJson(res, 201, {
|
|
365
|
+
ok: true,
|
|
366
|
+
sessionId: handle.sessionId,
|
|
367
|
+
forkedFrom: sourceSessionId,
|
|
368
|
+
copiedContextRefs: copied.copiedCount,
|
|
369
|
+
warnings: [
|
|
370
|
+
"已发生的 task/DAG/Worker mutation 不会撤销或重放。",
|
|
371
|
+
"operation idempotency / clientRequestId ownership 不会复制到新会话。",
|
|
372
|
+
],
|
|
373
|
+
});
|
|
270
374
|
}
|
|
271
375
|
catch (error) {
|
|
272
|
-
sendJson(res, 409, {
|
|
376
|
+
sendJson(res, 409, {
|
|
377
|
+
ok: false,
|
|
378
|
+
error: {
|
|
379
|
+
code: "SESSION_FORK_FAILED",
|
|
380
|
+
message: error instanceof Error ? error.message : String(error),
|
|
381
|
+
},
|
|
382
|
+
});
|
|
273
383
|
}
|
|
274
384
|
}
|
|
275
385
|
export async function handleBranchChatSession(req, res, deps, sessionId) {
|
|
276
386
|
const gate = gateMutation(req, deps);
|
|
277
387
|
if (!gate.ok) {
|
|
278
|
-
sendJson(res, gate.status, {
|
|
388
|
+
sendJson(res, gate.status, {
|
|
389
|
+
ok: false,
|
|
390
|
+
error: { code: gate.code, message: gate.message },
|
|
391
|
+
});
|
|
279
392
|
return;
|
|
280
393
|
}
|
|
281
394
|
if (deps.events.getActiveTurn(sessionId)) {
|
|
282
|
-
sendJson(res, 409, {
|
|
395
|
+
sendJson(res, 409, {
|
|
396
|
+
ok: false,
|
|
397
|
+
error: { code: "TURN_ACTIVE", message: "cannot branch an active turn" },
|
|
398
|
+
});
|
|
283
399
|
return;
|
|
284
400
|
}
|
|
285
401
|
const record = await deps.store.get(sessionId);
|
|
286
402
|
if (!record) {
|
|
287
|
-
sendJson(res, 404, {
|
|
403
|
+
sendJson(res, 404, {
|
|
404
|
+
ok: false,
|
|
405
|
+
error: {
|
|
406
|
+
code: "NOT_FOUND",
|
|
407
|
+
message: `chat session not found: ${sessionId}`,
|
|
408
|
+
},
|
|
409
|
+
});
|
|
288
410
|
return;
|
|
289
411
|
}
|
|
290
412
|
try {
|
|
291
413
|
if (!deps.runtime.hasSession(sessionId)) {
|
|
292
|
-
const reopened = await deps.runtime.reopenSession({
|
|
414
|
+
const reopened = await deps.runtime.reopenSession({
|
|
415
|
+
sessionId,
|
|
416
|
+
sessionFile: record.sessionFile,
|
|
417
|
+
});
|
|
293
418
|
if (!reopened.ok)
|
|
294
419
|
throw new Error(reopened.message);
|
|
295
420
|
}
|
|
@@ -297,20 +422,41 @@ export async function handleBranchChatSession(req, res, deps, sessionId) {
|
|
|
297
422
|
if ("clientRequestId" in body || "requestIndex" in body)
|
|
298
423
|
throw new Error("operation idempotency ownership cannot be copied");
|
|
299
424
|
const current = deps.runtime.getBranchContext(sessionId);
|
|
300
|
-
const branchFromEntryId = typeof body.branchFromEntryId === "string"
|
|
425
|
+
const branchFromEntryId = typeof body.branchFromEntryId === "string"
|
|
426
|
+
? body.branchFromEntryId
|
|
427
|
+
: current.leafId;
|
|
301
428
|
if (!branchFromEntryId)
|
|
302
429
|
throw new Error("branchFromEntryId is required");
|
|
303
|
-
const result = await deps.runtime.branchSession({
|
|
304
|
-
|
|
430
|
+
const result = await deps.runtime.branchSession({
|
|
431
|
+
sessionId,
|
|
432
|
+
branchFromEntryId,
|
|
433
|
+
...(typeof body.summary === "string" && body.summary
|
|
434
|
+
? { summary: body.summary }
|
|
435
|
+
: {}),
|
|
436
|
+
});
|
|
437
|
+
sendJson(res, 200, {
|
|
438
|
+
ok: true,
|
|
439
|
+
...result,
|
|
440
|
+
note: "仅影响后续 LLM 上下文,未持久化第二历史",
|
|
441
|
+
});
|
|
305
442
|
}
|
|
306
443
|
catch (error) {
|
|
307
|
-
sendJson(res, 400, {
|
|
444
|
+
sendJson(res, 400, {
|
|
445
|
+
ok: false,
|
|
446
|
+
error: {
|
|
447
|
+
code: "SESSION_BRANCH_FAILED",
|
|
448
|
+
message: error instanceof Error ? error.message : String(error),
|
|
449
|
+
},
|
|
450
|
+
});
|
|
308
451
|
}
|
|
309
452
|
}
|
|
310
453
|
export async function handleSwitchMainlineChatSession(req, res, deps, sessionId) {
|
|
311
454
|
const gate = gateMutation(req, deps);
|
|
312
455
|
if (!gate.ok) {
|
|
313
|
-
sendJson(res, gate.status, {
|
|
456
|
+
sendJson(res, gate.status, {
|
|
457
|
+
ok: false,
|
|
458
|
+
error: { code: gate.code, message: gate.message },
|
|
459
|
+
});
|
|
314
460
|
return;
|
|
315
461
|
}
|
|
316
462
|
try {
|
|
@@ -318,7 +464,13 @@ export async function handleSwitchMainlineChatSession(req, res, deps, sessionId)
|
|
|
318
464
|
sendJson(res, 200, { ok: true, sessionId, active: "mainline" });
|
|
319
465
|
}
|
|
320
466
|
catch (error) {
|
|
321
|
-
sendJson(res, 409, {
|
|
467
|
+
sendJson(res, 409, {
|
|
468
|
+
ok: false,
|
|
469
|
+
error: {
|
|
470
|
+
code: "MAINLINE_SWITCH_FAILED",
|
|
471
|
+
message: error instanceof Error ? error.message : String(error),
|
|
472
|
+
},
|
|
473
|
+
});
|
|
322
474
|
}
|
|
323
475
|
}
|
|
324
476
|
export async function handleGetChatSession(_req, res, deps, sessionId) {
|
|
@@ -326,7 +478,10 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
|
|
|
326
478
|
if (!record) {
|
|
327
479
|
sendJson(res, 404, {
|
|
328
480
|
ok: false,
|
|
329
|
-
error: {
|
|
481
|
+
error: {
|
|
482
|
+
code: "NOT_FOUND",
|
|
483
|
+
message: `chat session not found: ${sessionId}`,
|
|
484
|
+
},
|
|
330
485
|
});
|
|
331
486
|
return;
|
|
332
487
|
}
|
|
@@ -346,6 +501,34 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
|
|
|
346
501
|
lastEventSeq: lastEvent?.seq ?? 0,
|
|
347
502
|
});
|
|
348
503
|
}
|
|
504
|
+
/** Lightweight busy-truth endpoint for the client reconcile loop (pi-web
|
|
505
|
+
* get_state equivalent): exposes the active turn + the latest turn's durable
|
|
506
|
+
* outcome WITHOUT the full message payload, so the UI can poll it on an
|
|
507
|
+
* interval / visibility / online without paying for session detail. */
|
|
508
|
+
export async function handleGetChatSessionState(_req, res, deps, sessionId) {
|
|
509
|
+
const record = await deps.store.get(sessionId);
|
|
510
|
+
if (!record) {
|
|
511
|
+
sendJson(res, 404, {
|
|
512
|
+
ok: false,
|
|
513
|
+
error: {
|
|
514
|
+
code: "NOT_FOUND",
|
|
515
|
+
message: `chat session not found: ${sessionId}`,
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const active = deps.events.getActiveTurn(sessionId);
|
|
521
|
+
const latest = deps.events.latestTurn(sessionId);
|
|
522
|
+
sendJson(res, 200, {
|
|
523
|
+
ok: true,
|
|
524
|
+
state: {
|
|
525
|
+
activeTurnId: active?.turnId ?? null,
|
|
526
|
+
activeTurnState: active?.state ?? null,
|
|
527
|
+
latestTurnId: latest?.turnId ?? null,
|
|
528
|
+
latestTurnState: latest?.state ?? null,
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
}
|
|
349
532
|
export async function handleListChatSessions(_req, res, deps) {
|
|
350
533
|
const sessions = await deps.store.list({ includeArchived: true });
|
|
351
534
|
sendJson(res, 200, {
|
|
@@ -361,7 +544,10 @@ export async function handleListChatSessions(_req, res, deps) {
|
|
|
361
544
|
export async function handlePatchChatSession(req, res, deps, sessionId) {
|
|
362
545
|
const gate = gateMutation(req, deps);
|
|
363
546
|
if (!gate.ok) {
|
|
364
|
-
sendJson(res, gate.status, {
|
|
547
|
+
sendJson(res, gate.status, {
|
|
548
|
+
ok: false,
|
|
549
|
+
error: { code: gate.code, message: gate.message },
|
|
550
|
+
});
|
|
365
551
|
return;
|
|
366
552
|
}
|
|
367
553
|
try {
|
|
@@ -373,28 +559,55 @@ export async function handlePatchChatSession(req, res, deps, sessionId) {
|
|
|
373
559
|
record = await deps.store.setState(sessionId, body.state);
|
|
374
560
|
}
|
|
375
561
|
else {
|
|
376
|
-
sendJson(res, 400, {
|
|
562
|
+
sendJson(res, 400, {
|
|
563
|
+
ok: false,
|
|
564
|
+
error: {
|
|
565
|
+
code: "INVALID_INPUT",
|
|
566
|
+
message: "title or state(active|archived) is required",
|
|
567
|
+
},
|
|
568
|
+
});
|
|
377
569
|
return;
|
|
378
570
|
}
|
|
379
571
|
sendJson(res, 200, { ok: true, session: record });
|
|
380
572
|
}
|
|
381
573
|
catch (error) {
|
|
382
|
-
sendJson(res, 404, {
|
|
574
|
+
sendJson(res, 404, {
|
|
575
|
+
ok: false,
|
|
576
|
+
error: {
|
|
577
|
+
code: "NOT_FOUND",
|
|
578
|
+
message: error instanceof Error ? error.message : String(error),
|
|
579
|
+
},
|
|
580
|
+
});
|
|
383
581
|
}
|
|
384
582
|
}
|
|
385
583
|
export async function handleReopenChatSession(req, res, deps, sessionId) {
|
|
386
584
|
const gate = gateMutation(req, deps);
|
|
387
585
|
if (!gate.ok) {
|
|
388
|
-
sendJson(res, gate.status, {
|
|
586
|
+
sendJson(res, gate.status, {
|
|
587
|
+
ok: false,
|
|
588
|
+
error: { code: gate.code, message: gate.message },
|
|
589
|
+
});
|
|
389
590
|
return;
|
|
390
591
|
}
|
|
391
592
|
const record = await deps.store.get(sessionId);
|
|
392
593
|
if (!record) {
|
|
393
|
-
sendJson(res, 404, {
|
|
594
|
+
sendJson(res, 404, {
|
|
595
|
+
ok: false,
|
|
596
|
+
error: {
|
|
597
|
+
code: "NOT_FOUND",
|
|
598
|
+
message: `chat session not found: ${sessionId}`,
|
|
599
|
+
},
|
|
600
|
+
});
|
|
394
601
|
return;
|
|
395
602
|
}
|
|
396
603
|
if (deps.runtime.hasSession(sessionId)) {
|
|
397
|
-
sendJson(res, 200, {
|
|
604
|
+
sendJson(res, 200, {
|
|
605
|
+
ok: true,
|
|
606
|
+
sessionId,
|
|
607
|
+
reopened: false,
|
|
608
|
+
alreadyActive: true,
|
|
609
|
+
hasBash: true,
|
|
610
|
+
});
|
|
398
611
|
return;
|
|
399
612
|
}
|
|
400
613
|
const result = await deps.runtime.reopenSession({
|
|
@@ -405,7 +618,10 @@ export async function handleReopenChatSession(req, res, deps, sessionId) {
|
|
|
405
618
|
: {}),
|
|
406
619
|
});
|
|
407
620
|
if (!result.ok) {
|
|
408
|
-
sendJson(res, 409, {
|
|
621
|
+
sendJson(res, 409, {
|
|
622
|
+
ok: false,
|
|
623
|
+
error: { code: result.code, message: result.message },
|
|
624
|
+
});
|
|
409
625
|
return;
|
|
410
626
|
}
|
|
411
627
|
sendJson(res, 200, {
|
|
@@ -447,7 +663,13 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
|
|
|
447
663
|
images = parseChatImages(body.images);
|
|
448
664
|
}
|
|
449
665
|
catch (error) {
|
|
450
|
-
sendJson(res, 400, {
|
|
666
|
+
sendJson(res, 400, {
|
|
667
|
+
ok: false,
|
|
668
|
+
error: {
|
|
669
|
+
code: "INVALID_ATTACHMENT",
|
|
670
|
+
message: error instanceof Error ? error.message : String(error),
|
|
671
|
+
},
|
|
672
|
+
});
|
|
451
673
|
return;
|
|
452
674
|
}
|
|
453
675
|
if (!text) {
|
|
@@ -464,7 +686,13 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
|
|
|
464
686
|
// fresh session under the same id, which would lose prior context.
|
|
465
687
|
const record = await deps.store.get(sessionId);
|
|
466
688
|
if (!record) {
|
|
467
|
-
sendJson(res, 404, {
|
|
689
|
+
sendJson(res, 404, {
|
|
690
|
+
ok: false,
|
|
691
|
+
error: {
|
|
692
|
+
code: "NOT_FOUND",
|
|
693
|
+
message: `chat session not found: ${sessionId}`,
|
|
694
|
+
},
|
|
695
|
+
});
|
|
468
696
|
return;
|
|
469
697
|
}
|
|
470
698
|
const reopen = await deps.runtime.reopenSession({
|
|
@@ -475,7 +703,10 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
|
|
|
475
703
|
: {}),
|
|
476
704
|
});
|
|
477
705
|
if (!reopen.ok) {
|
|
478
|
-
sendJson(res, 409, {
|
|
706
|
+
sendJson(res, 409, {
|
|
707
|
+
ok: false,
|
|
708
|
+
error: { code: reopen.code, message: reopen.message },
|
|
709
|
+
});
|
|
479
710
|
return;
|
|
480
711
|
}
|
|
481
712
|
}
|
|
@@ -541,13 +772,17 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
|
|
|
541
772
|
onEvent: (event) => {
|
|
542
773
|
if (closed)
|
|
543
774
|
return;
|
|
544
|
-
if (event.type === "tool_result" &&
|
|
775
|
+
if (event.type === "tool_result" &&
|
|
776
|
+
event.toolName === "prepareDagConfirmation" &&
|
|
777
|
+
!event.isError) {
|
|
545
778
|
const result = event.result;
|
|
546
779
|
const confirmationId = result?.confirmation?.confirmationId;
|
|
547
780
|
if (confirmationId)
|
|
548
781
|
void ctxConfirmationCard(deps, sessionId, turnId, confirmationId);
|
|
549
782
|
}
|
|
550
|
-
if (event.type === "tool_result" &&
|
|
783
|
+
if (event.type === "tool_result" &&
|
|
784
|
+
event.toolName === "prepareMutationGate" &&
|
|
785
|
+
!event.isError) {
|
|
551
786
|
const result = event.result;
|
|
552
787
|
if (result?.receiptId)
|
|
553
788
|
void ctxMutationGateCard(deps, sessionId, result.receiptId);
|
|
@@ -569,12 +804,14 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
|
|
|
569
804
|
},
|
|
570
805
|
});
|
|
571
806
|
if (abortedByClient || ac.signal.aborted) {
|
|
807
|
+
ensureAgentEndEvent(deps, sessionId, turnId);
|
|
572
808
|
deps.events.setTurnState(sessionId, turnId, "aborted", {
|
|
573
809
|
code: "CLIENT_ABORTED",
|
|
574
810
|
message: "client disconnected before turn settled",
|
|
575
811
|
});
|
|
576
812
|
}
|
|
577
813
|
else {
|
|
814
|
+
ensureAgentEndEvent(deps, sessionId, turnId);
|
|
578
815
|
deps.events.setTurnState(sessionId, turnId, outcome.ok ? "settled" : "failed", outcome.error);
|
|
579
816
|
}
|
|
580
817
|
}
|
|
@@ -585,8 +822,11 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
|
|
|
585
822
|
// stream is already open. Persist + emit an error event so a reconnecting
|
|
586
823
|
// client also sees the failure (not just the original requester).
|
|
587
824
|
const message = error instanceof Error ? error.message : String(error);
|
|
825
|
+
ensureAgentEndEvent(deps, sessionId, turnId);
|
|
588
826
|
deps.events.setTurnState(sessionId, turnId, abortedByClient || ac.signal.aborted ? "aborted" : "failed", {
|
|
589
|
-
code: abortedByClient || ac.signal.aborted
|
|
827
|
+
code: abortedByClient || ac.signal.aborted
|
|
828
|
+
? "CLIENT_ABORTED"
|
|
829
|
+
: "CHAT_TURN_FAILED",
|
|
590
830
|
message,
|
|
591
831
|
});
|
|
592
832
|
if (!closed) {
|
|
@@ -615,7 +855,10 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
|
|
|
615
855
|
export async function handleCreateChatTurn(req, res, deps, sessionId) {
|
|
616
856
|
const gate = gateMutation(req, deps);
|
|
617
857
|
if (!gate.ok) {
|
|
618
|
-
sendJson(res, gate.status, {
|
|
858
|
+
sendJson(res, gate.status, {
|
|
859
|
+
ok: false,
|
|
860
|
+
error: { code: gate.code, message: gate.message },
|
|
861
|
+
});
|
|
619
862
|
return;
|
|
620
863
|
}
|
|
621
864
|
let body;
|
|
@@ -623,7 +866,13 @@ export async function handleCreateChatTurn(req, res, deps, sessionId) {
|
|
|
623
866
|
body = await readJsonBody(req);
|
|
624
867
|
}
|
|
625
868
|
catch (error) {
|
|
626
|
-
sendJson(res, 400, {
|
|
869
|
+
sendJson(res, 400, {
|
|
870
|
+
ok: false,
|
|
871
|
+
error: {
|
|
872
|
+
code: "INVALID_INPUT",
|
|
873
|
+
message: error instanceof Error ? error.message : String(error),
|
|
874
|
+
},
|
|
875
|
+
});
|
|
627
876
|
return;
|
|
628
877
|
}
|
|
629
878
|
const text = typeof body.text === "string" ? body.text.trim() : "";
|
|
@@ -632,17 +881,32 @@ export async function handleCreateChatTurn(req, res, deps, sessionId) {
|
|
|
632
881
|
images = parseChatImages(body.images);
|
|
633
882
|
}
|
|
634
883
|
catch (error) {
|
|
635
|
-
sendJson(res, 400, {
|
|
884
|
+
sendJson(res, 400, {
|
|
885
|
+
ok: false,
|
|
886
|
+
error: {
|
|
887
|
+
code: "INVALID_ATTACHMENT",
|
|
888
|
+
message: error instanceof Error ? error.message : String(error),
|
|
889
|
+
},
|
|
890
|
+
});
|
|
636
891
|
return;
|
|
637
892
|
}
|
|
638
893
|
if (!text) {
|
|
639
|
-
sendJson(res, 400, {
|
|
894
|
+
sendJson(res, 400, {
|
|
895
|
+
ok: false,
|
|
896
|
+
error: { code: "INVALID_INPUT", message: "text is required" },
|
|
897
|
+
});
|
|
640
898
|
return;
|
|
641
899
|
}
|
|
642
900
|
if (!deps.runtime.hasSession(sessionId)) {
|
|
643
901
|
const record = await deps.store.get(sessionId);
|
|
644
902
|
if (!record) {
|
|
645
|
-
sendJson(res, 404, {
|
|
903
|
+
sendJson(res, 404, {
|
|
904
|
+
ok: false,
|
|
905
|
+
error: {
|
|
906
|
+
code: "NOT_FOUND",
|
|
907
|
+
message: `chat session not found: ${sessionId}`,
|
|
908
|
+
},
|
|
909
|
+
});
|
|
646
910
|
return;
|
|
647
911
|
}
|
|
648
912
|
const reopen = await deps.runtime.reopenSession({
|
|
@@ -653,26 +917,43 @@ export async function handleCreateChatTurn(req, res, deps, sessionId) {
|
|
|
653
917
|
: {}),
|
|
654
918
|
});
|
|
655
919
|
if (!reopen.ok) {
|
|
656
|
-
sendJson(res, 409, {
|
|
920
|
+
sendJson(res, 409, {
|
|
921
|
+
ok: false,
|
|
922
|
+
error: { code: reopen.code, message: reopen.message },
|
|
923
|
+
});
|
|
657
924
|
return;
|
|
658
925
|
}
|
|
659
926
|
}
|
|
660
927
|
const actionContext = deps.runtime.actionContext;
|
|
661
928
|
if (!actionContext) {
|
|
662
|
-
sendJson(res, 503, {
|
|
929
|
+
sendJson(res, 503, {
|
|
930
|
+
ok: false,
|
|
931
|
+
error: {
|
|
932
|
+
code: "CHAT_NO_ACTION_CONTEXT",
|
|
933
|
+
message: "Chat runtime has no operator action context wired",
|
|
934
|
+
},
|
|
935
|
+
});
|
|
663
936
|
return;
|
|
664
937
|
}
|
|
665
938
|
const created = deps.events.createTurn(sessionId);
|
|
666
939
|
if (!created.ok) {
|
|
667
940
|
sendJson(res, 409, {
|
|
668
941
|
ok: false,
|
|
669
|
-
error: {
|
|
942
|
+
error: {
|
|
943
|
+
code: "TURN_ACTIVE",
|
|
944
|
+
message: `chat turn already active: ${created.activeTurn.turnId}`,
|
|
945
|
+
},
|
|
670
946
|
});
|
|
671
947
|
return;
|
|
672
948
|
}
|
|
673
949
|
const turn = created.turn;
|
|
674
950
|
deps.events.setTurnState(sessionId, turn.turnId, "running");
|
|
675
|
-
sendJson(res, 202, {
|
|
951
|
+
sendJson(res, 202, {
|
|
952
|
+
ok: true,
|
|
953
|
+
turnId: turn.turnId,
|
|
954
|
+
ordinal: turn.ordinal,
|
|
955
|
+
state: "running",
|
|
956
|
+
});
|
|
676
957
|
// Deliberately detached from the request lifecycle: closing the POST
|
|
677
958
|
// response cannot abort the prompt; clients follow GET /events instead.
|
|
678
959
|
void (async () => {
|
|
@@ -699,11 +980,16 @@ export async function handleCreateChatTurn(req, res, deps, sessionId) {
|
|
|
699
980
|
deps.events.append(sessionId, turn.turnId, partial);
|
|
700
981
|
},
|
|
701
982
|
});
|
|
983
|
+
ensureAgentEndEvent(deps, sessionId, turn.turnId);
|
|
702
984
|
deps.events.setTurnState(sessionId, turn.turnId, outcome.ok ? "settled" : "failed", outcome.error);
|
|
703
985
|
}
|
|
704
986
|
catch (error) {
|
|
705
987
|
const message = error instanceof Error ? error.message : String(error);
|
|
706
|
-
deps.events.append(sessionId, turn.turnId, {
|
|
988
|
+
deps.events.append(sessionId, turn.turnId, {
|
|
989
|
+
kind: "error",
|
|
990
|
+
data: { message },
|
|
991
|
+
});
|
|
992
|
+
ensureAgentEndEvent(deps, sessionId, turn.turnId);
|
|
707
993
|
deps.events.setTurnState(sessionId, turn.turnId, "failed", {
|
|
708
994
|
code: "CHAT_TURN_FAILED",
|
|
709
995
|
message,
|
|
@@ -714,7 +1000,10 @@ export async function handleCreateChatTurn(req, res, deps, sessionId) {
|
|
|
714
1000
|
export async function handleGetChatTurn(_req, res, deps, sessionId, turnId) {
|
|
715
1001
|
const turn = deps.events.getTurn(sessionId, turnId);
|
|
716
1002
|
if (!turn) {
|
|
717
|
-
sendJson(res, 404, {
|
|
1003
|
+
sendJson(res, 404, {
|
|
1004
|
+
ok: false,
|
|
1005
|
+
error: { code: "NOT_FOUND", message: `chat turn not found: ${turnId}` },
|
|
1006
|
+
});
|
|
718
1007
|
return;
|
|
719
1008
|
}
|
|
720
1009
|
sendJson(res, 200, { ok: true, turn });
|
|
@@ -725,7 +1014,13 @@ export async function handleChatEventsStream(req, res, deps, sessionId) {
|
|
|
725
1014
|
// the boot cookie so only the Console browser can poll a live session.
|
|
726
1015
|
const record = await deps.store.get(sessionId);
|
|
727
1016
|
if (!record) {
|
|
728
|
-
sendJson(res, 404, {
|
|
1017
|
+
sendJson(res, 404, {
|
|
1018
|
+
ok: false,
|
|
1019
|
+
error: {
|
|
1020
|
+
code: "NOT_FOUND",
|
|
1021
|
+
message: `chat session not found: ${sessionId}`,
|
|
1022
|
+
},
|
|
1023
|
+
});
|
|
729
1024
|
return;
|
|
730
1025
|
}
|
|
731
1026
|
await deps.operationLinker.recoverSession(sessionId);
|
|
@@ -812,13 +1107,19 @@ export async function handleDeleteChatSession(req, res, deps, sessionId) {
|
|
|
812
1107
|
const removed = await deps.store.remove(sessionId);
|
|
813
1108
|
await new ComposerDraftStore(deps.appData).remove(sessionId);
|
|
814
1109
|
if (!removed) {
|
|
815
|
-
sendJson(res, 404, {
|
|
1110
|
+
sendJson(res, 404, {
|
|
1111
|
+
ok: false,
|
|
1112
|
+
error: {
|
|
1113
|
+
code: "NOT_FOUND",
|
|
1114
|
+
message: `chat session not found: ${sessionId}`,
|
|
1115
|
+
},
|
|
1116
|
+
});
|
|
816
1117
|
return;
|
|
817
1118
|
}
|
|
818
1119
|
sendJson(res, 200, { ok: true, sessionId, disposed: true, removed: true });
|
|
819
1120
|
}
|
|
820
1121
|
export async function handleGetTaskContext(_res, res, deps, sessionId) {
|
|
821
|
-
if (!await requireChatSession(res, deps, sessionId))
|
|
1122
|
+
if (!(await requireChatSession(res, deps, sessionId)))
|
|
822
1123
|
return;
|
|
823
1124
|
const events = deps.events.snapshot(sessionId);
|
|
824
1125
|
const context = projectTaskContext(events);
|
|
@@ -833,10 +1134,13 @@ export async function handleGetTaskContext(_res, res, deps, sessionId) {
|
|
|
833
1134
|
async function handleComposerDraft(req, res, deps, sessionId) {
|
|
834
1135
|
const gate = gateMutation(req, deps);
|
|
835
1136
|
if (!gate.ok) {
|
|
836
|
-
sendJson(res, gate.status, {
|
|
1137
|
+
sendJson(res, gate.status, {
|
|
1138
|
+
ok: false,
|
|
1139
|
+
error: { code: gate.code, message: gate.message },
|
|
1140
|
+
});
|
|
837
1141
|
return;
|
|
838
1142
|
}
|
|
839
|
-
if (!await requireChatSession(res, deps, sessionId))
|
|
1143
|
+
if (!(await requireChatSession(res, deps, sessionId)))
|
|
840
1144
|
return;
|
|
841
1145
|
try {
|
|
842
1146
|
const body = await readJsonBody(req, 70 * 1024);
|
|
@@ -845,7 +1149,13 @@ async function handleComposerDraft(req, res, deps, sessionId) {
|
|
|
845
1149
|
sendJson(res, 200, { ok: true });
|
|
846
1150
|
}
|
|
847
1151
|
catch (error) {
|
|
848
|
-
sendJson(res, 400, {
|
|
1152
|
+
sendJson(res, 400, {
|
|
1153
|
+
ok: false,
|
|
1154
|
+
error: {
|
|
1155
|
+
code: "INVALID_INPUT",
|
|
1156
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1157
|
+
},
|
|
1158
|
+
});
|
|
849
1159
|
}
|
|
850
1160
|
}
|
|
851
1161
|
async function handleRepoBrowser(res, deps, kind, rawPath) {
|
|
@@ -859,7 +1169,10 @@ async function handleRepoBrowser(res, deps, kind, rawPath) {
|
|
|
859
1169
|
const failure = error instanceof RepoBrowserError
|
|
860
1170
|
? error
|
|
861
1171
|
: new RepoBrowserError(500, "READ_FAILED", error instanceof Error ? error.message : String(error));
|
|
862
|
-
sendJson(res, failure.status, {
|
|
1172
|
+
sendJson(res, failure.status, {
|
|
1173
|
+
ok: false,
|
|
1174
|
+
error: { code: failure.code, message: failure.message },
|
|
1175
|
+
});
|
|
863
1176
|
}
|
|
864
1177
|
}
|
|
865
1178
|
async function handlePiConfig(req, res, deps, resource) {
|
|
@@ -867,26 +1180,43 @@ async function handlePiConfig(req, res, deps, resource) {
|
|
|
867
1180
|
if (resource === "models") {
|
|
868
1181
|
const agentDir = await deps.runtime.getAgentDir();
|
|
869
1182
|
if (req.method === "GET")
|
|
870
|
-
sendJson(res, 200, {
|
|
1183
|
+
sendJson(res, 200, {
|
|
1184
|
+
ok: true,
|
|
1185
|
+
data: await readModelsConfig(agentDir),
|
|
1186
|
+
});
|
|
871
1187
|
else {
|
|
872
1188
|
const gate = gateMutation(req, deps);
|
|
873
1189
|
if (!gate.ok) {
|
|
874
|
-
sendJson(res, gate.status, {
|
|
1190
|
+
sendJson(res, gate.status, {
|
|
1191
|
+
ok: false,
|
|
1192
|
+
error: { code: gate.code, message: gate.message },
|
|
1193
|
+
});
|
|
875
1194
|
return;
|
|
876
1195
|
}
|
|
877
1196
|
const body = await readJsonBody(req);
|
|
878
|
-
sendJson(res, 200, {
|
|
1197
|
+
sendJson(res, 200, {
|
|
1198
|
+
ok: true,
|
|
1199
|
+
data: await patchModelsConfig(agentDir, body),
|
|
1200
|
+
});
|
|
879
1201
|
}
|
|
880
1202
|
return;
|
|
881
1203
|
}
|
|
882
1204
|
if (resource === "packages") {
|
|
883
|
-
sendJson(res, 200, {
|
|
1205
|
+
sendJson(res, 200, {
|
|
1206
|
+
ok: true,
|
|
1207
|
+
data: {
|
|
1208
|
+
packages: await readPackageInventory(await deps.runtime.getAgentDir(), deps.appData.repoRoot),
|
|
1209
|
+
mutationSupported: false,
|
|
1210
|
+
},
|
|
1211
|
+
});
|
|
884
1212
|
return;
|
|
885
1213
|
}
|
|
886
1214
|
if (req.method === "GET") {
|
|
887
1215
|
const disabledNames = await readSkillPreferences(deps.appData);
|
|
888
1216
|
const loaded = await loadOperatorChatInstructionSkills(deps.skillsDir);
|
|
889
|
-
sendJson(res, 200, {
|
|
1217
|
+
sendJson(res, 200, {
|
|
1218
|
+
ok: true,
|
|
1219
|
+
data: {
|
|
890
1220
|
allowed: OPERATOR_CHAT_ALLOWED_INSTRUCTION_SKILLS.map((name) => {
|
|
891
1221
|
const skill = loaded.loaded.find((item) => item.name === name);
|
|
892
1222
|
return {
|
|
@@ -900,32 +1230,53 @@ async function handlePiConfig(req, res, deps, resource) {
|
|
|
900
1230
|
conditional: OPERATOR_CHAT_WORKFLOW_CONDITIONAL_SKILLS,
|
|
901
1231
|
denied: OPERATOR_CHAT_DENIED_INSTRUCTION_SKILLS,
|
|
902
1232
|
disabledNames,
|
|
903
|
-
}
|
|
1233
|
+
},
|
|
1234
|
+
});
|
|
904
1235
|
}
|
|
905
1236
|
else {
|
|
906
1237
|
const gate = gateMutation(req, deps);
|
|
907
1238
|
if (!gate.ok) {
|
|
908
|
-
sendJson(res, gate.status, {
|
|
1239
|
+
sendJson(res, gate.status, {
|
|
1240
|
+
ok: false,
|
|
1241
|
+
error: { code: gate.code, message: gate.message },
|
|
1242
|
+
});
|
|
909
1243
|
return;
|
|
910
1244
|
}
|
|
911
1245
|
const body = await readJsonBody(req);
|
|
912
|
-
const disabledNames = await writeSkillPreferences(deps.appData, Array.isArray(body.disabledNames)
|
|
1246
|
+
const disabledNames = await writeSkillPreferences(deps.appData, Array.isArray(body.disabledNames)
|
|
1247
|
+
? body.disabledNames.filter((name) => typeof name === "string")
|
|
1248
|
+
: []);
|
|
913
1249
|
deps.runtime.setDisabledInstructionSkills(disabledNames);
|
|
914
|
-
sendJson(res, 200, {
|
|
1250
|
+
sendJson(res, 200, {
|
|
1251
|
+
ok: true,
|
|
1252
|
+
data: { disabledNames, appliesTo: "new-sessions" },
|
|
1253
|
+
});
|
|
915
1254
|
}
|
|
916
1255
|
}
|
|
917
1256
|
catch (error) {
|
|
918
1257
|
const failure = error;
|
|
919
|
-
sendJson(res, failure.status ?? 400, {
|
|
1258
|
+
sendJson(res, failure.status ?? 400, {
|
|
1259
|
+
ok: false,
|
|
1260
|
+
error: {
|
|
1261
|
+
code: failure.code ?? "INVALID_INPUT",
|
|
1262
|
+
message: failure.message,
|
|
1263
|
+
},
|
|
1264
|
+
});
|
|
920
1265
|
}
|
|
921
1266
|
}
|
|
922
1267
|
async function handleChatFiles(res, deps, sessionId, query) {
|
|
923
|
-
if (!await requireChatSession(res, deps, sessionId))
|
|
1268
|
+
if (!(await requireChatSession(res, deps, sessionId)))
|
|
924
1269
|
return;
|
|
925
1270
|
const q = query.trim().toLowerCase();
|
|
926
|
-
const files = (await walkRepoFiles(deps.appData.repoRoot, {
|
|
927
|
-
|
|
928
|
-
|
|
1271
|
+
const files = (await walkRepoFiles(deps.appData.repoRoot, {
|
|
1272
|
+
skipSensitive: true,
|
|
1273
|
+
maxFiles: 5000,
|
|
1274
|
+
}))
|
|
1275
|
+
.map((entry) => entry.repoRelative)
|
|
1276
|
+
.filter((file) => !isSensitivePath(file) && (!q || file.toLowerCase().includes(q)))
|
|
1277
|
+
.sort((a, b) => a.localeCompare(b))
|
|
1278
|
+
.slice(0, 50)
|
|
1279
|
+
.map((path) => ({ path }));
|
|
929
1280
|
sendJson(res, 200, { ok: true, files });
|
|
930
1281
|
}
|
|
931
1282
|
async function handleRuntimeContext(res, deps, sessionId) {
|
|
@@ -958,123 +1309,226 @@ async function handleRuntimeContext(res, deps, sessionId) {
|
|
|
958
1309
|
});
|
|
959
1310
|
}
|
|
960
1311
|
async function handleModels(req, res, deps, sessionId) {
|
|
961
|
-
if (!await requireChatSession(res, deps, sessionId))
|
|
1312
|
+
if (!(await requireChatSession(res, deps, sessionId)))
|
|
962
1313
|
return;
|
|
963
1314
|
try {
|
|
964
1315
|
if ((req.method ?? "GET").toUpperCase() === "GET") {
|
|
965
|
-
sendJson(res, 200, {
|
|
1316
|
+
sendJson(res, 200, {
|
|
1317
|
+
ok: true,
|
|
1318
|
+
models: await deps.runtime.listModels(sessionId),
|
|
1319
|
+
thinkingLevels: THINKING_LEVELS,
|
|
1320
|
+
});
|
|
966
1321
|
return;
|
|
967
1322
|
}
|
|
968
1323
|
const gate = gateMutation(req, deps);
|
|
969
1324
|
if (!gate.ok) {
|
|
970
|
-
sendJson(res, gate.status, {
|
|
1325
|
+
sendJson(res, gate.status, {
|
|
1326
|
+
ok: false,
|
|
1327
|
+
error: { code: gate.code, message: gate.message },
|
|
1328
|
+
});
|
|
971
1329
|
return;
|
|
972
1330
|
}
|
|
973
1331
|
const body = await readJsonBody(req);
|
|
974
1332
|
let updated;
|
|
975
1333
|
if (typeof body.provider === "string" && typeof body.modelId === "string") {
|
|
976
|
-
const model = await deps.runtime.applyModel(sessionId, {
|
|
977
|
-
|
|
1334
|
+
const model = await deps.runtime.applyModel(sessionId, {
|
|
1335
|
+
provider: body.provider,
|
|
1336
|
+
modelId: body.modelId,
|
|
1337
|
+
});
|
|
1338
|
+
updated = await deps.store.setRuntimeSelection(sessionId, {
|
|
1339
|
+
modelProvider: model.provider,
|
|
1340
|
+
modelId: model.modelId,
|
|
1341
|
+
});
|
|
978
1342
|
}
|
|
979
|
-
else if (typeof body.thinkingLevel === "string" &&
|
|
1343
|
+
else if (typeof body.thinkingLevel === "string" &&
|
|
1344
|
+
THINKING_LEVELS.includes(body.thinkingLevel)) {
|
|
980
1345
|
const thinkingLevel = deps.runtime.applyThinkingLevel(sessionId, body.thinkingLevel);
|
|
981
|
-
updated = await deps.store.setRuntimeSelection(sessionId, {
|
|
1346
|
+
updated = await deps.store.setRuntimeSelection(sessionId, {
|
|
1347
|
+
thinkingLevel,
|
|
1348
|
+
});
|
|
982
1349
|
}
|
|
983
1350
|
else {
|
|
984
|
-
sendJson(res, 400, {
|
|
1351
|
+
sendJson(res, 400, {
|
|
1352
|
+
ok: false,
|
|
1353
|
+
error: {
|
|
1354
|
+
code: "INVALID_INPUT",
|
|
1355
|
+
message: "provider/modelId or thinkingLevel is required",
|
|
1356
|
+
},
|
|
1357
|
+
});
|
|
985
1358
|
return;
|
|
986
1359
|
}
|
|
987
1360
|
sendJson(res, 200, { ok: true, session: updated });
|
|
988
1361
|
}
|
|
989
1362
|
catch (error) {
|
|
990
|
-
sendJson(res, 409, {
|
|
1363
|
+
sendJson(res, 409, {
|
|
1364
|
+
ok: false,
|
|
1365
|
+
error: {
|
|
1366
|
+
code: "MODEL_SWITCH_FAILED",
|
|
1367
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1368
|
+
},
|
|
1369
|
+
});
|
|
991
1370
|
}
|
|
992
1371
|
}
|
|
993
1372
|
async function requireChatSession(res, deps, sessionId) {
|
|
994
1373
|
if (await deps.store.get(sessionId))
|
|
995
1374
|
return true;
|
|
996
|
-
sendJson(res, 404, {
|
|
1375
|
+
sendJson(res, 404, {
|
|
1376
|
+
ok: false,
|
|
1377
|
+
error: {
|
|
1378
|
+
code: "NOT_FOUND",
|
|
1379
|
+
message: `chat session not found: ${sessionId}`,
|
|
1380
|
+
},
|
|
1381
|
+
});
|
|
997
1382
|
return false;
|
|
998
1383
|
}
|
|
999
1384
|
async function handleLinkWorkspaceOperation(req, res, deps, sessionId) {
|
|
1000
1385
|
const gate = gateMutation(req, deps);
|
|
1001
1386
|
if (!gate.ok) {
|
|
1002
|
-
sendJson(res, gate.status, {
|
|
1387
|
+
sendJson(res, gate.status, {
|
|
1388
|
+
ok: false,
|
|
1389
|
+
error: { code: gate.code, message: gate.message },
|
|
1390
|
+
});
|
|
1003
1391
|
return;
|
|
1004
1392
|
}
|
|
1005
|
-
if (!await requireChatSession(res, deps, sessionId))
|
|
1393
|
+
if (!(await requireChatSession(res, deps, sessionId)))
|
|
1006
1394
|
return;
|
|
1007
1395
|
try {
|
|
1008
1396
|
const body = await readJsonBody(req);
|
|
1009
1397
|
const operationId = typeof body.operationId === "string" ? body.operationId.trim() : "";
|
|
1010
1398
|
if (!operationId) {
|
|
1011
|
-
sendJson(res, 400, {
|
|
1399
|
+
sendJson(res, 400, {
|
|
1400
|
+
ok: false,
|
|
1401
|
+
error: { code: "INVALID_INPUT", message: "operationId is required" },
|
|
1402
|
+
});
|
|
1012
1403
|
return;
|
|
1013
1404
|
}
|
|
1014
1405
|
const operations = deps.runtime.actionContext?.operations;
|
|
1015
|
-
if (!operations || !await operations.get(operationId)) {
|
|
1016
|
-
sendJson(res, 404, {
|
|
1406
|
+
if (!operations || !(await operations.get(operationId))) {
|
|
1407
|
+
sendJson(res, 404, {
|
|
1408
|
+
ok: false,
|
|
1409
|
+
error: {
|
|
1410
|
+
code: "NOT_FOUND",
|
|
1411
|
+
message: `operation not found: ${operationId}`,
|
|
1412
|
+
},
|
|
1413
|
+
});
|
|
1017
1414
|
return;
|
|
1018
1415
|
}
|
|
1019
|
-
await deps.operationLinker.link({
|
|
1416
|
+
await deps.operationLinker.link({
|
|
1417
|
+
sessionId,
|
|
1418
|
+
turnId: deps.events.latestTurnId(sessionId) ?? "workspace",
|
|
1419
|
+
toolCallId: `workspace-${operationId}`,
|
|
1420
|
+
operationId,
|
|
1421
|
+
});
|
|
1020
1422
|
sendJson(res, 200, { ok: true });
|
|
1021
1423
|
}
|
|
1022
1424
|
catch (error) {
|
|
1023
|
-
sendJson(res, 400, {
|
|
1425
|
+
sendJson(res, 400, {
|
|
1426
|
+
ok: false,
|
|
1427
|
+
error: {
|
|
1428
|
+
code: "INVALID_INPUT",
|
|
1429
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1430
|
+
},
|
|
1431
|
+
});
|
|
1024
1432
|
}
|
|
1025
1433
|
}
|
|
1026
1434
|
async function handleStartInterview(req, res, deps, sessionId) {
|
|
1027
1435
|
const gate = gateMutation(req, deps);
|
|
1028
1436
|
if (!gate.ok) {
|
|
1029
|
-
sendJson(res, gate.status, {
|
|
1437
|
+
sendJson(res, gate.status, {
|
|
1438
|
+
ok: false,
|
|
1439
|
+
error: { code: gate.code, message: gate.message },
|
|
1440
|
+
});
|
|
1030
1441
|
return;
|
|
1031
1442
|
}
|
|
1032
|
-
if (!await requireChatSession(res, deps, sessionId))
|
|
1443
|
+
if (!(await requireChatSession(res, deps, sessionId)))
|
|
1033
1444
|
return;
|
|
1034
1445
|
try {
|
|
1035
1446
|
const body = await readJsonBody(req);
|
|
1036
1447
|
const taskId = typeof body.taskId === "string" ? body.taskId.trim() : "";
|
|
1037
1448
|
if (!taskId) {
|
|
1038
|
-
sendJson(res, 400, {
|
|
1449
|
+
sendJson(res, 400, {
|
|
1450
|
+
ok: false,
|
|
1451
|
+
error: { code: "INVALID_INPUT", message: "taskId is required" },
|
|
1452
|
+
});
|
|
1039
1453
|
return;
|
|
1040
1454
|
}
|
|
1041
1455
|
const adapter = new ChatInterviewAdapter(deps.appData, deps.events);
|
|
1042
|
-
const state = await adapter.start({
|
|
1456
|
+
const state = await adapter.start({
|
|
1457
|
+
operatorSessionId: sessionId,
|
|
1458
|
+
taskId,
|
|
1459
|
+
...(typeof body.title === "string" ? { title: body.title } : {}),
|
|
1460
|
+
...(body.initialDraft && typeof body.initialDraft === "object"
|
|
1461
|
+
? { initialDraft: body.initialDraft }
|
|
1462
|
+
: {}),
|
|
1463
|
+
});
|
|
1043
1464
|
sendJson(res, 201, { ok: true, ...projectChatInterviewState(state) });
|
|
1044
1465
|
}
|
|
1045
1466
|
catch (error) {
|
|
1046
|
-
sendJson(res, 400, {
|
|
1467
|
+
sendJson(res, 400, {
|
|
1468
|
+
ok: false,
|
|
1469
|
+
error: {
|
|
1470
|
+
code: "INTERVIEW_START_FAILED",
|
|
1471
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1472
|
+
},
|
|
1473
|
+
});
|
|
1047
1474
|
}
|
|
1048
1475
|
}
|
|
1049
1476
|
async function handleAnswerInterview(req, res, deps, sessionId, interviewSessionId) {
|
|
1050
1477
|
const gate = gateMutation(req, deps);
|
|
1051
1478
|
if (!gate.ok) {
|
|
1052
|
-
sendJson(res, gate.status, {
|
|
1479
|
+
sendJson(res, gate.status, {
|
|
1480
|
+
ok: false,
|
|
1481
|
+
error: { code: gate.code, message: gate.message },
|
|
1482
|
+
});
|
|
1053
1483
|
return;
|
|
1054
1484
|
}
|
|
1055
|
-
if (!await requireChatSession(res, deps, sessionId))
|
|
1485
|
+
if (!(await requireChatSession(res, deps, sessionId)))
|
|
1056
1486
|
return;
|
|
1057
1487
|
try {
|
|
1058
1488
|
const body = await readJsonBody(req);
|
|
1059
1489
|
const questionId = typeof body.questionId === "string" ? body.questionId : "";
|
|
1060
1490
|
const response = typeof body.response === "string" ? body.response : "";
|
|
1061
1491
|
if (!questionId || !response) {
|
|
1062
|
-
sendJson(res, 400, {
|
|
1492
|
+
sendJson(res, 400, {
|
|
1493
|
+
ok: false,
|
|
1494
|
+
error: {
|
|
1495
|
+
code: "INVALID_INPUT",
|
|
1496
|
+
message: "questionId and response are required",
|
|
1497
|
+
},
|
|
1498
|
+
});
|
|
1063
1499
|
return;
|
|
1064
1500
|
}
|
|
1065
|
-
const state = await new ChatInterviewAdapter(deps.appData, deps.events).answer({
|
|
1501
|
+
const state = await new ChatInterviewAdapter(deps.appData, deps.events).answer({
|
|
1502
|
+
operatorSessionId: sessionId,
|
|
1503
|
+
interviewSessionId,
|
|
1504
|
+
questionId,
|
|
1505
|
+
response,
|
|
1506
|
+
...(typeof body.text === "string" ? { text: body.text } : {}),
|
|
1507
|
+
});
|
|
1066
1508
|
sendJson(res, 200, { ok: true, ...projectChatInterviewState(state) });
|
|
1067
1509
|
}
|
|
1068
1510
|
catch (error) {
|
|
1069
|
-
sendJson(res, 400, {
|
|
1511
|
+
sendJson(res, 400, {
|
|
1512
|
+
ok: false,
|
|
1513
|
+
error: {
|
|
1514
|
+
code: "INTERVIEW_ANSWER_FAILED",
|
|
1515
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1516
|
+
},
|
|
1517
|
+
});
|
|
1070
1518
|
}
|
|
1071
1519
|
}
|
|
1072
1520
|
async function handleGetInterviewFact(res, deps, sessionId, interviewSessionId, fact) {
|
|
1073
|
-
if (!await requireChatSession(res, deps, sessionId))
|
|
1521
|
+
if (!(await requireChatSession(res, deps, sessionId)))
|
|
1074
1522
|
return;
|
|
1075
1523
|
const state = await new ChatInterviewAdapter(deps.appData, deps.events).get(interviewSessionId);
|
|
1076
1524
|
if (!state || state.interview.operatorSessionId !== sessionId) {
|
|
1077
|
-
sendJson(res, 404, {
|
|
1525
|
+
sendJson(res, 404, {
|
|
1526
|
+
ok: false,
|
|
1527
|
+
error: {
|
|
1528
|
+
code: "NOT_FOUND",
|
|
1529
|
+
message: `interview session not found: ${interviewSessionId}`,
|
|
1530
|
+
},
|
|
1531
|
+
});
|
|
1078
1532
|
return;
|
|
1079
1533
|
}
|
|
1080
1534
|
const projected = projectChatInterviewState(state);
|
|
@@ -1094,7 +1548,12 @@ async function ctxConfirmationCard(deps, sessionId, turnId, confirmationId) {
|
|
|
1094
1548
|
expiresAt: confirmation.expiresAt,
|
|
1095
1549
|
inspectHref: `/inspect/?taskId=${encodeURIComponent(taskId)}#/`,
|
|
1096
1550
|
humanGateToken: confirmation.humanGateToken,
|
|
1097
|
-
displayOnlyChallenges: [
|
|
1551
|
+
displayOnlyChallenges: [
|
|
1552
|
+
"source",
|
|
1553
|
+
"writer-scope",
|
|
1554
|
+
"verification",
|
|
1555
|
+
"human-gates",
|
|
1556
|
+
],
|
|
1098
1557
|
contractRevision: confirmation.taskContractBinding.revision,
|
|
1099
1558
|
}));
|
|
1100
1559
|
void turnId;
|
|
@@ -1130,26 +1589,44 @@ async function liveContractBinding(ctx, taskId) {
|
|
|
1130
1589
|
const draft = await ctx.drafts.get(taskId);
|
|
1131
1590
|
if (!draft)
|
|
1132
1591
|
throw new Error(`draft not found: ${taskId}`);
|
|
1133
|
-
const shown = await dispatchOperatorAction(ctx, {
|
|
1592
|
+
const shown = await dispatchOperatorAction(ctx, {
|
|
1593
|
+
action: "contractShow",
|
|
1594
|
+
actionParams: { taskId },
|
|
1595
|
+
});
|
|
1134
1596
|
if (shown.kind === "error")
|
|
1135
1597
|
throw new Error("contractShow failed");
|
|
1136
1598
|
const result = actionResultBody(shown);
|
|
1137
1599
|
const state = result.state;
|
|
1138
1600
|
const revision = state?.ref?.revision ?? 0;
|
|
1139
1601
|
const expectedObservedHash = state?.observedCanonicalHash ?? "0".repeat(64);
|
|
1140
|
-
return {
|
|
1602
|
+
return {
|
|
1603
|
+
taskId,
|
|
1604
|
+
revision,
|
|
1605
|
+
canonicalHash: state?.ref?.canonicalHash ?? expectedObservedHash,
|
|
1606
|
+
draftSha256: draft.draftSha256,
|
|
1607
|
+
expectedObservedHash,
|
|
1608
|
+
};
|
|
1141
1609
|
}
|
|
1142
1610
|
async function handleContractApplyHumanGate(req, res, deps, sessionId, step) {
|
|
1143
1611
|
const gate = gateMutation(req, deps);
|
|
1144
1612
|
if (!gate.ok) {
|
|
1145
|
-
sendJson(res, gate.status, {
|
|
1613
|
+
sendJson(res, gate.status, {
|
|
1614
|
+
ok: false,
|
|
1615
|
+
error: { code: gate.code, message: gate.message },
|
|
1616
|
+
});
|
|
1146
1617
|
return;
|
|
1147
1618
|
}
|
|
1148
|
-
if (!await requireChatSession(res, deps, sessionId))
|
|
1619
|
+
if (!(await requireChatSession(res, deps, sessionId)))
|
|
1149
1620
|
return;
|
|
1150
1621
|
const ctx = deps.runtime.actionContext;
|
|
1151
1622
|
if (!ctx) {
|
|
1152
|
-
sendJson(res, 503, {
|
|
1623
|
+
sendJson(res, 503, {
|
|
1624
|
+
ok: false,
|
|
1625
|
+
error: {
|
|
1626
|
+
code: "CHAT_NO_ACTION_CONTEXT",
|
|
1627
|
+
message: "Chat runtime has no operator action context wired",
|
|
1628
|
+
},
|
|
1629
|
+
});
|
|
1153
1630
|
return;
|
|
1154
1631
|
}
|
|
1155
1632
|
try {
|
|
@@ -1162,9 +1639,30 @@ async function handleContractApplyHumanGate(req, res, deps, sessionId, step) {
|
|
|
1162
1639
|
const binding = await liveContractBinding(ctx, taskId);
|
|
1163
1640
|
const expiresAt = new Date(Date.now() + 30 * 60_000).toISOString();
|
|
1164
1641
|
const receiptId = `apply_${crypto.randomUUID()}`;
|
|
1165
|
-
const humanGateToken = issueHumanGateToken({
|
|
1166
|
-
|
|
1167
|
-
|
|
1642
|
+
const humanGateToken = issueHumanGateToken({
|
|
1643
|
+
confirmationId: receiptId,
|
|
1644
|
+
payloadHash: contractApplyPayloadHash(binding),
|
|
1645
|
+
expiresAt,
|
|
1646
|
+
}, deps.bootToken.confirmationToken);
|
|
1647
|
+
const receipt = await receipts.prepare({
|
|
1648
|
+
receiptId,
|
|
1649
|
+
operatorSessionId: sessionId,
|
|
1650
|
+
...binding,
|
|
1651
|
+
expiresAt,
|
|
1652
|
+
humanGateToken,
|
|
1653
|
+
});
|
|
1654
|
+
const card = makeHumanGateCard({
|
|
1655
|
+
cardId: `contract-${receipt.receiptId}`,
|
|
1656
|
+
gateType: "contract-apply",
|
|
1657
|
+
state: "prepared",
|
|
1658
|
+
taskId,
|
|
1659
|
+
receiptId: receipt.receiptId,
|
|
1660
|
+
expiresAt,
|
|
1661
|
+
humanGateToken,
|
|
1662
|
+
contractRevision: binding.revision,
|
|
1663
|
+
contractDiffSummary: `revision ${binding.revision}; draft ${binding.draftSha256.slice(0, 12)}; observed ${binding.expectedObservedHash.slice(0, 12)}`,
|
|
1664
|
+
inspectHref: `/inspect/?taskId=${encodeURIComponent(taskId)}#/`,
|
|
1665
|
+
});
|
|
1168
1666
|
appendHumanGateCard(deps, sessionId, card);
|
|
1169
1667
|
sendJson(res, 201, { ok: true, card });
|
|
1170
1668
|
return;
|
|
@@ -1172,18 +1670,34 @@ async function handleContractApplyHumanGate(req, res, deps, sessionId, step) {
|
|
|
1172
1670
|
const receiptId = typeof body.receiptId === "string" ? body.receiptId : "";
|
|
1173
1671
|
const receipt = await receipts.get(receiptId);
|
|
1174
1672
|
if (!receipt || receipt.operatorSessionId !== sessionId) {
|
|
1175
|
-
sendJson(res, 404, {
|
|
1673
|
+
sendJson(res, 404, {
|
|
1674
|
+
ok: false,
|
|
1675
|
+
error: {
|
|
1676
|
+
code: "NOT_FOUND",
|
|
1677
|
+
message: "contract apply receipt not found",
|
|
1678
|
+
},
|
|
1679
|
+
});
|
|
1176
1680
|
return;
|
|
1177
1681
|
}
|
|
1178
1682
|
const current = await liveContractBinding(ctx, receipt.taskId);
|
|
1179
|
-
const check = verifyHumanGateToken(body.humanGateToken, {
|
|
1683
|
+
const check = verifyHumanGateToken(body.humanGateToken, {
|
|
1684
|
+
confirmationId: receipt.receiptId,
|
|
1685
|
+
payloadHash: contractApplyPayloadHash(current),
|
|
1686
|
+
}, deps.bootToken.confirmationToken);
|
|
1180
1687
|
if (!check.ok) {
|
|
1181
|
-
sendJson(res, 403, {
|
|
1688
|
+
sendJson(res, 403, {
|
|
1689
|
+
ok: false,
|
|
1690
|
+
error: { code: check.code, message: check.message },
|
|
1691
|
+
});
|
|
1182
1692
|
return;
|
|
1183
1693
|
}
|
|
1184
1694
|
const reserved = await receipts.tryReserve(receiptId);
|
|
1185
1695
|
if (!reserved.ok) {
|
|
1186
|
-
sendJson(res, reserved.code === "NOT_FOUND"
|
|
1696
|
+
sendJson(res, reserved.code === "NOT_FOUND"
|
|
1697
|
+
? 404
|
|
1698
|
+
: reserved.code === "EXPIRED"
|
|
1699
|
+
? 410
|
|
1700
|
+
: 409, {
|
|
1187
1701
|
ok: false,
|
|
1188
1702
|
error: { code: reserved.code, message: reserved.message },
|
|
1189
1703
|
});
|
|
@@ -1209,7 +1723,12 @@ async function handleContractApplyHumanGate(req, res, deps, sessionId, step) {
|
|
|
1209
1723
|
ok: true,
|
|
1210
1724
|
card,
|
|
1211
1725
|
result: operationId
|
|
1212
|
-
? {
|
|
1726
|
+
? {
|
|
1727
|
+
operationId,
|
|
1728
|
+
state: "accepted",
|
|
1729
|
+
action: "contractApply",
|
|
1730
|
+
clientRequestId: `chat-apply-${receiptId}`,
|
|
1731
|
+
}
|
|
1213
1732
|
: { state: reserved.reason },
|
|
1214
1733
|
});
|
|
1215
1734
|
return;
|
|
@@ -1233,55 +1752,110 @@ async function handleContractApplyHumanGate(req, res, deps, sessionId, step) {
|
|
|
1233
1752
|
}
|
|
1234
1753
|
const operationId = result.kind === "accepted" ? result.body.operationId : undefined;
|
|
1235
1754
|
await receipts.markDispatched(receiptId, operationId);
|
|
1236
|
-
const card = makeHumanGateCard({
|
|
1755
|
+
const card = makeHumanGateCard({
|
|
1756
|
+
cardId: `contract-${receiptId}`,
|
|
1757
|
+
gateType: "contract-apply",
|
|
1758
|
+
state: "dispatched",
|
|
1759
|
+
taskId: receipt.taskId,
|
|
1760
|
+
receiptId,
|
|
1761
|
+
expiresAt: receipt.expiresAt,
|
|
1762
|
+
contractRevision: receipt.revision,
|
|
1763
|
+
operationId,
|
|
1764
|
+
inspectHref: operationId
|
|
1765
|
+
? `/inspect/?operationId=${encodeURIComponent(operationId)}#/`
|
|
1766
|
+
: `/inspect/?taskId=${encodeURIComponent(receipt.taskId)}#/`,
|
|
1767
|
+
});
|
|
1237
1768
|
appendHumanGateCard(deps, sessionId, card);
|
|
1238
1769
|
if (operationId)
|
|
1239
|
-
await deps.operationLinker.link({
|
|
1240
|
-
|
|
1770
|
+
await deps.operationLinker.link({
|
|
1771
|
+
sessionId,
|
|
1772
|
+
turnId: deps.events.latestTurnId(sessionId) ?? "human-gate",
|
|
1773
|
+
toolCallId: "human-contract-apply",
|
|
1774
|
+
operationId,
|
|
1775
|
+
});
|
|
1776
|
+
sendJson(res, result.status, {
|
|
1777
|
+
ok: true,
|
|
1778
|
+
card,
|
|
1779
|
+
result: actionResultBody(result),
|
|
1780
|
+
});
|
|
1241
1781
|
}
|
|
1242
1782
|
catch (error) {
|
|
1243
|
-
sendJson(res, 400, {
|
|
1783
|
+
sendJson(res, 400, {
|
|
1784
|
+
ok: false,
|
|
1785
|
+
error: {
|
|
1786
|
+
code: "HUMAN_GATE_FAILED",
|
|
1787
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1788
|
+
},
|
|
1789
|
+
});
|
|
1244
1790
|
}
|
|
1245
1791
|
}
|
|
1246
1792
|
async function handleDagHumanGate(req, res, deps, sessionId, step) {
|
|
1247
1793
|
const gate = gateMutation(req, deps);
|
|
1248
1794
|
if (!gate.ok) {
|
|
1249
|
-
sendJson(res, gate.status, {
|
|
1795
|
+
sendJson(res, gate.status, {
|
|
1796
|
+
ok: false,
|
|
1797
|
+
error: { code: gate.code, message: gate.message },
|
|
1798
|
+
});
|
|
1250
1799
|
return;
|
|
1251
1800
|
}
|
|
1252
|
-
if (!await requireChatSession(res, deps, sessionId))
|
|
1801
|
+
if (!(await requireChatSession(res, deps, sessionId)))
|
|
1253
1802
|
return;
|
|
1254
1803
|
const ctx = deps.runtime.actionContext;
|
|
1255
1804
|
if (!ctx) {
|
|
1256
|
-
sendJson(res, 503, {
|
|
1805
|
+
sendJson(res, 503, {
|
|
1806
|
+
ok: false,
|
|
1807
|
+
error: {
|
|
1808
|
+
code: "CHAT_NO_ACTION_CONTEXT",
|
|
1809
|
+
message: "Chat runtime has no operator action context wired",
|
|
1810
|
+
},
|
|
1811
|
+
});
|
|
1257
1812
|
return;
|
|
1258
1813
|
}
|
|
1259
1814
|
try {
|
|
1260
1815
|
const body = await readJsonBody(req);
|
|
1261
|
-
const action = step === "prepare"
|
|
1816
|
+
const action = step === "prepare"
|
|
1817
|
+
? "prepareDagConfirmation"
|
|
1818
|
+
: step === "confirm"
|
|
1819
|
+
? "confirmDagConfirmation"
|
|
1820
|
+
: "runDag";
|
|
1262
1821
|
const result = await dispatchOperatorAction(ctx, {
|
|
1263
1822
|
action,
|
|
1264
1823
|
actionParams: body,
|
|
1265
|
-
clientRequestId: typeof body.clientRequestId === "string"
|
|
1824
|
+
clientRequestId: typeof body.clientRequestId === "string"
|
|
1825
|
+
? body.clientRequestId
|
|
1826
|
+
: `chat-${sessionId}-${Date.now()}`,
|
|
1266
1827
|
});
|
|
1267
1828
|
if (result.kind === "error") {
|
|
1268
1829
|
sendJson(res, result.status, result.body);
|
|
1269
1830
|
return;
|
|
1270
1831
|
}
|
|
1271
1832
|
const projected = actionResultBody(result);
|
|
1272
|
-
const confirmation = (projected.confirmation ??
|
|
1833
|
+
const confirmation = (projected.confirmation ??
|
|
1834
|
+
(await ctx.confirmations.get(String(body.confirmationId ?? ""))));
|
|
1273
1835
|
const confirmationId = String(confirmation?.confirmationId ?? body.confirmationId ?? "");
|
|
1274
1836
|
const taskBinding = confirmation?.taskContractBinding;
|
|
1275
1837
|
const taskId = String(taskBinding?.taskId ?? body.taskId ?? "");
|
|
1276
|
-
const state = step === "prepare"
|
|
1838
|
+
const state = step === "prepare"
|
|
1839
|
+
? "prepared"
|
|
1840
|
+
: step === "confirm"
|
|
1841
|
+
? "confirmed"
|
|
1842
|
+
: "dispatched";
|
|
1277
1843
|
const operationId = result.kind === "accepted" ? result.body.operationId : undefined;
|
|
1278
|
-
const dagText = typeof body.dagText === "string"
|
|
1844
|
+
const dagText = typeof body.dagText === "string"
|
|
1845
|
+
? body.dagText
|
|
1846
|
+
: typeof body.dagJson === "string"
|
|
1847
|
+
? body.dagJson
|
|
1848
|
+
: "";
|
|
1279
1849
|
let dagSpine;
|
|
1280
1850
|
try {
|
|
1281
1851
|
const parsed = JSON.parse(dagText);
|
|
1282
|
-
dagSpine = parsed.nodes
|
|
1852
|
+
dagSpine = parsed.nodes
|
|
1853
|
+
?.map((node) => String(node.id ?? ""))
|
|
1854
|
+
.filter(Boolean);
|
|
1855
|
+
}
|
|
1856
|
+
catch {
|
|
1857
|
+
/* display summary is optional */
|
|
1283
1858
|
}
|
|
1284
|
-
catch { /* display summary is optional */ }
|
|
1285
1859
|
const card = makeHumanGateCard({
|
|
1286
1860
|
cardId: `dag-${confirmationId}`,
|
|
1287
1861
|
gateType: "dag-run",
|
|
@@ -1289,26 +1863,48 @@ async function handleDagHumanGate(req, res, deps, sessionId, step) {
|
|
|
1289
1863
|
taskId,
|
|
1290
1864
|
confirmationId,
|
|
1291
1865
|
expiresAt: String(confirmation?.expiresAt ?? new Date().toISOString()),
|
|
1292
|
-
inspectHref: operationId
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1866
|
+
inspectHref: operationId
|
|
1867
|
+
? `/inspect/?operationId=${encodeURIComponent(operationId)}#/`
|
|
1868
|
+
: `/inspect/?taskId=${encodeURIComponent(taskId)}#/`,
|
|
1869
|
+
...(confirmation?.humanGateToken
|
|
1870
|
+
? { humanGateToken: confirmation.humanGateToken }
|
|
1871
|
+
: {}),
|
|
1872
|
+
displayOnlyChallenges: Array.isArray(projected.requiredChallenges)
|
|
1873
|
+
? projected.requiredChallenges.map(String)
|
|
1874
|
+
: undefined,
|
|
1875
|
+
contractRevision: typeof taskBinding?.revision === "number"
|
|
1876
|
+
? taskBinding.revision
|
|
1877
|
+
: undefined,
|
|
1296
1878
|
dagSpine,
|
|
1297
1879
|
operationId,
|
|
1298
1880
|
});
|
|
1299
1881
|
appendHumanGateCard(deps, sessionId, card);
|
|
1300
1882
|
if (operationId)
|
|
1301
|
-
await deps.operationLinker.link({
|
|
1883
|
+
await deps.operationLinker.link({
|
|
1884
|
+
sessionId,
|
|
1885
|
+
turnId: deps.events.latestTurnId(sessionId) ?? "human-gate",
|
|
1886
|
+
toolCallId: `human-${step}`,
|
|
1887
|
+
operationId,
|
|
1888
|
+
});
|
|
1302
1889
|
sendJson(res, result.status, { ok: true, card, result: projected });
|
|
1303
1890
|
}
|
|
1304
1891
|
catch (error) {
|
|
1305
|
-
sendJson(res, 400, {
|
|
1892
|
+
sendJson(res, 400, {
|
|
1893
|
+
ok: false,
|
|
1894
|
+
error: {
|
|
1895
|
+
code: "HUMAN_GATE_FAILED",
|
|
1896
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1897
|
+
},
|
|
1898
|
+
});
|
|
1306
1899
|
}
|
|
1307
1900
|
}
|
|
1308
1901
|
async function handleMutationHumanGate(req, res, deps, sessionId) {
|
|
1309
1902
|
const gate = gateMutation(req, deps);
|
|
1310
1903
|
if (!gate.ok) {
|
|
1311
|
-
sendJson(res, gate.status, {
|
|
1904
|
+
sendJson(res, gate.status, {
|
|
1905
|
+
ok: false,
|
|
1906
|
+
error: { code: gate.code, message: gate.message },
|
|
1907
|
+
});
|
|
1312
1908
|
return;
|
|
1313
1909
|
}
|
|
1314
1910
|
if (!(await requireChatSession(res, deps, sessionId)))
|
|
@@ -1333,7 +1929,10 @@ async function handleMutationHumanGate(req, res, deps, sessionId) {
|
|
|
1333
1929
|
if (!receipt || receipt.operatorSessionId !== ctx.operatorSessionId) {
|
|
1334
1930
|
sendJson(res, 404, {
|
|
1335
1931
|
ok: false,
|
|
1336
|
-
error: {
|
|
1932
|
+
error: {
|
|
1933
|
+
code: "NOT_FOUND",
|
|
1934
|
+
message: "mutation gate receipt not found",
|
|
1935
|
+
},
|
|
1337
1936
|
});
|
|
1338
1937
|
return;
|
|
1339
1938
|
}
|
|
@@ -1372,7 +1971,11 @@ async function handleMutationHumanGate(req, res, deps, sessionId) {
|
|
|
1372
1971
|
operationId,
|
|
1373
1972
|
});
|
|
1374
1973
|
}
|
|
1375
|
-
sendJson(res, result.status, {
|
|
1974
|
+
sendJson(res, result.status, {
|
|
1975
|
+
ok: true,
|
|
1976
|
+
card,
|
|
1977
|
+
result: actionResultBody(result),
|
|
1978
|
+
});
|
|
1376
1979
|
}
|
|
1377
1980
|
catch (error) {
|
|
1378
1981
|
sendJson(res, 400, {
|
|
@@ -1387,43 +1990,80 @@ async function handleMutationHumanGate(req, res, deps, sessionId) {
|
|
|
1387
1990
|
async function handleConfirmTaskKind(req, res, deps, sessionId, interviewSessionId) {
|
|
1388
1991
|
const gate = gateMutation(req, deps);
|
|
1389
1992
|
if (!gate.ok) {
|
|
1390
|
-
sendJson(res, gate.status, {
|
|
1993
|
+
sendJson(res, gate.status, {
|
|
1994
|
+
ok: false,
|
|
1995
|
+
error: { code: gate.code, message: gate.message },
|
|
1996
|
+
});
|
|
1391
1997
|
return;
|
|
1392
1998
|
}
|
|
1393
|
-
if (!await requireChatSession(res, deps, sessionId))
|
|
1999
|
+
if (!(await requireChatSession(res, deps, sessionId)))
|
|
1394
2000
|
return;
|
|
1395
2001
|
try {
|
|
1396
2002
|
const body = await readJsonBody(req);
|
|
1397
2003
|
if (typeof body.taskKind !== "string" || !body.taskKind.trim()) {
|
|
1398
|
-
sendJson(res, 400, {
|
|
2004
|
+
sendJson(res, 400, {
|
|
2005
|
+
ok: false,
|
|
2006
|
+
error: { code: "INVALID_INPUT", message: "taskKind is required" },
|
|
2007
|
+
});
|
|
1399
2008
|
return;
|
|
1400
2009
|
}
|
|
1401
|
-
const state = await new ChatInterviewAdapter(deps.appData, deps.events).confirmTaskKind({
|
|
2010
|
+
const state = await new ChatInterviewAdapter(deps.appData, deps.events).confirmTaskKind({
|
|
2011
|
+
operatorSessionId: sessionId,
|
|
2012
|
+
interviewSessionId,
|
|
2013
|
+
taskKind: body.taskKind,
|
|
2014
|
+
});
|
|
1402
2015
|
sendJson(res, 200, { ok: true, ...projectChatInterviewState(state) });
|
|
1403
2016
|
}
|
|
1404
2017
|
catch (error) {
|
|
1405
|
-
sendJson(res, 400, {
|
|
2018
|
+
sendJson(res, 400, {
|
|
2019
|
+
ok: false,
|
|
2020
|
+
error: {
|
|
2021
|
+
code: "TASK_KIND_CONFIRM_FAILED",
|
|
2022
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2023
|
+
},
|
|
2024
|
+
});
|
|
1406
2025
|
}
|
|
1407
2026
|
}
|
|
1408
2027
|
export async function handleChatCompact(req, res, deps, sessionId) {
|
|
1409
2028
|
const gate = gateMutation(req, deps);
|
|
1410
2029
|
if (!gate.ok) {
|
|
1411
|
-
sendJson(res, gate.status, {
|
|
2030
|
+
sendJson(res, gate.status, {
|
|
2031
|
+
ok: false,
|
|
2032
|
+
error: { code: gate.code, message: gate.message },
|
|
2033
|
+
});
|
|
1412
2034
|
return;
|
|
1413
2035
|
}
|
|
1414
2036
|
if (deps.events.getActiveTurn(sessionId)) {
|
|
1415
|
-
sendJson(res, 409, {
|
|
2037
|
+
sendJson(res, 409, {
|
|
2038
|
+
ok: false,
|
|
2039
|
+
error: { code: "TURN_ACTIVE", message: "cannot compact an active turn" },
|
|
2040
|
+
});
|
|
1416
2041
|
return;
|
|
1417
2042
|
}
|
|
1418
2043
|
const record = await deps.store.get(sessionId);
|
|
1419
2044
|
if (!record) {
|
|
1420
|
-
sendJson(res, 404, {
|
|
2045
|
+
sendJson(res, 404, {
|
|
2046
|
+
ok: false,
|
|
2047
|
+
error: {
|
|
2048
|
+
code: "NOT_FOUND",
|
|
2049
|
+
message: `chat session not found: ${sessionId}`,
|
|
2050
|
+
},
|
|
2051
|
+
});
|
|
1421
2052
|
return;
|
|
1422
2053
|
}
|
|
1423
2054
|
if (!deps.runtime.hasSession(sessionId)) {
|
|
1424
|
-
const reopened = await deps.runtime.reopenSession({
|
|
2055
|
+
const reopened = await deps.runtime.reopenSession({
|
|
2056
|
+
sessionId,
|
|
2057
|
+
sessionFile: record.sessionFile,
|
|
2058
|
+
...(record.modelProvider && record.modelId
|
|
2059
|
+
? { model: { provider: record.modelProvider, modelId: record.modelId } }
|
|
2060
|
+
: {}),
|
|
2061
|
+
});
|
|
1425
2062
|
if (!reopened.ok) {
|
|
1426
|
-
sendJson(res, 409, {
|
|
2063
|
+
sendJson(res, 409, {
|
|
2064
|
+
ok: false,
|
|
2065
|
+
error: { code: reopened.code, message: reopened.message },
|
|
2066
|
+
});
|
|
1427
2067
|
return;
|
|
1428
2068
|
}
|
|
1429
2069
|
}
|
|
@@ -1443,11 +2083,20 @@ export async function handleChatCompact(req, res, deps, sessionId) {
|
|
|
1443
2083
|
export async function handleChatMessages(res, deps, sessionId, url) {
|
|
1444
2084
|
try {
|
|
1445
2085
|
const limit = Number(url.searchParams.get("limit") ?? 30);
|
|
1446
|
-
const page = await deps.store.listMessages(sessionId, {
|
|
2086
|
+
const page = await deps.store.listMessages(sessionId, {
|
|
2087
|
+
limit: Number.isFinite(limit) ? limit : 30,
|
|
2088
|
+
beforeId: url.searchParams.get("beforeId") ?? undefined,
|
|
2089
|
+
});
|
|
1447
2090
|
sendJson(res, 200, { ok: true, ...page });
|
|
1448
2091
|
}
|
|
1449
2092
|
catch (error) {
|
|
1450
|
-
sendJson(res, 404, {
|
|
2093
|
+
sendJson(res, 404, {
|
|
2094
|
+
ok: false,
|
|
2095
|
+
error: {
|
|
2096
|
+
code: "NOT_FOUND",
|
|
2097
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2098
|
+
},
|
|
2099
|
+
});
|
|
1451
2100
|
}
|
|
1452
2101
|
}
|
|
1453
2102
|
/**
|
|
@@ -1457,28 +2106,46 @@ export async function handleChatMessages(res, deps, sessionId, url) {
|
|
|
1457
2106
|
export async function handleChatRequest(req, res, deps, pathname) {
|
|
1458
2107
|
if (!deps)
|
|
1459
2108
|
return false;
|
|
1460
|
-
if (!pathname.startsWith("/api/operator/v1/chat/") &&
|
|
2109
|
+
if (!pathname.startsWith("/api/operator/v1/chat/") &&
|
|
2110
|
+
!pathname.startsWith("/api/operator/v1/pi/"))
|
|
1461
2111
|
return false;
|
|
1462
2112
|
const method = (req.method ?? "GET").toUpperCase();
|
|
1463
2113
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1464
|
-
if (pathname === "/api/operator/v1/pi/models-config/test" &&
|
|
2114
|
+
if (pathname === "/api/operator/v1/pi/models-config/test" &&
|
|
2115
|
+
method === "POST") {
|
|
1465
2116
|
const gate = gateMutation(req, deps);
|
|
1466
2117
|
if (!gate.ok) {
|
|
1467
|
-
sendJson(res, gate.status, {
|
|
2118
|
+
sendJson(res, gate.status, {
|
|
2119
|
+
ok: false,
|
|
2120
|
+
error: { code: gate.code, message: gate.message },
|
|
2121
|
+
});
|
|
1468
2122
|
return true;
|
|
1469
2123
|
}
|
|
1470
2124
|
try {
|
|
1471
2125
|
const body = await readJsonBody(req);
|
|
1472
2126
|
if (typeof body.provider !== "string" || typeof body.modelId !== "string")
|
|
1473
2127
|
throw new Error("provider and modelId are required");
|
|
1474
|
-
sendJson(res, 200, {
|
|
2128
|
+
sendJson(res, 200, {
|
|
2129
|
+
ok: true,
|
|
2130
|
+
data: await deps.runtime.testConfiguredModel({
|
|
2131
|
+
provider: body.provider,
|
|
2132
|
+
modelId: body.modelId,
|
|
2133
|
+
}),
|
|
2134
|
+
});
|
|
1475
2135
|
}
|
|
1476
2136
|
catch (error) {
|
|
1477
|
-
sendJson(res, 400, {
|
|
2137
|
+
sendJson(res, 400, {
|
|
2138
|
+
ok: false,
|
|
2139
|
+
error: {
|
|
2140
|
+
code: "MODEL_TEST_FAILED",
|
|
2141
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2142
|
+
},
|
|
2143
|
+
});
|
|
1478
2144
|
}
|
|
1479
2145
|
return true;
|
|
1480
2146
|
}
|
|
1481
|
-
if (pathname === "/api/operator/v1/pi/models-config" &&
|
|
2147
|
+
if (pathname === "/api/operator/v1/pi/models-config" &&
|
|
2148
|
+
(method === "GET" || method === "PATCH")) {
|
|
1482
2149
|
await handlePiConfig(req, res, deps, "models");
|
|
1483
2150
|
return true;
|
|
1484
2151
|
}
|
|
@@ -1486,11 +2153,13 @@ export async function handleChatRequest(req, res, deps, pathname) {
|
|
|
1486
2153
|
await handlePiConfig(req, res, deps, "packages");
|
|
1487
2154
|
return true;
|
|
1488
2155
|
}
|
|
1489
|
-
if (pathname === "/api/operator/v1/chat/instruction-skills" &&
|
|
2156
|
+
if (pathname === "/api/operator/v1/chat/instruction-skills" &&
|
|
2157
|
+
method === "GET") {
|
|
1490
2158
|
await handlePiConfig(req, res, deps, "skills");
|
|
1491
2159
|
return true;
|
|
1492
2160
|
}
|
|
1493
|
-
if (pathname === "/api/operator/v1/chat/instruction-skills/preferences" &&
|
|
2161
|
+
if (pathname === "/api/operator/v1/chat/instruction-skills/preferences" &&
|
|
2162
|
+
method === "PUT") {
|
|
1494
2163
|
await handlePiConfig(req, res, deps, "skills");
|
|
1495
2164
|
return true;
|
|
1496
2165
|
}
|
|
@@ -1505,7 +2174,10 @@ export async function handleChatRequest(req, res, deps, pathname) {
|
|
|
1505
2174
|
if (method === "GET" && pathname === "/api/operator/v1/chat/repo/file") {
|
|
1506
2175
|
const filePath = url.searchParams.get("path");
|
|
1507
2176
|
if (!filePath)
|
|
1508
|
-
sendJson(res, 400, {
|
|
2177
|
+
sendJson(res, 400, {
|
|
2178
|
+
ok: false,
|
|
2179
|
+
error: { code: "INVALID_PATH", message: "path is required" },
|
|
2180
|
+
});
|
|
1509
2181
|
else
|
|
1510
2182
|
await handleRepoBrowser(res, deps, "file", filePath);
|
|
1511
2183
|
return true;
|
|
@@ -1629,6 +2301,11 @@ export async function handleChatRequest(req, res, deps, pathname) {
|
|
|
1629
2301
|
await handleGetTaskContext(req, res, deps, decodeURIComponent(sessionContext[1]));
|
|
1630
2302
|
return true;
|
|
1631
2303
|
}
|
|
2304
|
+
const sessionState = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/state$/);
|
|
2305
|
+
if (method === "GET" && sessionState) {
|
|
2306
|
+
await handleGetChatSessionState(req, res, deps, decodeURIComponent(sessionState[1]));
|
|
2307
|
+
return true;
|
|
2308
|
+
}
|
|
1632
2309
|
const sessionEvents = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/events$/);
|
|
1633
2310
|
if (method === "GET" && sessionEvents) {
|
|
1634
2311
|
await handleChatEventsStream(req, res, deps, decodeURIComponent(sessionEvents[1]));
|
|
@@ -1662,7 +2339,10 @@ export async function handleChatRequest(req, res, deps, pathname) {
|
|
|
1662
2339
|
});
|
|
1663
2340
|
return true;
|
|
1664
2341
|
}
|
|
1665
|
-
sendJson(res, 404, {
|
|
2342
|
+
sendJson(res, 404, {
|
|
2343
|
+
ok: false,
|
|
2344
|
+
error: { code: "NOT_FOUND", message: `unknown chat route: ${pathname}` },
|
|
2345
|
+
});
|
|
1666
2346
|
return true;
|
|
1667
2347
|
}
|
|
1668
2348
|
// Re-export for route wiring; writeSseEvent kept for parity with operation-sse.
|