@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
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
import type {
|
|
3
5
|
ComputerChatgptConversationIdentity,
|
|
4
6
|
ComputerChatgptConversationObservation,
|
|
@@ -21,6 +23,7 @@ import type {
|
|
|
21
23
|
WorkflowSupervisorBrowserCommand,
|
|
22
24
|
WorkflowSupervisorBrowserTask,
|
|
23
25
|
WorkflowSupervisorConsumerStatus,
|
|
26
|
+
WorkflowSupervisorPreflightComposerObservation,
|
|
24
27
|
} from './types';
|
|
25
28
|
|
|
26
29
|
const DEFAULT_INTERVAL_MS = 1_000;
|
|
@@ -44,6 +47,11 @@ const USER_VISIBLE_DEGRADED_AFTER_MS = 30_000;
|
|
|
44
47
|
const TASK_TARGET_RETRY_BASE_MS = 30_000;
|
|
45
48
|
/** Slow read-only probe cadence for an applied turn whose bounded resume is exhausted. */
|
|
46
49
|
const AWAITING_RECEIPT_OBSERVATION_MS = 60_000;
|
|
50
|
+
// Give a fully idle conversation longer than the 60-second late-receipt probe
|
|
51
|
+
// cadence before releasing its disposable Chrome attachment. Under pressure
|
|
52
|
+
// only the eight most recently idle exact resources retain that grace.
|
|
53
|
+
const SUPERVISOR_IDLE_TAB_GRACE_MS = 5 * 60_000;
|
|
54
|
+
const SUPERVISOR_IDLE_TAB_RETAIN_LIMIT = 8;
|
|
47
55
|
const TASK_TARGET_RETRY_MAX_MS = 10 * 60_000;
|
|
48
56
|
// Preparation may inspect/change a local draft, but never sends to ChatGPT.
|
|
49
57
|
// Stop after bounded unsuccessful preflights; an unchanged failing provider
|
|
@@ -63,6 +71,8 @@ export const TASK_TARGET_SPACED_FAILURE_CODES: ReadonlySet<string> = new Set([
|
|
|
63
71
|
'COMPUTER_CHATGPT_EXTENSION_EXACT_TARGET_AMBIGUOUS',
|
|
64
72
|
'COMPUTER_CHATGPT_RESTORED_TARGET_UNPROVEN',
|
|
65
73
|
'COMPUTER_CHATGPT_TARGET_RESTORE_FAILED',
|
|
74
|
+
// An absent manually closed tab cannot be re-created by a read-only probe.
|
|
75
|
+
'COMPUTER_CHATGPT_EXTENSION_EXACT_TARGET_ABSENT_READ_ONLY',
|
|
66
76
|
'COMPUTER_CHATGPT_BOOTSTRAP_TARGET_UNAVAILABLE',
|
|
67
77
|
'COMPUTER_CHATGPT_EXTENSION_TARGET_OPEN_OUTCOME_UNKNOWN',
|
|
68
78
|
'COMPUTER_CHATGPT_EXTENSION_BOOTSTRAP_OPEN_OUTCOME_UNKNOWN',
|
|
@@ -167,6 +177,159 @@ function unknownObservationFingerprint(effectId: string, reason: string, snapsho
|
|
|
167
177
|
isGenerating: snapshot.isGenerating,
|
|
168
178
|
}));
|
|
169
179
|
}
|
|
180
|
+
function preflightComposerObservation(snapshot: ComputerChatgptConversationObservation, effectId: string): WorkflowSupervisorPreflightComposerObservation {
|
|
181
|
+
const value = snapshot.composerText;
|
|
182
|
+
const selectorVersion = snapshot.composerSelectorVersion === 'visible-single-v2' ? snapshot.composerSelectorVersion : undefined;
|
|
183
|
+
if (typeof value !== 'string') return { chars: 0, fingerprint: '', classification: 'unavailable', ...(selectorVersion ? { selectorVersion } : {}) };
|
|
184
|
+
const marker = /^<<<FORGE_WORKFLOW_EFFECT_V1:((?:fx|crpe)_[a-zA-Z0-9_-]{8,120})>>>/.exec(value);
|
|
185
|
+
return {
|
|
186
|
+
chars: value.length,
|
|
187
|
+
fingerprint: sha256(`preflight-composer-v1:\0${value}`).slice(0, 24),
|
|
188
|
+
classification: !value.trim() ? 'empty' : marker ? (marker[1] === effectId ? 'current_effect' : 'other_effect') : 'unattributed',
|
|
189
|
+
...(selectorVersion ? { selectorVersion } : {}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Optional, single-effect operator backup: this records no draft by default.
|
|
194
|
+
* A 0600 on-device authorization marker must name the exact blocked effect,
|
|
195
|
+
* current task, project URL, and observed fingerprint before raw text is saved.
|
|
196
|
+
* The file remains local; no transcript, plugin receipt, or logs carry text.
|
|
197
|
+
* This never clears an editor or permits a Send.
|
|
198
|
+
*/
|
|
199
|
+
export function backupAuthorizedBlockedComposer(
|
|
200
|
+
controllerHome: string,
|
|
201
|
+
taskId: string,
|
|
202
|
+
effectId: string,
|
|
203
|
+
projectUrl: string,
|
|
204
|
+
snapshot: ComputerChatgptConversationObservation,
|
|
205
|
+
): string | undefined {
|
|
206
|
+
if (!/^supervisor:[a-z0-9]+$/.test(taskId) || !/^(?:fx|crpe)_[a-zA-Z0-9_-]{8,120}$/.test(effectId)) return;
|
|
207
|
+
if (snapshot.composerSelectorVersion !== 'visible-single-v2' || !snapshot.composerText?.trim()) return;
|
|
208
|
+
if (preflightComposerObservation(snapshot, effectId).classification !== 'unattributed') return;
|
|
209
|
+
let expectedUrl: URL;
|
|
210
|
+
let actualUrl: URL;
|
|
211
|
+
try { expectedUrl = new URL(projectUrl); actualUrl = new URL(snapshot.url); } catch { return; }
|
|
212
|
+
const projectRoute = (url: URL): string | undefined =>
|
|
213
|
+
/^\/g\/(g-p-[a-z0-9]+)(?:-[^/]+)?\/project\/?$/i.exec(url.pathname)?.[1]?.toLowerCase();
|
|
214
|
+
if (expectedUrl.origin !== 'https://chatgpt.com' || actualUrl.origin !== expectedUrl.origin
|
|
215
|
+
|| !projectRoute(expectedUrl) || projectRoute(actualUrl) !== projectRoute(expectedUrl)) return;
|
|
216
|
+
const observation = preflightComposerObservation(snapshot, effectId);
|
|
217
|
+
const base = join(controllerHome, 'supervisor', 'draft-backups');
|
|
218
|
+
const authorization = join(base, `authorization-${effectId}.json`);
|
|
219
|
+
let marker: unknown;
|
|
220
|
+
try { marker = JSON.parse(readFileSync(authorization, 'utf8')); } catch { return; }
|
|
221
|
+
if (!marker || typeof marker !== 'object' || Array.isArray(marker)) return;
|
|
222
|
+
const expected = marker as Record<string, unknown>;
|
|
223
|
+
if (expected.schemaVersion !== 1 || expected.taskId !== taskId
|
|
224
|
+
|| expected.effectId !== effectId || expected.projectUrl !== projectUrl
|
|
225
|
+
|| expected.fingerprint !== observation.fingerprint || expected.consent !== 'user-authorized-exact-backup') return;
|
|
226
|
+
mkdirSync(base, { recursive: true, mode: 0o700 });
|
|
227
|
+
const path = join(base, `composer-${effectId}-${observation.fingerprint}.json`);
|
|
228
|
+
const payload = { schemaVersion: 1, taskId, effectId, projectUrl, observedUrl: snapshot.url,
|
|
229
|
+
selectorVersion: snapshot.composerSelectorVersion, fingerprint: observation.fingerprint,
|
|
230
|
+
composerText: snapshot.composerText };
|
|
231
|
+
try {
|
|
232
|
+
writeFileSync(path, JSON.stringify(payload) + '\n', { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
233
|
+
} catch (error) {
|
|
234
|
+
// A repeated preflight must not overwrite the original evidence.
|
|
235
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') return;
|
|
236
|
+
try {
|
|
237
|
+
const existing = JSON.parse(readFileSync(path, 'utf8')) as typeof payload;
|
|
238
|
+
if (existing.fingerprint !== payload.fingerprint || existing.composerText !== payload.composerText
|
|
239
|
+
|| existing.taskId !== taskId || existing.observedUrl !== snapshot.url) return;
|
|
240
|
+
} catch { return; }
|
|
241
|
+
}
|
|
242
|
+
return path;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** An explicit on-device authorization + verified backup permits exactly one
|
|
246
|
+
* existing Chrome-extension editor replacement at prepare time. The dispatch
|
|
247
|
+
* still uses the unmodified canonical effect prompt and sole provider writer.
|
|
248
|
+
*/
|
|
249
|
+
export function authorizedDraftReplacementPrompt(
|
|
250
|
+
controllerHome: string, taskId: string, effectId: string, projectUrl: string,
|
|
251
|
+
snapshot: ComputerChatgptConversationObservation, prompt: string,
|
|
252
|
+
): string | undefined {
|
|
253
|
+
// Disk projection does not attest that this exact Chrome tab loaded the latest script.
|
|
254
|
+
// Never spend a one-use authorization before a matching read-only contract receipt.
|
|
255
|
+
if (snapshot.composerReplacementContract !== 'exact-backed-up-project-draft-v1') return;
|
|
256
|
+
const backup = backupAuthorizedBlockedComposer(controllerHome, taskId, effectId, projectUrl, snapshot);
|
|
257
|
+
if (!backup || !prompt.startsWith(`<<<FORGE_WORKFLOW_EFFECT_V1:${effectId}>>>`)) return;
|
|
258
|
+
const observation = preflightComposerObservation(snapshot, effectId);
|
|
259
|
+
let marker: unknown;
|
|
260
|
+
try { marker = JSON.parse(readFileSync(join(controllerHome, 'supervisor', 'draft-backups', `authorization-${effectId}.json`), 'utf8')); }
|
|
261
|
+
catch { return; }
|
|
262
|
+
if (!marker || typeof marker !== 'object' || Array.isArray(marker)) return;
|
|
263
|
+
const auth = marker as Record<string, unknown>;
|
|
264
|
+
if (auth.taskId !== taskId || auth.effectId !== effectId || auth.projectUrl !== projectUrl
|
|
265
|
+
|| auth.fingerprint !== observation.fingerprint || auth.consent !== 'user-authorized-exact-backup'
|
|
266
|
+
|| typeof auth.nonce !== 'string' || !/^[a-f0-9]{32}$/.test(auth.nonce)) return;
|
|
267
|
+
// The authorization is a single-use physical mutation capability, not a
|
|
268
|
+
// reusable password. Atomic exclusive creation remains durable across
|
|
269
|
+
// Runtime/extension restarts and concurrent preflights. A failed prepare
|
|
270
|
+
// consumes this token; the operator must explicitly authorize another one.
|
|
271
|
+
const consumption = join(controllerHome, 'supervisor', 'draft-backups',
|
|
272
|
+
`consumed-${effectId}-${auth.nonce}.json`);
|
|
273
|
+
try {
|
|
274
|
+
writeFileSync(consumption, JSON.stringify({ schemaVersion: 1, taskId, effectId,
|
|
275
|
+
projectUrl, fingerprint: observation.fingerprint, nonce: auth.nonce }) + '\n',
|
|
276
|
+
{ encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
277
|
+
} catch { return; }
|
|
278
|
+
return `<<<FORGE_DRAFT_REPLACE_V1:${observation.fingerprint}:${auth.nonce}>>>\n${prompt}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* An exact machine-owned draft can outlive its effect in a Project editor.
|
|
283
|
+
* Already-applied effects may be reflected by ChatGPT in other Project tabs;
|
|
284
|
+
* only the immutable full ledger prompt (allowing layout-only whitespace)
|
|
285
|
+
* qualifies. For never-sent STOPPED effects retain the original stricter
|
|
286
|
+
* preflight digest rule. Human, unknown-send and modified drafts never qualify.
|
|
287
|
+
*/
|
|
288
|
+
export function retiredBootstrapDraftReplacementPrompt(
|
|
289
|
+
control: WorkflowSupervisorControlPlane,
|
|
290
|
+
task: WorkflowSupervisorBrowserTask,
|
|
291
|
+
snapshot: ComputerChatgptConversationObservation,
|
|
292
|
+
prompt: string,
|
|
293
|
+
): string | undefined {
|
|
294
|
+
if (snapshot.composerSelectorVersion !== 'visible-single-v2' || !snapshot.composerText
|
|
295
|
+
|| snapshot.isGenerating || !prompt.startsWith('<<<FORGE_WORKFLOW_EFFECT_V1:')) return;
|
|
296
|
+
const draft = snapshot.composerText.replace(/\r\n?/g, '\n');
|
|
297
|
+
const match = /^<<<FORGE_WORKFLOW_EFFECT_V1:((?:fx|crpe)_[a-zA-Z0-9_-]{8,120})>>>/.exec(draft);
|
|
298
|
+
if (!match) return;
|
|
299
|
+
const staleEffect = control.getEffect(match[1]!);
|
|
300
|
+
if (!staleEffect || staleEffect.taskId === task.taskId) return;
|
|
301
|
+
const alreadyApplied = control.store.effectApplied(staleEffect.effectId);
|
|
302
|
+
// A live effect can only be retired after durable proof that its own Send
|
|
303
|
+
// already applied. This never treats an unknown Send as a safe draft.
|
|
304
|
+
if (alreadyApplied) {
|
|
305
|
+
if (snapshot.composerReplacementContract !== 'exact-backed-up-project-draft-v1') return;
|
|
306
|
+
let expected: URL; let actual: URL;
|
|
307
|
+
try { expected = new URL(control.bootstrapProjectUrl(task.taskId)); actual = new URL(snapshot.url); }
|
|
308
|
+
catch { return; }
|
|
309
|
+
const projectRoute = (url: URL) => /^\/g\/(g-p-[a-z0-9]+)(?:-[^/]+)?\/project\/?$/i.exec(url.pathname)?.[1]?.toLowerCase();
|
|
310
|
+
if (expected.origin !== 'https://chatgpt.com' || actual.origin !== expected.origin
|
|
311
|
+
|| !projectRoute(expected) || projectRoute(actual) !== projectRoute(expected)) return;
|
|
312
|
+
const canonical = staleEffect.prompt.replace(/\r\n?/g, '\n');
|
|
313
|
+
if (draft.replace(/\s+/g, ' ').trim() !== canonical.replace(/\s+/g, ' ').trim()) return;
|
|
314
|
+
} else {
|
|
315
|
+
if (control.effectDispatchBudget(staleEffect.effectId).generations !== 0) return;
|
|
316
|
+
const oldTask = control.getTask(staleEffect.taskId);
|
|
317
|
+
const retired = oldTask && control.getTaskByConversationId(oldTask.conversationId);
|
|
318
|
+
if (retired?.terminal !== 'STOPPED'
|
|
319
|
+
|| control.bootstrapProjectUrl(staleEffect.taskId) !== control.bootstrapProjectUrl(task.taskId)) return;
|
|
320
|
+
}
|
|
321
|
+
const attested = control.preSendComposerObservation(staleEffect.effectId);
|
|
322
|
+
// Both proofs are exact: the immutable effect text, or the previously
|
|
323
|
+
// attested pre-Send editor bytes. The older extension could synthesize an
|
|
324
|
+
// extra newline inside a URL; the corrected extension reads the canonical
|
|
325
|
+
// text without it. Require the persisted no-click preflight in either case.
|
|
326
|
+
const fingerprint = sha256('preflight-composer-v1:\0' + draft).slice(0, 24);
|
|
327
|
+
const exactSource = draft === staleEffect.prompt.replace(/\r\n?/g, '\n');
|
|
328
|
+
const exactPreflight = attested?.fingerprint === fingerprint && attested.chars === draft.length;
|
|
329
|
+
if (!alreadyApplied && (!attested || (!exactSource && !exactPreflight))) return;
|
|
330
|
+
return '<<<FORGE_RETIRED_DRAFT_V1:' + staleEffect.effectId + ':' + fingerprint + '>>>\n' + prompt;
|
|
331
|
+
}
|
|
332
|
+
|
|
170
333
|
function exactConversation(snapshot: ComputerChatgptConversationObservation, task: WorkflowSupervisorBrowserTask): boolean {
|
|
171
334
|
try { return parseChatgptConversationIdentity(snapshot.url).conversationId === task.conversationId; }
|
|
172
335
|
catch { return false; }
|
|
@@ -222,6 +385,8 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
222
385
|
private readonly projectedTaskStalls = new Map<string, Omit<WorkflowSupervisorStallProjection, 'taskId' | 'state' | 'observedAt'>>();
|
|
223
386
|
/** Spacing for read-only observation of a turn whose bounded resume is exhausted. */
|
|
224
387
|
private readonly awaitingReceiptObservedAtMs = new Map<string, number>();
|
|
388
|
+
/** Rebuildable idle-grace projection; Supervisor effects remain the sole authority. */
|
|
389
|
+
private readonly idleResourceSinceMs = new Map<string, number>();
|
|
225
390
|
|
|
226
391
|
constructor(
|
|
227
392
|
private readonly control: WorkflowSupervisorControlPlane,
|
|
@@ -253,7 +418,49 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
253
418
|
}
|
|
254
419
|
}
|
|
255
420
|
|
|
256
|
-
|
|
421
|
+
/** Rebuildable de-duplication only; SQLite events and CAS own the decision. */
|
|
422
|
+
private readonly automaticMigrationAttempted = new Map<string, { key: string; atMs: number }>();
|
|
423
|
+
/** Read-only provider progress, not another durable effect/Send ledger. */
|
|
424
|
+
private readonly stalledProviderObservations = new Map<string, { fingerprint: string; changedAtMs: number; failedProbeAtMs: number }>();
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Fail over a proven repeatedly unusable conversation, not a provider-wide
|
|
428
|
+
* outage or an outcome-unknown Send. The canonical migration transaction
|
|
429
|
+
* enforces the existing maximum of two unsuccessful conversation moves.
|
|
430
|
+
*/
|
|
431
|
+
private maybeAutoMigrate(task: WorkflowSupervisorBrowserTask): boolean {
|
|
432
|
+
if (task.conversationId.startsWith('bootstrap:')
|
|
433
|
+
|| chatgptProviderBackpressureRemainingMs(this.deps.providerScopeKey, this.deps.nowMs()) > 0) return false;
|
|
434
|
+
const evidence = this.control.store.automaticConversationFailureEvidence(task.taskId);
|
|
435
|
+
if (evidence.count < 3 || evidence.latestEventId <= 0) return false;
|
|
436
|
+
const key = `${task.taskId}:${task.conversationId}:${evidence.latestEventId}`;
|
|
437
|
+
const attempt = this.automaticMigrationAttempted.get(task.taskId);
|
|
438
|
+
if (attempt?.key === key && this.deps.nowMs() - attempt.atMs < TASK_TARGET_RETRY_MAX_MS) return false;
|
|
439
|
+
this.automaticMigrationAttempted.set(task.taskId, { key, atMs: this.deps.nowMs() });
|
|
440
|
+
try {
|
|
441
|
+
const migrated = this.control.migrateConversation({
|
|
442
|
+
taskId: task.taskId,
|
|
443
|
+
expectedConversationId: task.conversationId,
|
|
444
|
+
fresh: true,
|
|
445
|
+
requestId: `automatic-delivery-fuse:${task.conversationId}:${evidence.latestEventId}`,
|
|
446
|
+
reason: `The same Supervisor conversation failed ${evidence.count} task-local delivery/progress attempts without an applied effect or completion. The prior conversation is not reused; the exact effect journal and existing two-migration cap remain authoritative.`,
|
|
447
|
+
authorizedBy: 'supervisor-automatic-delivery-fuse',
|
|
448
|
+
});
|
|
449
|
+
if (migrated.migrated) {
|
|
450
|
+
this.clearTaskFailure(task.taskId);
|
|
451
|
+
this.awaitingReceiptObservedAtMs.delete(task.taskId);
|
|
452
|
+
}
|
|
453
|
+
return migrated.migrated;
|
|
454
|
+
} catch (error) {
|
|
455
|
+
// A pending unknown send, exhausted migration budget, or stale task is a
|
|
456
|
+
// true safety barrier: do not replay it or bypass the canonical store.
|
|
457
|
+
const code = consumerFailureCode(error, 'WORKFLOW_SUPERVISOR_AUTOMATIC_MIGRATION_DENIED');
|
|
458
|
+
this.noteFailure(code, task);
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private noteTaskFailure(code: string, task: WorkflowSupervisorBrowserTask, effectId?: string, countTowardFuse = true): void {
|
|
257
464
|
const nowMs = this.deps.nowMs();
|
|
258
465
|
const previous = this.taskTransportFailures.get(task.taskId);
|
|
259
466
|
const failure: TaskTransportFailure = {
|
|
@@ -275,6 +482,17 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
275
482
|
});
|
|
276
483
|
}
|
|
277
484
|
this.taskTransportFailures.set(task.taskId, failure);
|
|
485
|
+
// Account-wide cooldown is not evidence that this *conversation* failed.
|
|
486
|
+
if (!countTowardFuse || chatgptProviderBackpressureRemainingMs(this.deps.providerScopeKey, nowMs) > 0) return;
|
|
487
|
+
// Preflight rejections are already one durable failed attempt in the
|
|
488
|
+
// Supervisor effect ledger; counting them again would double the fuse.
|
|
489
|
+
if (code !== SUPERVISOR_PRE_SEND_BLOCKED) {
|
|
490
|
+
this.control.store.recordBrowserDeliveryAttemptFailure({
|
|
491
|
+
taskId: task.taskId, conversationId: task.conversationId, code,
|
|
492
|
+
...(effectId ? { effectId } : {}),
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
this.maybeAutoMigrate(task);
|
|
278
496
|
}
|
|
279
497
|
|
|
280
498
|
private clearTaskFailure(taskId: string, effectId?: string): void {
|
|
@@ -290,10 +508,14 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
290
508
|
}
|
|
291
509
|
|
|
292
510
|
private noteTargetUnavailable(task: WorkflowSupervisorBrowserTask, effectId: string | undefined, failure: { code: string; humanAction?: 'login' | 'grant_permission' }): void {
|
|
293
|
-
//
|
|
294
|
-
|
|
511
|
+
// A user login/grant problem cannot be fixed by a different conversation.
|
|
512
|
+
if (failure.humanAction) {
|
|
513
|
+
this.noteTaskFailure(failure.code, task, effectId, false);
|
|
514
|
+
this.deps.requestHumanAction?.({ taskId: task.taskId, ...(effectId ? { effectId } : {}), action: failure.humanAction, code: failure.code });
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
// Task-local failures are spaced independently of unrelated conversations.
|
|
295
518
|
this.noteTaskFailure(failure.code, task, effectId);
|
|
296
|
-
if (failure.humanAction) this.deps.requestHumanAction?.({ taskId: task.taskId, ...(effectId ? { effectId } : {}), action: failure.humanAction, code: failure.code });
|
|
297
519
|
}
|
|
298
520
|
|
|
299
521
|
private syncTaskStallProjections(): void {
|
|
@@ -335,6 +557,14 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
335
557
|
catch (error) { this.deps.onError(error); }
|
|
336
558
|
this.projectedTaskStalls.delete(taskId);
|
|
337
559
|
}
|
|
560
|
+
this.pruneAutomaticRecoveryProjections(activeTaskIds);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private pruneAutomaticRecoveryProjections(activeTaskIds: ReadonlySet<string>): void {
|
|
564
|
+
for (const id of this.automaticMigrationAttempted.keys())
|
|
565
|
+
if (!activeTaskIds.has(id)) this.automaticMigrationAttempted.delete(id);
|
|
566
|
+
for (const id of this.stalledProviderObservations.keys())
|
|
567
|
+
if (!activeTaskIds.has(id)) this.stalledProviderObservations.delete(id);
|
|
338
568
|
}
|
|
339
569
|
|
|
340
570
|
private dueCommand(nowMs: number): WorkflowSupervisorConsumerStatus['dueCommand'] {
|
|
@@ -403,7 +633,7 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
403
633
|
this.timer = undefined;
|
|
404
634
|
await this.inflight?.catch(() => undefined);
|
|
405
635
|
await this.deps.targetPort.close().catch(() => undefined);
|
|
406
|
-
this.observedAssistant.clear(); this.providerFailureSeen.clear(); this.providerFailureAwaitingClear.clear(); this.freshSendCheckedAt.clear(); this.taskTransportFailures.clear(); this.projectedTaskStalls.clear(); this.awaitingReceiptObservedAtMs.clear();
|
|
636
|
+
this.observedAssistant.clear(); this.providerFailureSeen.clear(); this.providerFailureAwaitingClear.clear(); this.freshSendCheckedAt.clear(); this.taskTransportFailures.clear(); this.projectedTaskStalls.clear(); this.awaitingReceiptObservedAtMs.clear(); this.idleResourceSinceMs.clear(); this.automaticMigrationAttempted.clear(); this.stalledProviderObservations.clear();
|
|
407
637
|
}
|
|
408
638
|
|
|
409
639
|
async runOnce(): Promise<void> {
|
|
@@ -412,7 +642,29 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
412
642
|
// Preserve fresh-send fairness across ticks. serviceFreshSend prunes
|
|
413
643
|
// retired effect ids; resetting here repeatedly favors the first task.
|
|
414
644
|
this.lastRunTransportUnavailable = false;
|
|
645
|
+
// One Supervisor terminal-event authority. A Work closed while this
|
|
646
|
+
// consumer was offline cannot keep delivering a stale effect on restart.
|
|
647
|
+
this.control.reconcileTerminalWorkBoundTasks();
|
|
415
648
|
this.syncTaskStallProjections();
|
|
649
|
+
const previouslyObservedTasks = this.control.browserTasks();
|
|
650
|
+
// Read-only watchdog for a pre-send blocked task: after each bounded
|
|
651
|
+
// interval with no applied effect or completion, record a failed progress
|
|
652
|
+
// check in the same journal. This is not a Send/retry attempt.
|
|
653
|
+
for (const task of previouslyObservedTasks) {
|
|
654
|
+
const stall = this.control.taskStall(task.taskId);
|
|
655
|
+
if (stall.state === 'spaced' && stall.reason === 'pre_send_infrastructure_blocked') {
|
|
656
|
+
const evidence = this.control.store.automaticConversationFailureEvidence(task.taskId);
|
|
657
|
+
if (evidence.count > 0 && evidence.count < 3
|
|
658
|
+
&& this.deps.nowMs() - evidence.lastFailureAtMs >= AWAITING_RECEIPT_OBSERVATION_MS
|
|
659
|
+
&& chatgptProviderBackpressureRemainingMs(this.deps.providerScopeKey, this.deps.nowMs()) === 0) {
|
|
660
|
+
this.control.store.recordBrowserDeliveryAttemptFailure({ taskId: task.taskId,
|
|
661
|
+
conversationId: task.conversationId, effectId: stall.effectId, code: 'WORKFLOW_SUPERVISOR_NO_SEND_PROGRESS_WATCHDOG' });
|
|
662
|
+
}
|
|
663
|
+
this.maybeAutoMigrate(task);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// The watchdog may have changed the exact Task conversation identity.
|
|
667
|
+
// Never process a stale pre-migration browser task in this tick.
|
|
416
668
|
const tasks = this.control.browserTasks();
|
|
417
669
|
this.lastRunHadTasks = tasks.length > 0;
|
|
418
670
|
let inventory;
|
|
@@ -428,15 +680,44 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
428
680
|
...(conversation.title ? { title: conversation.title } : {}), ...(conversation.projectTitle ? { projectTitle: conversation.projectTitle } : {}),
|
|
429
681
|
...(conversation.projectUrl ? { projectUrl: conversation.projectUrl } : {}), ...(conversation.isCurrent ? { is_current: true } : {}) }));
|
|
430
682
|
if (!inventory.complete && tasks.length > 0) {
|
|
431
|
-
|
|
683
|
+
// Partial cross-browser discovery is degraded evidence, but must not
|
|
684
|
+
// throttle an independently healthy provider's exact-target delivery.
|
|
685
|
+
this.lastRunTransportUnavailable = inventory.providerInventoryReady !== true;
|
|
432
686
|
this.noteFailure('WORKFLOW_SUPERVISOR_NATIVE_INVENTORY_INCOMPLETE');
|
|
433
687
|
} else if (inventory.complete) {
|
|
434
688
|
this.clearTransportFailure();
|
|
435
689
|
}
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
|
|
690
|
+
// Retire only disposable provider attachments. The durable Supervisor
|
|
691
|
+
// effect ledger (not textual DONE or a browser tab) proves protection: a
|
|
692
|
+
// queued/reconciling command and an applied turn awaiting its receipt both
|
|
693
|
+
// remain attached even during cooldown. A terminal task is absent here.
|
|
694
|
+
const taskResourceKeys = new Set<string>();
|
|
695
|
+
const protectedResourceKeys = new Set<string>();
|
|
696
|
+
for (const task of this.control.listTasks(true)) {
|
|
697
|
+
const key = task.conversationId.startsWith('bootstrap:')
|
|
698
|
+
? `chatgpt.bootstrap:${task.taskId}`
|
|
699
|
+
: `chatgpt.conversation:${task.conversationId}`;
|
|
700
|
+
taskResourceKeys.add(key);
|
|
701
|
+
// Eligibility is time-dependent; an unresolved effect still owns its
|
|
702
|
+
// exact attachment during retry spacing or a blocked preparation.
|
|
703
|
+
if (this.control.store.currentUnappliedEffect(task.taskId)
|
|
704
|
+
|| this.control.store.hasAppliedEffectAwaitingCompletion(task.taskId)) protectedResourceKeys.add(key);
|
|
705
|
+
}
|
|
706
|
+
for (const key of this.idleResourceSinceMs.keys()) {
|
|
707
|
+
if (!taskResourceKeys.has(key) || protectedResourceKeys.has(key)) this.idleResourceSinceMs.delete(key);
|
|
708
|
+
}
|
|
709
|
+
const now = this.deps.nowMs();
|
|
710
|
+
for (const key of taskResourceKeys) {
|
|
711
|
+
if (!protectedResourceKeys.has(key) && !this.idleResourceSinceMs.has(key)) this.idleResourceSinceMs.set(key, now);
|
|
712
|
+
}
|
|
713
|
+
// Enforce a bounded idle attachment budget. This does not cap truly active
|
|
714
|
+
// effects: they cannot be retired merely because tab pressure is high.
|
|
715
|
+
const graceKeys = [...this.idleResourceSinceMs.entries()]
|
|
716
|
+
.filter(([key, since]) => taskResourceKeys.has(key) && now - since < SUPERVISOR_IDLE_TAB_GRACE_MS)
|
|
717
|
+
.sort((left, right) => right[1] - left[1])
|
|
718
|
+
.slice(0, SUPERVISOR_IDLE_TAB_RETAIN_LIMIT)
|
|
719
|
+
.map(([key]) => key);
|
|
720
|
+
const activeResourceKeys = [...new Set([...protectedResourceKeys, ...graceKeys])];
|
|
440
721
|
await this.deps.targetPort.cleanup(activeResourceKeys).catch((error) => this.deps.onError(error));
|
|
441
722
|
for (const task of tasks) {
|
|
442
723
|
if (this.closed) break;
|
|
@@ -486,7 +767,15 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
486
767
|
const transportFailure = this.taskTransportFailures.get(task.taskId);
|
|
487
768
|
// Derived from the durable effect log, not this adapter's process memory.
|
|
488
769
|
const stall = this.control.taskStall(task.taskId);
|
|
489
|
-
if (stall.state === 'spaced' && stall.reason === 'pre_send_infrastructure_blocked')
|
|
770
|
+
if (stall.state === 'spaced' && stall.reason === 'pre_send_infrastructure_blocked') {
|
|
771
|
+
// A blocked new recovery cannot authorize a Send, but it must not hide
|
|
772
|
+
// the completed final reply of its already-applied source effect.
|
|
773
|
+
// Probe read-only at the existing bounded receipt cadence.
|
|
774
|
+
if (!this.control.store.hasAppliedEffectAwaitingCompletion(task.taskId)) return;
|
|
775
|
+
const checkedAt = this.awaitingReceiptObservedAtMs.get(task.taskId) ?? 0;
|
|
776
|
+
if (this.deps.nowMs() - checkedAt < AWAITING_RECEIPT_OBSERVATION_MS) return;
|
|
777
|
+
this.awaitingReceiptObservedAtMs.set(task.taskId, this.deps.nowMs());
|
|
778
|
+
}
|
|
490
779
|
if (transportFailure
|
|
491
780
|
&& TASK_TARGET_SPACED_FAILURE_CODES.has(transportFailure.code)
|
|
492
781
|
&& this.deps.nowMs() - transportFailure.observedAtMs < taskTargetRetryDelayMs(transportFailure.streak)) return;
|
|
@@ -510,7 +799,12 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
510
799
|
// strands the task.
|
|
511
800
|
if (chatgptProviderBackpressureRemainingMs(this.deps.providerScopeKey, this.deps.nowMs()) > 0
|
|
512
801
|
&& !this.conversations.some((conversation) => conversation.conversation_id === task.conversationId)) return;
|
|
513
|
-
|
|
802
|
+
// Only a new, explicitly admitted send may open a missing tab. A late
|
|
803
|
+
// receipt probe or unknown-effect reconciliation uses existing attachments
|
|
804
|
+
// and never resurrects a manually closed conversation by observation.
|
|
805
|
+
const ensured = await this.deps.targetPort.ensureExact(identityForTask(task), {
|
|
806
|
+
createIfMissing: poll.command?.mode === 'send',
|
|
807
|
+
});
|
|
514
808
|
if (ensured.state !== 'ready') { this.noteTargetUnavailable(task, poll.command?.effectId, ensured.failure); return; }
|
|
515
809
|
const target = ensured.target;
|
|
516
810
|
const snapshot = ensured.observation ?? await target.observe({ includeUserHistory: false, includePageText: false });
|
|
@@ -522,6 +816,26 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
522
816
|
this.conversations.push({ conversation_id: task.conversationId, canonical_url: task.conversationUrl,
|
|
523
817
|
...(snapshot.title.trim() ? { title: snapshot.title.trim().slice(0, 512) } : {}), ...projectMetadataFromConversationUrl(snapshot.url) });
|
|
524
818
|
const providerBusy = snapshot.isGenerating;
|
|
819
|
+
// Recovery-exhausted tasks can otherwise remain visibly stalled without a
|
|
820
|
+
// new exception. Three spaced, unchanged exact-page observations trip the
|
|
821
|
+
// same durable fuse; changing assistant activity resets this observation.
|
|
822
|
+
if (!poll.command && this.control.taskStall(task.taskId).state === 'provider_resume_exhausted') {
|
|
823
|
+
const fingerprint = sha256(JSON.stringify({ text: snapshot.latestAssistantResponse,
|
|
824
|
+
activity: snapshot.providerActivityText, failure: snapshot.providerFailureText, generating: providerBusy }));
|
|
825
|
+
const previous = this.stalledProviderObservations.get(task.taskId);
|
|
826
|
+
if (!previous || previous.fingerprint !== fingerprint) {
|
|
827
|
+
this.stalledProviderObservations.set(task.taskId, {
|
|
828
|
+
fingerprint, changedAtMs: this.deps.nowMs(), failedProbeAtMs: this.deps.nowMs(),
|
|
829
|
+
});
|
|
830
|
+
} else if (this.deps.nowMs() - previous.changedAtMs >= 2 * AWAITING_RECEIPT_OBSERVATION_MS
|
|
831
|
+
&& this.deps.nowMs() - previous.failedProbeAtMs >= AWAITING_RECEIPT_OBSERVATION_MS
|
|
832
|
+
&& chatgptProviderBackpressureRemainingMs(this.deps.providerScopeKey, this.deps.nowMs()) === 0) {
|
|
833
|
+
previous.failedProbeAtMs = this.deps.nowMs();
|
|
834
|
+
this.control.store.recordBrowserDeliveryAttemptFailure({ taskId: task.taskId,
|
|
835
|
+
conversationId: task.conversationId, code: 'WORKFLOW_SUPERVISOR_PROVIDER_NO_PROGRESS_WATCHDOG' });
|
|
836
|
+
if (this.maybeAutoMigrate(task)) return;
|
|
837
|
+
}
|
|
838
|
+
} else this.stalledProviderObservations.delete(task.taskId);
|
|
525
839
|
const latestRoleStillUser = snapshot.latestTurnRole === 'user';
|
|
526
840
|
// Capacity is only authoritative on dedicated live provider UI. A model
|
|
527
841
|
// can quote the same phrase in an ordinary assistant turn; historical
|
|
@@ -556,7 +870,11 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
556
870
|
let providerBackpressureMs = chatgptProviderBackpressureRemainingMs(this.deps.providerScopeKey, this.deps.nowMs());
|
|
557
871
|
if (poll.command?.mode !== 'reconcile' && providerFailureCode === CHATGPT_AUTOMATION_RATE_LIMITED && providerBackpressureMs > 0) return;
|
|
558
872
|
let missingReceiptForAppliedTurn = false;
|
|
559
|
-
|
|
873
|
+
// A queued, never-dispatched recovery still leaves its source reply eligible
|
|
874
|
+
// for completion. Consume that reply before preparing an unnecessary Send.
|
|
875
|
+
const unsentRecovery = poll.command?.mode === 'send'
|
|
876
|
+
&& poll.command.kind === 'recovery' && poll.command.dispatchGeneration === 1;
|
|
877
|
+
if ((!poll.command || unsentRecovery) && !providerBusy && snapshot.latestTurnRole === 'assistant' && snapshot.latestAssistantResponse.trim()) {
|
|
560
878
|
const responseFingerprint = sha256(snapshot.latestAssistantResponse);
|
|
561
879
|
const pending = this.control.taskStall(task.taskId);
|
|
562
880
|
// Text-only replies are not receipts. Observe their stable idle digest,
|
|
@@ -631,14 +949,45 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
631
949
|
});
|
|
632
950
|
return;
|
|
633
951
|
}
|
|
952
|
+
if (preflightComposerObservation(beforeSend, command.effectId).classification === 'unattributed'
|
|
953
|
+
&& beforeSend.composerReplacementContract !== 'exact-backed-up-project-draft-v1') {
|
|
954
|
+
// Not a draft conflict or a Send attempt: Chrome retained a stale content script.
|
|
955
|
+
this.control.recordProviderPreparationFailure({ taskId: task.taskId, effectId: command.effectId,
|
|
956
|
+
reasonCode: 'COMPUTER_CHATGPT_BOOTSTRAP_LOADED_SCRIPT_STALE',
|
|
957
|
+
composerObservation: preflightComposerObservation(beforeSend, command.effectId) });
|
|
958
|
+
this.noteTaskFailure(SUPERVISOR_PRE_SEND_BLOCKED, task, command.effectId);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
634
961
|
let prepared: Awaited<ReturnType<ComputerChatgptConversationTarget['prepare']>>;
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
962
|
+
const exactReplacement = authorizedDraftReplacementPrompt(this.deps.providerScopeKey, task.taskId,
|
|
963
|
+
command.effectId, this.control.bootstrapProjectUrl(task.taskId), beforeSend, command.prompt);
|
|
964
|
+
const retiredReplacement = exactReplacement ? undefined
|
|
965
|
+
: retiredBootstrapDraftReplacementPrompt(this.control, task, beforeSend, command.prompt);
|
|
966
|
+
try {
|
|
967
|
+
prepared = await target.prepare(exactReplacement ?? retiredReplacement ?? command.prompt);
|
|
968
|
+
if (!prepared.ready && prepared.reasonCode === 'composer_not_empty'
|
|
969
|
+
&& !exactReplacement && !retiredReplacement) {
|
|
970
|
+
// ChatGPT may hydrate an old Project draft after the initial tab-ready
|
|
971
|
+
// observation. A refused prepare never clicked Send. Read the SAME
|
|
972
|
+
// pinned target once more and offer a second prepare only when the
|
|
973
|
+
// STOPPED effect's canonical ledger proves this exact editor safe.
|
|
974
|
+
const hydrated = await target.observe({ includeUserHistory: false, includePageText: false }).catch(() => undefined);
|
|
975
|
+
const verifiedRetirement = hydrated
|
|
976
|
+
? retiredBootstrapDraftReplacementPrompt(this.control, task, hydrated, command.prompt) : undefined;
|
|
977
|
+
if (verifiedRetirement) prepared = await target.prepare(verifiedRetirement);
|
|
978
|
+
}
|
|
979
|
+
} catch (error) {
|
|
980
|
+
this.control.recordProviderPreparationFailure({ taskId: task.taskId, effectId: command.effectId, reasonCode: consumerFailureCode(error, 'COMPUTER_PREPARE_FAILED'), composerObservation: preflightComposerObservation(beforeSend, command.effectId) });
|
|
638
981
|
this.noteTaskFailure(SUPERVISOR_PRE_SEND_BLOCKED, task, command.effectId); this.deps.onError(error); return;
|
|
639
982
|
}
|
|
640
983
|
if (!prepared.ready) {
|
|
641
|
-
|
|
984
|
+
const observed = await target.observe({ includeUserHistory: false, includePageText: false }).catch(() => beforeSend);
|
|
985
|
+
if (prepared.reasonCode === 'composer_not_empty') {
|
|
986
|
+
// Snapshot backups require an exact, separately authorized fingerprint.
|
|
987
|
+
backupAuthorizedBlockedComposer(this.deps.providerScopeKey, task.taskId, command.effectId,
|
|
988
|
+
this.control.bootstrapProjectUrl(task.taskId), observed);
|
|
989
|
+
}
|
|
990
|
+
this.control.recordProviderPreparationFailure({ taskId: task.taskId, effectId: command.effectId, reasonCode: prepared.reasonCode, composerObservation: preflightComposerObservation(observed, command.effectId) });
|
|
642
991
|
this.noteTaskFailure(SUPERVISOR_PRE_SEND_BLOCKED, task, command.effectId);
|
|
643
992
|
this.deps.onError(new Error(prepared.reasonCode));
|
|
644
993
|
return;
|
|
@@ -646,10 +995,12 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
646
995
|
this.clearTaskFailure(task.taskId, command.effectId);
|
|
647
996
|
if (!this.control.bootstrapBeginEffect({ taskId: task.taskId, effectId: command.effectId, dispatchId: `bootstrap-${randomUUID()}`, dispatchGeneration: command.dispatchGeneration })) return;
|
|
648
997
|
try {
|
|
649
|
-
const dispatch = await withChatgptProviderDispatchLane(this.deps.providerScopeKey, () => target.dispatch(command.prompt
|
|
998
|
+
const dispatch = await withChatgptProviderDispatchLane(this.deps.providerScopeKey, () => target.dispatch(command.prompt),
|
|
650
999
|
(result) => result.mutation === 'attempted' ? { providerAccepted: result.confirmed === true } : { code: result.reasonCode, message: result.reasonCode });
|
|
651
1000
|
if (dispatch.mutation === 'not_attempted') {
|
|
652
|
-
this.control.bootstrapObserveEffect({ taskId: task.taskId, effectId: command.effectId, observationId: `bootstrap-${randomUUID()}`, outcome: 'not_applied', evidence: {
|
|
1001
|
+
this.control.bootstrapObserveEffect({ taskId: task.taskId, effectId: command.effectId, observationId: `bootstrap-${randomUUID()}`, outcome: 'not_applied', evidence: { dispatch_mutation: 'not_attempted', send_clicked: false, reason: dispatch.reasonCode } });
|
|
1002
|
+
this.control.recordProviderPreparationFailure({ taskId: task.taskId, effectId: command.effectId,
|
|
1003
|
+
reasonCode: dispatch.reasonCode ?? 'COMPUTER_CHATGPT_DISPATCH_NOT_ATTEMPTED', composerObservation: preflightComposerObservation(beforeSend, command.effectId) });
|
|
653
1004
|
await this.deps.targetPort.release(target.targetId).catch(() => undefined);
|
|
654
1005
|
return;
|
|
655
1006
|
}
|
|
@@ -689,8 +1040,17 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
689
1040
|
let identity;
|
|
690
1041
|
try { identity = parseChatgptConversationIdentity(snapshot.url); } catch { observeUnknown('bootstrap_conversation_identity_unavailable'); return; }
|
|
691
1042
|
if (!snapshotTargetMarkerPresent(snapshot, command.effectId)) { observeUnknown('bootstrap_effect_marker_not_observed'); return; }
|
|
692
|
-
|
|
693
|
-
|
|
1043
|
+
// Marker recovery may already return a Computer target with this exact
|
|
1044
|
+
// conversation identity. Promoting it again would tombstone its own binding.
|
|
1045
|
+
// Only a still-bootstrap target needs the provider's identity transition.
|
|
1046
|
+
if (match.target.identity.namespace === 'chatgpt.conversation') {
|
|
1047
|
+
if (match.target.identity.conversationId !== identity.conversationId) {
|
|
1048
|
+
observeUnknown('bootstrap_reconcile_target_identity_mismatch'); return;
|
|
1049
|
+
}
|
|
1050
|
+
} else {
|
|
1051
|
+
const promoted = await this.deps.targetPort.promoteBootstrap(match.target.targetId, { namespace: 'chatgpt.conversation', conversationId: identity.conversationId, canonicalUrl: identity.canonicalUrl });
|
|
1052
|
+
if (promoted.state !== 'ready') { observeUnknown(promoted.failure.code); return; }
|
|
1053
|
+
}
|
|
694
1054
|
this.control.bindBootstrapConversation({ taskId: task.taskId, conversationId: identity.conversationId, conversationUrl: identity.canonicalUrl });
|
|
695
1055
|
this.control.bootstrapObserveEffect({ taskId: task.taskId, effectId: command.effectId, observationId: `bootstrap-reconcile-${randomUUID()}`, outcome: 'applied' });
|
|
696
1056
|
this.clearTaskFailure(task.taskId, command.effectId);
|
|
@@ -707,14 +1067,19 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
707
1067
|
if (mode === 'send') {
|
|
708
1068
|
let prepared: Awaited<ReturnType<ComputerChatgptConversationTarget['prepare']>>;
|
|
709
1069
|
try { prepared = await target.prepare(command.prompt, {
|
|
710
|
-
...(command.kind === 'recovery' ? { mode: 'recover' as const } : {}),
|
|
1070
|
+
...(command.kind === 'recovery' ? { mode: 'recover' as const } : {}),
|
|
711
1071
|
}); }
|
|
712
1072
|
catch (error) {
|
|
713
1073
|
this.control.recordProviderPreparationFailure({ taskId: task.taskId, effectId: command.effectId, reasonCode: consumerFailureCode(error, 'COMPUTER_PREPARE_FAILED') });
|
|
714
1074
|
this.noteTaskFailure(SUPERVISOR_PRE_SEND_BLOCKED, task, command.effectId); this.deps.onError(error); return;
|
|
715
1075
|
}
|
|
716
1076
|
if (!prepared.ready) {
|
|
717
|
-
|
|
1077
|
+
// The exact recovery target still exposes its Stop control. That is a
|
|
1078
|
+
// provider-turn observation, not an infrastructure preparation failure:
|
|
1079
|
+
// preserve this one reserved recovery effect without spending a send
|
|
1080
|
+
// generation or the bounded preflight-failure budget.
|
|
1081
|
+
if (command.kind === 'recovery' && prepared.reasonCode === 'provider_recovery_stop_pending') return;
|
|
1082
|
+
this.control.recordProviderPreparationFailure({ taskId: task.taskId, effectId: command.effectId, reasonCode: prepared.reasonCode });
|
|
718
1083
|
this.noteTaskFailure(SUPERVISOR_PRE_SEND_BLOCKED, task, command.effectId);
|
|
719
1084
|
this.deps.onError(new Error(prepared.reasonCode));
|
|
720
1085
|
return;
|
|
@@ -726,7 +1091,7 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
726
1091
|
if (!begin.started) mode = 'reconcile';
|
|
727
1092
|
}
|
|
728
1093
|
if (mode === 'reconcile') {
|
|
729
|
-
const exact = normalize(snapshot.latestUserText) === normalize(command.prompt);
|
|
1094
|
+
const exact = normalize(snapshot.latestUserText) === normalize(command.prompt.replace(/^@forge\s+(?=<<<FORGE_WORKFLOW_EFFECT_V1:(?:fx|crpe)_[a-zA-Z0-9_-]{8,120}>>>)/, ''));
|
|
730
1095
|
const markerPresent = snapshotTargetMarkerPresent(snapshot, command.effectId);
|
|
731
1096
|
if (exact || markerPresent) {
|
|
732
1097
|
this.control.browserObserveEffect({ conversationId: command.conversationId, conversationUrl: command.conversationUrl, effectId: command.effectId,
|
|
@@ -743,7 +1108,7 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
743
1108
|
}
|
|
744
1109
|
let dispatch: Awaited<ReturnType<ComputerChatgptConversationTarget['dispatch']>>;
|
|
745
1110
|
try {
|
|
746
|
-
dispatch = await withChatgptProviderDispatchLane(this.deps.providerScopeKey, () => target.dispatch(command.prompt, { ...(command.kind === 'recovery' ? { mode: 'recover' as const } : {})
|
|
1111
|
+
dispatch = await withChatgptProviderDispatchLane(this.deps.providerScopeKey, () => target.dispatch(command.prompt, { ...(command.kind === 'recovery' ? { mode: 'recover' as const } : {}) }),
|
|
747
1112
|
(result) => result.mutation === 'attempted' ? { providerAccepted: result.confirmed === true } : { code: result.reasonCode, message: result.reasonCode });
|
|
748
1113
|
} catch (error) {
|
|
749
1114
|
this.control.browserObserveEffect({ conversationId: command.conversationId, conversationUrl: command.conversationUrl, effectId: command.effectId,
|
|
@@ -758,19 +1123,19 @@ export class WorkflowSupervisorNativeBrowserAdapter {
|
|
|
758
1123
|
}
|
|
759
1124
|
if (dispatch.confirmed) {
|
|
760
1125
|
this.control.browserObserveEffect({ conversationId: command.conversationId, conversationUrl: command.conversationUrl, effectId: command.effectId,
|
|
761
|
-
observationId: `computer-observe-${randomUUID()}`, outcome: 'applied', evidence: { surface: 'computer-chatgpt-target', target_id: target.targetId, provider_confirmed: true
|
|
1126
|
+
observationId: `computer-observe-${randomUUID()}`, outcome: 'applied', evidence: { surface: 'computer-chatgpt-target', target_id: target.targetId, provider_confirmed: true } });
|
|
762
1127
|
return;
|
|
763
1128
|
}
|
|
764
1129
|
let exact = false; let markerPresent = false;
|
|
765
1130
|
for (let attempt = 1; attempt <= MAX_LOCAL_OBSERVATION_ATTEMPTS; attempt += 1) {
|
|
766
1131
|
snapshot = await target.observe({ includeUserHistory: true, includePageText: true });
|
|
767
|
-
exact = normalize(snapshot.latestUserText) === normalize(command.prompt); markerPresent = snapshotTargetMarkerPresent(snapshot, command.effectId);
|
|
1132
|
+
exact = normalize(snapshot.latestUserText) === normalize(command.prompt.replace(/^@forge\s+(?=<<<FORGE_WORKFLOW_EFFECT_V1:(?:fx|crpe)_[a-zA-Z0-9_-]{8,120}>>>)/, '')); markerPresent = snapshotTargetMarkerPresent(snapshot, command.effectId);
|
|
768
1133
|
if (exact || markerPresent || attempt >= MAX_LOCAL_OBSERVATION_ATTEMPTS) break;
|
|
769
1134
|
await this.deps.sleep(localObservationDelayMs(attempt, 1_000, 4_000));
|
|
770
1135
|
}
|
|
771
1136
|
this.control.browserObserveEffect({ conversationId: command.conversationId, conversationUrl: command.conversationUrl, effectId: command.effectId,
|
|
772
1137
|
observationId: `computer-observe-${randomUUID()}`, outcome: exact || markerPresent ? 'applied' : 'unknown',
|
|
773
|
-
evidence: { surface: 'computer-chatgpt-target', target_id: target.targetId, exact_user_message: exact, target_marker_present: markerPresent,
|
|
1138
|
+
evidence: { surface: 'computer-chatgpt-target', target_id: target.targetId, exact_user_message: exact, target_marker_present: markerPresent,
|
|
774
1139
|
...(!exact && !markerPresent ? { reason: 'outbound_not_confirmed', observation_fingerprint: unknownObservationFingerprint(command.effectId, 'outbound_not_confirmed', snapshot) } : {}) } });
|
|
775
1140
|
}
|
|
776
1141
|
}
|