@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
|
@@ -102,6 +102,9 @@ implements ComputerChatgptConversationTargetPort, ComputerChatgptExtensionBroker
|
|
|
102
102
|
private readonly providerStates = new Map<string, ProviderState>();
|
|
103
103
|
private readonly pending = new Map<string, PendingCommand>();
|
|
104
104
|
private readonly queues = new Map<string, string[]>();
|
|
105
|
+
// Process-local physical-operation singleflight, not a second tab lifecycle
|
|
106
|
+
// authority. Exact Surface binding/CAS remains the only durable owner.
|
|
107
|
+
private readonly retiring = new Map<string, Promise<boolean>>();
|
|
105
108
|
|
|
106
109
|
constructor(
|
|
107
110
|
private readonly controllerHome: string,
|
|
@@ -183,11 +186,15 @@ implements ComputerChatgptConversationTargetPort, ComputerChatgptExtensionBroker
|
|
|
183
186
|
|
|
184
187
|
private selectProviderInstance(preferredInstanceId?: string): { instanceId?: string; ambiguous: boolean } {
|
|
185
188
|
const fresh = this.freshProviders();
|
|
186
|
-
if (preferredInstanceId
|
|
187
|
-
|
|
189
|
+
if (preferredInstanceId) {
|
|
190
|
+
// A disconnected profile is unavailable, not permission to switch to
|
|
191
|
+
// the only other signed-in (or signed-out) Chrome instance.
|
|
192
|
+
return { ...(fresh.some(([instanceId]) => instanceId === preferredInstanceId)
|
|
193
|
+
? { instanceId: preferredInstanceId } : {}), ambiguous: false };
|
|
188
194
|
}
|
|
189
|
-
|
|
190
|
-
|
|
195
|
+
// Foreground tab is not a durable browser-principal binding. If more
|
|
196
|
+
// than one Extension instance is connected, require a known pin instead
|
|
197
|
+
// of guessing which profile should receive a new Supervisor effect.
|
|
191
198
|
if (fresh.length === 1) return { instanceId: fresh[0]![0], ambiguous: false };
|
|
192
199
|
return { ambiguous: fresh.length > 1 };
|
|
193
200
|
}
|
|
@@ -239,7 +246,20 @@ implements ComputerChatgptConversationTargetPort, ComputerChatgptExtensionBroker
|
|
|
239
246
|
const record = this.upsert(identity);
|
|
240
247
|
const normalized = normalizeBinding(binding, instanceId);
|
|
241
248
|
if (!normalized) return record;
|
|
242
|
-
return await this.authority.withSurfaceLease(this.controllerHome, record.targetId, async (lease) =>
|
|
249
|
+
return await this.authority.withSurfaceLease(this.controllerHome, record.targetId, async (lease) => {
|
|
250
|
+
const previous = lease.current().providerBinding;
|
|
251
|
+
// Heartbeat inventory omits the Forge-create token. Carry it forward only
|
|
252
|
+
// for the same exact physical attachment; never transfer it to a new tab.
|
|
253
|
+
const sameAttachment = previous?.providerId === normalized.providerId
|
|
254
|
+
&& previous.providerSessionId === normalized.providerSessionId
|
|
255
|
+
&& previous.browserProduct === normalized.browserProduct
|
|
256
|
+
&& previous.windowId === normalized.windowId
|
|
257
|
+
&& previous.tabId === normalized.tabId;
|
|
258
|
+
return lease.bind({
|
|
259
|
+
...normalized,
|
|
260
|
+
...(normalized.ownerToken ? {} : sameAttachment && previous?.ownerToken ? { ownerToken: previous.ownerToken } : {}),
|
|
261
|
+
});
|
|
262
|
+
});
|
|
243
263
|
}
|
|
244
264
|
|
|
245
265
|
private target(identity: ComputerChatgptTargetIdentity, record: ComputerSurfaceTarget, instanceId: string): ComputerChatgptConversationTarget {
|
|
@@ -250,7 +270,19 @@ implements ComputerChatgptConversationTargetPort, ComputerChatgptExtensionBroker
|
|
|
250
270
|
const response = await this.submit(instanceId, { kind: 'observe', identity, ...(record.providerBinding ? { providerBinding: record.providerBinding } : {}), ...(options ? { options } : {}) });
|
|
251
271
|
if (!response.result) throw new Error(response.claimed ? 'COMPUTER_CHATGPT_EXTENSION_OBSERVATION_OUTCOME_UNKNOWN' : 'COMPUTER_CHATGPT_EXTENSION_NOT_CONNECTED');
|
|
252
272
|
if (response.result.kind === 'observation') {
|
|
253
|
-
if (response.result.providerBinding)
|
|
273
|
+
if (response.result.providerBinding) {
|
|
274
|
+
const observed = normalizeBinding(response.result.providerBinding, instanceId);
|
|
275
|
+
const saved = this.authority.getSurface(this.controllerHome, record.targetId)?.providerBinding;
|
|
276
|
+
// A newer observedAt is not a new physical binding. Normal reads
|
|
277
|
+
// must not rewrite the Surface and its binding indexes on every poll.
|
|
278
|
+
if (!observed || !saved || saved.providerId !== observed.providerId
|
|
279
|
+
|| saved.providerSessionId !== observed.providerSessionId
|
|
280
|
+
|| saved.browserProduct !== observed.browserProduct
|
|
281
|
+
|| saved.windowId !== observed.windowId || saved.tabId !== observed.tabId
|
|
282
|
+
|| (observed.ownerToken && saved.ownerToken !== observed.ownerToken)) {
|
|
283
|
+
await this.bindAsync(identity, response.result.providerBinding, instanceId);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
254
286
|
return response.result.observation;
|
|
255
287
|
}
|
|
256
288
|
if (response.result.kind === 'failed') throw new Error(response.result.code);
|
|
@@ -259,29 +291,30 @@ implements ComputerChatgptConversationTargetPort, ComputerChatgptExtensionBroker
|
|
|
259
291
|
prepare: async (prompt, options) => {
|
|
260
292
|
const response = await this.submit(instanceId, { kind: 'prepare', identity, ...(record.providerBinding ? { providerBinding: record.providerBinding } : {}), prompt,
|
|
261
293
|
...(options?.mode ? { mode: options.mode } : {}),
|
|
262
|
-
...(options?.reasoning ? { reasoning: options.reasoning } : {}),
|
|
263
294
|
});
|
|
264
295
|
if (!response.result) return { ready: false, reasonCode: response.claimed
|
|
265
296
|
? 'COMPUTER_CHATGPT_EXTENSION_PREPARE_OUTCOME_UNKNOWN' : 'COMPUTER_CHATGPT_EXTENSION_NOT_CONNECTED' };
|
|
266
|
-
if (response.result.kind === 'prepared')
|
|
267
|
-
|
|
297
|
+
if (response.result.kind === 'prepared') {
|
|
298
|
+
if (!response.result.ready) return { ready: false, reasonCode: response.result.reasonCode };
|
|
299
|
+
return { ready: true };
|
|
300
|
+
}
|
|
268
301
|
return { ready: false, reasonCode: response.result.kind === 'failed'
|
|
269
302
|
? response.result.code : 'COMPUTER_CHATGPT_EXTENSION_PREPARE_INVALID' };
|
|
270
303
|
},
|
|
271
304
|
dispatch: async (prompt, options) => {
|
|
272
|
-
const response = await this.submit(instanceId, { kind: 'dispatch', identity, ...(record.providerBinding ? { providerBinding: record.providerBinding } : {}), prompt, ...(options?.mode ? { mode: options.mode } : {})
|
|
305
|
+
const response = await this.submit(instanceId, { kind: 'dispatch', identity, ...(record.providerBinding ? { providerBinding: record.providerBinding } : {}), prompt, ...(options?.mode ? { mode: options.mode } : {}) });
|
|
273
306
|
if (!response.result) {
|
|
274
307
|
return response.claimed
|
|
275
308
|
? { mutation: 'attempted' as const }
|
|
276
309
|
: { mutation: 'not_attempted' as const, reasonCode: 'COMPUTER_CHATGPT_EXTENSION_NOT_CONNECTED' };
|
|
277
310
|
}
|
|
278
311
|
if (response.result.kind === 'dispatch') {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
: { mutation: 'not_attempted' as const, reasonCode: response.result.reasonCode };
|
|
312
|
+
if (response.result.mutation === 'attempted') return { mutation: 'attempted' as const, ...(response.result.confirmed === undefined ? {} : { confirmed: response.result.confirmed }) };
|
|
313
|
+
if (response.result.mutation === 'not_attempted') return { mutation: 'not_attempted' as const, reasonCode: response.result.reasonCode };
|
|
282
314
|
}
|
|
283
|
-
|
|
284
|
-
|
|
315
|
+
// Only a typed dispatch/not_attempted result proves no Send click. A
|
|
316
|
+
// generic failure or wrong response kind cannot authorize another send.
|
|
317
|
+
return { mutation: 'attempted' as const, confirmed: false };
|
|
285
318
|
},
|
|
286
319
|
};
|
|
287
320
|
}
|
|
@@ -299,21 +332,44 @@ implements ComputerChatgptConversationTargetPort, ComputerChatgptExtensionBroker
|
|
|
299
332
|
return { conversations: [...byConversationId.values()], complete: true, unavailableProviders: [] };
|
|
300
333
|
}
|
|
301
334
|
|
|
302
|
-
async ensureExact(identity: ComputerChatgptConversationIdentity): Promise<ComputerChatgptTargetResult> {
|
|
335
|
+
async ensureExact(identity: ComputerChatgptConversationIdentity, options: { createIfMissing?: boolean } = {}): Promise<ComputerChatgptTargetResult> {
|
|
336
|
+
const preferredInstanceId = this.preferredProviderInstance(identity);
|
|
303
337
|
const fresh = this.freshProviders();
|
|
304
|
-
if (fresh.length === 0
|
|
305
|
-
|
|
338
|
+
if (fresh.length === 0 || (preferredInstanceId && !fresh.some(([instanceId]) => instanceId === preferredInstanceId))) {
|
|
339
|
+
return failure('COMPUTER_CHATGPT_EXTENSION_NOT_CONNECTED', { failoverSafe: !preferredInstanceId });
|
|
340
|
+
}
|
|
341
|
+
const exact = fresh.filter(([instanceId]) => !preferredInstanceId || instanceId === preferredInstanceId)
|
|
342
|
+
.flatMap(([instanceId, state]) => state.heartbeat.conversations
|
|
306
343
|
.filter((entry) => entry.conversationId === identity.conversationId)
|
|
307
344
|
.map((entry) => ({ instanceId, entry })));
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
345
|
+
// Multiple tabs can show one conversation. Prefer the existing exact
|
|
346
|
+
// Computer binding over heartbeat focus or an arbitrary matching tab.
|
|
347
|
+
const binding = this.authority.findSurfaceByStableIdentity(this.controllerHome, stableIdentity(identity))?.providerBinding;
|
|
348
|
+
let candidates = exact;
|
|
349
|
+
if (binding?.providerId === PROVIDER_ID && binding.windowId && binding.tabId) {
|
|
350
|
+
const bound = exact.filter(({ instanceId, entry }) => instanceId === binding.providerSessionId
|
|
351
|
+
&& entry.providerBinding?.tabId === binding.tabId
|
|
352
|
+
&& (!binding.browserProduct || entry.providerBinding?.browserProduct === binding.browserProduct));
|
|
353
|
+
// Chrome restart replaces physical tab IDs, not the profile principal or
|
|
354
|
+
// conversation identity. A unique exact tab observed by that SAME live
|
|
355
|
+
// provider may replace an absent pin through the Surface lease below.
|
|
356
|
+
// Never choose between multiple matching tabs or switch browser profiles.
|
|
357
|
+
const replacement = exact.length === 1 && exact[0]!.instanceId === binding.providerSessionId
|
|
358
|
+
&& (!binding.browserProduct || exact[0]!.entry.providerBinding?.browserProduct === binding.browserProduct)
|
|
359
|
+
? exact : [];
|
|
360
|
+
candidates = bound.length ? bound : replacement;
|
|
361
|
+
}
|
|
362
|
+
// A stale binding must not turn two observed exact tabs into a third tab.
|
|
363
|
+
if (exact.length > 1 && candidates.length !== 1) return failure('COMPUTER_CHATGPT_EXTENSION_EXACT_TARGET_AMBIGUOUS', { failoverSafe: false });
|
|
364
|
+
if (candidates.length === 1) {
|
|
365
|
+
const candidate = candidates[0]!;
|
|
366
|
+
if (!candidate.entry.providerBinding) return failure('COMPUTER_CHATGPT_EXTENSION_PHYSICAL_TAB_UNVERIFIED', { failoverSafe: false });
|
|
311
367
|
const record = await this.bindAsync(identity, candidate.entry.providerBinding, candidate.instanceId);
|
|
312
368
|
const target = this.target(identity, record, candidate.instanceId);
|
|
313
369
|
try {
|
|
314
370
|
const observation = await target.observe({ includeUserHistory: false, includePageText: false });
|
|
315
371
|
if (!conversationContentAvailable(observation)) {
|
|
316
|
-
return failure('COMPUTER_CHATGPT_CONVERSATION_CONTENT_UNAVAILABLE', { failoverSafe:
|
|
372
|
+
return failure('COMPUTER_CHATGPT_CONVERSATION_CONTENT_UNAVAILABLE', { failoverSafe: false });
|
|
317
373
|
}
|
|
318
374
|
return { state: 'ready', target, observation };
|
|
319
375
|
} catch (error) {
|
|
@@ -321,15 +377,49 @@ implements ComputerChatgptConversationTargetPort, ComputerChatgptExtensionBroker
|
|
|
321
377
|
error instanceof Error && error.message.trim()
|
|
322
378
|
? error.message.trim()
|
|
323
379
|
: 'COMPUTER_CHATGPT_EXTENSION_OBSERVATION_UNAVAILABLE',
|
|
324
|
-
{ failoverSafe:
|
|
380
|
+
{ failoverSafe: false },
|
|
325
381
|
);
|
|
326
382
|
}
|
|
327
383
|
}
|
|
328
|
-
|
|
384
|
+
// A heartbeat is an inventory hint, not the exact-target authority.
|
|
385
|
+
// When it has no match, let the SAME live Chrome instance perform one
|
|
386
|
+
// read-only physical-tab lookup. This does not create a tab or replay Send;
|
|
387
|
+
// the provider refuses ambiguous matches and drifted pinned tabs.
|
|
388
|
+
if (options.createIfMissing === false) {
|
|
389
|
+
const selected = this.selectProviderInstance(preferredInstanceId);
|
|
390
|
+
if (!selected.instanceId) return failure(
|
|
391
|
+
selected.ambiguous ? 'COMPUTER_CHATGPT_EXTENSION_PROVIDER_INSTANCE_AMBIGUOUS' : 'COMPUTER_CHATGPT_EXTENSION_NOT_CONNECTED',
|
|
392
|
+
{ failoverSafe: false },
|
|
393
|
+
);
|
|
394
|
+
const response = await this.submit(selected.instanceId, { kind: 'observe', identity,
|
|
395
|
+
...(binding?.providerId === PROVIDER_ID ? { providerBinding: binding } : {}),
|
|
396
|
+
options: { includeUserHistory: false, includePageText: false },
|
|
397
|
+
});
|
|
398
|
+
if (!response.result) return failure('COMPUTER_CHATGPT_EXTENSION_OBSERVATION_UNAVAILABLE', { failoverSafe: false });
|
|
399
|
+
if (response.result.kind === 'observation') {
|
|
400
|
+
const { observation, providerBinding } = response.result;
|
|
401
|
+
let exactUrl = false;
|
|
402
|
+
try {
|
|
403
|
+
const parsed = new URL(observation.url);
|
|
404
|
+
exactUrl = parsed.origin === 'https://chatgpt.com'
|
|
405
|
+
&& parsed.pathname.endsWith(`/c/${identity.conversationId}`);
|
|
406
|
+
} catch { /* Never bind an unparseable or changed conversation target. */ }
|
|
407
|
+
if (!exactUrl || !providerBinding) return failure('COMPUTER_CHATGPT_EXTENSION_TARGET_IDENTITY_UNVERIFIED', { failoverSafe: false });
|
|
408
|
+
if (!conversationContentAvailable(observation)) return failure('COMPUTER_CHATGPT_CONVERSATION_CONTENT_UNAVAILABLE', { failoverSafe: false });
|
|
409
|
+
const record = await this.bindAsync(identity, providerBinding, selected.instanceId);
|
|
410
|
+
return { state: 'ready', target: this.target(identity, record, selected.instanceId), observation };
|
|
411
|
+
}
|
|
412
|
+
return failure(response.result.kind === 'failed'
|
|
413
|
+
? response.result.code === 'COMPUTER_CHATGPT_EXTENSION_TARGET_MISSING'
|
|
414
|
+
? 'COMPUTER_CHATGPT_EXTENSION_EXACT_TARGET_ABSENT_READ_ONLY'
|
|
415
|
+
: response.result.code
|
|
416
|
+
: 'COMPUTER_CHATGPT_EXTENSION_OBSERVATION_INVALID', { failoverSafe: false });
|
|
417
|
+
}
|
|
418
|
+
const selected = this.selectProviderInstance(preferredInstanceId);
|
|
329
419
|
if (!selected.instanceId) {
|
|
330
420
|
return failure(
|
|
331
421
|
selected.ambiguous ? 'COMPUTER_CHATGPT_EXTENSION_PROVIDER_INSTANCE_AMBIGUOUS' : 'COMPUTER_CHATGPT_EXTENSION_NOT_CONNECTED',
|
|
332
|
-
{ failoverSafe: !selected.ambiguous },
|
|
422
|
+
{ failoverSafe: !selected.ambiguous && !preferredInstanceId },
|
|
333
423
|
);
|
|
334
424
|
}
|
|
335
425
|
const previous = this.authority.findSurfaceByStableIdentity(this.controllerHome, stableIdentity(identity));
|
|
@@ -338,44 +428,96 @@ implements ComputerChatgptConversationTargetPort, ComputerChatgptExtensionBroker
|
|
|
338
428
|
});
|
|
339
429
|
if (!response.result) return failure(
|
|
340
430
|
response.claimed ? 'COMPUTER_CHATGPT_EXTENSION_TARGET_OPEN_OUTCOME_UNKNOWN' : 'COMPUTER_CHATGPT_EXTENSION_NOT_CONNECTED',
|
|
341
|
-
{ failoverSafe: !response.claimed },
|
|
431
|
+
{ failoverSafe: !response.claimed && !preferredInstanceId },
|
|
342
432
|
);
|
|
343
433
|
if (response.result.kind === 'ensured') {
|
|
434
|
+
const record = await this.bindAsync(identity, response.result.providerBinding, selected.instanceId);
|
|
344
435
|
if (response.result.observation && !conversationContentAvailable(response.result.observation)) {
|
|
345
|
-
return failure('COMPUTER_CHATGPT_CONVERSATION_CONTENT_UNAVAILABLE', { failoverSafe:
|
|
436
|
+
return failure('COMPUTER_CHATGPT_CONVERSATION_CONTENT_UNAVAILABLE', { failoverSafe: false });
|
|
346
437
|
}
|
|
347
|
-
const record = await this.bindAsync(identity, response.result.providerBinding, selected.instanceId);
|
|
348
438
|
return { state: 'ready', target: this.target(identity, record, selected.instanceId), ...(response.result.observation ? { observation: response.result.observation } : {}) };
|
|
349
439
|
}
|
|
350
|
-
if (response.result.kind === 'failed')
|
|
351
|
-
|
|
440
|
+
if (response.result.kind === 'failed') {
|
|
441
|
+
if (response.result.providerBinding) await this.bindAsync(identity, response.result.providerBinding, selected.instanceId);
|
|
442
|
+
return failure(response.result.code, { retryable: response.result.retryable,
|
|
443
|
+
failoverSafe: !response.result.providerBinding && !preferredInstanceId && response.result.failoverSafe === true,
|
|
444
|
+
humanAction: response.result.humanAction });
|
|
445
|
+
}
|
|
446
|
+
return failure('COMPUTER_CHATGPT_EXTENSION_ENSURE_INVALID', { failoverSafe: false });
|
|
352
447
|
}
|
|
353
448
|
|
|
354
|
-
async openBootstrap(projectUrl: string, bootstrapKey: string): Promise<ComputerChatgptTargetResult> {
|
|
449
|
+
async openBootstrap(projectUrl: string, bootstrapKey: string, recoverStalePin = true): Promise<ComputerChatgptTargetResult> {
|
|
355
450
|
const identity: ComputerChatgptBootstrapIdentity = { namespace: 'chatgpt.bootstrap', bootstrapKey, projectUrl };
|
|
451
|
+
const previous = this.authority.findSurfaceByStableIdentity(this.controllerHome, stableIdentity(identity));
|
|
452
|
+
const binding = previous?.providerBinding;
|
|
453
|
+
// Existing bootstrap tab identity survives extension reconnects. A foreign
|
|
454
|
+
// provider owns its tab; extension must not create another one for the
|
|
455
|
+
// same task. Conversely, extension absence cannot authorize native tab
|
|
456
|
+
// creation while an extension-owned tab might still exist.
|
|
457
|
+
if (binding && binding.providerId !== PROVIDER_ID) {
|
|
458
|
+
return failure('COMPUTER_CHATGPT_EXTENSION_BOOTSTRAP_PROVIDER_MISMATCH', { retryable: false, failoverSafe: true });
|
|
459
|
+
}
|
|
356
460
|
const selected = this.selectProviderInstance(this.preferredProviderInstance(identity));
|
|
357
461
|
if (!selected.instanceId) {
|
|
358
462
|
return failure(
|
|
359
463
|
selected.ambiguous ? 'COMPUTER_CHATGPT_EXTENSION_PROVIDER_INSTANCE_AMBIGUOUS' : 'COMPUTER_CHATGPT_EXTENSION_NOT_CONNECTED',
|
|
360
|
-
{ failoverSafe: !selected.ambiguous },
|
|
464
|
+
{ failoverSafe: !selected.ambiguous && !binding },
|
|
361
465
|
);
|
|
362
466
|
}
|
|
363
|
-
// Controller
|
|
364
|
-
//
|
|
365
|
-
const previous = this.authority.findSurfaceByStableIdentity(this.controllerHome, stableIdentity(identity));
|
|
467
|
+
// Controller's exact tab attachment, not the Project URL, is recovery
|
|
468
|
+
// authority after a worker restart.
|
|
366
469
|
const response = await this.submit(selected.instanceId, { kind: 'ensure', identity,
|
|
367
470
|
...(previous?.providerBinding?.providerId === PROVIDER_ID ? { providerBinding: previous.providerBinding } : {}),
|
|
368
471
|
});
|
|
369
472
|
if (!response.result) return failure(
|
|
370
473
|
response.claimed ? 'COMPUTER_CHATGPT_EXTENSION_BOOTSTRAP_OPEN_OUTCOME_UNKNOWN' : 'COMPUTER_CHATGPT_EXTENSION_NOT_CONNECTED',
|
|
371
|
-
{ failoverSafe: !response.claimed },
|
|
474
|
+
{ failoverSafe: !response.claimed && !binding },
|
|
372
475
|
);
|
|
373
476
|
if (response.result.kind === 'ensured') {
|
|
374
477
|
const record = await this.bindAsync(identity, response.result.providerBinding, selected.instanceId);
|
|
375
478
|
return { state: 'ready', target: this.target(identity, record, selected.instanceId), ...(response.result.observation ? { observation: response.result.observation } : {}) };
|
|
376
479
|
}
|
|
377
|
-
if (response.result.kind === 'failed')
|
|
378
|
-
|
|
480
|
+
if (response.result.kind === 'failed') {
|
|
481
|
+
if (response.result.providerBinding) await this.bindAsync(identity, response.result.providerBinding, selected.instanceId);
|
|
482
|
+
// Only openBootstrap's pre-dispatch caller may rebuild a tab attachment.
|
|
483
|
+
// The SAME live Chrome provider must prove that the exact pinned tab is
|
|
484
|
+
// either absent or now serves a DIFFERENT Project. A reused physical tab
|
|
485
|
+
// id is not a valid target and must not permanently strand the unspent
|
|
486
|
+
// bootstrap. Never close/navigate the mismatched foreign tab. Keep the
|
|
487
|
+
// one durable target and lease/CAS-clear only its stale binding; never
|
|
488
|
+
// recover from an unknown tab-create result or switch profiles/providers.
|
|
489
|
+
if (recoverStalePin && (response.result.code === 'COMPUTER_CHATGPT_BOOTSTRAP_PINNED_TAB_MISSING'
|
|
490
|
+
|| response.result.code === 'COMPUTER_CHATGPT_BOOTSTRAP_PINNED_TAB_IDENTITY_CHANGED')
|
|
491
|
+
&& previous && binding?.providerId === PROVIDER_ID
|
|
492
|
+
&& providerInstanceId(binding.providerSessionId) === selected.instanceId
|
|
493
|
+
&& binding.windowId && binding.tabId) {
|
|
494
|
+
try {
|
|
495
|
+
await this.authority.withSurfaceLease(this.controllerHome, previous.targetId, async (lease) => {
|
|
496
|
+
const current = lease.current();
|
|
497
|
+
const attached = current.providerBinding;
|
|
498
|
+
if (current.stableIdentity.resource?.namespace !== identity.namespace
|
|
499
|
+
|| current.stableIdentity.resource.key !== identity.bootstrapKey
|
|
500
|
+
|| attached?.providerId !== binding.providerId
|
|
501
|
+
|| attached.providerSessionId !== binding.providerSessionId
|
|
502
|
+
|| attached.windowId !== binding.windowId || attached.tabId !== binding.tabId
|
|
503
|
+
|| attached.browserProduct !== binding.browserProduct) {
|
|
504
|
+
throw new Error('COMPUTER_CHATGPT_BOOTSTRAP_PINNED_BINDING_CHANGED');
|
|
505
|
+
}
|
|
506
|
+
lease.clearBinding();
|
|
507
|
+
});
|
|
508
|
+
} catch {
|
|
509
|
+
return failure('COMPUTER_CHATGPT_BOOTSTRAP_PINNED_BINDING_CHANGED', { failoverSafe: false });
|
|
510
|
+
}
|
|
511
|
+
// Exactly one same-provider re-open. A claimed but unanswered tabCreate
|
|
512
|
+
// still reports outcome unknown and must never create another tab.
|
|
513
|
+
return await this.openBootstrap(projectUrl, bootstrapKey, false);
|
|
514
|
+
}
|
|
515
|
+
return failure(response.result.code, {
|
|
516
|
+
retryable: response.result.retryable, failoverSafe: response.result.failoverSafe === true && !binding && !response.result.providerBinding,
|
|
517
|
+
humanAction: response.result.humanAction,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
return failure('COMPUTER_CHATGPT_EXTENSION_BOOTSTRAP_INVALID', { failoverSafe: false });
|
|
379
521
|
}
|
|
380
522
|
|
|
381
523
|
async findBySubmittedMarker(marker: string, bootstrapKey?: string, betweenObservations?: () => Promise<void>): Promise<ComputerChatgptTargetResult[]> {
|
|
@@ -388,15 +530,68 @@ implements ComputerChatgptConversationTargetPort, ComputerChatgptExtensionBroker
|
|
|
388
530
|
})));
|
|
389
531
|
await betweenObservations?.();
|
|
390
532
|
const results: ComputerChatgptTargetResult[] = [];
|
|
391
|
-
|
|
533
|
+
type MarkerMatch = Extract<ComputerChatgptExtensionCommandResult, { kind: 'marker_matches' }>['matches'][number];
|
|
534
|
+
const matchesByConversation = new Map<string, Array<{ instanceId: string; match: MarkerMatch }>>();
|
|
392
535
|
for (const { instanceId, response } of responses) {
|
|
393
536
|
if (response.result?.kind !== 'marker_matches') continue;
|
|
394
537
|
for (const match of response.result.matches) {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
538
|
+
const entries = matchesByConversation.get(match.identity.conversationId) ?? [];
|
|
539
|
+
entries.push({ instanceId, match });
|
|
540
|
+
matchesByConversation.set(match.identity.conversationId, entries);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
for (const matches of matchesByConversation.values()) {
|
|
544
|
+
const first = matches[0]!;
|
|
545
|
+
const existing = this.authority.findSurfaceByStableIdentity(this.controllerHome, stableIdentity(first.match.identity))?.providerBinding;
|
|
546
|
+
const samePhysicalBinding = (match: MarkerMatch, binding: NonNullable<typeof existing>, instanceId: string): boolean => {
|
|
547
|
+
const observed = match.providerBinding;
|
|
548
|
+
return Boolean(observed
|
|
549
|
+
&& observed.providerId === binding.providerId
|
|
550
|
+
&& observed.providerSessionId === binding.providerSessionId
|
|
551
|
+
&& observed.browserProduct === binding.browserProduct
|
|
552
|
+
&& observed.windowId === binding.windowId
|
|
553
|
+
&& observed.tabId === binding.tabId
|
|
554
|
+
&& observed.ownerToken === binding.ownerToken
|
|
555
|
+
&& instanceId === binding.providerSessionId);
|
|
556
|
+
};
|
|
557
|
+
const existingMatch = existing?.providerId === PROVIDER_ID
|
|
558
|
+
? matches.find(({ instanceId, match }) => samePhysicalBinding(match, existing, instanceId))
|
|
559
|
+
: undefined;
|
|
560
|
+
const bootstrap = bootstrapKey
|
|
561
|
+
? this.authority.findSurfaceByStableIdentity(this.controllerHome, {
|
|
562
|
+
surfaceType: 'browser-tab', ownership: 'provider_owned',
|
|
563
|
+
resource: { namespace: 'chatgpt.bootstrap', key: bootstrapKey },
|
|
564
|
+
})
|
|
565
|
+
: undefined;
|
|
566
|
+
const pinned = bootstrap?.providerBinding;
|
|
567
|
+
const bootstrapMatch = pinned?.providerId === PROVIDER_ID
|
|
568
|
+
? matches.find(({ instanceId, match }) => samePhysicalBinding(match, pinned, instanceId))
|
|
569
|
+
: undefined;
|
|
570
|
+
// A task-specific bootstrap pin outranks a conversation surface for the
|
|
571
|
+
// same conversationId. If that exact physical pin is absent, preserve the
|
|
572
|
+
// unknown outcome instead of letting another tab's marker become authority.
|
|
573
|
+
if (pinned && !bootstrapMatch) continue;
|
|
574
|
+
const candidate = pinned
|
|
575
|
+
? bootstrapMatch
|
|
576
|
+
: existingMatch ?? (matches.length === 1 && first.match.providerBinding ? first : undefined);
|
|
577
|
+
const bootstrapMatchBinding = bootstrapMatch?.match.providerBinding;
|
|
578
|
+
const existingMatchesBootstrap = Boolean(existingMatch && bootstrapMatch && bootstrapMatchBinding
|
|
579
|
+
&& existingMatch.instanceId === bootstrapMatch.instanceId
|
|
580
|
+
&& samePhysicalBinding(existingMatch.match, bootstrapMatchBinding, existingMatch.instanceId));
|
|
581
|
+
// Resolve duplicate marker tabs only with an exact persisted physical pin.
|
|
582
|
+
// A bootstrap pin already owns its tab: return that target so the caller
|
|
583
|
+
// can promote it, instead of prematurely binding a second conversation
|
|
584
|
+
// Surface to the same tab and hitting COMPUTER_SURFACE_INDEX_CONFLICT.
|
|
585
|
+
if (!candidate) continue;
|
|
586
|
+
if (bootstrap && candidate === bootstrapMatch && !existingMatchesBootstrap) {
|
|
587
|
+
const metadata = bootstrap.compatibilityRecords.find((entry) => entry.namespace === 'chatgpt.target')?.value as Record<string, unknown> | undefined;
|
|
588
|
+
if (typeof metadata?.projectUrl !== 'string' || !metadata.projectUrl) continue;
|
|
589
|
+
const identity: ComputerChatgptBootstrapIdentity = { namespace: 'chatgpt.bootstrap', bootstrapKey: bootstrapKey!, projectUrl: metadata.projectUrl };
|
|
590
|
+
results.push({ state: 'ready', target: this.target(identity, bootstrap, candidate.instanceId), observation: candidate.match.observation });
|
|
591
|
+
continue;
|
|
399
592
|
}
|
|
593
|
+
const record = await this.bindAsync(candidate.match.identity, candidate.match.providerBinding, candidate.instanceId);
|
|
594
|
+
results.push({ state: 'ready', target: this.target(candidate.match.identity, record, candidate.instanceId), observation: candidate.match.observation });
|
|
400
595
|
}
|
|
401
596
|
return results;
|
|
402
597
|
}
|
|
@@ -417,6 +612,60 @@ implements ComputerChatgptConversationTargetPort, ComputerChatgptExtensionBroker
|
|
|
417
612
|
return { state: 'ready', target: this.target(identity, record, selected.instanceId) };
|
|
418
613
|
}
|
|
419
614
|
|
|
615
|
+
/** Existing Surface binding is the only retirement authority. A claimed
|
|
616
|
+
* Chrome close with missing acknowledgement is outcome-unknown: retain the
|
|
617
|
+
* exact attachment so the same owner can reconcile/retry, never make it
|
|
618
|
+
* eligible for another physical owner by clearing the index prematurely. */
|
|
619
|
+
private retirePhysicalBinding(record: ComputerSurfaceTarget, identity?: ComputerChatgptTargetIdentity): Promise<boolean> {
|
|
620
|
+
const existing = this.retiring.get(record.targetId);
|
|
621
|
+
if (existing) return existing;
|
|
622
|
+
const pending = this.retirePhysicalBindingOnce(record, identity);
|
|
623
|
+
this.retiring.set(record.targetId, pending);
|
|
624
|
+
const release = (): void => {
|
|
625
|
+
if (this.retiring.get(record.targetId) === pending) this.retiring.delete(record.targetId);
|
|
626
|
+
};
|
|
627
|
+
void pending.then(release, release);
|
|
628
|
+
return pending;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
private async retirePhysicalBindingOnce(record: ComputerSurfaceTarget, identity?: ComputerChatgptTargetIdentity): Promise<boolean> {
|
|
632
|
+
const previous = record.providerBinding;
|
|
633
|
+
if (previous?.providerId !== PROVIDER_ID) return false;
|
|
634
|
+
const resource = record.stableIdentity.resource;
|
|
635
|
+
if (previous.ownerToken) {
|
|
636
|
+
const instanceId = providerInstanceId(previous.providerSessionId);
|
|
637
|
+
if (!identity || !instanceId || !this.providerIsFresh(instanceId)) return false;
|
|
638
|
+
if ([...this.pending.values()].some((entry) => 'identity' in entry.command
|
|
639
|
+
&& entry.command.kind !== 'close'
|
|
640
|
+
&& entry.command.identity.namespace === resource?.namespace
|
|
641
|
+
&& (entry.command.identity.namespace === 'chatgpt.conversation'
|
|
642
|
+
? entry.command.identity.conversationId === resource?.key
|
|
643
|
+
: entry.command.identity.namespace === 'chatgpt.bootstrap' && entry.command.identity.bootstrapKey === resource?.key))) return false;
|
|
644
|
+
let acknowledged = false;
|
|
645
|
+
try {
|
|
646
|
+
const response = await this.submit(instanceId, { kind: 'close', identity, providerBinding: previous });
|
|
647
|
+
acknowledged = response.result?.kind === 'closed';
|
|
648
|
+
} catch { return false; }
|
|
649
|
+
if (!acknowledged) return false;
|
|
650
|
+
}
|
|
651
|
+
// A legacy or user-attached tab has no Forge-create token: detach only,
|
|
652
|
+
// never turn a matching ChatGPT URL into permission for physical closure.
|
|
653
|
+
let cleared = false;
|
|
654
|
+
try {
|
|
655
|
+
await this.authority.withSurfaceLease(this.controllerHome, record.targetId, async (lease) => {
|
|
656
|
+
const binding = lease.current().providerBinding;
|
|
657
|
+
if (binding?.providerId !== previous.providerId
|
|
658
|
+
|| binding.providerSessionId !== previous.providerSessionId
|
|
659
|
+
|| binding.browserProduct !== previous.browserProduct
|
|
660
|
+
|| binding.windowId !== previous.windowId || binding.tabId !== previous.tabId
|
|
661
|
+
|| binding.ownerToken !== previous.ownerToken) return;
|
|
662
|
+
lease.clearBinding();
|
|
663
|
+
cleared = true;
|
|
664
|
+
});
|
|
665
|
+
} catch { return false; }
|
|
666
|
+
return cleared;
|
|
667
|
+
}
|
|
668
|
+
|
|
420
669
|
async cleanup(activeResourceKeys: readonly string[]): Promise<void> {
|
|
421
670
|
const active = new Set(activeResourceKeys);
|
|
422
671
|
for (const record of this.authority.listAllSurfaces(this.controllerHome)) {
|
|
@@ -427,10 +676,8 @@ implements ComputerChatgptConversationTargetPort, ComputerChatgptExtensionBroker
|
|
|
427
676
|
const identity: ComputerChatgptTargetIdentity | undefined = resource.namespace === 'chatgpt.conversation'
|
|
428
677
|
? (typeof compatibility?.canonicalUrl === 'string' ? { namespace: 'chatgpt.conversation', conversationId: resource.key, canonicalUrl: compatibility.canonicalUrl } : undefined)
|
|
429
678
|
: (typeof compatibility?.projectUrl === 'string' ? { namespace: 'chatgpt.bootstrap', bootstrapKey: resource.key, projectUrl: compatibility.projectUrl } : undefined);
|
|
430
|
-
const
|
|
431
|
-
if (
|
|
432
|
-
await this.authority.withSurfaceLease(this.controllerHome, record.targetId, async (lease) => { lease.clearBinding(); }).catch(() => undefined);
|
|
433
|
-
if (resource.namespace === 'chatgpt.bootstrap') this.authority.tombstoneSurface(this.controllerHome, record.targetId);
|
|
679
|
+
const cleared = await this.retirePhysicalBinding(record, identity);
|
|
680
|
+
if (cleared && resource.namespace === 'chatgpt.bootstrap') this.authority.tombstoneSurface(this.controllerHome, record.targetId);
|
|
434
681
|
}
|
|
435
682
|
}
|
|
436
683
|
|
|
@@ -444,9 +691,7 @@ implements ComputerChatgptConversationTargetPort, ComputerChatgptExtensionBroker
|
|
|
444
691
|
: resource?.namespace === 'chatgpt.bootstrap' && typeof compatibility?.projectUrl === 'string'
|
|
445
692
|
? { namespace: 'chatgpt.bootstrap', bootstrapKey: resource.key, projectUrl: compatibility.projectUrl }
|
|
446
693
|
: undefined;
|
|
447
|
-
|
|
448
|
-
if (identity && instanceId && this.providerIsFresh(instanceId)) await this.submit(instanceId, { kind: 'close', identity }).catch(() => undefined);
|
|
449
|
-
await this.authority.withSurfaceLease(this.controllerHome, targetId, async (lease) => { lease.clearBinding(); }).catch(() => undefined);
|
|
694
|
+
if (record.providerBinding) await this.retirePhysicalBinding(record, identity);
|
|
450
695
|
}
|
|
451
696
|
|
|
452
697
|
async close(): Promise<void> {
|