@moretea-labs/forge 1.8.1 → 1.8.2
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/AGENTS.md +5 -2
- package/CHANGELOG.md +20 -0
- package/README.en.md +24 -3
- package/README.md +10 -6
- package/README.zh-CN.md +5 -3
- package/THIRD_PARTY_NOTICES.md +1 -2
- package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
- package/adapters/chatgpt/provider-delivery.ts +1 -1
- package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
- package/adapters/computer/chatgpt-extension-target.ts +295 -50
- package/adapters/computer/chrome-extension/background.js +227 -71
- package/adapters/computer/chrome-extension/content.js +181 -157
- package/adapters/computer/chrome-extension/manifest.json +3 -1
- package/adapters/computer/desktop-operator-provider.ts +18 -6
- package/adapters/computer/interaction-target-authority.ts +39 -9
- package/adapters/mcp/access-mode.ts +18 -39
- package/adapters/mcp/auth.ts +2 -6
- package/adapters/mcp/instructions.ts +2 -2
- package/adapters/mcp/multi-repository.ts +7 -28
- package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
- package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
- package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
- package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
- package/adapters/mcp/runtime-gateway/router.ts +3 -5
- package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
- package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
- package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
- package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
- package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
- package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
- package/adapters/mcp/server.ts +24 -3
- package/adapters/mcp/setup.ts +4 -10
- package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
- package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
- package/adapters/mcp/toolset-names.ts +11 -42
- package/adapters/mcp/toolset.ts +16 -90
- package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
- package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
- package/adapters/mcp/transports/http-observation.ts +18 -6
- package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
- package/adapters/mcp/transports/http.ts +54 -14
- package/adapters/mcp/types.ts +0 -6
- package/assets/hooks/lib/session-state.sh +0 -38
- package/assets/hooks/post-bash.sh +1 -1
- package/assets/hooks/post-tool-observer.sh +0 -5
- package/assets/hooks/prompt-guard.sh +0 -55
- package/assets/partials/04-project-structure.partial.md +1 -1
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +1 -1
- package/assets/partials-agents/08-deep-docs.partial.md +1 -1
- package/assets/reference-configs/agentic-development-flow.md +1 -1
- package/assets/reference-configs/external-tooling.md +0 -76
- package/assets/reference-configs/release-deploy.md +1 -1
- package/assets/templates/helpers/architecture-queue.sh +1 -1
- package/assets/templates/helpers/check-agent-tooling.sh +1 -327
- package/assets/templates/helpers/check-task-workflow.sh +1 -1
- package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
- package/assets/templates/helpers/migrate-project-template.sh +2 -2
- package/assets/workflow-contract.v1.json +0 -2
- package/docs/README.md +5 -2
- package/docs/architecture/CURRENT.md +114 -30
- package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
- package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
- package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
- package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
- package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
- package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
- package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
- package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
- package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
- package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
- package/docs/architecture/domains/public-surface.md +3 -5
- package/docs/architecture/domains/verification.md +5 -5
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
- package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
- package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
- package/docs/forge-plugin-management.md +14 -2
- package/docs/operations/controller-browser-plugin.md +23 -0
- package/docs/operations/features.md +6 -7
- package/docs/operations/features.zh-CN.md +1 -1
- package/docs/operations/github-repository.md +4 -4
- package/docs/operations/github-semantic-release-projection.md +20 -0
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +13 -54
- package/docs/operations/runtime-performance-diagnostics.md +8 -0
- package/docs/operations/standalone-disaster-recovery.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/tutorials/03-first-repository-task.md +2 -0
- package/docs/wiki/Computer-and-Plugins.md +78 -0
- package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
- package/docs/wiki/Home.md +16 -2
- package/docs/wiki/Implementation.md +24 -12
- package/docs/wiki/Installation.md +28 -32
- package/docs/wiki/Integrations.md +4 -0
- package/docs/wiki/Quick-Start.md +35 -22
- package/docs/wiki/Releases-and-Upgrades.md +15 -25
- package/docs/wiki/_Sidebar.md +5 -1
- package/package.json +1 -12
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
- package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
- package/packages/kernel/scheduler/application/eligibility.ts +4 -1
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
- package/packages/kernel/work/application/work-service.ts +1 -0
- package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
- package/packages/kernel/work/domain/state-machine.ts +9 -2
- package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
- package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
- package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
- package/scripts/assemble-template.ts +11 -72
- package/scripts/check-mcp-compatibility.ts +55 -74
- package/scripts/check-package-identity.mjs +0 -5
- package/scripts/check-runtime-architecture.mjs +305 -28
- package/scripts/init-project.sh +0 -46
- package/scripts/install-ios-hid-toolchain.sh +60 -0
- package/scripts/install-standalone-recovery.ts +1 -1
- package/scripts/lib/project-init-lib.sh +3 -3
- package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
- package/scripts/open-source-audit-allowlist.txt +0 -1
- package/scripts/run-governed-gate.ts +2 -0
- package/scripts/smoke-ios-physical-device.ts +38 -10
- package/scripts/smoke-mcp-http-runtime.ts +7 -8
- package/scripts/smoke-mcp-tool-surface.ts +27 -41
- package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
- package/scripts/sync-generated-authority.mjs +0 -1
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/agent-jobs/integration.ts +2 -2
- package/src/cli/chatgpt-browser/file-policy.ts +2 -2
- package/src/cli/commands/doctor.ts +1 -143
- package/src/cli/commands/global-runtime.ts +3 -196
- package/src/cli/commands/init-hook.ts +6 -23
- package/src/cli/commands/init.ts +3 -252
- package/src/cli/commands/mcp.ts +0 -5
- package/src/cli/commands/prompt-guard-decision.ts +0 -2
- package/src/cli/commands/recovery.ts +2 -14
- package/src/cli/commands/repository.ts +17 -1
- package/src/cli/commands/setup-execution.ts +41 -0
- package/src/cli/controller/completion-orchestrator.ts +1 -1
- package/src/cli/controller/composite-operations.ts +1 -1
- package/src/cli/controller/context/instruction-context.ts +1 -1
- package/src/cli/controller/context/known-paths.ts +2 -2
- package/src/cli/controller/context/query-planning.ts +11 -4
- package/src/cli/controller/context/rule-resolution.ts +2 -2
- package/src/cli/controller/context/source-materializer.ts +2 -2
- package/src/cli/controller/context/structural-provider.ts +84 -0
- package/src/cli/controller/context/structural.ts +6 -6
- package/src/cli/controller/context/types.ts +8 -9
- package/src/cli/controller/context-pack.ts +97 -76
- package/src/cli/controller/operational-plan.ts +1 -1
- package/src/cli/controller/systemd-user.ts +4 -10
- package/src/cli/editing/edit-session.ts +17 -5
- package/src/cli/github/github.ts +4 -4
- package/src/cli/github/semantic-projection.ts +174 -0
- package/src/cli/hook/prompt-intents.ts +0 -9
- package/src/cli/index.ts +1 -43
- package/src/cli/local-bridge/connector-freshness.ts +1 -12
- package/src/cli/local-bridge/console-automations.ts +1 -1
- package/src/cli/local-bridge/console-view-models.ts +1 -1
- package/src/cli/local-bridge/facade-api.ts +0 -2
- package/src/cli/repositories/command-classifier.ts +0 -9
- package/src/cli/repositories/command-scope.ts +2 -2
- package/src/cli/repositories/locks.ts +20 -0
- package/src/cli/repositories/registry.ts +2 -1
- package/src/cli/repositories/safe-patch.ts +2 -12
- package/src/cli/repositories/structured-git.ts +39 -3
- package/src/cli/repository/inspector.ts +185 -343
- package/src/effects/process-runner.ts +17 -1
- package/src/runtime/context/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +7 -0
- package/src/runtime/context/semantic-navigation.ts +1 -1
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
- package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
- package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
- package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
- package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
- package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
- package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
- package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
- package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
- package/src/runtime/control-plane/governance/external-effects.ts +1 -1
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
- package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
- package/src/runtime/evidence/sensitive-output.ts +22 -2
- package/src/runtime/execution/jobs/child-reference.ts +11 -0
- package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
- package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
- package/src/runtime/execution/process-runtime/runtime.ts +20 -16
- package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
- package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
- package/src/runtime/execution/workers/executor.ts +1 -2
- package/src/runtime/maintenance/cleanup.ts +41 -5
- package/src/runtime/plugins/action-application.ts +12 -1
- package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
- package/src/runtime/plugins/browser-adapter.ts +381 -313
- package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
- package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
- package/src/runtime/plugins/browser-registration.ts +2 -0
- package/src/runtime/plugins/browser-runtime.ts +26 -35
- package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
- package/src/runtime/plugins/computer-registration.ts +76 -9
- package/src/runtime/plugins/github-adapter.ts +30 -3
- package/src/runtime/plugins/gmail-adapter.ts +166 -1
- package/src/runtime/plugins/google-shared.ts +10 -5
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
- package/src/runtime/plugins/ios-adapter.ts +45 -40
- package/src/runtime/plugins/ios-agent-device.ts +54 -4
- package/src/runtime/plugins/ios-device-ownership.ts +103 -0
- package/src/runtime/plugins/ios-physical-device.ts +497 -79
- package/src/runtime/plugins/local-system-adapter.ts +1 -99
- package/src/runtime/plugins/store.ts +80 -7
- package/src/runtime/plugins/types.ts +4 -0
- package/src/runtime/projections/git-status-sampler.ts +2 -19
- package/src/runtime/recovery/actions.ts +1 -1
- package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
- package/src/runtime/recovery/probe.ts +2 -2
- package/src/runtime/release/release-coordinator.ts +20 -4
- package/src/runtime/root/controller-progression-composition.ts +67 -5
- package/src/runtime/root/package-connector-service.ts +123 -11
- package/src/runtime/root/package-runtime-release.ts +6 -18
- package/src/runtime/root/package-runtime-service.ts +7 -13
- package/src/runtime/root/release-execution-canary.ts +18 -5
- package/src/runtime/root/release-manifest.ts +0 -34
- package/src/runtime/root/release-materialize.ts +0 -78
- package/src/runtime/root/runtime.ts +1 -2
- package/src/runtime/root/scheduled-controller-composition.ts +2 -1
- package/src/runtime/root/service.ts +90 -11
- package/src/runtime/root/types.ts +0 -6
- package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
- package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
- package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
- package/src/runtime/safe-tooling/ios-development.ts +22 -8
- package/src/runtime/safe-tooling/job-summary.ts +1 -1
- package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
- package/src/runtime/shared/atomic-text-file.ts +21 -0
- package/src/runtime/shared/json-files.ts +8 -3
- package/src/runtime/standalone-recovery/core.ts +189 -174
- package/src/runtime/standalone-recovery/entry.ts +62 -30
- package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
- package/src/runtime/workflow/schedules/engine.ts +36 -14
- package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
- package/supervisor/client.ts +31 -14
- package/supervisor/control-plane.ts +210 -115
- package/supervisor/forge-validators.ts +88 -21
- package/supervisor/native-browser-adapter.ts +393 -28
- package/supervisor/native-messaging/host.ts +21 -2
- package/supervisor/protocol.ts +74 -26
- package/supervisor/server.ts +16 -8
- package/supervisor/store.ts +476 -57
- package/supervisor/types.ts +19 -13
- package/SKILL.md +0 -427
- package/adapters/computer/chatgpt-target-router.ts +0 -103
- package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
- package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
- package/adapters/mcp/toolset-selection.ts +0 -41
- package/agents/openai.yaml +0 -4
- package/assets/skill-hooks.json +0 -41
- package/assets/skill-version.json +0 -189
- package/assets/skills/claude-review/SKILL.md +0 -231
- package/assets/skills/codex-review/SKILL.md +0 -103
- package/assets/templates/helpers/check-skill-version.ts +0 -193
- package/scripts/check-skill-version.ts +0 -5
- package/scripts/ensure-codegraph.sh +0 -16
- package/scripts/run-skill-hook.ts +0 -244
- package/scripts/sync-codex-installed-copies.sh +0 -182
- package/src/cli/commands/tools.ts +0 -167
- package/src/cli/mcp/access-mode.ts +0 -2
- package/src/cli/mcp/access-tools.ts +0 -2
- package/src/cli/mcp/audit.ts +0 -2
- package/src/cli/mcp/auth.ts +0 -2
- package/src/cli/mcp/instructions.ts +0 -2
- package/src/cli/mcp/legacy-context.ts +0 -2
- package/src/cli/mcp/legacy-tool-service.ts +0 -2
- package/src/cli/mcp/multi-repository.ts +0 -2
- package/src/cli/mcp/oauth.ts +0 -2
- package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
- package/src/cli/mcp/paths.ts +0 -2
- package/src/cli/mcp/policy.ts +0 -2
- package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
- package/src/cli/mcp/proxy-env.ts +0 -2
- package/src/cli/mcp/readiness-projection.ts +0 -2
- package/src/cli/mcp/redaction.ts +0 -2
- package/src/cli/mcp/repo.ts +0 -2
- package/src/cli/mcp/repository-tools.ts +0 -2
- package/src/cli/mcp/server.ts +0 -2
- package/src/cli/mcp/setup.ts +0 -2
- package/src/cli/mcp/tool-capability-cache.ts +0 -2
- package/src/cli/mcp/tool-contract.ts +0 -2
- package/src/cli/mcp/tools.ts +0 -2
- package/src/cli/mcp/toolset-names.ts +0 -2
- package/src/cli/mcp/toolset-selection.ts +0 -2
- package/src/cli/mcp/toolset.ts +0 -2
- package/src/cli/mcp/transports/http.ts +0 -2
- package/src/cli/mcp/transports/session-registry.ts +0 -2
- package/src/cli/mcp/transports/stdio.ts +0 -2
- package/src/cli/mcp/types.ts +0 -2
- package/src/cli/mcp/umbrella-tools.ts +0 -2
- package/src/cli/tools/codegraph.ts +0 -695
- package/src/core/adoption/helper-wrapper-plan.ts +0 -159
- package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
- package/src/runtime/context/codegraph-read-provider.ts +0 -611
- package/src/runtime/context/codegraph-sidecar.cjs +0 -248
- package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
- package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
- package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
- package/src/runtime/gateway/mcp/process-tools.ts +0 -2
- package/src/runtime/gateway/mcp/router.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
- package/src/runtime/plugins/browser-provider-registry.ts +0 -2
- package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
|
@@ -6,9 +6,25 @@ const PROVIDER_ID = 'browser.chrome-extension';
|
|
|
6
6
|
const PROVIDER_TICK_MS = 750;
|
|
7
7
|
const DISCOVERY_MIN_INTERVAL_MS = 2_000;
|
|
8
8
|
const RPC_TIMEOUT_MS = 30_000;
|
|
9
|
+
// The broker budgets 12 seconds for a claimed command; a hung content
|
|
10
|
+
// callback must settle earlier without claiming a Send never happened.
|
|
11
|
+
const TAB_MESSAGE_TIMEOUT_MS = 7_000;
|
|
9
12
|
const bootstrapTabs = new Map();
|
|
10
13
|
const randomId = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
11
|
-
|
|
14
|
+
let providerInstancePromise;
|
|
15
|
+
function providerInstanceId() {
|
|
16
|
+
// Profile-local storage survives worker and browser restarts. It identifies
|
|
17
|
+
// this Chrome profile, never a task or an outbound effect.
|
|
18
|
+
// A fresh worker UUID would invalidate every Controller-pinned tab.
|
|
19
|
+
providerInstancePromise ??= (async () => {
|
|
20
|
+
const saved = await chrome.storage.local.get('forgeComputerProviderInstanceId');
|
|
21
|
+
if (typeof saved.forgeComputerProviderInstanceId === 'string' && saved.forgeComputerProviderInstanceId) return saved.forgeComputerProviderInstanceId;
|
|
22
|
+
const instanceId = randomId();
|
|
23
|
+
await chrome.storage.local.set({ forgeComputerProviderInstanceId: instanceId });
|
|
24
|
+
return instanceId;
|
|
25
|
+
})().catch((error) => { providerInstancePromise = undefined; throw error; });
|
|
26
|
+
return providerInstancePromise;
|
|
27
|
+
}
|
|
12
28
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
29
|
function browserProduct() {
|
|
14
30
|
const ua = String(globalThis.navigator?.userAgent ?? '');
|
|
@@ -56,11 +72,24 @@ function nativeRpc(method, params = {}) {
|
|
|
56
72
|
});
|
|
57
73
|
}
|
|
58
74
|
function tabMessage(tabId, message) {
|
|
59
|
-
return new Promise((resolve, reject) =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
let settled = false;
|
|
77
|
+
const finish = (error, response) => {
|
|
78
|
+
if (settled) return;
|
|
79
|
+
settled = true;
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
if (error) reject(error); else resolve(response ?? {});
|
|
82
|
+
};
|
|
83
|
+
const timer = setTimeout(() => finish(new Error('COMPUTER_CHATGPT_EXTENSION_TAB_MESSAGE_TIMEOUT')), TAB_MESSAGE_TIMEOUT_MS);
|
|
84
|
+
try {
|
|
85
|
+
chrome.tabs.sendMessage(tabId, message, (response) => {
|
|
86
|
+
// Read lastError inside the callback, even if the request timed out,
|
|
87
|
+
// so Chrome does not report an unhandled transport error.
|
|
88
|
+
const error = chrome.runtime.lastError;
|
|
89
|
+
finish(error ? new Error(error.message) : undefined, response);
|
|
90
|
+
});
|
|
91
|
+
} catch (error) { finish(error); }
|
|
92
|
+
});
|
|
64
93
|
}
|
|
65
94
|
function tabGet(tabId) {
|
|
66
95
|
return new Promise((resolve, reject) => chrome.tabs.get(tabId, (tab) => {
|
|
@@ -76,10 +105,27 @@ function tabCreate(url) {
|
|
|
76
105
|
resolve(tab);
|
|
77
106
|
}));
|
|
78
107
|
}
|
|
79
|
-
function tabRemove(tabId) { return new Promise((resolve) => chrome.tabs.remove(tabId, () =>
|
|
108
|
+
function tabRemove(tabId) { return new Promise((resolve, reject) => chrome.tabs.remove(tabId, () => {
|
|
109
|
+
const error = chrome.runtime.lastError;
|
|
110
|
+
if (error) reject(new Error(error.message)); else resolve();
|
|
111
|
+
})); }
|
|
80
112
|
function tabReload(tabId) { return new Promise((resolve) => chrome.tabs.reload(tabId, () => resolve())); }
|
|
81
113
|
async function chatgptTabs() { return await chrome.tabs.query({ url: 'https://chatgpt.com/*' }); }
|
|
82
|
-
|
|
114
|
+
// Controller binding owns the durable source identity. This browser-session
|
|
115
|
+
// attestation proves that the exact physical tab was created by this extension;
|
|
116
|
+
// browser restarts invalidate it rather than permitting tab-id reuse to close
|
|
117
|
+
// an unrelated restored/user tab.
|
|
118
|
+
function ownedTabKey(tabId) { return `forgeComputerOwnedTabV1:${tabId}`; }
|
|
119
|
+
async function attestedOwnerToken(tab, pinnedBinding) {
|
|
120
|
+
if (!pinnedBinding?.ownerToken || String(pinnedBinding.tabId) !== String(tab.id)
|
|
121
|
+
|| String(pinnedBinding.windowId) !== String(tab.windowId)) return;
|
|
122
|
+
try {
|
|
123
|
+
const key = ownedTabKey(tab.id);
|
|
124
|
+
const stored = await chrome.storage.session.get(key);
|
|
125
|
+
if (stored[key] === pinnedBinding.ownerToken) return pinnedBinding.ownerToken;
|
|
126
|
+
} catch { /* missing browser-session provenance is never closure authority */ }
|
|
127
|
+
}
|
|
128
|
+
function binding(tab, instanceId, ownerToken) {
|
|
83
129
|
return {
|
|
84
130
|
providerId: PROVIDER_ID,
|
|
85
131
|
providerSessionId: instanceId,
|
|
@@ -87,27 +133,72 @@ function binding(tab, instanceId) {
|
|
|
87
133
|
browserProduct: browserProduct(),
|
|
88
134
|
windowId: String(tab.windowId),
|
|
89
135
|
tabId: String(tab.id),
|
|
136
|
+
...(ownerToken ? { ownerToken } : {}),
|
|
90
137
|
};
|
|
91
138
|
}
|
|
92
139
|
async function snapshot(tabId, options = {}) {
|
|
93
140
|
return await tabMessage(tabId, { type: 'forge-computer-chatgpt-snapshot', options });
|
|
94
141
|
}
|
|
95
|
-
async function
|
|
142
|
+
async function restoreExactContentReceiver(tabId, identity) {
|
|
143
|
+
// The physical tab is already resolved by the one Computer target authority.
|
|
144
|
+
// A missing MV3 message receiver is not permission to reload/navigate that tab,
|
|
145
|
+
// replace its draft, or create another tab. Inject only read-only listeners on
|
|
146
|
+
// this SAME proven conversation/Project, then verify its identity again.
|
|
147
|
+
const matches = (tab) => identity?.namespace === 'chatgpt.conversation'
|
|
148
|
+
? core.parseConversation(tab.url ?? '')?.conversationId === identity.conversationId
|
|
149
|
+
: identity?.namespace === 'chatgpt.bootstrap'
|
|
150
|
+
&& core.projectId(tab.url ?? '') === core.projectId(identity.projectUrl)
|
|
151
|
+
&& !core.parseConversation(tab.url ?? '');
|
|
152
|
+
const before = await tabGet(tabId);
|
|
153
|
+
if (before.discarded || !matches(before)) throw new Error('COMPUTER_CHATGPT_EXTENSION_RECEIVER_TARGET_CHANGED');
|
|
154
|
+
await new Promise((resolve, reject) => chrome.scripting.executeScript({
|
|
155
|
+
target: { tabId }, files: ['core.js', 'content.js'],
|
|
156
|
+
}, () => {
|
|
157
|
+
const error = chrome.runtime.lastError;
|
|
158
|
+
if (error) reject(new Error(error.message)); else resolve();
|
|
159
|
+
}));
|
|
160
|
+
const after = await tabGet(tabId);
|
|
161
|
+
if (!matches(after)) throw new Error('COMPUTER_CHATGPT_EXTENSION_RECEIVER_TARGET_CHANGED');
|
|
162
|
+
}
|
|
163
|
+
async function waitForContent(tabId, requireBootstrapUi = false, recoverReceiver) {
|
|
96
164
|
let lastError;
|
|
97
|
-
|
|
165
|
+
let recoveryAttempted = false;
|
|
166
|
+
// Bootstrap is a new provider page: seeing its URL proves location, not
|
|
167
|
+
// that the React composer is hydrated. The Supervisor uses the user's
|
|
168
|
+
// exact target page readiness. Bound this wait below the
|
|
169
|
+
// broker's 12-second timeout; missing composer never authorizes Send.
|
|
170
|
+
const attempts = requireBootstrapUi ? 20 : 40;
|
|
171
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
98
172
|
try {
|
|
99
173
|
const tab = await tabGet(tabId);
|
|
100
174
|
if (tab?.discarded) await tabReload(tabId);
|
|
101
175
|
const observation = await snapshot(tabId);
|
|
102
|
-
if (typeof observation?.url === 'string' && observation.url
|
|
103
|
-
|
|
176
|
+
if (typeof observation?.url === 'string' && observation.url
|
|
177
|
+
&& (!requireBootstrapUi || typeof observation.composerText === 'string')) return observation;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
lastError = error;
|
|
180
|
+
if (!recoveryAttempted && recoverReceiver
|
|
181
|
+
&& /Receiving end does not exist|Could not establish connection/i.test(String(error?.message ?? error))) {
|
|
182
|
+
recoveryAttempted = true;
|
|
183
|
+
// Reattach the receiver only after exact tab/Project revalidation.
|
|
184
|
+
// Never retry the original Send: this is an observation-only preflight.
|
|
185
|
+
await recoverReceiver();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
104
188
|
await sleep(250);
|
|
105
189
|
}
|
|
106
190
|
throw lastError ?? new Error('COMPUTER_CHATGPT_TAB_NOT_READY');
|
|
107
191
|
}
|
|
108
192
|
function matchingTabs(tabs, identity, pinnedBinding) {
|
|
109
193
|
if (identity?.namespace === 'chatgpt.conversation') {
|
|
110
|
-
|
|
194
|
+
const exact = tabs.filter((tab) => core.parseConversation(tab.url ?? '')?.conversationId === identity.conversationId);
|
|
195
|
+
if (pinnedBinding?.providerId === PROVIDER_ID && pinnedBinding.tabId && pinnedBinding.windowId) {
|
|
196
|
+
const pinned = exact.filter((tab) => String(tab.id) === String(pinnedBinding.tabId));
|
|
197
|
+
if (pinned.length === 1) return pinned;
|
|
198
|
+
}
|
|
199
|
+
// Duplicate exact URLs without a verifiable pin must never select an
|
|
200
|
+
// arbitrary foreground tab (or silently create another one).
|
|
201
|
+
return exact;
|
|
111
202
|
}
|
|
112
203
|
if (identity?.namespace === 'chatgpt.bootstrap') {
|
|
113
204
|
// The Project is a scope, not a tab identity. Multiple tasks share the
|
|
@@ -117,8 +208,7 @@ function matchingTabs(tabs, identity, pinnedBinding) {
|
|
|
117
208
|
const belongsToProject = (tab) => String(tab.url ?? '') === identity.projectUrl
|
|
118
209
|
|| Boolean(projectId && core.projectId(tab.url ?? '') === projectId);
|
|
119
210
|
if (pinnedBinding?.providerId === PROVIDER_ID && pinnedBinding.tabId && pinnedBinding.windowId) {
|
|
120
|
-
const exact = tabs.filter((tab) => String(tab.id) === String(pinnedBinding.tabId)
|
|
121
|
-
&& String(tab.windowId) === String(pinnedBinding.windowId));
|
|
211
|
+
const exact = tabs.filter((tab) => String(tab.id) === String(pinnedBinding.tabId));
|
|
122
212
|
if (exact.length && !belongsToProject(exact[0])) throw new Error('COMPUTER_CHATGPT_BOOTSTRAP_PINNED_TAB_IDENTITY_CHANGED');
|
|
123
213
|
return exact;
|
|
124
214
|
}
|
|
@@ -129,12 +219,28 @@ function matchingTabs(tabs, identity, pinnedBinding) {
|
|
|
129
219
|
}
|
|
130
220
|
async function resolveTarget(identity, createIfMissing, pinnedBinding) {
|
|
131
221
|
const tabs = await chatgptTabs();
|
|
222
|
+
if (identity?.namespace === 'chatgpt.conversation' && pinnedBinding?.providerId === PROVIDER_ID && pinnedBinding.tabId) {
|
|
223
|
+
// Window moves preserve Chrome's profile-local tab id. A vanished id may
|
|
224
|
+
// be recovered by exact conversation, but an existing tab at another URL
|
|
225
|
+
// is identity drift and must never be silently replaced.
|
|
226
|
+
let pinned;
|
|
227
|
+
try { pinned = await tabGet(Number(pinnedBinding.tabId)); }
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (!/No tab with id/i.test(String(error?.message ?? error))) throw error;
|
|
230
|
+
}
|
|
231
|
+
if (pinned) {
|
|
232
|
+
if (core.parseConversation(pinned.url ?? '')?.conversationId !== identity.conversationId) {
|
|
233
|
+
throw new Error('COMPUTER_CHATGPT_EXTENSION_PINNED_TAB_IDENTITY_CHANGED');
|
|
234
|
+
}
|
|
235
|
+
return { tab: pinned, created: false, ownerToken: await attestedOwnerToken(pinned, pinnedBinding) };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
132
238
|
const matches = matchingTabs(tabs, identity, pinnedBinding);
|
|
133
239
|
if (matches.length > 1) throw new Error('COMPUTER_CHATGPT_EXTENSION_EXACT_TARGET_AMBIGUOUS');
|
|
134
240
|
if (matches.length === 1) {
|
|
135
241
|
const tab = matches[0];
|
|
136
242
|
if (identity.namespace === 'chatgpt.bootstrap') bootstrapTabs.set(identity.bootstrapKey, tab.id);
|
|
137
|
-
return { tab, created: false };
|
|
243
|
+
return { tab, created: false, ownerToken: await attestedOwnerToken(tab, pinnedBinding) };
|
|
138
244
|
}
|
|
139
245
|
if (identity?.namespace === 'chatgpt.bootstrap' && pinnedBinding?.providerId === PROVIDER_ID
|
|
140
246
|
&& pinnedBinding.tabId && pinnedBinding.windowId) {
|
|
@@ -144,9 +250,14 @@ async function resolveTarget(identity, createIfMissing, pinnedBinding) {
|
|
|
144
250
|
const url = identity.namespace === 'chatgpt.conversation' ? identity.canonicalUrl : identity.projectUrl;
|
|
145
251
|
const tab = await tabCreate(url);
|
|
146
252
|
if (identity.namespace === 'chatgpt.bootstrap') bootstrapTabs.set(identity.bootstrapKey, tab.id);
|
|
147
|
-
|
|
253
|
+
const token = randomId();
|
|
254
|
+
let ownerToken;
|
|
255
|
+
try {
|
|
256
|
+
await chrome.storage.session.set({ [ownedTabKey(tab.id)]: token });
|
|
257
|
+
ownerToken = token;
|
|
258
|
+
} catch { /* still preserve the created tab's exact binding, but never auto-close without provenance */ }
|
|
259
|
+
return { tab, created: true, ownerToken };
|
|
148
260
|
}
|
|
149
|
-
function normalize(value) { return core.normalizeText(value); }
|
|
150
261
|
function dispatchTransportNotReached(error) {
|
|
151
262
|
const text = String(error?.message ?? error);
|
|
152
263
|
return /Receiving end does not exist|Could not establish connection|No tab with id/i.test(text);
|
|
@@ -155,15 +266,25 @@ async function executeEnsure(command, instanceId) {
|
|
|
155
266
|
let target;
|
|
156
267
|
try {
|
|
157
268
|
target = await resolveTarget(command.identity, true, command.providerBinding);
|
|
158
|
-
|
|
269
|
+
// Created and already-bound tabs share one exact-identity receiver repair.
|
|
270
|
+
// The editor must hydrate before 'ensured'; on failure the existing typed
|
|
271
|
+
// failed receipt still carries this tab's binding without permitting Send.
|
|
272
|
+
const observation = await waitForContent(target.tab.id, command.identity.namespace === 'chatgpt.bootstrap',
|
|
273
|
+
() => restoreExactContentReceiver(target.tab.id, command.identity));
|
|
159
274
|
const refreshed = await tabGet(target.tab.id);
|
|
160
|
-
return { kind: 'ensured', providerBinding: binding(refreshed, instanceId), observation };
|
|
275
|
+
return { kind: 'ensured', providerBinding: binding(refreshed, instanceId, target.ownerToken), observation };
|
|
161
276
|
} catch (error) {
|
|
162
277
|
return {
|
|
163
278
|
kind: 'failed',
|
|
164
279
|
code: String(error?.message ?? error).split(':')[0] || 'COMPUTER_CHATGPT_EXTENSION_ENSURE_FAILED',
|
|
165
280
|
retryable: true,
|
|
166
|
-
|
|
281
|
+
// A hydration failure must retain the created tab's physical attachment.
|
|
282
|
+
// This receipt grants neither a ready page nor permission to Send.
|
|
283
|
+
...(target ? { providerBinding: binding(target.tab, instanceId, target.ownerToken) } : {}),
|
|
284
|
+
// Bootstrap tabs can exist before this command even when no new tab was
|
|
285
|
+
// created now. A worker failure cannot prove that creating a different
|
|
286
|
+
// native tab would preserve the one-task/one-tab binding.
|
|
287
|
+
failoverSafe: false,
|
|
167
288
|
};
|
|
168
289
|
}
|
|
169
290
|
}
|
|
@@ -173,76 +294,76 @@ async function executeObserve(command, instanceId) {
|
|
|
173
294
|
if (!target) return { kind: 'failed', code: 'COMPUTER_CHATGPT_EXTENSION_TARGET_MISSING', retryable: true, failoverSafe: true };
|
|
174
295
|
const observation = await snapshot(target.tab.id, command.options ?? {});
|
|
175
296
|
const refreshed = await tabGet(target.tab.id);
|
|
176
|
-
return { kind: 'observation', providerBinding: binding(refreshed, instanceId), observation };
|
|
297
|
+
return { kind: 'observation', providerBinding: binding(refreshed, instanceId, await attestedOwnerToken(refreshed, command.providerBinding)), observation };
|
|
177
298
|
} catch (error) {
|
|
178
299
|
return { kind: 'failed', code: String(error?.message ?? error).split(':')[0] || 'COMPUTER_CHATGPT_EXTENSION_OBSERVE_FAILED', retryable: true, failoverSafe: true };
|
|
179
300
|
}
|
|
180
301
|
}
|
|
181
302
|
async function executePrepare(command) {
|
|
182
|
-
const target = await resolveTarget(command.identity, false).catch(() => undefined);
|
|
303
|
+
const target = await resolveTarget(command.identity, false, command.providerBinding).catch(() => undefined);
|
|
183
304
|
if (!target) return { kind: 'prepared', ready: false, reasonCode: 'COMPUTER_CHATGPT_EXTENSION_TARGET_MISSING' };
|
|
184
|
-
|
|
305
|
+
const expectedProjectId = command.identity?.namespace === 'chatgpt.bootstrap' ? core.projectId(command.identity.projectUrl) : undefined;
|
|
306
|
+
if (command.identity?.namespace === 'chatgpt.bootstrap' && !expectedProjectId)
|
|
307
|
+
return { kind: 'prepared', ready: false, reasonCode: 'COMPUTER_CHATGPT_BOOTSTRAP_PROJECT_UNPROVEN' };
|
|
308
|
+
const requiresDraftReplacement = typeof command.prompt === 'string' && command.prompt.startsWith('<<<FORGE_DRAFT_REPLACE_V1:');
|
|
309
|
+
if (expectedProjectId || requiresDraftReplacement) {
|
|
185
310
|
let capability;
|
|
186
311
|
try { capability = await tabMessage(target.tab.id, { type: 'forge-computer-chatgpt-capabilities' }); }
|
|
187
|
-
catch { return { kind: 'prepared', ready: false, reasonCode: '
|
|
188
|
-
|
|
189
|
-
|
|
312
|
+
catch { return { kind: 'prepared', ready: false, reasonCode: 'COMPUTER_CHATGPT_CONTENT_CAPABILITIES_UNAVAILABLE' }; }
|
|
313
|
+
// Older content scripts ignore expectedProjectId. No bootstrap draft/send
|
|
314
|
+
// may cross this version boundary without target verification.
|
|
315
|
+
if (expectedProjectId && capability?.bootstrapTargetGuard !== 'project-checked-before-send-v1')
|
|
316
|
+
return { kind: 'prepared', ready: false, reasonCode: 'COMPUTER_CHATGPT_BOOTSTRAP_PROJECT_GUARD_UNSUPPORTED' };
|
|
317
|
+
if (requiresDraftReplacement && capability?.composerReplacementContract !== 'exact-backed-up-project-draft-v1')
|
|
318
|
+
return { kind: 'prepared', ready: false, reasonCode: 'COMPUTER_CHATGPT_BOOTSTRAP_LOADED_SCRIPT_STALE' };
|
|
190
319
|
}
|
|
191
320
|
try {
|
|
192
321
|
const result = await tabMessage(target.tab.id, {
|
|
193
322
|
type: 'forge-computer-chatgpt-prepare', prompt: command.prompt, mode: command.mode,
|
|
194
|
-
reasoning: command.reasoning,
|
|
195
323
|
...(command.identity?.namespace === 'chatgpt.conversation' ? { expectedConversationId: command.identity.conversationId } : {}),
|
|
324
|
+
...(expectedProjectId ? { expectedProjectId } : {}),
|
|
196
325
|
});
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
: { kind: 'prepared', ready: false, reasonCode: String(result?.reason ?? 'COMPUTER_CHATGPT_EXTENSION_PREPARE_REJECTED') };
|
|
326
|
+
if (result?.prepared !== true) return { kind: 'prepared', ready: false, reasonCode: String(result?.reason ?? 'COMPUTER_CHATGPT_EXTENSION_PREPARE_REJECTED') };
|
|
327
|
+
return { kind: 'prepared', ready: true };
|
|
200
328
|
} catch {
|
|
201
329
|
// A prepare command contains no Send action. Failure is not a submitted effect.
|
|
202
330
|
return { kind: 'prepared', ready: false, reasonCode: 'COMPUTER_CHATGPT_EXTENSION_PREPARE_UNAVAILABLE' };
|
|
203
331
|
}
|
|
204
332
|
}
|
|
205
333
|
async function executeDispatch(command) {
|
|
206
|
-
const target = await resolveTarget(command.identity, false).catch(() => undefined);
|
|
334
|
+
const target = await resolveTarget(command.identity, false, command.providerBinding).catch(() => undefined);
|
|
207
335
|
if (!target) return { kind: 'dispatch', mutation: 'not_attempted', reasonCode: 'COMPUTER_CHATGPT_EXTENSION_TARGET_MISSING' };
|
|
336
|
+
const expectedProjectId = command.identity?.namespace === 'chatgpt.bootstrap' ? core.projectId(command.identity.projectUrl) : undefined;
|
|
337
|
+
if (command.identity?.namespace === 'chatgpt.bootstrap' && !expectedProjectId)
|
|
338
|
+
return { kind: 'dispatch', mutation: 'not_attempted', reasonCode: 'COMPUTER_CHATGPT_BOOTSTRAP_PROJECT_UNPROVEN' };
|
|
208
339
|
// An old tab may still contain a pre-upgrade content script after the
|
|
209
340
|
// extension/runtime has been upgraded. Probe the *exact target tab* before
|
|
210
341
|
// submitting; a background/version heartbeat alone cannot prove it.
|
|
211
|
-
if (
|
|
342
|
+
if (expectedProjectId) {
|
|
212
343
|
let capability;
|
|
213
344
|
try { capability = await tabMessage(target.tab.id, { type: 'forge-computer-chatgpt-capabilities' }); }
|
|
214
|
-
catch { return { kind: 'dispatch', mutation: 'not_attempted', reasonCode: '
|
|
215
|
-
if (capability?.
|
|
216
|
-
return { kind: 'dispatch', mutation: 'not_attempted', reasonCode: '
|
|
217
|
-
}
|
|
345
|
+
catch { return { kind: 'dispatch', mutation: 'not_attempted', reasonCode: 'COMPUTER_CHATGPT_CONTENT_CAPABILITIES_UNAVAILABLE' }; }
|
|
346
|
+
if (expectedProjectId && capability?.bootstrapTargetGuard !== 'project-checked-before-send-v1')
|
|
347
|
+
return { kind: 'dispatch', mutation: 'not_attempted', reasonCode: 'COMPUTER_CHATGPT_BOOTSTRAP_PROJECT_GUARD_UNSUPPORTED' };
|
|
218
348
|
}
|
|
219
349
|
let result;
|
|
220
350
|
try { result = await tabMessage(target.tab.id, {
|
|
221
351
|
type: 'forge-computer-chatgpt-dispatch', prompt: command.prompt, mode: command.mode,
|
|
222
|
-
reasoning: command.reasoning,
|
|
223
352
|
...(command.identity?.namespace === 'chatgpt.conversation' ? { expectedConversationId: command.identity.conversationId } : {}),
|
|
353
|
+
...(expectedProjectId ? { expectedProjectId } : {}),
|
|
224
354
|
}); }
|
|
225
355
|
catch (error) {
|
|
226
356
|
return dispatchTransportNotReached(error)
|
|
227
357
|
? { kind: 'dispatch', mutation: 'not_attempted', reasonCode: 'COMPUTER_CHATGPT_EXTENSION_CONTENT_UNAVAILABLE' }
|
|
228
358
|
: { kind: 'dispatch', mutation: 'attempted' };
|
|
229
359
|
}
|
|
230
|
-
if (result?.dispatched
|
|
231
|
-
// A
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
let observation;
|
|
238
|
-
for (let attempt = 1; attempt <= 4; attempt += 1) {
|
|
239
|
-
await sleep(attempt * 200);
|
|
240
|
-
try {
|
|
241
|
-
observation = await snapshot(target.tab.id, { includeUserHistory: false, includePageText: false });
|
|
242
|
-
if (normalize(observation.latestUserText) === normalize(command.prompt)) return { kind: 'dispatch', mutation: 'attempted', confirmed: true, ...(result.reasoningVerified ? { reasoningVerified: result.reasoningVerified } : {}), observation };
|
|
243
|
-
} catch { /* mutation already happened; only confirmation is missing */ }
|
|
244
|
-
}
|
|
245
|
-
return { kind: 'dispatch', mutation: 'attempted', confirmed: false, ...(result.reasoningVerified ? { reasoningVerified: result.reasoningVerified } : {}), ...(observation ? { observation } : {}) };
|
|
360
|
+
if (result?.dispatched === false) return { kind: 'dispatch', mutation: 'not_attempted', reasonCode: String(result?.reason ?? 'COMPUTER_CHATGPT_EXTENSION_PRE_MUTATION_REJECTION') };
|
|
361
|
+
// A missing/malformed reply after dispatch crossed the content-script boundary
|
|
362
|
+
// is not an attestation that Send never ran.
|
|
363
|
+
if (result?.dispatched !== true) return { kind: 'dispatch', mutation: 'attempted', confirmed: false };
|
|
364
|
+
// The content script observes the committed user turn within the same page.
|
|
365
|
+
// A missing/negative receipt is an outcome-unknown submission, not a retry grant.
|
|
366
|
+
return { kind: 'dispatch', mutation: 'attempted', confirmed: result.confirmed === true };
|
|
246
367
|
}
|
|
247
368
|
async function executeFindMarker(command, instanceId) {
|
|
248
369
|
const matches = [];
|
|
@@ -259,12 +380,37 @@ async function executeFindMarker(command, instanceId) {
|
|
|
259
380
|
}
|
|
260
381
|
return { kind: 'marker_matches', matches };
|
|
261
382
|
}
|
|
262
|
-
async function executeClose(command) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
383
|
+
async function executeClose(command, instanceId) {
|
|
384
|
+
const pinned = command.providerBinding;
|
|
385
|
+
if (!pinned?.ownerToken || pinned.providerId !== PROVIDER_ID
|
|
386
|
+
|| pinned.providerSessionId !== instanceId || !pinned.tabId || !pinned.windowId)
|
|
387
|
+
return { kind: 'failed', code: 'COMPUTER_CHATGPT_TAB_OWNERSHIP_UNPROVEN', retryable: false };
|
|
388
|
+
const tabId = Number(pinned.tabId);
|
|
389
|
+
if (!Number.isSafeInteger(tabId) || tabId <= 0)
|
|
390
|
+
return { kind: 'failed', code: 'COMPUTER_CHATGPT_TAB_ID_INVALID', retryable: false };
|
|
391
|
+
let tab;
|
|
392
|
+
try { tab = await tabGet(tabId); }
|
|
393
|
+
catch (error) {
|
|
394
|
+
// This exact Chrome profile has already proved the physical tab absent.
|
|
395
|
+
if (/No tab with id/i.test(String(error?.message ?? error))) return { kind: 'closed' };
|
|
396
|
+
return { kind: 'failed', code: 'COMPUTER_CHATGPT_TAB_CLOSE_UNVERIFIED', retryable: true };
|
|
267
397
|
}
|
|
398
|
+
if (String(tab.windowId) !== String(pinned.windowId)
|
|
399
|
+
|| await attestedOwnerToken(tab, pinned) !== pinned.ownerToken)
|
|
400
|
+
return { kind: 'failed', code: 'COMPUTER_CHATGPT_TAB_OWNERSHIP_UNPROVEN', retryable: false };
|
|
401
|
+
const identity = command.identity;
|
|
402
|
+
const exactConversation = identity?.namespace === 'chatgpt.conversation'
|
|
403
|
+
&& core.parseConversation(tab.url ?? '')?.conversationId === identity.conversationId;
|
|
404
|
+
const exactBootstrap = identity?.namespace === 'chatgpt.bootstrap'
|
|
405
|
+
&& (tab.url === identity.projectUrl || (core.projectId(identity.projectUrl)
|
|
406
|
+
&& core.projectId(tab.url ?? '') === core.projectId(identity.projectUrl)));
|
|
407
|
+
if (!exactConversation && !exactBootstrap)
|
|
408
|
+
return { kind: 'failed', code: 'COMPUTER_CHATGPT_TAB_IDENTITY_CHANGED', retryable: false };
|
|
409
|
+
try { await tabRemove(tabId); }
|
|
410
|
+
catch { return { kind: 'failed', code: 'COMPUTER_CHATGPT_TAB_CLOSE_UNVERIFIED', retryable: true }; }
|
|
411
|
+
await chrome.storage.session.remove(ownedTabKey(tabId)).catch(() => undefined);
|
|
412
|
+
if (identity.namespace === 'chatgpt.bootstrap' && bootstrapTabs.get(identity.bootstrapKey) === tabId)
|
|
413
|
+
bootstrapTabs.delete(identity.bootstrapKey);
|
|
268
414
|
return { kind: 'closed' };
|
|
269
415
|
}
|
|
270
416
|
async function executeCommand(command, instanceId) {
|
|
@@ -274,7 +420,7 @@ async function executeCommand(command, instanceId) {
|
|
|
274
420
|
if (command.kind === 'prepare') return await executePrepare(command);
|
|
275
421
|
if (command.kind === 'dispatch') return await executeDispatch(command);
|
|
276
422
|
if (command.kind === 'find_marker') return await executeFindMarker(command, instanceId);
|
|
277
|
-
if (command.kind === 'close') return await executeClose(command);
|
|
423
|
+
if (command.kind === 'close') return await executeClose(command, instanceId);
|
|
278
424
|
return { kind: 'failed', code: 'COMPUTER_CHATGPT_EXTENSION_COMMAND_UNKNOWN', retryable: false, failoverSafe: true };
|
|
279
425
|
}
|
|
280
426
|
async function publishHeartbeat(instanceId) {
|
|
@@ -295,18 +441,26 @@ async function publishHeartbeat(instanceId) {
|
|
|
295
441
|
await nativeRpc('computer_extension_heartbeat', { providerId: PROVIDER_ID, providerInstanceId: instanceId, observedAt: new Date().toISOString(), conversations });
|
|
296
442
|
}
|
|
297
443
|
let providerTickInFlight;
|
|
444
|
+
let providerHeartbeatInFlight;
|
|
445
|
+
function heartbeatTick() {
|
|
446
|
+
providerHeartbeatInFlight ??= providerInstanceId().then(publishHeartbeat)
|
|
447
|
+
.finally(() => { providerHeartbeatInFlight = undefined; });
|
|
448
|
+
return providerHeartbeatInFlight;
|
|
449
|
+
}
|
|
298
450
|
async function providerTick() {
|
|
299
|
-
|
|
451
|
+
// One claimed command retains the sole mutation lane; it must not also
|
|
452
|
+
// suppress profile heartbeat when a page/Chrome callback is slow or hangs.
|
|
453
|
+
if (providerTickInFlight) { await heartbeatTick(); return; }
|
|
300
454
|
providerTickInFlight = (async () => {
|
|
301
|
-
const instanceId =
|
|
302
|
-
await
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
455
|
+
const instanceId = await providerInstanceId();
|
|
456
|
+
await heartbeatTick();
|
|
457
|
+
// One command per tick keeps heartbeats and other tasks fair. The broker
|
|
458
|
+
// remains the only authority for a claimed Send and its unknown outcome.
|
|
459
|
+
const claim = await nativeRpc('computer_extension_claim', { provider_instance_id: instanceId });
|
|
460
|
+
const command = claim?.command;
|
|
461
|
+
if (!command) return;
|
|
462
|
+
const result = await executeCommand(command, instanceId);
|
|
463
|
+
await nativeRpc('computer_extension_complete', { provider_instance_id: instanceId, command_id: command.commandId, result });
|
|
310
464
|
})().finally(() => { providerTickInFlight = undefined; });
|
|
311
465
|
return await providerTickInFlight;
|
|
312
466
|
}
|
|
@@ -352,7 +506,9 @@ async function refreshDiscovery() {
|
|
|
352
506
|
const title = String(tab.title ?? '').trim();
|
|
353
507
|
conversations.push({ conversation_id: identity.conversationId, canonical_url: identity.canonicalUrl, ...projectByConversation.get(identity.conversationId), ...(title ? { title: title.slice(0, 512) } : {}), ...(tab.id === current?.id ? { is_current: true } : {}) });
|
|
354
508
|
}
|
|
355
|
-
|
|
509
|
+
// Discovery and command routing share one persisted Chrome-profile identity.
|
|
510
|
+
const instanceId = await providerInstanceId();
|
|
511
|
+
await nativeRpc('browser_discovery_update', { source: `chrome-extension:${instanceId}`, conversations }).catch(() => undefined);
|
|
356
512
|
}
|
|
357
513
|
let discoveryInFlight;
|
|
358
514
|
let discoveryTimer;
|