@markusylisiurunen/tau 0.3.34 → 0.3.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +6 -1
  2. package/dist/core/agent/agent_runtime.js +6 -5
  3. package/dist/core/agent/agent_runtime.js.map +1 -1
  4. package/dist/core/commands/registry.js +24 -2
  5. package/dist/core/commands/registry.js.map +1 -1
  6. package/dist/core/runtime/chat_runtime.js +13 -2
  7. package/dist/core/runtime/chat_runtime.js.map +1 -1
  8. package/dist/core/runtime/steering.js +2 -1
  9. package/dist/core/runtime/steering.js.map +1 -1
  10. package/dist/core/session/compaction.js +1 -1
  11. package/dist/core/session/compaction.js.map +1 -1
  12. package/dist/core/session/goal.js +54 -0
  13. package/dist/core/session/goal.js.map +1 -0
  14. package/dist/core/subagents/agent_supervisor.js +4 -1
  15. package/dist/core/subagents/agent_supervisor.js.map +1 -1
  16. package/dist/core/tools/catalog.js +14 -1
  17. package/dist/core/tools/catalog.js.map +1 -1
  18. package/dist/core/tools/goal.js +77 -0
  19. package/dist/core/tools/goal.js.map +1 -0
  20. package/dist/core/tools/tool_names.js +3 -0
  21. package/dist/core/tools/tool_names.js.map +1 -1
  22. package/dist/core/utils/user_metadata.js +14 -0
  23. package/dist/core/utils/user_metadata.js.map +1 -1
  24. package/dist/core/version.js +1 -1
  25. package/dist/host/client_tool_broker.js +4 -0
  26. package/dist/host/client_tool_broker.js.map +1 -1
  27. package/dist/host/local_session_host.js +391 -45
  28. package/dist/host/local_session_host.js.map +1 -1
  29. package/dist/host/session_host.js +2 -0
  30. package/dist/host/session_host.js.map +1 -1
  31. package/dist/host/session_protocol_handler.js +253 -236
  32. package/dist/host/session_protocol_handler.js.map +1 -1
  33. package/dist/protocol/index.d.ts +1 -1
  34. package/dist/protocol/index.js.map +1 -1
  35. package/dist/protocol/session_protocol.d.ts +28 -3
  36. package/dist/protocol/session_protocol.js +53 -1
  37. package/dist/protocol/session_protocol.js.map +1 -1
  38. package/dist/sdk/index.d.ts +1 -1
  39. package/dist/sdk/index.js.map +1 -1
  40. package/dist/sdk/session.js +20 -0
  41. package/dist/sdk/session.js.map +1 -1
  42. package/dist/sdk/types.d.ts +7 -1
  43. package/dist/store/session_snapshot_migrations.js +10 -1
  44. package/dist/store/session_snapshot_migrations.js.map +1 -1
  45. package/dist/tui/chat_view.js +1 -0
  46. package/dist/tui/chat_view.js.map +1 -1
  47. package/dist/tui/session_chat_controller.js +40 -3
  48. package/dist/tui/session_chat_controller.js.map +1 -1
  49. package/dist/tui/ui/footer.js +7 -3
  50. package/dist/tui/ui/footer.js.map +1 -1
  51. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { createSessionProtocolErrorResponse, createSessionProtocolPendingUserMessagesMessage, createSessionProtocolReadyMessage, createSessionProtocolSuccessResponse, SESSION_PROTOCOL_ERROR_CODES, SESSION_PROTOCOL_METHODS, SESSION_PROTOCOL_VERSION, } from "../protocol/session_protocol.js";
3
- import { EphemeralThreadBusyError, SessionExecBusyError, } from "./session_host.js";
3
+ import { EphemeralThreadBusyError, SessionExecBusyError, SessionRetryUnavailableError, } from "./session_host.js";
4
4
  function isAbortError(error) {
5
5
  return error instanceof Error && error.name === "AbortError";
6
6
  }
@@ -12,29 +12,30 @@ function getSessionLiveState(session) {
12
12
  state = {
13
13
  interrupting: false,
14
14
  revision: 1,
15
- pendingSteeringSubmits: [],
16
- pendingQueuedSubmits: [],
15
+ pendingSubmissions: [],
17
16
  listeners: new Set(),
18
17
  };
19
18
  sessionLiveStates.set(session, state);
20
19
  }
21
20
  return state;
22
21
  }
22
+ function pendingSubmissionsInDeliveryOrder(state) {
23
+ return [
24
+ ...state.pendingSubmissions.filter((pending) => pending.delivery === "turn-boundary"),
25
+ ...state.pendingSubmissions.filter((pending) => pending.delivery === "when-idle"),
26
+ ];
27
+ }
28
+ function toPendingUserMessage(pending) {
29
+ return {
30
+ id: pending.id,
31
+ mode: pending.request.method === "session.queue" ? "queue" : "steer",
32
+ text: pending.request.params.text,
33
+ };
34
+ }
23
35
  function buildPendingUserMessagesState(state) {
24
36
  return {
25
37
  revision: state.revision,
26
- messages: [
27
- ...state.pendingSteeringSubmits.map((pending) => ({
28
- id: pending.id,
29
- mode: pending.mode,
30
- text: pending.request.params.text,
31
- })),
32
- ...state.pendingQueuedSubmits.map((pending) => ({
33
- id: pending.id,
34
- mode: pending.mode,
35
- text: pending.request.params.text,
36
- })),
37
- ],
38
+ messages: pendingSubmissionsInDeliveryOrder(state).map(toPendingUserMessage),
38
39
  };
39
40
  }
40
41
  function publishPendingUserMessages(session, state) {
@@ -44,7 +45,12 @@ function publishPendingUserMessages(session, state) {
44
45
  state: buildPendingUserMessagesState(state),
45
46
  });
46
47
  for (const listener of [...state.listeners]) {
47
- listener(message);
48
+ try {
49
+ listener(message);
50
+ }
51
+ catch {
52
+ // Pending-message observers must not be able to fail shared session work.
53
+ }
48
54
  }
49
55
  }
50
56
  function getSessionMutationQueueState(session) {
@@ -124,6 +130,15 @@ export class SessionProtocolHandler {
124
130
  case "session.snapshot":
125
131
  await this.handleSnapshot(request);
126
132
  return;
133
+ case "session.startGoal":
134
+ await this.handleStartGoal(request);
135
+ return;
136
+ case "session.resumeGoal":
137
+ await this.handleResumeGoal(request);
138
+ return;
139
+ case "session.clearGoal":
140
+ await this.handleClearGoal(request);
141
+ return;
127
142
  case "session.setReasoning":
128
143
  await this.handleSetReasoning(request);
129
144
  return;
@@ -239,33 +254,15 @@ export class SessionProtocolHandler {
239
254
  this.sendMessage(createSessionProtocolSuccessResponse(request.id, "session.clientTool.result", { accepted }));
240
255
  }
241
256
  async handleSubmit(request) {
242
- const state = await this.getSessionState(request.params.sessionId);
243
- if (!state) {
244
- this.sendSessionNotFound(request.id, request.params.sessionId);
245
- return;
246
- }
247
- if (this.getPendingMutationCount(state) > 0) {
248
- this.sendSubmitBusy(state, request.id);
249
- return;
250
- }
251
- const startedSubmit = await this.enqueueMutation(state, () => this.startUserMessageTurn(state, request));
252
- if (!startedSubmit) {
253
- return;
254
- }
255
- const { activeSubmit } = startedSubmit;
256
- try {
257
- await activeSubmit.promise;
258
- }
259
- finally {
260
- await this.enqueueMutation(state, () => {
261
- if (state.live.activeSubmit === activeSubmit) {
262
- state.live.activeSubmit = undefined;
263
- }
264
- });
265
- this.schedulePendingSubmitDrains(state);
266
- }
257
+ await this.handleUserSubmission(request);
267
258
  }
268
259
  async handleQueue(request) {
260
+ await this.handleUserSubmission(request);
261
+ }
262
+ async handleSteer(request) {
263
+ await this.handleUserSubmission(request);
264
+ }
265
+ async handleUserSubmission(request) {
269
266
  const state = await this.getSessionState(request.params.sessionId);
270
267
  if (!state) {
271
268
  this.sendSessionNotFound(request.id, request.params.sessionId);
@@ -275,128 +272,103 @@ export class SessionProtocolHandler {
275
272
  this.sendSubmitBusy(state, request.id);
276
273
  return;
277
274
  }
278
- const startedSubmit = await this.enqueueMutation(state, () => {
279
- if (state.live.activeSubmit || state.session.isTurnRunning) {
280
- state.live.pendingQueuedSubmits.push({
281
- id: randomUUID(),
282
- mode: "queue",
283
- handler: this,
284
- request,
285
- });
286
- publishPendingUserMessages(state.session, state.live);
287
- return undefined;
288
- }
289
- return this.startUserMessageTurn(state, request);
290
- });
291
- if (!startedSubmit) {
275
+ const action = await this.enqueueMutation(state, () => this.dispatchUserSubmission(state, request));
276
+ if (action.type === "busy") {
277
+ this.sendSubmitBusy(state, request.id);
292
278
  return;
293
279
  }
294
- const { activeSubmit } = startedSubmit;
295
- try {
296
- await activeSubmit.promise;
297
- }
298
- finally {
299
- await this.enqueueMutation(state, () => {
300
- if (state.live.activeSubmit === activeSubmit) {
301
- state.live.activeSubmit = undefined;
302
- }
303
- });
304
- this.schedulePendingSubmitDrains(state);
305
- }
306
- }
307
- async handleSteer(request) {
308
- const state = await this.getSessionState(request.params.sessionId);
309
- if (!state) {
310
- this.sendSessionNotFound(request.id, request.params.sessionId);
280
+ if (action.type === "pending") {
311
281
  return;
312
282
  }
313
- if (this.getPendingMutationCount(state) > 0) {
314
- this.sendSubmitBusy(state, request.id);
283
+ if (action.type === "started") {
284
+ await this.finishActiveSubmit(state, action.activeSubmit);
315
285
  return;
316
286
  }
317
- const action = await this.enqueueMutation(state, async () => {
287
+ await this.finishBoundarySteering(state, action.pending, action.submission);
288
+ }
289
+ async dispatchUserSubmission(state, request) {
290
+ if (request.method === "session.steer") {
318
291
  if (state.live.interrupting) {
319
292
  return { type: "busy" };
320
293
  }
321
- if (state.session.isTurnRunning) {
294
+ if (state.session.canAcceptSteering) {
322
295
  const submission = state.session.steer(request.params.text);
323
296
  const pending = {
324
297
  id: randomUUID(),
325
- mode: "steer",
298
+ delivery: "turn-boundary",
326
299
  steeringId: submission.id,
327
300
  handler: this,
328
301
  request,
329
302
  };
330
- state.live.pendingSteeringSubmits.push(pending);
331
- publishPendingUserMessages(state.session, state.live);
332
- return { type: "steer", pending, submission };
333
- }
334
- if (state.live.activeSubmit) {
335
- state.live.pendingQueuedSubmits.push({
336
- id: randomUUID(),
337
- mode: "steer",
338
- handler: this,
339
- request,
340
- });
341
- publishPendingUserMessages(state.session, state.live);
342
- return { type: "queued" };
303
+ this.addPendingSubmission(state, pending);
304
+ return { type: "boundary", pending, submission };
343
305
  }
344
- return {
345
- type: "submit",
346
- started: await this.startUserMessageTurn(state, request),
347
- };
348
- });
349
- if (action.type === "busy") {
350
- this.sendSubmitBusy(state, request.id);
351
- return;
352
306
  }
353
- if (action.type === "queued") {
354
- return;
355
- }
356
- if (action.type === "submit") {
357
- const started = await action.started;
358
- if (!started)
359
- return;
360
- try {
361
- await started.activeSubmit.promise;
362
- }
363
- finally {
364
- await this.enqueueMutation(state, () => {
365
- if (state.live.activeSubmit === started.activeSubmit) {
366
- state.live.activeSubmit = undefined;
367
- }
368
- });
369
- this.schedulePendingSubmitDrains(state);
307
+ if (state.live.activeSubmit || state.session.isTurnRunning) {
308
+ if (request.method === "session.submit") {
309
+ return { type: "busy" };
370
310
  }
371
- return;
311
+ const pending = {
312
+ id: randomUUID(),
313
+ delivery: "when-idle",
314
+ handler: this,
315
+ request,
316
+ };
317
+ this.addPendingSubmission(state, pending);
318
+ return { type: "pending" };
372
319
  }
320
+ return {
321
+ type: "started",
322
+ activeSubmit: await this.startUserMessageTurn(state, request),
323
+ };
324
+ }
325
+ async finishBoundarySteering(state, pending, submission) {
373
326
  let applied = false;
374
- void action.submission.result.catch(() => { });
327
+ void submission.result.catch(() => { });
375
328
  try {
376
- await action.submission.applied;
329
+ await submission.applied;
377
330
  applied = true;
378
- await this.enqueueMutation(state, () => {
379
- const index = state.live.pendingSteeringSubmits.indexOf(action.pending);
380
- if (index >= 0)
381
- state.live.pendingSteeringSubmits.splice(index, 1);
382
- publishPendingUserMessages(state.session, state.live);
383
- });
384
- const result = await action.submission.result;
385
- this.sendMessage(createSessionProtocolSuccessResponse(request.id, "session.steer", result));
331
+ await this.enqueueMutation(state, () => this.removePendingSubmission(state, pending));
332
+ const result = await submission.result;
333
+ this.sendMessage(createSessionProtocolSuccessResponse(pending.request.id, "session.steer", result));
386
334
  }
387
335
  catch (error) {
388
- if (applied || state.live.pendingSteeringSubmits.includes(action.pending)) {
389
- this.sendMessage(createSessionProtocolErrorResponse(request.id, SESSION_PROTOCOL_ERROR_CODES.internalError, "steering turn failed", { cause: error instanceof Error ? error.message : String(error) }));
336
+ if (applied || state.live.pendingSubmissions.includes(pending)) {
337
+ this.sendMessage(createSessionProtocolErrorResponse(pending.request.id, SESSION_PROTOCOL_ERROR_CODES.internalError, "steering turn failed", { cause: error instanceof Error ? error.message : String(error) }));
390
338
  }
391
339
  }
340
+ finally {
341
+ await this.enqueueMutation(state, () => this.removePendingSubmission(state, pending));
342
+ }
343
+ }
344
+ addPendingSubmission(state, pending) {
345
+ state.live.pendingSubmissions.push(pending);
346
+ publishPendingUserMessages(state.session, state.live);
347
+ }
348
+ removePendingSubmission(state, pending) {
349
+ return this.removePendingSubmissions(state, [pending]);
350
+ }
351
+ removePendingSubmissions(state, pending) {
352
+ const removed = new Set(pending);
353
+ const remaining = state.live.pendingSubmissions.filter((submission) => !removed.has(submission));
354
+ if (remaining.length === state.live.pendingSubmissions.length) {
355
+ return false;
356
+ }
357
+ state.live.pendingSubmissions = remaining;
358
+ publishPendingUserMessages(state.session, state.live);
359
+ return true;
360
+ }
361
+ async finishActiveSubmit(state, activeSubmit) {
362
+ try {
363
+ await activeSubmit.promise;
364
+ }
392
365
  finally {
393
366
  await this.enqueueMutation(state, () => {
394
- const index = state.live.pendingSteeringSubmits.indexOf(action.pending);
395
- if (index >= 0) {
396
- state.live.pendingSteeringSubmits.splice(index, 1);
397
- publishPendingUserMessages(state.session, state.live);
367
+ if (state.live.activeSubmit === activeSubmit) {
368
+ state.live.activeSubmit = undefined;
398
369
  }
399
370
  });
371
+ this.schedulePendingSubmissionDrain(state);
400
372
  }
401
373
  }
402
374
  async handleCancelPendingMessages(request) {
@@ -407,21 +379,15 @@ export class SessionProtocolHandler {
407
379
  }
408
380
  const cancelled = await this.enqueueMutation(state, () => {
409
381
  const cancelledSteeringIds = new Set(state.session.cancelSteering().map((submission) => submission.id));
410
- const cancelledSteering = state.live.pendingSteeringSubmits.filter((pending) => cancelledSteeringIds.has(pending.steeringId));
411
- state.live.pendingSteeringSubmits = state.live.pendingSteeringSubmits.filter((pending) => !cancelledSteeringIds.has(pending.steeringId));
412
- const pending = [...cancelledSteering, ...state.live.pendingQueuedSubmits.splice(0)];
382
+ const pending = pendingSubmissionsInDeliveryOrder(state.live).filter((submission) => submission.delivery === "when-idle" || cancelledSteeringIds.has(submission.steeringId));
413
383
  if (pending.length === 0) {
414
384
  return [];
415
385
  }
416
- publishPendingUserMessages(state.session, state.live);
386
+ this.removePendingSubmissions(state, pending);
417
387
  for (const item of pending) {
418
388
  item.handler.sendMessage(createSessionProtocolErrorResponse(item.request.id, SESSION_PROTOCOL_ERROR_CODES.cancelled, "pending user message was cancelled"));
419
389
  }
420
- return pending.map((item) => ({
421
- id: item.id,
422
- mode: item.mode,
423
- text: item.request.params.text,
424
- }));
390
+ return pending.map(toPendingUserMessage);
425
391
  });
426
392
  this.sendMessage(createSessionProtocolSuccessResponse(request.id, "session.cancelPendingMessages", {
427
393
  cancelled,
@@ -489,21 +455,9 @@ export class SessionProtocolHandler {
489
455
  this.sendSubmitBusy(state, request.id);
490
456
  return;
491
457
  }
492
- const startedRetry = await this.enqueueMutation(state, () => this.startRetry(state, request));
493
- if (!startedRetry) {
494
- return;
495
- }
496
- const { activeSubmit } = startedRetry;
497
- try {
498
- await activeSubmit.promise;
499
- }
500
- finally {
501
- await this.enqueueMutation(state, () => {
502
- if (state.live.activeSubmit === activeSubmit) {
503
- state.live.activeSubmit = undefined;
504
- }
505
- });
506
- this.schedulePendingSubmitDrains(state);
458
+ const activeSubmit = await this.enqueueMutation(state, () => this.startRetry(state, request));
459
+ if (activeSubmit) {
460
+ await this.finishActiveSubmit(state, activeSubmit);
507
461
  }
508
462
  }
509
463
  async handleCreate(request) {
@@ -584,10 +538,6 @@ export class SessionProtocolHandler {
584
538
  });
585
539
  }
586
540
  async startUserMessageTurn(state, request) {
587
- if (state.live.activeSubmit || state.session.isTurnRunning) {
588
- this.sendSubmitBusy(state, request.id);
589
- return undefined;
590
- }
591
541
  const addOptions = request.method !== "session.steer" && request.params.historyEntryId
592
542
  ? { historyEntryId: request.params.historyEntryId }
593
543
  : undefined;
@@ -595,67 +545,59 @@ export class SessionProtocolHandler {
595
545
  text: request.params.text,
596
546
  ...(addOptions ? { historyEntryId: addOptions.historyEntryId } : {}),
597
547
  });
598
- const submitPromise = this.executeSubmit(state, request.id, request.method, userHistoryEntryId);
599
- const activeSubmit = { requestId: request.id, promise: submitPromise };
600
- state.live.activeSubmit = activeSubmit;
601
- return {
602
- activeSubmit,
548
+ const activeSubmit = {
549
+ promise: this.executeSubmit(state, request.id, request.method, userHistoryEntryId),
603
550
  };
551
+ state.live.activeSubmit = activeSubmit;
552
+ return activeSubmit;
604
553
  }
605
554
  startRetry(state, request) {
606
555
  if (state.live.activeSubmit || state.session.isTurnRunning) {
607
556
  this.sendSubmitBusy(state, request.id);
608
557
  return undefined;
609
558
  }
610
- const retryPromise = this.executeRetry(state, request.id);
611
- const activeSubmit = { requestId: request.id, promise: retryPromise };
612
- state.live.activeSubmit = activeSubmit;
613
- return {
614
- activeSubmit,
559
+ const activeSubmit = {
560
+ promise: this.executeRetry(state, request.id),
615
561
  };
562
+ state.live.activeSubmit = activeSubmit;
563
+ return activeSubmit;
616
564
  }
617
- schedulePendingSubmitDrains(state) {
618
- void this.drainPendingQueuedSubmits(state).catch((error) => {
619
- this.failPendingUserMessageRequests(state, error);
565
+ schedulePendingSubmissionDrain(state) {
566
+ void this.drainPendingIdleSubmissions(state).catch((error) => {
567
+ this.failPendingIdleSubmissions(state, error);
620
568
  });
621
569
  }
622
- async drainPendingQueuedSubmits(state) {
623
- const next = await this.enqueueMutation(state, async () => {
624
- if (state.live.pendingQueuedSubmits.length === 0 ||
625
- state.live.activeSubmit ||
626
- state.session.isTurnRunning) {
627
- return undefined;
570
+ async drainPendingIdleSubmissions(state) {
571
+ const action = await this.enqueueMutation(state, async () => {
572
+ const pendingIndex = state.live.pendingSubmissions.findIndex((pending) => pending.delivery === "when-idle");
573
+ if (pendingIndex < 0 || state.live.activeSubmit || state.session.isTurnRunning) {
574
+ return { type: "idle" };
628
575
  }
629
- const pending = state.live.pendingQueuedSubmits.shift();
576
+ const [pending] = state.live.pendingSubmissions.splice(pendingIndex, 1);
630
577
  publishPendingUserMessages(state.session, state.live);
631
578
  try {
632
- return await pending.handler.startUserMessageTurn(state, pending.request);
579
+ return {
580
+ type: "started",
581
+ activeSubmit: await pending.handler.startUserMessageTurn(state, pending.request),
582
+ };
633
583
  }
634
584
  catch (error) {
635
585
  this.sendUserMessageDrainFailure([pending], error);
636
- return undefined;
586
+ return { type: "failed", error };
637
587
  }
638
588
  });
639
- if (!next) {
589
+ if (action.type === "idle") {
640
590
  return;
641
591
  }
642
- try {
643
- await next.activeSubmit.promise;
644
- }
645
- finally {
646
- await this.enqueueMutation(state, () => {
647
- if (state.live.activeSubmit === next.activeSubmit) {
648
- state.live.activeSubmit = undefined;
649
- }
650
- });
651
- this.schedulePendingSubmitDrains(state);
592
+ if (action.type === "failed") {
593
+ this.failPendingIdleSubmissions(state, action.error);
594
+ return;
652
595
  }
596
+ await this.finishActiveSubmit(state, action.activeSubmit);
653
597
  }
654
- failPendingUserMessageRequests(state, error) {
655
- const pending = state.live.pendingQueuedSubmits.splice(0);
656
- if (pending.length > 0) {
657
- publishPendingUserMessages(state.session, state.live);
658
- }
598
+ failPendingIdleSubmissions(state, error) {
599
+ const pending = state.live.pendingSubmissions.filter((submission) => submission.delivery === "when-idle");
600
+ this.removePendingSubmissions(state, pending);
659
601
  this.sendUserMessageDrainFailure(pending, error);
660
602
  }
661
603
  sendUserMessageDrainFailure(requests, error) {
@@ -663,7 +605,7 @@ export class SessionProtocolHandler {
663
605
  handler.sendMessage(createSessionProtocolErrorResponse(request.id, SESSION_PROTOCOL_ERROR_CODES.internalError, "failed to drain pending user message", { cause: error instanceof Error ? error.message : String(error) }));
664
606
  }
665
607
  }
666
- async executeSubmit(state, requestId, method, userHistoryEntryId, responseRequests) {
608
+ async executeSubmit(state, requestId, method, userHistoryEntryId) {
667
609
  try {
668
610
  const turnResult = await state.session.runTurn();
669
611
  await state.session.snapshot();
@@ -671,25 +613,11 @@ export class SessionProtocolHandler {
671
613
  userHistoryEntryId,
672
614
  turn: turnResult,
673
615
  };
674
- if (responseRequests) {
675
- for (const { handler, request } of responseRequests) {
676
- handler.sendMessage(createSessionProtocolSuccessResponse(request.id, method, result));
677
- }
678
- }
679
- else {
680
- this.sendMessage(createSessionProtocolSuccessResponse(requestId, method, result));
681
- }
616
+ this.sendMessage(createSessionProtocolSuccessResponse(requestId, method, result));
682
617
  }
683
618
  catch (error) {
684
619
  await this.snapshotAfterFailedSubmit(state);
685
- if (responseRequests) {
686
- for (const { handler, request } of responseRequests) {
687
- handler.sendMessage(createSessionProtocolErrorResponse(request.id, SESSION_PROTOCOL_ERROR_CODES.internalError, "failed to run session turn", { cause: error instanceof Error ? error.message : String(error) }));
688
- }
689
- }
690
- else {
691
- this.sendMessage(createSessionProtocolErrorResponse(requestId, SESSION_PROTOCOL_ERROR_CODES.internalError, "failed to run session turn", { cause: error instanceof Error ? error.message : String(error) }));
692
- }
620
+ this.sendMessage(createSessionProtocolErrorResponse(requestId, SESSION_PROTOCOL_ERROR_CODES.internalError, "failed to run session turn", { cause: error instanceof Error ? error.message : String(error) }));
693
621
  }
694
622
  }
695
623
  async snapshotAfterFailedSubmit(state) {
@@ -703,7 +631,7 @@ export class SessionProtocolHandler {
703
631
  }
704
632
  async executeRetry(state, requestId) {
705
633
  try {
706
- const turnResult = await state.session.runTurn();
634
+ const turnResult = await state.session.retryTurn();
707
635
  await state.session.snapshot();
708
636
  const result = {
709
637
  turn: turnResult,
@@ -711,7 +639,12 @@ export class SessionProtocolHandler {
711
639
  this.sendMessage(createSessionProtocolSuccessResponse(requestId, "session.retry", result));
712
640
  }
713
641
  catch (error) {
714
- this.sendMessage(createSessionProtocolErrorResponse(requestId, SESSION_PROTOCOL_ERROR_CODES.internalError, "failed to run session turn", { cause: error instanceof Error ? error.message : String(error) }));
642
+ const retryUnavailable = error instanceof SessionRetryUnavailableError;
643
+ this.sendMessage(createSessionProtocolErrorResponse(requestId, retryUnavailable
644
+ ? SESSION_PROTOCOL_ERROR_CODES.invalidRequest
645
+ : SESSION_PROTOCOL_ERROR_CODES.internalError, retryUnavailable ? error.message : "failed to run session turn", retryUnavailable
646
+ ? undefined
647
+ : { cause: error instanceof Error ? error.message : String(error) }));
715
648
  }
716
649
  }
717
650
  execKey(sessionId, execId) {
@@ -783,7 +716,7 @@ export class SessionProtocolHandler {
783
716
  if (interrupted) {
784
717
  state.live.interrupting = true;
785
718
  }
786
- this.rejectPendingSteeringSubmits(state, "session was interrupted");
719
+ this.rejectPendingBoundarySubmissions(state, "session was interrupted");
787
720
  return {
788
721
  interrupted,
789
722
  isTurnRunning: state.session.isTurnRunning || interrupted,
@@ -808,6 +741,95 @@ export class SessionProtocolHandler {
808
741
  }
809
742
  this.sendMessage(createSessionProtocolSuccessResponse(request.id, "session.snapshot", await state.session.snapshot()));
810
743
  }
744
+ async handleStartGoal(request) {
745
+ await this.handleGoalTurn(request);
746
+ }
747
+ async handleResumeGoal(request) {
748
+ await this.handleGoalTurn(request);
749
+ }
750
+ async handleGoalTurn(request) {
751
+ const state = await this.getSessionState(request.params.sessionId);
752
+ if (!state) {
753
+ this.sendSessionNotFound(request.id, request.params.sessionId);
754
+ return;
755
+ }
756
+ if (this.getPendingMutationCount(state) > 0) {
757
+ this.sendSubmitBusy(state, request.id);
758
+ return;
759
+ }
760
+ const started = await this.enqueueMutation(state, () => {
761
+ if (state.live.activeSubmit || state.session.isTurnRunning) {
762
+ this.sendSubmitBusy(state, request.id);
763
+ return undefined;
764
+ }
765
+ const promise = this.executeGoalTurn(state, request);
766
+ const activeSubmit = { requestId: request.id, promise };
767
+ state.live.activeSubmit = activeSubmit;
768
+ return { activeSubmit };
769
+ });
770
+ if (!started)
771
+ return;
772
+ try {
773
+ await started.activeSubmit.promise;
774
+ }
775
+ finally {
776
+ await this.enqueueMutation(state, () => {
777
+ if (state.live.activeSubmit === started.activeSubmit) {
778
+ state.live.activeSubmit = undefined;
779
+ }
780
+ });
781
+ this.schedulePendingSubmissionDrain(state);
782
+ }
783
+ }
784
+ async executeGoalTurn(state, request) {
785
+ try {
786
+ if (request.method === "session.startGoal") {
787
+ const result = await state.session.startGoal(request.params.objective);
788
+ await state.session.snapshot();
789
+ this.sendMessage(createSessionProtocolSuccessResponse(request.id, "session.startGoal", result));
790
+ }
791
+ else {
792
+ const result = await state.session.resumeGoal();
793
+ await state.session.snapshot();
794
+ this.sendMessage(createSessionProtocolSuccessResponse(request.id, "session.resumeGoal", result));
795
+ }
796
+ }
797
+ catch (error) {
798
+ await this.snapshotAfterFailedSubmit(state);
799
+ this.sendMessage(createSessionProtocolErrorResponse(request.id, SESSION_PROTOCOL_ERROR_CODES.internalError, "failed to run session goal", { cause: error instanceof Error ? error.message : String(error) }));
800
+ }
801
+ }
802
+ async handleClearGoal(request) {
803
+ const state = await this.getSessionState(request.params.sessionId);
804
+ if (!state) {
805
+ this.sendSessionNotFound(request.id, request.params.sessionId);
806
+ return;
807
+ }
808
+ if (!state.session.getGoal()) {
809
+ this.sendMessage(createSessionProtocolErrorResponse(request.id, SESSION_PROTOCOL_ERROR_CODES.invalidRequest, "no goal exists"));
810
+ return;
811
+ }
812
+ await this.runSessionMutation(state, async () => {
813
+ if (this.closed) {
814
+ return;
815
+ }
816
+ if (state.session.sessionId !== request.params.sessionId) {
817
+ this.sendSessionNotFound(request.id, request.params.sessionId);
818
+ return;
819
+ }
820
+ if (!state.session.getGoal()) {
821
+ this.sendMessage(createSessionProtocolErrorResponse(request.id, SESSION_PROTOCOL_ERROR_CODES.invalidRequest, "no goal exists"));
822
+ return;
823
+ }
824
+ await this.interruptAndWaitForActiveSubmit(state);
825
+ this.rejectPendingBoundarySubmissions(state, "session goal cleared");
826
+ this.rejectPendingIdleSubmissions(state, "session goal cleared");
827
+ const snapshot = state.session.getGoal()
828
+ ? await state.session.clearGoal()
829
+ : await state.session.snapshot();
830
+ this.sendMessage(createSessionProtocolSuccessResponse(request.id, "session.clearGoal", snapshot));
831
+ });
832
+ }
811
833
  async handleSetReasoning(request) {
812
834
  const state = await this.getSessionState(request.params.sessionId);
813
835
  if (!state) {
@@ -869,8 +891,7 @@ export class SessionProtocolHandler {
869
891
  await this.withNonInterruptingSessionMutation(request, async (state) => {
870
892
  if (state.live.activeSubmit ||
871
893
  state.session.isTurnRunning ||
872
- state.live.pendingSteeringSubmits.length > 0 ||
873
- state.live.pendingQueuedSubmits.length > 0) {
894
+ state.live.pendingSubmissions.length > 0) {
874
895
  this.sendMessage(createSessionProtocolErrorResponse(request.id, SESSION_PROTOCOL_ERROR_CODES.busy, "cannot rewind while session work is active or pending"));
875
896
  return;
876
897
  }
@@ -945,8 +966,8 @@ export class SessionProtocolHandler {
945
966
  return;
946
967
  }
947
968
  await this.interruptAndWaitForActiveSubmit(state);
948
- this.rejectPendingSteeringSubmits(state, queuedSteeringRejectionMessage);
949
- this.rejectPendingQueuedSubmits(state, queuedSteeringRejectionMessage);
969
+ this.rejectPendingBoundarySubmissions(state, queuedSteeringRejectionMessage);
970
+ this.rejectPendingIdleSubmissions(state, queuedSteeringRejectionMessage);
950
971
  await handler(state);
951
972
  });
952
973
  }
@@ -967,23 +988,19 @@ export class SessionProtocolHandler {
967
988
  await handler(state);
968
989
  });
969
990
  }
970
- rejectPendingSteeringSubmits(state, message) {
991
+ rejectPendingBoundarySubmissions(state, message) {
971
992
  const cancelledIds = new Set(state.session.cancelSteering().map((submission) => submission.id));
972
- const requests = state.live.pendingSteeringSubmits.filter((pending) => cancelledIds.has(pending.steeringId));
973
- state.live.pendingSteeringSubmits = state.live.pendingSteeringSubmits.filter((pending) => !cancelledIds.has(pending.steeringId));
974
- if (requests.length > 0) {
975
- publishPendingUserMessages(state.session, state.live);
976
- }
977
- for (const { handler, request } of requests) {
978
- handler.sendMessage(createSessionProtocolErrorResponse(request.id, SESSION_PROTOCOL_ERROR_CODES.invalidRequest, message));
979
- }
993
+ this.rejectPendingSubmissions(state, state.live.pendingSubmissions.filter((pending) => pending.delivery === "turn-boundary" && cancelledIds.has(pending.steeringId)), message);
980
994
  }
981
- rejectPendingQueuedSubmits(state, message) {
982
- const requests = state.live.pendingQueuedSubmits.splice(0);
983
- if (requests.length > 0) {
984
- publishPendingUserMessages(state.session, state.live);
995
+ rejectPendingIdleSubmissions(state, message) {
996
+ this.rejectPendingSubmissions(state, state.live.pendingSubmissions.filter((pending) => pending.delivery === "when-idle"), message);
997
+ }
998
+ rejectPendingSubmissions(state, pending, message) {
999
+ if (pending.length === 0) {
1000
+ return;
985
1001
  }
986
- for (const { handler, request } of requests) {
1002
+ this.removePendingSubmissions(state, pending);
1003
+ for (const { handler, request } of pending) {
987
1004
  handler.sendMessage(createSessionProtocolErrorResponse(request.id, SESSION_PROTOCOL_ERROR_CODES.invalidRequest, message));
988
1005
  }
989
1006
  }