@tomflow/proflow-execution-browser-extension 0.1.37 → 0.1.39

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.
@@ -1,4 +1,20 @@
1
+ import { createCarrierAttentionRegistry } from "../src/carrier-attention.js";
2
+ import {
3
+ type CarrierContinuationDenial,
4
+ createCarrierContinuationControl,
5
+ } from "../src/carrier-continuation-control.js";
6
+ import type {
7
+ ActionPermissionFacts,
8
+ PermissionSemanticAction,
9
+ } from "../src/carrier-permission.js";
10
+ import { createCarrierPermissionAttemptRegistry } from "../src/carrier-permission-attempt.js";
11
+ import {
12
+ type CarrierPermissionDecision,
13
+ resolveHumanCarrierPermission,
14
+ resolveRoutineCarrierPermission,
15
+ } from "../src/carrier-permission-lifecycle.js";
1
16
  import { createCollaborationCarrierApplication } from "../src/collaboration-carrier.js";
17
+ import { shouldTriggerObserverRecovery } from "../src/recovery-trigger.js";
2
18
  import {
3
19
  createSystemObserver,
4
20
  type SystemObserverReasonFailure,
@@ -12,7 +28,6 @@ import {
12
28
  type TaskObserverDiagnosticAssessment,
13
29
  type TaskObserverDiagnosticFailure,
14
30
  } from "../src/task-observer.js";
15
- import { shouldTriggerObserverRecovery } from "../src/recovery-trigger.js";
16
31
 
17
32
  type PageState = "IDLE" | "BUSY" | "BLOCKED" | "UNKNOWN";
18
33
  type ActivityKind =
@@ -30,6 +45,7 @@ type ContentObservation = {
30
45
  contentInstanceId: string;
31
46
  pageState: PageState;
32
47
  activityKind: ActivityKind;
48
+ blockerFacts?: ActionPermissionFacts;
33
49
  observedAt: string;
34
50
  };
35
51
  type RuntimeMessage = {
@@ -38,6 +54,7 @@ type RuntimeMessage = {
38
54
  | "PROFLOW_SIDE_PANEL_SNAPSHOT"
39
55
  | "PROFLOW_TASK_APPLICATION"
40
56
  | "PROFLOW_APPROVAL_APPLICATION"
57
+ | "PROFLOW_CARRIER_ATTENTION_ACTION"
41
58
  | "PROFLOW_PROVISIONING_RELAY_FETCH";
42
59
  observation?: Omit<ContentObservation, "tabId" | "windowId">;
43
60
  operation?: string;
@@ -54,21 +71,33 @@ type BridgeCommand = {
54
71
  | "SUBMIT"
55
72
  | "VERIFY"
56
73
  | "SCREENSHOT"
57
- | "PERFORM";
74
+ | "PERFORM"
75
+ | "CARRIER_ATTENTION_ACTION";
58
76
  tabId?: number;
59
77
  url?: string;
60
78
  text?: string;
61
79
  fingerprint?: string;
62
80
  request?: Record<string, unknown>;
81
+ attentionRef?: string;
82
+ action?: "allowOnce" | "deny";
63
83
  };
84
+ type ContentSnapshotRequest = { type: "PROFLOW_PAGE_SNAPSHOT_REQUEST" };
64
85
  type ContentCommand = {
65
86
  type: "PROFLOW_PAGE_COMMAND";
66
87
  contentInstanceId: string;
67
88
  expectedUrl: string;
68
- operation: "observe" | "input" | "click" | "submit" | "verify";
89
+ operation:
90
+ | "observe"
91
+ | "input"
92
+ | "click"
93
+ | "submit"
94
+ | "verify"
95
+ | "permissionAction";
69
96
  selector?: string;
70
97
  value?: string;
71
98
  fingerprint?: string;
99
+ permissionFingerprint?: string;
100
+ permissionAction?: PermissionSemanticAction;
72
101
  };
73
102
  type ProvisioningOperation =
74
103
  | "PROVISION_CUSTOM_GPT"
@@ -119,7 +148,10 @@ type ChromeRuntime = {
119
148
  onClicked: { addListener(listener: () => void): void };
120
149
  };
121
150
  tabs: {
122
- query(query: { url?: string; currentWindow?: boolean }): Promise<ChromeTab[]>;
151
+ query(query: {
152
+ url?: string;
153
+ currentWindow?: boolean;
154
+ }): Promise<ChromeTab[]>;
123
155
  get(tabId: number): Promise<ChromeTab>;
124
156
  create(create: { url: string; active: boolean }): Promise<ChromeTab>;
125
157
  reload(tabId: number): Promise<void>;
@@ -129,7 +161,10 @@ type ChromeRuntime = {
129
161
  ): Promise<ChromeTab>;
130
162
  sendMessage(
131
163
  tabId: number,
132
- message: ContentCommand | ProvisioningContentCommand,
164
+ message:
165
+ | ContentCommand
166
+ | ContentSnapshotRequest
167
+ | ProvisioningContentCommand,
133
168
  ): Promise<unknown>;
134
169
  captureVisibleTab(
135
170
  windowId: number,
@@ -141,28 +176,70 @@ declare const chrome: ChromeRuntime;
141
176
 
142
177
  const extensionInstanceId = `extension:${crypto.randomUUID()}`;
143
178
  const sessions = new Map<number, ContentObservation>();
179
+ const permissionHandling = new Map<number, string>();
180
+ const permissionAutoAttempts = createCarrierPermissionAttemptRegistry();
181
+ const carrierAttentions = createCarrierAttentionRegistry();
182
+ const carrierContinuationControl = createCarrierContinuationControl();
144
183
  const BROWSER_CARRIER_KEEPALIVE_KEY = "proflowBrowserSnapshot";
145
184
  const BROWSER_CARRIER_KEEPALIVE_MS = 20_000;
146
185
  const BROWSER_BRIDGE_FETCH_TIMEOUT_MS = 5_000;
186
+ let snapshotPersistence = Promise.resolve();
187
+ let transientPermissionAttemptsRestore: Promise<boolean> | null = null;
147
188
 
148
189
  const sleep = (milliseconds: number) =>
149
190
  new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
150
191
 
151
- async function persistSnapshot() {
152
- await chrome.storage.session.set({
192
+ function persistSnapshot(): Promise<void> {
193
+ const value = {
153
194
  proflowBrowserSnapshot: {
154
195
  extensionInstanceId,
155
196
  observedAt: new Date().toISOString(),
156
197
  sessions: [...sessions.values()],
198
+ permissionAutoAttempts: permissionAutoAttempts.snapshot(),
199
+ carrierContinuationDenials: carrierContinuationControl.snapshot(),
157
200
  recoveryScan: "BOUNDED_ON_START",
158
201
  },
159
- });
202
+ };
203
+ // Preserve invocation order so an older empty snapshot cannot overwrite a
204
+ // later pre-click uncertainty record.
205
+ snapshotPersistence = snapshotPersistence
206
+ .catch(() => undefined)
207
+ .then(() => chrome.storage.session.set(value));
208
+ return snapshotPersistence;
160
209
  }
161
210
 
162
211
  function isRecord(value: unknown): value is Record<string, unknown> {
163
212
  return typeof value === "object" && value !== null && !Array.isArray(value);
164
213
  }
165
214
 
215
+ function restoreTransientPermissionAttempts(): Promise<boolean> {
216
+ if (transientPermissionAttemptsRestore)
217
+ return transientPermissionAttemptsRestore;
218
+ transientPermissionAttemptsRestore = (async () => {
219
+ try {
220
+ const stored = await chrome.storage.session.get(
221
+ BROWSER_CARRIER_KEEPALIVE_KEY,
222
+ );
223
+ const snapshot = stored[BROWSER_CARRIER_KEEPALIVE_KEY];
224
+ const validSnapshot = isRecord(snapshot);
225
+ const attemptsLoaded = permissionAutoAttempts.load(
226
+ validSnapshot ? snapshot.permissionAutoAttempts : undefined,
227
+ );
228
+ const denialsLoaded = carrierContinuationControl.load(
229
+ validSnapshot ? snapshot.carrierContinuationDenials : undefined,
230
+ );
231
+ return attemptsLoaded && denialsLoaded;
232
+ } catch {
233
+ // A later pre-click persistence failure still prevents the click. No
234
+ // automatic-action fact is manufactured from unreadable session data.
235
+ permissionAutoAttempts.load(undefined);
236
+ carrierContinuationControl.load(undefined);
237
+ return false;
238
+ }
239
+ })();
240
+ return transientPermissionAttemptsRestore;
241
+ }
242
+
166
243
  function parseConfig(value: unknown): BridgeConfig | null {
167
244
  if (!isRecord(value)) return null;
168
245
  const endpoint = value.endpoint;
@@ -450,6 +527,8 @@ const taskObserver = createTaskObserver({
450
527
  },
451
528
  carrier: {
452
529
  async requestWake(input) {
530
+ if (carrierContinuationControl.hasMatchingDispatchDenial(input))
531
+ throw new Error("CARRIER_CONTINUATION_HUMAN_DENIED");
453
532
  return invokeObserverApplication("task.wake", input);
454
533
  },
455
534
  },
@@ -589,8 +668,16 @@ async function persistSystemObserverState(
589
668
 
590
669
  let observerRecoveryInFlight: Promise<void> | null = null;
591
670
  let observerRecoveryRetryCount = 0;
671
+ let nextRecoverySuppressions: readonly CarrierContinuationDenial[] = [];
672
+ function suppressNextObserverRecovery(
673
+ denials: readonly CarrierContinuationDenial[],
674
+ ): void {
675
+ nextRecoverySuppressions = denials;
676
+ }
592
677
  function runObserverRecovery() {
593
678
  if (observerRecoveryInFlight) return observerRecoveryInFlight;
679
+ const suppressedContinuations = nextRecoverySuppressions;
680
+ nextRecoverySuppressions = [];
594
681
  observerRecoveryInFlight = (async () => {
595
682
  let recoveryNeedsRetry = false;
596
683
  await collaborationCarrier.recoverPending(50).catch(() => undefined);
@@ -609,6 +696,14 @@ function runObserverRecovery() {
609
696
  )
610
697
  continue;
611
698
  try {
699
+ if (
700
+ suppressedContinuations.some(
701
+ (denial) =>
702
+ denial.taskId === candidate.taskId &&
703
+ denial.workerRef === candidate.workerRef,
704
+ )
705
+ )
706
+ continue;
612
707
  const decision =
613
708
  candidate.kind === "RECOVERY_RESUME"
614
709
  ? await taskObserver.drive(candidate.taskId, {
@@ -668,6 +763,12 @@ function runObserverRecovery() {
668
763
  }).catch(() => {
669
764
  recoveryNeedsRetry = true;
670
765
  });
766
+ if (
767
+ suppressedContinuations.some(
768
+ (denial) => denial.taskId === candidate.taskId,
769
+ )
770
+ )
771
+ continue;
671
772
  await taskObserver.drive(candidate.taskId).catch(() => {
672
773
  recoveryNeedsRetry = true;
673
774
  });
@@ -738,6 +839,462 @@ async function contentCommand(
738
839
  return response.value;
739
840
  }
740
841
 
842
+ function carrierIdentity(
843
+ url: string,
844
+ ): { roleRef: string; workerRef: string | null } | null {
845
+ try {
846
+ const parsed = new URL(url);
847
+ const segments = parsed.pathname.split("/").filter(Boolean);
848
+ if (
849
+ parsed.protocol !== "https:" ||
850
+ parsed.hostname !== "chatgpt.com" ||
851
+ segments[0] !== "g" ||
852
+ !segments[1]?.startsWith("g-")
853
+ )
854
+ return null;
855
+ return {
856
+ roleRef: segments[1],
857
+ workerRef: segments[2] === "c" && segments[3] ? segments[3] : null,
858
+ };
859
+ } catch {
860
+ return null;
861
+ }
862
+ }
863
+
864
+ function permissionAttemptKey(observed: ContentObservation): string | null {
865
+ return observed.blockerFacts
866
+ ? `${observed.url}:${observed.blockerFacts.fingerprint}`
867
+ : null;
868
+ }
869
+
870
+ function setCarrierAttention(
871
+ observed: ContentObservation,
872
+ reason: string,
873
+ ): void {
874
+ const facts = observed.blockerFacts;
875
+ if (!facts) return;
876
+ const identity = carrierIdentity(observed.url);
877
+ carrierAttentions.derive({
878
+ tabId: observed.tabId,
879
+ contentInstanceId: observed.contentInstanceId,
880
+ url: observed.url,
881
+ permissionFingerprint: facts.fingerprint,
882
+ taskId: facts.taskId,
883
+ roleRef: identity?.roleRef ?? null,
884
+ workerRef: identity?.workerRef ?? null,
885
+ targetHost: facts.targetHost,
886
+ operationId: facts.operationId,
887
+ reason,
888
+ actions: facts.actions.filter(
889
+ (action): action is "allowOnce" | "deny" =>
890
+ action === "allowOnce" || action === "deny",
891
+ ),
892
+ observedAt: new Date().toISOString(),
893
+ });
894
+ void publishCarrierAttentions();
895
+ }
896
+
897
+ function carrierAttentionViews() {
898
+ return carrierAttentions
899
+ .values()
900
+ .map(
901
+ ({
902
+ attentionRef,
903
+ occurrenceRef,
904
+ taskId,
905
+ roleRef,
906
+ workerRef,
907
+ targetHost,
908
+ operationId,
909
+ reason,
910
+ actions,
911
+ observedAt,
912
+ }) => ({
913
+ attentionRef,
914
+ occurrenceRef,
915
+ taskId,
916
+ roleRef,
917
+ workerRef,
918
+ targetHost,
919
+ operationId,
920
+ reason,
921
+ actions,
922
+ observedAt,
923
+ }),
924
+ );
925
+ }
926
+
927
+ async function publishCarrierAttentions(): Promise<void> {
928
+ const config = await bridgeConfig().catch(() => null);
929
+ if (!config) return;
930
+ const query = `?extensionInstanceId=${encodeURIComponent(extensionInstanceId)}`;
931
+ const response = await bridgeFetch(config, `/v1/carrier/attentions${query}`, {
932
+ method: "POST",
933
+ body: JSON.stringify({ carrierAttentions: carrierAttentionViews() }),
934
+ });
935
+ if (!response.ok) throw new Error("CARRIER_ATTENTION_PUBLISH_REJECTED");
936
+ }
937
+
938
+ function rawPermissionFingerprint(value: unknown): string | null {
939
+ if (!isRecord(value) || !isRecord(value.blockerFacts)) return null;
940
+ return typeof value.blockerFacts.fingerprint === "string"
941
+ ? value.blockerFacts.fingerprint
942
+ : null;
943
+ }
944
+
945
+ function parsePermissionFacts(
946
+ value: unknown,
947
+ ): ActionPermissionFacts | undefined {
948
+ if (!isRecord(value) || value.kind !== "ACTION_PERMISSION") return undefined;
949
+ const actions = value.actions;
950
+ if (
951
+ (value.targetHost !== null && typeof value.targetHost !== "string") ||
952
+ typeof value.operationId !== "string" ||
953
+ (value.taskId !== null && typeof value.taskId !== "string") ||
954
+ !Array.isArray(actions) ||
955
+ actions.some(
956
+ (action) =>
957
+ action !== "allowAlways" && action !== "allowOnce" && action !== "deny",
958
+ ) ||
959
+ typeof value.fingerprint !== "string"
960
+ )
961
+ return undefined;
962
+ return {
963
+ kind: "ACTION_PERMISSION",
964
+ targetHost: value.targetHost,
965
+ operationId: value.operationId,
966
+ taskId: value.taskId,
967
+ actions: [...actions],
968
+ fingerprint: value.fingerprint,
969
+ };
970
+ }
971
+
972
+ function parseSnapshotObservation(
973
+ value: unknown,
974
+ tab: ChromeTab,
975
+ ): ContentObservation | null {
976
+ if (!isRecord(value) || tab.id === undefined || tab.windowId === undefined)
977
+ return null;
978
+ const pageState = value.pageState;
979
+ const activityKind = value.activityKind;
980
+ if (
981
+ typeof value.url !== "string" ||
982
+ typeof value.contentInstanceId !== "string" ||
983
+ (pageState !== "IDLE" &&
984
+ pageState !== "BUSY" &&
985
+ pageState !== "BLOCKED" &&
986
+ pageState !== "UNKNOWN") ||
987
+ (activityKind !== null &&
988
+ activityKind !== "GENERATING" &&
989
+ activityKind !== "ACTION_PERMISSION" &&
990
+ activityKind !== "ACTION_RUNNING" &&
991
+ activityKind !== "WAITING_HUMAN" &&
992
+ activityKind !== "WAITING_PEER" &&
993
+ activityKind !== "RECOVERING") ||
994
+ typeof value.observedAt !== "string"
995
+ )
996
+ return null;
997
+ const blockerFacts = parsePermissionFacts(value.blockerFacts);
998
+ if (activityKind === "ACTION_PERMISSION" && blockerFacts === undefined)
999
+ return null;
1000
+ return {
1001
+ tabId: tab.id,
1002
+ windowId: tab.windowId,
1003
+ url: value.url,
1004
+ contentInstanceId: value.contentInstanceId,
1005
+ pageState,
1006
+ activityKind,
1007
+ ...(blockerFacts ? { blockerFacts } : {}),
1008
+ observedAt: value.observedAt,
1009
+ };
1010
+ }
1011
+
1012
+ async function waitForPermissionReleased(
1013
+ tabId: number,
1014
+ fingerprint: string,
1015
+ ): Promise<boolean> {
1016
+ for (let attempt = 0; attempt < 40; attempt += 1) {
1017
+ const current = sessions.get(tabId);
1018
+ if (current && current.blockerFacts?.fingerprint !== fingerprint)
1019
+ return true;
1020
+ try {
1021
+ const value = await contentCommand(tabId, { operation: "observe" });
1022
+ if (rawPermissionFingerprint(value) !== fingerprint) return true;
1023
+ } catch {
1024
+ // Navigation/content replacement is re-observed on the next bounded pass.
1025
+ }
1026
+ await sleep(250);
1027
+ }
1028
+ return false;
1029
+ }
1030
+
1031
+ async function handleActionPermission(
1032
+ observed: ContentObservation,
1033
+ ): Promise<void> {
1034
+ const facts = observed.blockerFacts;
1035
+ const key = permissionAttemptKey(observed);
1036
+ if (
1037
+ observed.pageState !== "BLOCKED" ||
1038
+ observed.activityKind !== "ACTION_PERMISSION" ||
1039
+ !facts ||
1040
+ !key
1041
+ )
1042
+ return;
1043
+ if (permissionHandling.get(observed.tabId) === key) return;
1044
+ const existing = carrierAttentions.current(observed.tabId);
1045
+ if (
1046
+ existing?.contentInstanceId === observed.contentInstanceId &&
1047
+ existing.permissionFingerprint === facts.fingerprint
1048
+ )
1049
+ return;
1050
+ permissionHandling.set(observed.tabId, key);
1051
+ try {
1052
+ const identity = carrierIdentity(observed.url);
1053
+ const humanDenied = () =>
1054
+ carrierContinuationControl.hasMatchingPermissionDenial({
1055
+ tabId: observed.tabId,
1056
+ contentInstanceId: observed.contentInstanceId,
1057
+ url: observed.url,
1058
+ permissionFingerprint: facts.fingerprint,
1059
+ taskId: facts.taskId,
1060
+ roleRef: identity?.roleRef ?? null,
1061
+ workerRef: identity?.workerRef ?? null,
1062
+ });
1063
+ if (humanDenied()) {
1064
+ setCarrierAttention(observed, "HUMAN_DENIED");
1065
+ return;
1066
+ }
1067
+ if (!(await restoreTransientPermissionAttempts())) {
1068
+ setCarrierAttention(observed, "PERMISSION_ATTEMPT_STATE_UNAVAILABLE");
1069
+ return;
1070
+ }
1071
+ if (!identity || !facts.taskId) {
1072
+ setCarrierAttention(observed, "PERMISSION_CONTEXT_INCOMPLETE");
1073
+ return;
1074
+ }
1075
+ const result = await resolveRoutineCarrierPermission({
1076
+ facts,
1077
+ autoAlreadyAttempted: permissionAutoAttempts.has(observed.tabId, key),
1078
+ humanDenied,
1079
+ port: {
1080
+ async classify(): Promise<CarrierPermissionDecision> {
1081
+ const value = await invokeObserverApplication(
1082
+ "browser.permission.classify",
1083
+ {
1084
+ taskId: facts.taskId as string,
1085
+ roleRef: identity.roleRef,
1086
+ ...(identity.workerRef ? { workerRef: identity.workerRef } : {}),
1087
+ conversationLocator: observed.url,
1088
+ targetHost: facts.targetHost,
1089
+ operationId: facts.operationId,
1090
+ },
1091
+ );
1092
+ if (
1093
+ !isRecord(value) ||
1094
+ (value.decision !== "AUTO_ALLOW" &&
1095
+ value.decision !== "DEFER" &&
1096
+ value.decision !== "HUMAN_REQUIRED") ||
1097
+ typeof value.reason !== "string"
1098
+ )
1099
+ return {
1100
+ decision: "HUMAN_REQUIRED",
1101
+ reason: "PERMISSION_CLASSIFICATION_INVALID",
1102
+ };
1103
+ return {
1104
+ decision: value.decision,
1105
+ reason: value.reason,
1106
+ };
1107
+ },
1108
+ revalidate() {
1109
+ const current = observationFor(observed.tabId);
1110
+ return (
1111
+ current.contentInstanceId === observed.contentInstanceId &&
1112
+ current.url === observed.url &&
1113
+ current.blockerFacts?.fingerprint === facts.fingerprint
1114
+ );
1115
+ },
1116
+ waitBeforeReclassify: () => sleep(250),
1117
+ async act(action) {
1118
+ permissionAutoAttempts.begin(observed.tabId, key);
1119
+ // Persist uncertainty before the click. If the MV3 worker restarts
1120
+ // before readback, the same effect is never replayed blindly.
1121
+ await persistSnapshot();
1122
+ await contentCommand(observed.tabId, {
1123
+ operation: "permissionAction",
1124
+ permissionFingerprint: facts.fingerprint,
1125
+ permissionAction: action,
1126
+ });
1127
+ },
1128
+ released: () =>
1129
+ waitForPermissionReleased(observed.tabId, facts.fingerprint),
1130
+ },
1131
+ });
1132
+ if (result.status === "RELEASED") {
1133
+ permissionAutoAttempts.release(observed.tabId, key);
1134
+ carrierAttentions.removeTab(observed.tabId);
1135
+ void publishCarrierAttentions();
1136
+ await persistSnapshot();
1137
+ return;
1138
+ }
1139
+ if (result.status === "HUMAN_REQUIRED")
1140
+ setCarrierAttention(observed, result.reason);
1141
+ } catch {
1142
+ setCarrierAttention(observed, "AUTO_ALLOW_FAILED");
1143
+ } finally {
1144
+ if (permissionHandling.get(observed.tabId) === key)
1145
+ permissionHandling.delete(observed.tabId);
1146
+ }
1147
+ }
1148
+
1149
+ type CarrierBlockerHandler = (observed: ContentObservation) => Promise<void>;
1150
+
1151
+ // The application lines consume normalized reality only. New blocker cases
1152
+ // register a strategy here instead of branching Workflow/Collaboration/Observer.
1153
+ const carrierBlockerStrategies: ReadonlyMap<string, CarrierBlockerHandler> =
1154
+ new Map([["ACTION_PERMISSION", handleActionPermission]]);
1155
+
1156
+ function handleCarrierBlocker(observed: ContentObservation): void {
1157
+ if (observed.pageState !== "BLOCKED" || observed.activityKind === null)
1158
+ return;
1159
+ const strategy = carrierBlockerStrategies.get(observed.activityKind);
1160
+ if (strategy) void strategy(observed);
1161
+ }
1162
+
1163
+ function processContentObservation(
1164
+ observed: ContentObservation,
1165
+ triggerRecovery: boolean,
1166
+ ): void {
1167
+ const previous = sessions.get(observed.tabId);
1168
+ sessions.set(observed.tabId, observed);
1169
+ permissionAutoAttempts.observe(
1170
+ observed.tabId,
1171
+ permissionAttemptKey(observed),
1172
+ );
1173
+ const attention = carrierAttentions.current(observed.tabId);
1174
+ if (
1175
+ attention &&
1176
+ (observed.contentInstanceId !== attention.contentInstanceId ||
1177
+ observed.url !== attention.url ||
1178
+ observed.blockerFacts?.fingerprint !== attention.permissionFingerprint)
1179
+ ) {
1180
+ carrierAttentions.removeTab(observed.tabId);
1181
+ void publishCarrierAttentions();
1182
+ }
1183
+ handleCarrierBlocker(observed);
1184
+ const shouldRecover =
1185
+ triggerRecovery && shouldTriggerObserverRecovery(previous, observed);
1186
+ const suppressed =
1187
+ shouldRecover &&
1188
+ carrierContinuationControl.suppressRecovery(previous, observed);
1189
+ void persistSnapshot();
1190
+ if (shouldRecover && !suppressed) void runObserverRecovery();
1191
+ }
1192
+
1193
+ async function rebuildCarrierAttentionsFromTabs(): Promise<
1194
+ CarrierContinuationDenial[]
1195
+ > {
1196
+ const consumedDenials: CarrierContinuationDenial[] = [];
1197
+ const tabs = await chrome.tabs.query({ url: "https://chatgpt.com/g/*" });
1198
+ for (const tab of tabs) {
1199
+ if (tab.id === undefined || tab.windowId === undefined) continue;
1200
+ try {
1201
+ const response = await chrome.tabs.sendMessage(tab.id, {
1202
+ type: "PROFLOW_PAGE_SNAPSHOT_REQUEST",
1203
+ });
1204
+ if (!isRecord(response) || response.ok !== true) continue;
1205
+ const observed = parseSnapshotObservation(response.value, tab);
1206
+ if (observed) {
1207
+ processContentObservation(observed, false);
1208
+ const consumed = carrierContinuationControl.consumeRecovery(
1209
+ undefined,
1210
+ observed,
1211
+ );
1212
+ if (consumed) consumedDenials.push(consumed);
1213
+ }
1214
+ } catch {
1215
+ // A tab without a live content receiver is not reconstructed from guesses.
1216
+ }
1217
+ }
1218
+ await persistSnapshot();
1219
+ await publishCarrierAttentions().catch(() => undefined);
1220
+ return [
1221
+ ...consumedDenials,
1222
+ ...carrierContinuationControl
1223
+ .snapshot()
1224
+ .filter(
1225
+ (denial) =>
1226
+ !consumedDenials.some(
1227
+ (consumed) => consumed.attentionRef === denial.attentionRef,
1228
+ ),
1229
+ ),
1230
+ ];
1231
+ }
1232
+
1233
+ async function decideCarrierAttention(
1234
+ attentionRef: string,
1235
+ action: "allowOnce" | "deny",
1236
+ ): Promise<{
1237
+ attentionRef: string;
1238
+ action: "allowOnce" | "deny";
1239
+ status: "APPLIED";
1240
+ }> {
1241
+ const attention = carrierAttentions.find(attentionRef);
1242
+ if (!attention?.actions.includes(action))
1243
+ throw new Error("CARRIER_ATTENTION_ACTION_DENIED");
1244
+ await resolveHumanCarrierPermission({
1245
+ action,
1246
+ revalidate() {
1247
+ const current = observationFor(attention.tabId);
1248
+ return (
1249
+ current.contentInstanceId === attention.contentInstanceId &&
1250
+ current.url === attention.url &&
1251
+ current.blockerFacts?.fingerprint === attention.permissionFingerprint
1252
+ );
1253
+ },
1254
+ async act(semanticAction) {
1255
+ if (semanticAction === "deny") {
1256
+ carrierContinuationControl.beginDenied({
1257
+ attentionRef: attention.attentionRef,
1258
+ tabId: attention.tabId,
1259
+ taskId: attention.taskId,
1260
+ roleRef: attention.roleRef,
1261
+ workerRef: attention.workerRef,
1262
+ url: attention.url,
1263
+ contentInstanceId: attention.contentInstanceId,
1264
+ permissionFingerprint: attention.permissionFingerprint,
1265
+ });
1266
+ await persistSnapshot();
1267
+ }
1268
+ try {
1269
+ await contentCommand(attention.tabId, {
1270
+ operation: "permissionAction",
1271
+ permissionFingerprint: attention.permissionFingerprint,
1272
+ permissionAction: semanticAction,
1273
+ });
1274
+ } catch (error) {
1275
+ if (semanticAction === "deny") {
1276
+ carrierContinuationControl.cancelDenied(attention.attentionRef);
1277
+ await persistSnapshot();
1278
+ }
1279
+ throw error;
1280
+ }
1281
+ },
1282
+ released: () =>
1283
+ waitForPermissionReleased(
1284
+ attention.tabId,
1285
+ attention.permissionFingerprint,
1286
+ ),
1287
+ });
1288
+ permissionAutoAttempts.release(
1289
+ attention.tabId,
1290
+ `${attention.url}:${attention.permissionFingerprint}`,
1291
+ );
1292
+ carrierAttentions.delete(attention.attentionRef);
1293
+ void publishCarrierAttentions();
1294
+ await persistSnapshot();
1295
+ return { attentionRef, action, status: "APPLIED" };
1296
+ }
1297
+
741
1298
  async function waitForSubmittedMessage(
742
1299
  tabId: number,
743
1300
  fingerprint: string,
@@ -771,6 +1328,14 @@ function text(value: unknown, name: string): string {
771
1328
  }
772
1329
 
773
1330
  async function executeCommand(command: BridgeCommand): Promise<unknown> {
1331
+ if (command.type === "CARRIER_ATTENTION_ACTION") {
1332
+ if (command.action !== "allowOnce" && command.action !== "deny")
1333
+ throw new Error("ATTENTION_ACTION_INVALID");
1334
+ return decideCarrierAttention(
1335
+ text(command.attentionRef, "ATTENTION_REF"),
1336
+ command.action,
1337
+ );
1338
+ }
774
1339
  if (command.type === "LIST_TABS") {
775
1340
  const tabs = await chrome.tabs.query({ url: "https://chatgpt.com/g/*" });
776
1341
  return tabs
@@ -1008,6 +1573,7 @@ async function runBridgeLoop() {
1008
1573
  }),
1009
1574
  });
1010
1575
  if (!hello.ok) throw new Error("BRIDGE_HELLO_REJECTED");
1576
+ await publishCarrierAttentions();
1011
1577
  // Hello only establishes a session. The first inner-loop action must be
1012
1578
  // a real command poll so Runtime readiness cannot be granted by hello alone.
1013
1579
  let lastHeartbeatAt = Date.now();
@@ -1236,16 +1802,12 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1236
1802
  sender.tab?.id !== undefined &&
1237
1803
  sender.tab.windowId !== undefined
1238
1804
  ) {
1239
- const previous = sessions.get(sender.tab.id);
1240
1805
  const observed: ContentObservation = {
1241
1806
  ...message.observation,
1242
1807
  tabId: sender.tab.id,
1243
1808
  windowId: sender.tab.windowId,
1244
1809
  };
1245
- sessions.set(sender.tab.id, observed);
1246
- void persistSnapshot();
1247
- if (shouldTriggerObserverRecovery(previous, observed))
1248
- void runObserverRecovery();
1810
+ processContentObservation(observed, true);
1249
1811
  sendResponse({ accepted: true, extensionInstanceId });
1250
1812
  return;
1251
1813
  }
@@ -1259,6 +1821,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1259
1821
  extensionInstanceId,
1260
1822
  observedAt: new Date().toISOString(),
1261
1823
  sessions: [...sessions.values()],
1824
+ carrierAttentions: carrierAttentionViews(),
1262
1825
  taskApplicationConfigured: application !== null,
1263
1826
  approvalApplicationConfigured: approval !== null,
1264
1827
  systemObserver: observerState
@@ -1276,6 +1839,33 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1276
1839
  );
1277
1840
  return true;
1278
1841
  }
1842
+ if (message.type === "PROFLOW_CARRIER_ATTENTION_ACTION") {
1843
+ if (!message.input) {
1844
+ sendResponse({ ok: false, error: "CARRIER_ATTENTION_MESSAGE_INVALID" });
1845
+ return;
1846
+ }
1847
+ const attentionRef = message.input.attentionRef;
1848
+ const action = message.input.action;
1849
+ if (
1850
+ typeof attentionRef !== "string" ||
1851
+ (action !== "allowOnce" && action !== "deny")
1852
+ ) {
1853
+ sendResponse({ ok: false, error: "CARRIER_ATTENTION_MESSAGE_INVALID" });
1854
+ return;
1855
+ }
1856
+ void decideCarrierAttention(attentionRef, action).then(
1857
+ (value) => sendResponse({ ok: true, value }),
1858
+ (error: unknown) =>
1859
+ sendResponse({
1860
+ ok: false,
1861
+ error:
1862
+ error instanceof Error
1863
+ ? error.message
1864
+ : "CARRIER_ATTENTION_ACTION_FAILED",
1865
+ }),
1866
+ );
1867
+ return true;
1868
+ }
1279
1869
  if (message.type === "PROFLOW_APPROVAL_APPLICATION") {
1280
1870
  if (typeof message.operation !== "string" || !message.input) {
1281
1871
  sendResponse({
@@ -1352,9 +1942,15 @@ async function openTaskPage(): Promise<void> {
1352
1942
  body.url.startsWith(`${bridge.endpoint}/tasks/bootstrap/`)
1353
1943
  ) {
1354
1944
  const webUrl = `${bridge.endpoint}/tasks`;
1355
- const [existing] = await chrome.tabs.query({ url: webUrl, currentWindow: true });
1945
+ const [existing] = await chrome.tabs.query({
1946
+ url: webUrl,
1947
+ currentWindow: true,
1948
+ });
1356
1949
  if (existing?.id !== undefined) {
1357
- await chrome.tabs.update(existing.id, { url: body.url, active: true });
1950
+ await chrome.tabs.update(existing.id, {
1951
+ url: body.url,
1952
+ active: true,
1953
+ });
1358
1954
  } else {
1359
1955
  await chrome.tabs.create({ url: body.url, active: true });
1360
1956
  }
@@ -1377,10 +1973,13 @@ async function openTaskPage(): Promise<void> {
1377
1973
  }
1378
1974
 
1379
1975
  async function startBackgroundRuntime(): Promise<void> {
1976
+ await restoreTransientPermissionAttempts();
1380
1977
  await bootstrapManagedRuntimeConfig();
1381
1978
  await persistSnapshot();
1382
1979
  void runBridgeLoop();
1383
1980
  void runProvisioningBridgeLoop();
1981
+ const suppressedContinuations = await rebuildCarrierAttentionsFromTabs();
1982
+ suppressNextObserverRecovery(suppressedContinuations);
1384
1983
  void runObserverRecovery();
1385
1984
  }
1386
1985
 
@@ -1388,17 +1987,23 @@ chrome.action.onClicked.addListener(() => {
1388
1987
  void openTaskPage();
1389
1988
  });
1390
1989
  chrome.runtime.onInstalled.addListener(() => {
1391
- void bootstrapManagedRuntimeConfig().then(async () => {
1990
+ void restoreTransientPermissionAttempts().then(async () => {
1991
+ await bootstrapManagedRuntimeConfig();
1392
1992
  await persistSnapshot();
1393
1993
  void runBridgeLoop();
1994
+ const suppressedContinuations = await rebuildCarrierAttentionsFromTabs();
1995
+ suppressNextObserverRecovery(suppressedContinuations);
1394
1996
  void runObserverRecovery();
1395
1997
  });
1396
1998
  });
1397
1999
  chrome.runtime.onStartup.addListener(() => {
1398
2000
  sessions.clear();
1399
- void bootstrapManagedRuntimeConfig().then(async () => {
2001
+ void restoreTransientPermissionAttempts().then(async () => {
2002
+ await bootstrapManagedRuntimeConfig();
1400
2003
  await persistSnapshot();
1401
2004
  void runBridgeLoop();
2005
+ const suppressedContinuations = await rebuildCarrierAttentionsFromTabs();
2006
+ suppressNextObserverRecovery(suppressedContinuations);
1402
2007
  void runObserverRecovery();
1403
2008
  });
1404
2009
  });