@tea-agent/loop-agent 0.33.4 → 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.
@@ -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, } from "../security.js";
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 { kind: "message_update", data: { text: String(event.text ?? "") } };
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
- return JSON.parse(raw);
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(["image/png", "image/jpeg", "image/gif", "image/webp"]);
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" || typeof record.mimeType !== "string" || !IMAGE_MIME_TYPES.has(record.mimeType))
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" || !record.data || !/^[A-Za-z0-9+/]+={0,2}$/.test(record.data) || record.data.length % 4 !== 0)
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.subarray(0, Math.min(bytes.length, 64 * 1024)).toString("utf8");
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 { type: "image", mimeType: record.mimeType, data: bytes.toString("base64") };
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).length,
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, { ok: false, error: { code: "INVALID_INPUT", message: "operation idempotency ownership cannot be copied" } });
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 && (!snapshotInput || typeof snapshotInput !== "object" || snapshotInput.schemaVersion !== 1 || typeof snapshotInput.summary !== "string" || !snapshotInput.summary)) {
214
- sendJson(res, 400, { ok: false, error: { code: "INVALID_SNAPSHOT", message: "valid OperatorContextSnapshotV1 is required" } });
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 ? undefined : projectCompactSnapshot(snapshotInput);
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 ? { systemPromptSuffix: `Read-only initial Operator context snapshot; this is not runtime rollback.\n${snapshot.summary}` } : {}),
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`, { kind: "compact", data: 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, { ok: false, error: { code: gate.code, message: gate.message } });
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, { ok: false, error: { code: "TURN_ACTIVE", message: "cannot fork an active turn" } });
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, { ok: false, error: { code: "NOT_FOUND", message: `chat session not found: ${sourceSessionId}` } });
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({ sourceSessionFile: source.sessionFile ?? "", ...(source.modelProvider && source.modelId ? { model: { provider: source.modelProvider, modelId: source.modelId } } : {}) });
266
- await deps.store.forkFrom({ sourceSessionId, targetSessionId: handle.sessionId, targetSessionFile: handle.sessionFile });
267
- const copied = deps.events.forkContextRefs({ sourceSessionId, targetSessionId: handle.sessionId, targetTurnId: `${handle.sessionId}:fork` });
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, { ok: true, sessionId: handle.sessionId, forkedFrom: sourceSessionId, copiedContextRefs: copied.copiedCount, warnings: ["已发生的 task/DAG/Worker mutation 不会撤销或重放。", "operation idempotency / clientRequestId ownership 不会复制到新会话。"] });
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, { ok: false, error: { code: "SESSION_FORK_FAILED", message: error instanceof Error ? error.message : String(error) } });
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, { ok: false, error: { code: gate.code, message: gate.message } });
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, { ok: false, error: { code: "TURN_ACTIVE", message: "cannot branch an active turn" } });
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, { ok: false, error: { code: "NOT_FOUND", message: `chat session not found: ${sessionId}` } });
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({ sessionId, sessionFile: record.sessionFile });
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" ? body.branchFromEntryId : current.leafId;
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({ sessionId, branchFromEntryId, ...(typeof body.summary === "string" && body.summary ? { summary: body.summary } : {}) });
304
- sendJson(res, 200, { ok: true, ...result, note: "仅影响后续 LLM 上下文,未持久化第二历史" });
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, { ok: false, error: { code: "SESSION_BRANCH_FAILED", message: error instanceof Error ? error.message : String(error) } });
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, { ok: false, error: { code: gate.code, message: gate.message } });
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, { ok: false, error: { code: "MAINLINE_SWITCH_FAILED", message: error instanceof Error ? error.message : String(error) } });
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: { code: "NOT_FOUND", message: `chat session not found: ${sessionId}` },
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, { ok: false, error: { code: gate.code, message: gate.message } });
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, { ok: false, error: { code: "INVALID_INPUT", message: "title or state(active|archived) is required" } });
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, { ok: false, error: { code: "NOT_FOUND", message: error instanceof Error ? error.message : String(error) } });
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, { ok: false, error: { code: gate.code, message: gate.message } });
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, { ok: false, error: { code: "NOT_FOUND", message: `chat session not found: ${sessionId}` } });
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, { ok: true, sessionId, reopened: false, alreadyActive: true, hasBash: true });
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, { ok: false, error: { code: result.code, message: result.message } });
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, { ok: false, error: { code: "INVALID_ATTACHMENT", message: error instanceof Error ? error.message : String(error) } });
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, { ok: false, error: { code: "NOT_FOUND", message: `chat session not found: ${sessionId}` } });
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, { ok: false, error: { code: reopen.code, message: reopen.message } });
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" && event.toolName === "prepareDagConfirmation" && !event.isError) {
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" && event.toolName === "prepareMutationGate" && !event.isError) {
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 ? "CLIENT_ABORTED" : "CHAT_TURN_FAILED",
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, { ok: false, error: { code: gate.code, message: gate.message } });
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, { ok: false, error: { code: "INVALID_INPUT", message: error instanceof Error ? error.message : String(error) } });
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, { ok: false, error: { code: "INVALID_ATTACHMENT", message: error instanceof Error ? error.message : String(error) } });
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, { ok: false, error: { code: "INVALID_INPUT", message: "text is required" } });
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, { ok: false, error: { code: "NOT_FOUND", message: `chat session not found: ${sessionId}` } });
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, { ok: false, error: { code: reopen.code, message: reopen.message } });
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, { ok: false, error: { code: "CHAT_NO_ACTION_CONTEXT", message: "Chat runtime has no operator action context wired" } });
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: { code: "TURN_ACTIVE", message: `chat turn already active: ${created.activeTurn.turnId}` },
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, { ok: true, turnId: turn.turnId, ordinal: turn.ordinal, state: "running" });
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, { kind: "error", data: { message } });
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, { ok: false, error: { code: "NOT_FOUND", message: `chat turn not found: ${turnId}` } });
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, { ok: false, error: { code: "NOT_FOUND", message: `chat session not found: ${sessionId}` } });
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, { ok: false, error: { code: "NOT_FOUND", message: `chat session not found: ${sessionId}` } });
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, { ok: false, error: { code: gate.code, message: gate.message } });
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, { ok: false, error: { code: "INVALID_INPUT", message: error instanceof Error ? error.message : String(error) } });
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, { ok: false, error: { code: failure.code, message: failure.message } });
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, { ok: true, data: await readModelsConfig(agentDir) });
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, { ok: false, error: { code: gate.code, message: gate.message } });
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, { ok: true, data: await patchModelsConfig(agentDir, body) });
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, { ok: true, data: { packages: await readPackageInventory(await deps.runtime.getAgentDir(), deps.appData.repoRoot), mutationSupported: false } });
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, { ok: true, data: {
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, { ok: false, error: { code: gate.code, message: gate.message } });
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) ? body.disabledNames.filter((name) => typeof name === "string") : []);
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, { ok: true, data: { disabledNames, appliesTo: "new-sessions" } });
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, { ok: false, error: { code: failure.code ?? "INVALID_INPUT", message: failure.message } });
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, { skipSensitive: true, maxFiles: 5000 }))
927
- .map((entry) => entry.repoRelative).filter((file) => !isSensitivePath(file) && (!q || file.toLowerCase().includes(q)))
928
- .sort((a, b) => a.localeCompare(b)).slice(0, 50).map((path) => ({ path }));
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, { ok: true, models: await deps.runtime.listModels(sessionId), thinkingLevels: THINKING_LEVELS });
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, { ok: false, error: { code: gate.code, message: gate.message } });
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, { provider: body.provider, modelId: body.modelId });
977
- updated = await deps.store.setRuntimeSelection(sessionId, { modelProvider: model.provider, modelId: model.modelId });
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" && THINKING_LEVELS.includes(body.thinkingLevel)) {
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, { thinkingLevel });
1346
+ updated = await deps.store.setRuntimeSelection(sessionId, {
1347
+ thinkingLevel,
1348
+ });
982
1349
  }
983
1350
  else {
984
- sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "provider/modelId or thinkingLevel is required" } });
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, { ok: false, error: { code: "MODEL_SWITCH_FAILED", message: error instanceof Error ? error.message : String(error) } });
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, { ok: false, error: { code: "NOT_FOUND", message: `chat session not found: ${sessionId}` } });
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, { ok: false, error: { code: gate.code, message: gate.message } });
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, { ok: false, error: { code: "INVALID_INPUT", message: "operationId is required" } });
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, { ok: false, error: { code: "NOT_FOUND", message: `operation not found: ${operationId}` } });
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({ sessionId, turnId: deps.events.latestTurnId(sessionId) ?? "workspace", toolCallId: `workspace-${operationId}`, operationId });
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, { ok: false, error: { code: "INVALID_INPUT", message: error instanceof Error ? error.message : String(error) } });
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, { ok: false, error: { code: gate.code, message: gate.message } });
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, { ok: false, error: { code: "INVALID_INPUT", message: "taskId is required" } });
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({ operatorSessionId: sessionId, taskId, ...(typeof body.title === "string" ? { title: body.title } : {}), ...(body.initialDraft && typeof body.initialDraft === "object" ? { initialDraft: body.initialDraft } : {}) });
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, { ok: false, error: { code: "INTERVIEW_START_FAILED", message: error instanceof Error ? error.message : String(error) } });
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, { ok: false, error: { code: gate.code, message: gate.message } });
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, { ok: false, error: { code: "INVALID_INPUT", message: "questionId and response are required" } });
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({ operatorSessionId: sessionId, interviewSessionId, questionId, response, ...(typeof body.text === "string" ? { text: body.text } : {}) });
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, { ok: false, error: { code: "INTERVIEW_ANSWER_FAILED", message: error instanceof Error ? error.message : String(error) } });
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, { ok: false, error: { code: "NOT_FOUND", message: `interview session not found: ${interviewSessionId}` } });
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: ["source", "writer-scope", "verification", "human-gates"],
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, { action: "contractShow", actionParams: { taskId } });
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 { taskId, revision, canonicalHash: state?.ref?.canonicalHash ?? expectedObservedHash, draftSha256: draft.draftSha256, expectedObservedHash };
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, { ok: false, error: { code: gate.code, message: gate.message } });
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, { ok: false, error: { code: "CHAT_NO_ACTION_CONTEXT", message: "Chat runtime has no operator action context wired" } });
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({ confirmationId: receiptId, payloadHash: contractApplyPayloadHash(binding), expiresAt }, deps.bootToken.confirmationToken);
1166
- const receipt = await receipts.prepare({ receiptId, operatorSessionId: sessionId, ...binding, expiresAt, humanGateToken });
1167
- const card = makeHumanGateCard({ cardId: `contract-${receipt.receiptId}`, gateType: "contract-apply", state: "prepared", taskId, receiptId: receipt.receiptId, expiresAt, humanGateToken, contractRevision: binding.revision, contractDiffSummary: `revision ${binding.revision}; draft ${binding.draftSha256.slice(0, 12)}; observed ${binding.expectedObservedHash.slice(0, 12)}`, inspectHref: `/inspect/?taskId=${encodeURIComponent(taskId)}#/` });
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, { ok: false, error: { code: "NOT_FOUND", message: "contract apply receipt not found" } });
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, { confirmationId: receipt.receiptId, payloadHash: contractApplyPayloadHash(current) }, deps.bootToken.confirmationToken);
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, { ok: false, error: { code: check.code, message: check.message } });
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" ? 404 : reserved.code === "EXPIRED" ? 410 : 409, {
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
- ? { operationId, state: "accepted", action: "contractApply", clientRequestId: `chat-apply-${receiptId}` }
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({ cardId: `contract-${receiptId}`, gateType: "contract-apply", state: "dispatched", taskId: receipt.taskId, receiptId, expiresAt: receipt.expiresAt, contractRevision: receipt.revision, operationId, inspectHref: operationId ? `/inspect/?operationId=${encodeURIComponent(operationId)}#/` : `/inspect/?taskId=${encodeURIComponent(receipt.taskId)}#/` });
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({ sessionId, turnId: deps.events.latestTurnId(sessionId) ?? "human-gate", toolCallId: "human-contract-apply", operationId });
1240
- sendJson(res, result.status, { ok: true, card, result: actionResultBody(result) });
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, { ok: false, error: { code: "HUMAN_GATE_FAILED", message: error instanceof Error ? error.message : String(error) } });
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, { ok: false, error: { code: gate.code, message: gate.message } });
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, { ok: false, error: { code: "CHAT_NO_ACTION_CONTEXT", message: "Chat runtime has no operator action context wired" } });
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" ? "prepareDagConfirmation" : step === "confirm" ? "confirmDagConfirmation" : "runDag";
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" ? body.clientRequestId : `chat-${sessionId}-${Date.now()}`,
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 ?? await ctx.confirmations.get(String(body.confirmationId ?? "")));
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" ? "prepared" : step === "confirm" ? "confirmed" : "dispatched";
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" ? body.dagText : typeof body.dagJson === "string" ? body.dagJson : "";
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?.map((node) => String(node.id ?? "")).filter(Boolean);
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 ? `/inspect/?operationId=${encodeURIComponent(operationId)}#/` : `/inspect/?taskId=${encodeURIComponent(taskId)}#/`,
1293
- ...(confirmation?.humanGateToken ? { humanGateToken: confirmation.humanGateToken } : {}),
1294
- displayOnlyChallenges: Array.isArray(projected.requiredChallenges) ? projected.requiredChallenges.map(String) : undefined,
1295
- contractRevision: typeof taskBinding?.revision === "number" ? taskBinding.revision : undefined,
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({ sessionId, turnId: deps.events.latestTurnId(sessionId) ?? "human-gate", toolCallId: `human-${step}`, operationId });
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, { ok: false, error: { code: "HUMAN_GATE_FAILED", message: error instanceof Error ? error.message : String(error) } });
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, { ok: false, error: { code: gate.code, message: gate.message } });
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: { code: "NOT_FOUND", message: "mutation gate receipt not found" },
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, { ok: true, card, result: actionResultBody(result) });
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, { ok: false, error: { code: gate.code, message: gate.message } });
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, { ok: false, error: { code: "INVALID_INPUT", message: "taskKind is required" } });
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({ operatorSessionId: sessionId, interviewSessionId, taskKind: body.taskKind });
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, { ok: false, error: { code: "TASK_KIND_CONFIRM_FAILED", message: error instanceof Error ? error.message : String(error) } });
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, { ok: false, error: { code: gate.code, message: gate.message } });
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, { ok: false, error: { code: "TURN_ACTIVE", message: "cannot compact an active turn" } });
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, { ok: false, error: { code: "NOT_FOUND", message: `chat session not found: ${sessionId}` } });
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({ sessionId, sessionFile: record.sessionFile, ...(record.modelProvider && record.modelId ? { model: { provider: record.modelProvider, modelId: record.modelId } } : {}) });
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, { ok: false, error: { code: reopened.code, message: reopened.message } });
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, { limit: Number.isFinite(limit) ? limit : 30, beforeId: url.searchParams.get("beforeId") ?? undefined });
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, { ok: false, error: { code: "NOT_FOUND", message: error instanceof Error ? error.message : String(error) } });
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/") && !pathname.startsWith("/api/operator/v1/pi/"))
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" && method === "POST") {
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, { ok: false, error: { code: gate.code, message: gate.message } });
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, { ok: true, data: await deps.runtime.testConfiguredModel({ provider: body.provider, modelId: body.modelId }) });
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, { ok: false, error: { code: "MODEL_TEST_FAILED", message: error instanceof Error ? error.message : String(error) } });
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" && (method === "GET" || method === "PATCH")) {
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" && method === "GET") {
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" && method === "PUT") {
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, { ok: false, error: { code: "INVALID_PATH", message: "path is required" } });
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, { ok: false, error: { code: "NOT_FOUND", message: `unknown chat route: ${pathname}` } });
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.