@osolmaz/pi-workflows 0.15.1 → 0.15.3

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.
@@ -13,6 +13,8 @@ import { discoverWorkflows } from "../workflows/loader.js";
13
13
  import { createRunId, WorkflowRunStore } from "../workflows/store.js";
14
14
  import type { AgentStepContract, HumanDecisionResponse } from "../workflows/types.js";
15
15
  import { parseControllerArgs, type ParsedControllerArgs } from "./controller-command.js";
16
+ import { SessionDeliveryCoordinator, type ClaimedSessionDelivery } from "./session-delivery.js";
17
+ import { SessionWorkflowView } from "./session-view.js";
16
18
  import {
17
19
  recoverAssistantStep,
18
20
  registerWorkflowAgentStepMessageRenderer,
@@ -131,6 +133,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
131
133
  let pollTimer: ReturnType<typeof setInterval> | null = null;
132
134
  let presentationTail = Promise.resolve();
133
135
  let toolTail = Promise.resolve();
136
+ const sessionDelivery = new SessionDeliveryCoordinator();
137
+ const sessionView = new SessionWorkflowView();
134
138
 
135
139
  const presentInOrder = async (ctx: ExtensionContext): Promise<void> => {
136
140
  const prior = presentationTail;
@@ -140,12 +144,13 @@ export default function piWorkflows(pi: ExtensionAPI): void {
140
144
  });
141
145
  await prior;
142
146
  try {
143
- await presentPendingInteraction(pi, client, ctx);
144
- await deliverPendingNotification(pi, client, ctx);
145
- if (pendingInteractionForSession(ctx.sessionManager.getSessionId()) === undefined) {
146
- await presentPendingTurn(pi, client, ctx);
147
- }
147
+ await sessionDelivery.synchronize(ctx, [
148
+ () => claimPendingInteractionDelivery(pi, client, ctx),
149
+ () => claimPendingNotificationDelivery(pi, client, ctx),
150
+ () => claimPendingTurnDelivery(pi, client, ctx),
151
+ ]);
148
152
  } finally {
153
+ sessionView.refresh(ctx);
149
154
  release?.();
150
155
  }
151
156
  };
@@ -266,6 +271,16 @@ export default function piWorkflows(pi: ExtensionAPI): void {
266
271
  },
267
272
  });
268
273
 
274
+ pi.registerShortcut("shift+up", {
275
+ description: "Scroll the workflow widget up",
276
+ handler: (ctx) => sessionView.scrollUp(ctx),
277
+ });
278
+
279
+ pi.registerShortcut("shift+down", {
280
+ description: "Scroll the workflow widget down",
281
+ handler: (ctx) => sessionView.scrollDown(ctx),
282
+ });
283
+
269
284
  pi.on("session_start", async (_event, ctx) => {
270
285
  sessionContext = ctx;
271
286
  try {
@@ -280,6 +295,44 @@ export default function piWorkflows(pi: ExtensionAPI): void {
280
295
  pollTimer.unref?.();
281
296
  });
282
297
 
298
+ pi.on("agent_end", async (event, ctx) => {
299
+ const interrupted =
300
+ ctx.signal?.aborted === true ||
301
+ event.messages.some(
302
+ (message) =>
303
+ isRecord(message) && "stopReason" in message && message.stopReason === "aborted",
304
+ );
305
+ if (!interrupted) return;
306
+ const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
307
+ if (
308
+ interaction === undefined ||
309
+ interaction.kind === "decision" ||
310
+ workflowRunPaused(interaction.runId)
311
+ ) {
312
+ return;
313
+ }
314
+ const turnHasPrompt = event.messages.some(
315
+ (message) => interactionRequestId(message) === interaction.requestId,
316
+ );
317
+ if (!turnHasPrompt) return;
318
+ try {
319
+ const pauseId = `escape-pause-${interaction.runId}-${randomUUID()}`;
320
+ await requestAccepted(client, {
321
+ operation: "run.pause",
322
+ requestId: pauseId,
323
+ idempotencyKey: pauseId,
324
+ runId: interaction.runId,
325
+ });
326
+ sessionView.refresh(ctx);
327
+ ctx.ui.notify(
328
+ `Workflow ${interaction.runId} paused because its model turn was interrupted. Use /workflow resume to continue.`,
329
+ "info",
330
+ );
331
+ } catch (error) {
332
+ ctx.ui.notify(`Could not pause interrupted workflow: ${errorMessage(error)}`, "warning");
333
+ }
334
+ });
335
+
283
336
  pi.on("agent_settled", async (_event, ctx) => {
284
337
  try {
285
338
  await submitVisibleAssistantResponse(client, ctx);
@@ -289,8 +342,10 @@ export default function piWorkflows(pi: ExtensionAPI): void {
289
342
  await presentInOrder(ctx).catch(() => undefined);
290
343
  });
291
344
 
292
- pi.on("session_shutdown", async () => {
345
+ pi.on("session_shutdown", async (_event, ctx) => {
293
346
  sessionContext = null;
347
+ sessionDelivery.clear();
348
+ sessionView.clear(ctx);
294
349
  if (pollTimer !== null) clearInterval(pollTimer);
295
350
  pollTimer = null;
296
351
  });
@@ -514,94 +569,120 @@ async function executeControllerCommand(
514
569
  };
515
570
  }
516
571
 
517
- async function presentPendingInteraction(
572
+ async function claimPendingInteractionDelivery(
518
573
  pi: ExtensionAPI,
519
574
  client: WorkflowHostClient,
520
575
  ctx: ExtensionContext,
521
- ): Promise<void> {
576
+ ): Promise<ClaimedSessionDelivery | undefined> {
522
577
  const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
523
- if (interaction === undefined) return;
524
- const entries = ctx.sessionManager.getBranch();
525
- const existing = entries.find((entry) => interactionRequestId(entry) === interaction.requestId);
526
- if (existing !== undefined) {
527
- const entryId = entryIdentifier(existing);
528
- if (entryId !== undefined && interaction.presentationSessionEntryId !== entryId) {
578
+ if (interaction === undefined || interaction.presentationSessionEntryId !== null)
579
+ return undefined;
580
+
581
+ const findSessionEntryId = (entries: readonly unknown[]): string | undefined =>
582
+ entryIdentifier(entries.find((entry) => interactionRequestId(entry) === interaction.requestId));
583
+ const existingEntryId = findSessionEntryId(ctx.sessionManager.getBranch());
584
+ let presentationRevision = interaction.revision;
585
+ let claimExpiresAt = Number.POSITIVE_INFINITY;
586
+ if (existingEntryId === undefined) {
587
+ if (workflowRunPaused(interaction.runId)) return undefined;
588
+ if (
589
+ interaction.presentationClaimExpiresAt !== null &&
590
+ Date.parse(interaction.presentationClaimExpiresAt) > Date.now()
591
+ ) {
592
+ return undefined;
593
+ }
594
+ await client.ensureRunning();
595
+ const claim = await client.request({
596
+ operation: "interaction.update",
597
+ requestId: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
598
+ idempotencyKey: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
599
+ runId: interaction.runId,
600
+ expectedRevision: interaction.revision,
601
+ payload: { requestId: interaction.requestId, claimPresentation: true },
602
+ });
603
+ if (claim.outcome === "conflict") return undefined;
604
+ if (claim.outcome !== "accepted" && claim.outcome !== "adopted") {
605
+ throw new Error(claim.error ?? "Workflow host rejected interaction.update");
606
+ }
607
+ if (claim.revision === undefined) throw new Error("Presentation claim has no revision");
608
+ const receipt = isRecord(claim.receipt) ? claim.receipt : undefined;
609
+ presentationRevision = claim.revision;
610
+ claimExpiresAt = claimExpiry(receipt?.presentationClaimExpiresAt, "presentation claim");
611
+ }
612
+
613
+ const contract = interactionContract(interaction);
614
+ return {
615
+ deliveryId: `interaction:${interaction.requestId}`,
616
+ claimExpiresAt,
617
+ isStillDeliverable: () =>
618
+ existingEntryId === undefined &&
619
+ interactionPresentationClaimIsLive({
620
+ requestId: interaction.requestId,
621
+ runId: interaction.runId,
622
+ presenterId: client.clientId,
623
+ revision: presentationRevision,
624
+ claimExpiresAt,
625
+ }),
626
+ findSessionEntryId,
627
+ send: () => {
628
+ if (interaction.kind === "decision") {
629
+ pi.sendMessage(
630
+ {
631
+ customType: WORKFLOW_INTERACTION_MESSAGE_TYPE,
632
+ content: decisionPrompt(contract),
633
+ display: true,
634
+ details: {
635
+ requestId: interaction.requestId,
636
+ runId: interaction.runId,
637
+ kind: "decision",
638
+ },
639
+ },
640
+ { triggerTurn: false },
641
+ );
642
+ return;
643
+ }
644
+ const agent = agentContract(interaction);
645
+ if (agent === undefined) throw new Error("Stored workflow agent contract is invalid");
646
+ const details: WorkflowAgentStepMessageDetails & { requestId: string } = {
647
+ schema: WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
648
+ kind: "step",
649
+ contract: agent,
650
+ requestId: interaction.requestId,
651
+ ...(isRecord(contract.presentation)
652
+ ? { presentation: contract.presentation as never }
653
+ : {}),
654
+ };
655
+ pi.sendMessage(
656
+ {
657
+ customType: WORKFLOW_AGENT_STEP_MESSAGE_TYPE,
658
+ content:
659
+ typeof contract.prompt === "string" ? contract.prompt : "Continue the workflow step.",
660
+ display: true,
661
+ details,
662
+ },
663
+ { triggerTurn: true },
664
+ );
665
+ },
666
+ settle: async (sessionEntryId) => {
529
667
  await requestAccepted(client, {
530
668
  operation: "interaction.update",
531
- requestId: `present-${interaction.requestId}-${entryId}`,
532
- idempotencyKey: `present-${interaction.requestId}-${entryId}`,
669
+ requestId: `present-${interaction.requestId}-${sessionEntryId}`,
670
+ idempotencyKey: `present-${interaction.requestId}-${sessionEntryId}`,
533
671
  runId: interaction.runId,
534
- expectedRevision: interaction.revision,
535
- payload: { requestId: interaction.requestId, sessionEntryId: entryId },
672
+ expectedRevision: presentationRevision,
673
+ payload: { requestId: interaction.requestId, sessionEntryId },
536
674
  });
537
- }
538
- return;
539
- }
540
- if (interaction.presentationSessionEntryId !== null) return;
541
- const claim = await requestAccepted(client, {
542
- operation: "interaction.update",
543
- requestId: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
544
- idempotencyKey: `claim-presentation-${interaction.requestId}-${interaction.revision}-${client.clientId}`,
545
- runId: interaction.runId,
546
- expectedRevision: interaction.revision,
547
- payload: { requestId: interaction.requestId, claimPresentation: true },
548
- });
549
- if (claim.revision === undefined) throw new Error("Presentation claim has no revision");
550
- const presentationRevision = claim.revision;
551
- const contract = interactionContract(interaction);
552
- if (interaction.kind === "decision") {
553
- pi.sendMessage(
554
- {
555
- customType: WORKFLOW_INTERACTION_MESSAGE_TYPE,
556
- content: decisionPrompt(contract),
557
- display: true,
558
- details: { requestId: interaction.requestId, runId: interaction.runId, kind: "decision" },
559
- },
560
- { triggerTurn: false },
561
- );
562
- } else {
563
- const agent = agentContract(interaction);
564
- if (agent === undefined) throw new Error("Stored workflow agent contract is invalid");
565
- const details: WorkflowAgentStepMessageDetails & { requestId: string } = {
566
- schema: WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
567
- kind: "step",
568
- contract: agent,
569
- requestId: interaction.requestId,
570
- ...(isRecord(contract.presentation) ? { presentation: contract.presentation as never } : {}),
571
- };
572
- pi.sendMessage(
573
- {
574
- customType: WORKFLOW_AGENT_STEP_MESSAGE_TYPE,
575
- content:
576
- typeof contract.prompt === "string" ? contract.prompt : "Continue the workflow step.",
577
- display: true,
578
- details,
579
- },
580
- { triggerTurn: true, deliverAs: "followUp" },
581
- );
582
- }
583
- const inserted = ctx.sessionManager
584
- .getBranch()
585
- .find((entry) => interactionRequestId(entry) === interaction.requestId);
586
- const entryId = entryIdentifier(inserted);
587
- if (entryId === undefined) return;
588
- await requestAccepted(client, {
589
- operation: "interaction.update",
590
- requestId: `present-${interaction.requestId}-${entryId}`,
591
- idempotencyKey: `present-${interaction.requestId}-${entryId}`,
592
- runId: interaction.runId,
593
- expectedRevision: presentationRevision,
594
- payload: { requestId: interaction.requestId, sessionEntryId: entryId },
595
- });
675
+ },
676
+ };
596
677
  }
597
678
 
598
- async function deliverPendingNotification(
679
+ async function claimPendingNotificationDelivery(
599
680
  pi: ExtensionAPI,
600
681
  client: WorkflowHostClient,
601
682
  ctx: ExtensionContext,
602
- ): Promise<void> {
683
+ ): Promise<ClaimedSessionDelivery | undefined> {
603
684
  const sessionId = ctx.sessionManager.getSessionId();
604
- if (!hasClaimableNotification(sessionId)) return;
685
+ if (!hasClaimableNotification(sessionId)) return undefined;
605
686
  const claimRequestId = randomUUID();
606
687
  const claimed = await requestAccepted(client, {
607
688
  operation: "notification.claim",
@@ -611,57 +692,67 @@ async function deliverPendingNotification(
611
692
  });
612
693
  const receipt = isRecord(claimed.receipt) ? claimed.receipt : undefined;
613
694
  const notification = isRecord(receipt?.notification) ? receipt.notification : undefined;
614
- if (notification === undefined) return;
695
+ if (notification === undefined) return undefined;
615
696
  const claimId = requireText(receipt?.claimId, "notification claimId");
697
+ const claimExpiresAt = claimExpiry(receipt?.claimExpiresAt, "notification claim");
616
698
  const notificationId = requireText(notification.notificationId, "notificationId");
617
- const branch = ctx.sessionManager.getBranch();
618
- let entry = branch.find(
619
- (candidate) =>
620
- customMessageDetail(candidate, WORKFLOW_NOTIFICATION_MESSAGE_TYPE, "notificationId") ===
621
- notificationId,
622
- );
623
- if (entry === undefined) {
624
- pi.sendMessage(
625
- {
626
- customType: WORKFLOW_NOTIFICATION_MESSAGE_TYPE,
627
- content: requireText(notification.content, "notification content"),
628
- display: true,
629
- details: {
630
- notificationId,
631
- runId: requireText(notification.runId, "notification runId"),
632
- kind: notification.kind,
699
+ return {
700
+ deliveryId: `notification:${notificationId}`,
701
+ claimExpiresAt,
702
+ isStillDeliverable: () =>
703
+ hostDeliveryClaimIsLive(client, {
704
+ kind: "notification",
705
+ resourceId: notificationId,
706
+ targetSessionId: sessionId,
707
+ claimId,
708
+ }),
709
+ findSessionEntryId: (entries) =>
710
+ entryIdentifier(
711
+ entries.find(
712
+ (entry) =>
713
+ customMessageDetail(entry, WORKFLOW_NOTIFICATION_MESSAGE_TYPE, "notificationId") ===
714
+ notificationId,
715
+ ),
716
+ ),
717
+ send: () => {
718
+ pi.sendMessage(
719
+ {
720
+ customType: WORKFLOW_NOTIFICATION_MESSAGE_TYPE,
721
+ content: requireText(notification.content, "notification content"),
722
+ display: true,
723
+ details: {
724
+ notificationId,
725
+ runId: requireText(notification.runId, "notification runId"),
726
+ kind: notification.kind,
727
+ },
633
728
  },
634
- },
635
- { triggerTurn: false },
636
- );
637
- entry = ctx.sessionManager
638
- .getBranch()
639
- .find(
640
- (candidate) =>
641
- customMessageDetail(candidate, WORKFLOW_NOTIFICATION_MESSAGE_TYPE, "notificationId") ===
642
- notificationId,
729
+ { triggerTurn: false },
643
730
  );
644
- }
645
- if (entry === undefined) return;
646
- await requestAccepted(client, {
647
- operation: "notification.deliver",
648
- requestId: `notification-deliver-${notificationId}-${claimId}`,
649
- idempotencyKey: `notification-deliver-${notificationId}-${claimId}`,
650
- payload: {
651
- notificationId,
652
- targetSessionId: sessionId,
653
- claimId,
654
731
  },
655
- });
732
+ settle: async () => {
733
+ await requestAccepted(client, {
734
+ operation: "notification.deliver",
735
+ requestId: `notification-deliver-${notificationId}-${claimId}`,
736
+ idempotencyKey: `notification-deliver-${notificationId}-${claimId}`,
737
+ payload: {
738
+ notificationId,
739
+ targetSessionId: sessionId,
740
+ claimId,
741
+ },
742
+ });
743
+ },
744
+ };
656
745
  }
657
746
 
658
- async function presentPendingTurn(
747
+ async function claimPendingTurnDelivery(
659
748
  pi: ExtensionAPI,
660
749
  client: WorkflowHostClient,
661
750
  ctx: ExtensionContext,
662
- ): Promise<void> {
751
+ ): Promise<ClaimedSessionDelivery | undefined> {
663
752
  const sessionId = ctx.sessionManager.getSessionId();
664
- if (!hasClaimableTurn(sessionId)) return;
753
+ if (pendingInteractionForSession(sessionId) !== undefined || !hasClaimableTurn(sessionId)) {
754
+ return undefined;
755
+ }
665
756
  const claimRequestId = randomUUID();
666
757
  const claimed = await requestAccepted(client, {
667
758
  operation: "turn.claim",
@@ -671,49 +762,55 @@ async function presentPendingTurn(
671
762
  });
672
763
  const receipt = isRecord(claimed.receipt) ? claimed.receipt : undefined;
673
764
  const turn = isRecord(receipt?.turn) ? receipt.turn : undefined;
674
- if (turn === undefined) return;
765
+ if (turn === undefined) return undefined;
675
766
  const claimId = requireText(receipt?.claimId, "turn claimId");
767
+ const claimExpiresAt = claimExpiry(receipt?.claimExpiresAt, "turn claim");
676
768
  const intentId = requireText(turn.intentId, "turn intentId");
677
769
  const runId = requireText(turn.runId, "turn runId");
678
770
  const state = terminalRunState(runId);
679
- if (state === undefined) return;
680
- let entry = ctx.sessionManager
681
- .getBranch()
682
- .find(
683
- (candidate) =>
684
- customMessageDetail(candidate, WORKFLOW_PRESENTATION_MESSAGE_TYPE, "intentId") === intentId,
685
- );
686
- if (entry === undefined) {
687
- pi.sendMessage(
688
- {
689
- customType: WORKFLOW_PRESENTATION_MESSAGE_TYPE,
690
- content: presentationMessage(turn, state),
691
- display: false,
692
- details: { intentId, runId },
693
- },
694
- { triggerTurn: true, deliverAs: "followUp" },
695
- );
696
- entry = ctx.sessionManager
697
- .getBranch()
698
- .find(
699
- (candidate) =>
700
- customMessageDetail(candidate, WORKFLOW_PRESENTATION_MESSAGE_TYPE, "intentId") ===
701
- intentId,
771
+ if (state === undefined) return undefined;
772
+ return {
773
+ deliveryId: `turn:${intentId}`,
774
+ claimExpiresAt,
775
+ isStillDeliverable: () =>
776
+ hostDeliveryClaimIsLive(client, {
777
+ kind: "turn",
778
+ resourceId: intentId,
779
+ targetSessionId: sessionId,
780
+ claimId,
781
+ }),
782
+ findSessionEntryId: (entries) =>
783
+ entryIdentifier(
784
+ entries.find(
785
+ (entry) =>
786
+ customMessageDetail(entry, WORKFLOW_PRESENTATION_MESSAGE_TYPE, "intentId") === intentId,
787
+ ),
788
+ ),
789
+ send: () => {
790
+ pi.sendMessage(
791
+ {
792
+ customType: WORKFLOW_PRESENTATION_MESSAGE_TYPE,
793
+ content: presentationMessage(turn, state),
794
+ display: false,
795
+ details: { intentId, runId },
796
+ },
797
+ { triggerTurn: true },
702
798
  );
703
- }
704
- const messageId = entryIdentifier(entry);
705
- if (messageId === undefined) return;
706
- await requestAccepted(client, {
707
- operation: "turn.resolve",
708
- requestId: `turn-resolve-${intentId}-${messageId}`,
709
- idempotencyKey: `turn-resolve-${intentId}-${messageId}`,
710
- payload: {
711
- intentId,
712
- targetSessionId: sessionId,
713
- claimId,
714
- messageId,
715
799
  },
716
- });
800
+ settle: async (sessionEntryId) => {
801
+ await requestAccepted(client, {
802
+ operation: "turn.resolve",
803
+ requestId: `turn-resolve-${intentId}-${sessionEntryId}`,
804
+ idempotencyKey: `turn-resolve-${intentId}-${sessionEntryId}`,
805
+ payload: {
806
+ intentId,
807
+ targetSessionId: sessionId,
808
+ claimId,
809
+ messageId: sessionEntryId,
810
+ },
811
+ });
812
+ },
813
+ };
717
814
  }
718
815
 
719
816
  async function submitVisibleAssistantResponse(
@@ -721,7 +818,13 @@ async function submitVisibleAssistantResponse(
721
818
  ctx: ExtensionContext,
722
819
  ): Promise<void> {
723
820
  const interaction = pendingInteractionForSession(ctx.sessionManager.getSessionId());
724
- if (interaction === undefined || interaction.kind !== "assistant") return;
821
+ if (
822
+ interaction === undefined ||
823
+ interaction.kind !== "assistant" ||
824
+ workflowRunPaused(interaction.runId)
825
+ ) {
826
+ return;
827
+ }
725
828
  const contract = agentContract(interaction);
726
829
  if (contract === undefined) return;
727
830
  const submission = recoverAssistantStep(ctx.sessionManager.getBranch(), contract);
@@ -872,6 +975,65 @@ function terminalRunState(runId: string): Record<string, unknown> | undefined {
872
975
  }
873
976
  }
874
977
 
978
+ function workflowRunPaused(runId: string): boolean {
979
+ return terminalRunState(runId)?.paused === true;
980
+ }
981
+
982
+ function interactionPresentationClaimIsLive(options: {
983
+ requestId: string;
984
+ runId: string;
985
+ presenterId: string;
986
+ revision: number;
987
+ claimExpiresAt: number;
988
+ }): boolean {
989
+ try {
990
+ const store = new HostStateStore(workflowStatePath(), { readOnly: true });
991
+ try {
992
+ const interaction = store.getInteraction(options.requestId);
993
+ return (
994
+ interaction?.runId === options.runId &&
995
+ interaction.status === "presenting" &&
996
+ interaction.presenterId === options.presenterId &&
997
+ interaction.revision === options.revision &&
998
+ interaction.presentationSessionEntryId === null &&
999
+ interaction.presentationClaimExpiresAt !== null &&
1000
+ Date.parse(interaction.presentationClaimExpiresAt) === options.claimExpiresAt &&
1001
+ !workflowRunPaused(options.runId)
1002
+ );
1003
+ } finally {
1004
+ store.close();
1005
+ }
1006
+ } catch {
1007
+ return false;
1008
+ }
1009
+ }
1010
+
1011
+ async function hostDeliveryClaimIsLive(
1012
+ client: WorkflowHostClient,
1013
+ options: {
1014
+ kind: "notification" | "turn";
1015
+ resourceId: string;
1016
+ targetSessionId: string;
1017
+ claimId: string;
1018
+ },
1019
+ ): Promise<boolean> {
1020
+ try {
1021
+ const validationId = randomUUID();
1022
+ const response = await client.request({
1023
+ operation: options.kind === "notification" ? "notification.claim" : "turn.claim",
1024
+ requestId: `delivery-validate-${options.claimId}-${validationId}`,
1025
+ idempotencyKey: validationId,
1026
+ payload: { ...options, validateClaim: true },
1027
+ });
1028
+ const receipt = isRecord(response.receipt) ? response.receipt : undefined;
1029
+ return (
1030
+ (response.outcome === "accepted" || response.outcome === "adopted") && receipt?.live === true
1031
+ );
1032
+ } catch {
1033
+ return false;
1034
+ }
1035
+ }
1036
+
875
1037
  function presentationMessage(
876
1038
  turn: Record<string, unknown>,
877
1039
  state: Record<string, unknown>,
@@ -927,6 +1089,12 @@ function requireText(value: unknown, name: string): string {
927
1089
  return value;
928
1090
  }
929
1091
 
1092
+ function claimExpiry(value: unknown, name: string): number {
1093
+ const expiry = Date.parse(requireText(value, `${name} expiry`));
1094
+ if (!Number.isFinite(expiry)) throw new Error(`${name} expiry must be a timestamp`);
1095
+ return expiry;
1096
+ }
1097
+
930
1098
  function activeSessionRun(ctx: ExtensionContext): WorkflowRunQueueRecord | undefined {
931
1099
  return sessionRun(ctx);
932
1100
  }
@@ -1021,7 +1189,11 @@ function agentContract(interaction: InteractiveRequestRecord): AgentStepContract
1021
1189
  }
1022
1190
 
1023
1191
  function interactionRequestId(value: unknown): string | undefined {
1024
- if (!isRecord(value) || value.type !== "custom_message" || !isRecord(value.details)) {
1192
+ if (
1193
+ !isRecord(value) ||
1194
+ (value.type !== "custom_message" && value.role !== "custom") ||
1195
+ !isRecord(value.details)
1196
+ ) {
1025
1197
  return undefined;
1026
1198
  }
1027
1199
  return typeof value.details.requestId === "string" ? value.details.requestId : undefined;