@tomflow/proflow-execution-browser-extension 0.1.43 → 0.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @tomflow/proflow-execution-browser-extension
2
2
 
3
+ ## 0.1.45
4
+
5
+ ### Patch Changes
6
+
7
+ - Release the verified Real-3 B1 task recovery, exact execution identity, durable observer resume, bounded gateway error, and matching Agent Action contract fixes.
8
+
9
+ ## 0.1.44
10
+
11
+ ### Patch Changes
12
+
13
+ - Prefer the canonical ChatGPT prompt editor over earlier fallback textareas so worker wake messages are submitted through the real composer.
14
+
3
15
  ## 0.1.43
4
16
 
5
17
  ### Patch Changes
@@ -47,7 +47,7 @@ export declare const behaviorAdapter: {
47
47
  ok: true;
48
48
  status: "SUCCEEDED";
49
49
  moduleRef: "execution-browser-extension";
50
- moduleVersion: "0.1.43";
50
+ moduleVersion: "0.1.45";
51
51
  data: {
52
52
  loadDir: string;
53
53
  };
@@ -60,7 +60,7 @@ export declare const behaviorAdapter: {
60
60
  readonly ok: true;
61
61
  readonly status: "SUCCEEDED";
62
62
  readonly moduleRef: "execution-browser-extension";
63
- readonly moduleVersion: "0.1.43";
63
+ readonly moduleVersion: "0.1.45";
64
64
  };
65
65
  observedEffects: never[];
66
66
  }>;
@@ -70,7 +70,7 @@ export declare const behaviorAdapter: {
70
70
  ok: true;
71
71
  status: "SUCCEEDED";
72
72
  moduleRef: "execution-browser-extension";
73
- moduleVersion: "0.1.43";
73
+ moduleVersion: "0.1.45";
74
74
  data: {
75
75
  setupStatus: "ACTION_REQUIRED" | "READY";
76
76
  runtimeStatus: "NOT_APPLICABLE";
@@ -95,7 +95,7 @@ export declare const behaviorAdapter: {
95
95
  result: {
96
96
  contract: "deployment.result.v1";
97
97
  moduleRef: "execution-browser-extension";
98
- moduleVersion: "0.1.43";
98
+ moduleVersion: "0.1.45";
99
99
  ok: false;
100
100
  status: "ACTION_REQUIRED";
101
101
  data: {
@@ -127,14 +127,14 @@ export declare const behaviorAdapter: {
127
127
  readonly ok: true;
128
128
  readonly status: "SUCCEEDED";
129
129
  readonly moduleRef: "execution-browser-extension";
130
- readonly moduleVersion: "0.1.43";
130
+ readonly moduleVersion: "0.1.45";
131
131
  };
132
132
  observedEffects: string[];
133
133
  } | {
134
134
  result: {
135
135
  contract: "deployment.result.v1";
136
136
  moduleRef: "execution-browser-extension";
137
- moduleVersion: "0.1.43";
137
+ moduleVersion: "0.1.45";
138
138
  ok: false;
139
139
  status: "FAILED";
140
140
  error: {
@@ -167,7 +167,7 @@ export declare const behaviorAdapter: {
167
167
  ok: true;
168
168
  status: "SUCCEEDED";
169
169
  moduleRef: "execution-browser-extension";
170
- moduleVersion: "0.1.43";
170
+ moduleVersion: "0.1.45";
171
171
  data: {
172
172
  docs: string;
173
173
  };
@@ -180,11 +180,11 @@ export declare const behaviorAdapter: {
180
180
  readonly ok: true;
181
181
  readonly status: "SUCCEEDED";
182
182
  readonly moduleRef: "execution-browser-extension";
183
- readonly moduleVersion: "0.1.43";
183
+ readonly moduleVersion: "0.1.45";
184
184
  } | {
185
185
  contract: "deployment.result.v1";
186
186
  moduleRef: "execution-browser-extension";
187
- moduleVersion: "0.1.43";
187
+ moduleVersion: "0.1.45";
188
188
  ok: false;
189
189
  status: "FAILED";
190
190
  error: {
@@ -201,7 +201,7 @@ export declare const behaviorAdapter: {
201
201
  readonly ok: true;
202
202
  readonly status: "SUCCEEDED";
203
203
  readonly moduleRef: "execution-browser-extension";
204
- readonly moduleVersion: "0.1.43";
204
+ readonly moduleVersion: "0.1.45";
205
205
  };
206
206
  observedEffects: never[];
207
207
  }>;
@@ -3,7 +3,7 @@ export declare const descriptor: {
3
3
  readonly contractVersion: "1.0.0";
4
4
  readonly moduleRef: "execution-browser-extension";
5
5
  readonly packageName: "@tomflow/proflow-execution-browser-extension";
6
- readonly moduleVersion: "0.1.43";
6
+ readonly moduleVersion: "0.1.45";
7
7
  readonly kind: "browser-extension";
8
8
  readonly templateVersion: "1.0.0";
9
9
  readonly platformCompatibility: ">=1.0.0 <2.0.0";
@@ -3,7 +3,7 @@ export const descriptor = {
3
3
  contractVersion: "1.0.0",
4
4
  moduleRef: "execution-browser-extension",
5
5
  packageName: "@tomflow/proflow-execution-browser-extension",
6
- moduleVersion: "0.1.43",
6
+ moduleVersion: "0.1.45",
7
7
  kind: "browser-extension",
8
8
  templateVersion: "1.0.0",
9
9
  platformCompatibility: ">=1.0.0 <2.0.0",
@@ -14137,16 +14137,23 @@ function createTaskObserver(options) {
14137
14137
  conversationLocator: binding.conversationLocator
14138
14138
  };
14139
14139
  }
14140
- if (resumeSignal) {
14140
+ const effectiveResumeSignal = resumeSignal ?? (projection.resumeSignalRef && projection.taskStatus === "ACTIVE" && node.status === "IN_PROGRESS" ? {
14141
+ trigger: "TASK_RESUMED",
14142
+ ref: projection.resumeSignalRef,
14143
+ targetWorkerRef: binding.workerRef,
14144
+ nodeId: node.nodeId,
14145
+ runNo: node.runNo
14146
+ } : void 0);
14147
+ if (effectiveResumeSignal) {
14141
14148
  if (projection.taskStatus !== "ACTIVE" || node.status !== "IN_PROGRESS")
14142
14149
  return { kind: "NOOP", taskId, reason: "BINDING_NOT_READY" };
14143
- if (resumeSignal.nodeId !== node.nodeId || resumeSignal.runNo !== node.runNo)
14150
+ if (effectiveResumeSignal.nodeId !== node.nodeId || effectiveResumeSignal.runNo !== node.runNo)
14144
14151
  return {
14145
14152
  kind: "NOOP",
14146
14153
  taskId,
14147
14154
  reason: "RESUME_GENERATION_MISMATCH"
14148
14155
  };
14149
- if (resumeSignal.targetWorkerRef !== binding.workerRef)
14156
+ if (effectiveResumeSignal.targetWorkerRef !== binding.workerRef)
14150
14157
  return {
14151
14158
  kind: "NOOP",
14152
14159
  taskId,
@@ -14159,9 +14166,9 @@ function createTaskObserver(options) {
14159
14166
  runNo: node.runNo,
14160
14167
  roleRef: binding.roleRef,
14161
14168
  workerRef: binding.workerRef,
14162
- trigger: resumeSignal.trigger,
14169
+ trigger: effectiveResumeSignal.trigger,
14163
14170
  conversationLocator: binding.conversationLocator,
14164
- underlyingRef: resumeSignal.ref
14171
+ underlyingRef: effectiveResumeSignal.ref
14165
14172
  };
14166
14173
  }
14167
14174
  return { kind: "NOOP", taskId, reason: "NO_NEXT_STEP" };
@@ -14457,6 +14464,14 @@ var taskObserver = createTaskObserver({
14457
14464
  }
14458
14465
  }
14459
14466
  });
14467
+ async function resumeTaskWorker(taskId) {
14468
+ const projection = await invokeObserverApplication("task.projection", {
14469
+ taskId
14470
+ });
14471
+ if (projection.taskId !== taskId || projection.taskStatus !== "ACTIVE" || projection.currentNode?.status !== "IN_PROGRESS" || !projection.resumeSignalRef)
14472
+ throw new Error("TASK_RESUMED_OWNER_FACTS_INVALID");
14473
+ return taskObserver.drive(taskId);
14474
+ }
14460
14475
  async function resumeAfterApprovalDecision(approvalRef) {
14461
14476
  const context = await invokeObserverApplication("approval.executionContext", {
14462
14477
  approvalRef
@@ -15042,6 +15057,8 @@ async function executeCommand(command) {
15042
15057
  void runObserverRecovery();
15043
15058
  return { scheduled: true };
15044
15059
  }
15060
+ if (command.type === "TASK_OBSERVER_RESUME")
15061
+ return resumeTaskWorker(text(command.taskId, "TASK_ID"));
15045
15062
  if (command.type === "CARRIER_ATTENTION_ACTION") {
15046
15063
  if (command.action !== "allowOnce" && command.action !== "deny")
15047
15064
  throw new Error("ATTENTION_ACTION_INVALID");
@@ -15525,8 +15542,10 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
15525
15542
  return;
15526
15543
  }
15527
15544
  void invokeTaskApplication(message.operation, message.input).then(
15528
- (value) => {
15529
- void runObserverRecovery();
15545
+ async (value) => {
15546
+ if (message.operation === "task.resume")
15547
+ await resumeTaskWorker(text(message.input?.taskId, "TASK_ID"));
15548
+ else void runObserverRecovery();
15530
15549
  sendResponse({ ok: true, value });
15531
15550
  },
15532
15551
  (error46) => sendResponse({
@@ -154,9 +154,12 @@
154
154
  button.click();
155
155
  return permission.facts;
156
156
  }
157
+ function selectChatGptComposerElement(document2) {
158
+ return document2.querySelector("#prompt-textarea") ?? document2.querySelector("textarea") ?? document2.querySelector('[contenteditable="true"]');
159
+ }
157
160
  function composerElement(document2) {
158
161
  const view = document2.defaultView;
159
- const element = document2.querySelector(composerSelector);
162
+ const element = selectChatGptComposerElement(document2);
160
163
  if (!view || !(element instanceof view.HTMLElement))
161
164
  throw new Error("COMPOSER_NOT_FOUND");
162
165
  return element;
@@ -70,7 +70,9 @@ const nodesTarget = element("#nodes");
70
70
  const errorTarget = element("#error");
71
71
  const resultTarget = element("#result");
72
72
  const startButton = element("#start-task");
73
+ const resumeButton = element("#resume-task");
73
74
  const ensureWorkersButton = element("#ensure-workers");
75
+ const pendingMessagesTarget = element("#pending-messages");
74
76
  const newTaskForm = element("#new-task-form");
75
77
  const approvalsTarget = element("#approvals");
76
78
  const carrierAttentionsTarget = element("#carrier-attentions");
@@ -236,8 +238,38 @@ async function loadTask(taskId) {
236
238
  selected = (await taskApplication("task.get", { taskId }));
237
239
  selectedTarget.textContent = `${selected.taskId} · ${selected.status} · v${selected.version}`;
238
240
  startButton.disabled = selected.status !== "READY";
241
+ resumeButton.hidden = !["WAITING", "PAUSED"].includes(selected.status);
242
+ resumeButton.disabled =
243
+ selected.status === "WAITING" && selected.pendingMessages.length > 0;
239
244
  ensureWorkersButton.disabled =
240
245
  selected.status === "SUCCEEDED" || selected.status === "TERMINATED";
246
+ pendingMessagesTarget.replaceChildren();
247
+ for (const message of selected.pendingMessages) {
248
+ const row = document.createElement("div");
249
+ row.className = "task";
250
+ const label = document.createElement("div");
251
+ label.textContent = `${message.messageType} · ${message.reasonCode}`;
252
+ const detail = document.createElement("div");
253
+ detail.className = "meta";
254
+ detail.textContent = message.message;
255
+ const acknowledge = document.createElement("button");
256
+ acknowledge.type = "button";
257
+ acknowledge.textContent = "Resolve blocker";
258
+ acknowledge.addEventListener("click", () => {
259
+ void run(async () => {
260
+ if (!selected)
261
+ return;
262
+ await taskApplication("message.acknowledge", {
263
+ messageId: message.messageId,
264
+ resolution: "Resolved from Extension Task Page",
265
+ idempotencyKey: requestId("extension-acknowledge-message"),
266
+ });
267
+ await loadTask(selected.taskId);
268
+ });
269
+ });
270
+ row.append(label, detail, acknowledge);
271
+ pendingMessagesTarget.append(row);
272
+ }
241
273
  nodesTarget.replaceChildren();
242
274
  for (const node of selected.nodes) {
243
275
  const row = document.createElement("div");
@@ -245,7 +277,7 @@ async function loadTask(taskId) {
245
277
  const label = document.createElement("span");
246
278
  label.textContent = `${node.title} · ${node.status} · run ${node.runNo}`;
247
279
  row.append(label);
248
- if (["SUCCEEDED", "FAILED", "WAITING"].includes(node.status)) {
280
+ if (["SUCCEEDED", "FAILED"].includes(node.status)) {
249
281
  const reopen = document.createElement("button");
250
282
  reopen.type = "button";
251
283
  reopen.textContent = "Reopen";
@@ -392,6 +424,27 @@ startButton.addEventListener("click", () => {
392
424
  }
393
425
  });
394
426
  });
427
+ resumeButton.addEventListener("click", () => {
428
+ void run(async () => {
429
+ if (!selected)
430
+ return;
431
+ setBusy(resumeButton, true);
432
+ try {
433
+ await taskApplication("task.resume", {
434
+ taskId: selected.taskId,
435
+ expectedTaskVersion: selected.version,
436
+ idempotencyKey: requestId("extension-resume-task"),
437
+ });
438
+ await loadTask(selected.taskId);
439
+ await refreshTasks();
440
+ }
441
+ finally {
442
+ resumeButton.disabled =
443
+ selected?.status === "WAITING" &&
444
+ (selected.pendingMessages?.length ?? 0) > 0;
445
+ }
446
+ });
447
+ });
395
448
  ensureWorkersButton.addEventListener("click", () => {
396
449
  void run(async () => {
397
450
  if (!selected)
@@ -268,6 +268,14 @@ export async function createBrowserRealityBridgeServer(options) {
268
268
  ? await options.taskWeb.invokeTask(body.operation, body.input)
269
269
  : await options.taskWeb.invokeApproval(body.operation, body.input);
270
270
  if (taskRequest &&
271
+ body.operation === "task.resume" &&
272
+ commandConsumerReady()) {
273
+ void requestCommand({
274
+ type: "TASK_OBSERVER_RESUME",
275
+ taskId: stringField(body.input, "taskId"),
276
+ }).catch(() => undefined);
277
+ }
278
+ else if (taskRequest &&
271
279
  taskWebObserverRecoveryOperations.has(body.operation) &&
272
280
  commandConsumerReady()) {
273
281
  // The Task mutation is already durable and must not be coupled to
@@ -12,5 +12,6 @@ export declare function classifyChatGptPageSignals(input: {
12
12
  }): ChatGptPageReality;
13
13
  export declare function observeChatGptPage(document: Document): ChatGptPageReality;
14
14
  export declare function performChatGptPermissionAction(document: Document, expectedFingerprint: string, action: PermissionSemanticAction): ActionPermissionFacts;
15
+ export declare function selectChatGptComposerElement(document: Document): Element | null;
15
16
  export declare function submitChatGptComposer(document: Document, value: string): Promise<void>;
16
17
  export declare function writeChatGptInput(document: Document, selector: string, value: string): void;
@@ -72,9 +72,14 @@ export function performChatGptPermissionAction(document, expectedFingerprint, ac
72
72
  button.click();
73
73
  return permission.facts;
74
74
  }
75
+ export function selectChatGptComposerElement(document) {
76
+ return (document.querySelector("#prompt-textarea") ??
77
+ document.querySelector("textarea") ??
78
+ document.querySelector('[contenteditable="true"]'));
79
+ }
75
80
  function composerElement(document) {
76
81
  const view = document.defaultView;
77
- const element = document.querySelector(composerSelector);
82
+ const element = selectChatGptComposerElement(document);
78
83
  if (!view || !(element instanceof view.HTMLElement))
79
84
  throw new Error("COMPOSER_NOT_FOUND");
80
85
  return element;
@@ -27,9 +27,10 @@ export type TaskDriveProjection = {
27
27
  } | null;
28
28
  canDrive: boolean;
29
29
  blockedReason: string | null;
30
+ resumeSignalRef: string | null;
30
31
  };
31
32
  export type TaskObserverResumeSignal = {
32
- trigger: "EXECUTION_RESULT_READY" | "PEER_REPLY_READY" | "RECOVERY_RESUME";
33
+ trigger: "EXECUTION_RESULT_READY" | "PEER_REPLY_READY" | "RECOVERY_RESUME" | "TASK_RESUMED";
33
34
  ref: string;
34
35
  targetWorkerRef: string;
35
36
  nodeId: string;
@@ -71,17 +71,29 @@ export function createTaskObserver(options) {
71
71
  conversationLocator: binding.conversationLocator,
72
72
  };
73
73
  }
74
- if (resumeSignal) {
74
+ const effectiveResumeSignal = resumeSignal ??
75
+ (projection.resumeSignalRef &&
76
+ projection.taskStatus === "ACTIVE" &&
77
+ node.status === "IN_PROGRESS"
78
+ ? {
79
+ trigger: "TASK_RESUMED",
80
+ ref: projection.resumeSignalRef,
81
+ targetWorkerRef: binding.workerRef,
82
+ nodeId: node.nodeId,
83
+ runNo: node.runNo,
84
+ }
85
+ : undefined);
86
+ if (effectiveResumeSignal) {
75
87
  if (projection.taskStatus !== "ACTIVE" || node.status !== "IN_PROGRESS")
76
88
  return { kind: "NOOP", taskId, reason: "BINDING_NOT_READY" };
77
- if (resumeSignal.nodeId !== node.nodeId ||
78
- resumeSignal.runNo !== node.runNo)
89
+ if (effectiveResumeSignal.nodeId !== node.nodeId ||
90
+ effectiveResumeSignal.runNo !== node.runNo)
79
91
  return {
80
92
  kind: "NOOP",
81
93
  taskId,
82
94
  reason: "RESUME_GENERATION_MISMATCH",
83
95
  };
84
- if (resumeSignal.targetWorkerRef !== binding.workerRef)
96
+ if (effectiveResumeSignal.targetWorkerRef !== binding.workerRef)
85
97
  return {
86
98
  kind: "NOOP",
87
99
  taskId,
@@ -94,9 +106,9 @@ export function createTaskObserver(options) {
94
106
  runNo: node.runNo,
95
107
  roleRef: binding.roleRef,
96
108
  workerRef: binding.workerRef,
97
- trigger: resumeSignal.trigger,
109
+ trigger: effectiveResumeSignal.trigger,
98
110
  conversationLocator: binding.conversationLocator,
99
- underlyingRef: resumeSignal.ref,
111
+ underlyingRef: effectiveResumeSignal.ref,
100
112
  };
101
113
  }
102
114
  return { kind: "NOOP", taskId, reason: "NO_NEXT_STEP" };
@@ -29,6 +29,7 @@ import {
29
29
  import {
30
30
  createTaskObserver,
31
31
  type TaskDriveProjection,
32
+ type TaskObserverDecision,
32
33
  type TaskObserverDiagnosticAssessment,
33
34
  type TaskObserverDiagnosticFailure,
34
35
  } from "../src/task-observer.js";
@@ -77,7 +78,8 @@ type BridgeCommand = {
77
78
  | "SCREENSHOT"
78
79
  | "PERFORM"
79
80
  | "CARRIER_ATTENTION_ACTION"
80
- | "TASK_OBSERVER_RECOVER";
81
+ | "TASK_OBSERVER_RECOVER"
82
+ | "TASK_OBSERVER_RESUME";
81
83
  tabId?: number;
82
84
  url?: string;
83
85
  text?: string;
@@ -85,6 +87,7 @@ type BridgeCommand = {
85
87
  request?: Record<string, unknown>;
86
88
  attentionRef?: string;
87
89
  action?: "allowOnce" | "deny";
90
+ taskId?: string;
88
91
  };
89
92
  type ContentSnapshotRequest = { type: "PROFLOW_PAGE_SNAPSHOT_REQUEST" };
90
93
  type ContentCommand = {
@@ -540,6 +543,20 @@ const taskObserver = createTaskObserver({
540
543
  },
541
544
  });
542
545
 
546
+ async function resumeTaskWorker(taskId: string) {
547
+ const projection = (await invokeObserverApplication("task.projection", {
548
+ taskId,
549
+ })) as TaskDriveProjection;
550
+ if (
551
+ projection.taskId !== taskId ||
552
+ projection.taskStatus !== "ACTIVE" ||
553
+ projection.currentNode?.status !== "IN_PROGRESS" ||
554
+ !projection.resumeSignalRef
555
+ )
556
+ throw new Error("TASK_RESUMED_OWNER_FACTS_INVALID");
557
+ return taskObserver.drive(taskId);
558
+ }
559
+
543
560
  async function resumeAfterApprovalDecision(approvalRef: string) {
544
561
  const context = await invokeObserverApplication("approval.executionContext", {
545
562
  approvalRef,
@@ -734,7 +751,7 @@ function runObserverRecovery() {
734
751
  )
735
752
  )
736
753
  continue;
737
- let decision;
754
+ let decision: TaskObserverDecision | null;
738
755
  if (candidate.kind === "RECOVERY_RESUME") {
739
756
  if (
740
757
  typeof candidate.nodeId !== "string" ||
@@ -1355,6 +1372,8 @@ async function executeCommand(command: BridgeCommand): Promise<unknown> {
1355
1372
  void runObserverRecovery();
1356
1373
  return { scheduled: true };
1357
1374
  }
1375
+ if (command.type === "TASK_OBSERVER_RESUME")
1376
+ return resumeTaskWorker(text(command.taskId, "TASK_ID"));
1358
1377
  if (command.type === "CARRIER_ATTENTION_ACTION") {
1359
1378
  if (command.action !== "allowOnce" && command.action !== "deny")
1360
1379
  throw new Error("ATTENTION_ACTION_INVALID");
@@ -1941,8 +1960,10 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1941
1960
  return;
1942
1961
  }
1943
1962
  void invokeTaskApplication(message.operation, message.input).then(
1944
- (value) => {
1945
- void runObserverRecovery();
1963
+ async (value) => {
1964
+ if (message.operation === "task.resume")
1965
+ await resumeTaskWorker(text(message.input?.taskId, "TASK_ID"));
1966
+ else void runObserverRecovery();
1946
1967
  sendResponse({ ok: true, value });
1947
1968
  },
1948
1969
  (error: unknown) =>
@@ -51,7 +51,9 @@
51
51
  <h2>Selected Task</h2>
52
52
  <div id="selected-task" class="meta">None selected.</div>
53
53
  <button id="start-task" type="button" disabled>Confirm / Start</button>
54
+ <button id="resume-task" type="button" hidden>Resume</button>
54
55
  <button id="ensure-workers" type="button" disabled>Recover missing Workers</button>
56
+ <div id="pending-messages"></div>
55
57
  <div id="nodes"></div>
56
58
  </section>
57
59
 
@@ -108,6 +108,13 @@ type TaskView = TaskSummary & {
108
108
  runNo: number;
109
109
  version: number;
110
110
  }>;
111
+ pendingMessages: Array<{
112
+ messageId: string;
113
+ nodeId: string | null;
114
+ messageType: string;
115
+ reasonCode: string;
116
+ message: string;
117
+ }>;
111
118
  };
112
119
 
113
120
  function element<T extends HTMLElement>(selector: string): T {
@@ -124,7 +131,9 @@ const nodesTarget = element<HTMLElement>("#nodes");
124
131
  const errorTarget = element<HTMLElement>("#error");
125
132
  const resultTarget = element<HTMLElement>("#result");
126
133
  const startButton = element<HTMLButtonElement>("#start-task");
134
+ const resumeButton = element<HTMLButtonElement>("#resume-task");
127
135
  const ensureWorkersButton = element<HTMLButtonElement>("#ensure-workers");
136
+ const pendingMessagesTarget = element<HTMLElement>("#pending-messages");
128
137
  const newTaskForm = element<HTMLFormElement>("#new-task-form");
129
138
  const approvalsTarget = element<HTMLElement>("#approvals");
130
139
  const carrierAttentionsTarget = element<HTMLElement>("#carrier-attentions");
@@ -342,8 +351,37 @@ async function loadTask(taskId: string) {
342
351
  selected = (await taskApplication("task.get", { taskId })) as TaskView;
343
352
  selectedTarget.textContent = `${selected.taskId} · ${selected.status} · v${selected.version}`;
344
353
  startButton.disabled = selected.status !== "READY";
354
+ resumeButton.hidden = !["WAITING", "PAUSED"].includes(selected.status);
355
+ resumeButton.disabled =
356
+ selected.status === "WAITING" && selected.pendingMessages.length > 0;
345
357
  ensureWorkersButton.disabled =
346
358
  selected.status === "SUCCEEDED" || selected.status === "TERMINATED";
359
+ pendingMessagesTarget.replaceChildren();
360
+ for (const message of selected.pendingMessages) {
361
+ const row = document.createElement("div");
362
+ row.className = "task";
363
+ const label = document.createElement("div");
364
+ label.textContent = `${message.messageType} · ${message.reasonCode}`;
365
+ const detail = document.createElement("div");
366
+ detail.className = "meta";
367
+ detail.textContent = message.message;
368
+ const acknowledge = document.createElement("button");
369
+ acknowledge.type = "button";
370
+ acknowledge.textContent = "Resolve blocker";
371
+ acknowledge.addEventListener("click", () => {
372
+ void run(async () => {
373
+ if (!selected) return;
374
+ await taskApplication("message.acknowledge", {
375
+ messageId: message.messageId,
376
+ resolution: "Resolved from Extension Task Page",
377
+ idempotencyKey: requestId("extension-acknowledge-message"),
378
+ });
379
+ await loadTask(selected.taskId);
380
+ });
381
+ });
382
+ row.append(label, detail, acknowledge);
383
+ pendingMessagesTarget.append(row);
384
+ }
347
385
  nodesTarget.replaceChildren();
348
386
  for (const node of selected.nodes) {
349
387
  const row = document.createElement("div");
@@ -351,7 +389,7 @@ async function loadTask(taskId: string) {
351
389
  const label = document.createElement("span");
352
390
  label.textContent = `${node.title} · ${node.status} · run ${node.runNo}`;
353
391
  row.append(label);
354
- if (["SUCCEEDED", "FAILED", "WAITING"].includes(node.status)) {
392
+ if (["SUCCEEDED", "FAILED"].includes(node.status)) {
355
393
  const reopen = document.createElement("button");
356
394
  reopen.type = "button";
357
395
  reopen.textContent = "Reopen";
@@ -515,6 +553,26 @@ startButton.addEventListener("click", () => {
515
553
  });
516
554
  });
517
555
 
556
+ resumeButton.addEventListener("click", () => {
557
+ void run(async () => {
558
+ if (!selected) return;
559
+ setBusy(resumeButton, true);
560
+ try {
561
+ await taskApplication("task.resume", {
562
+ taskId: selected.taskId,
563
+ expectedTaskVersion: selected.version,
564
+ idempotencyKey: requestId("extension-resume-task"),
565
+ });
566
+ await loadTask(selected.taskId);
567
+ await refreshTasks();
568
+ } finally {
569
+ resumeButton.disabled =
570
+ selected?.status === "WAITING" &&
571
+ (selected.pendingMessages?.length ?? 0) > 0;
572
+ }
573
+ });
574
+ });
575
+
518
576
  ensureWorkersButton.addEventListener("click", () => {
519
577
  void run(async () => {
520
578
  if (!selected) return;
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "ProFlow Execution Browser",
4
- "version": "0.1.43",
4
+ "version": "0.1.45",
5
5
  "permissions": [
6
6
  "tabs",
7
7
  "storage"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomflow/proflow-execution-browser-extension",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "description": "Execution-owned MV3 Browser executor, evidence provider and browser application surface.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -27,8 +27,8 @@
27
27
  "SETUP.md"
28
28
  ],
29
29
  "dependencies": {
30
- "@tomflow/proflow-execution-contracts": "^0.1.10",
31
- "@tomflow/proflow-module-contract": "^0.1.13"
30
+ "@tomflow/proflow-module-contract": "^0.1.13",
31
+ "@tomflow/proflow-execution-contracts": "^0.1.10"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@tomflow/proflow-deployment-conformance": "^0.1.13",
@@ -3,7 +3,7 @@
3
3
  "contractVersion": "1.0.0",
4
4
  "moduleRef": "execution-browser-extension",
5
5
  "packageName": "@tomflow/proflow-execution-browser-extension",
6
- "moduleVersion": "0.1.43",
6
+ "moduleVersion": "0.1.45",
7
7
  "kind": "browser-extension",
8
8
  "templateVersion": "1.0.0",
9
9
  "platformCompatibility": ">=1.0.0 <2.0.0",