@bridge_gpt/mcp-server 0.2.66 → 0.2.68
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/README.md +421 -83
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +70 -16
- package/build/agent-launchers/executor-adapter.js +7 -4
- package/build/agents.generated.js +22 -2
- package/build/bitbucket-workspace-remote.js +136 -0
- package/build/bootstrap-project-identity.js +24 -0
- package/build/bounded-wait.js +67 -10
- package/build/bridge-permissions.js +829 -0
- package/build/claude-settings.js +18 -0
- package/build/cli-help.js +21 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +12 -12
- package/build/conductor/bridge-api-client.js +857 -1
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/command-drift-production.js +92 -0
- package/build/conductor/command-drift.js +89 -0
- package/build/conductor/doctor.js +72 -29
- package/build/conductor/epic-branch-ci-inspection.js +1375 -0
- package/build/conductor/epic-implementer-cli.js +98 -28
- package/build/conductor/errors.js +4 -3
- package/build/conductor/gate-discovery.js +890 -0
- package/build/conductor/guided-repair.js +6 -2
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +15 -3
- package/build/conductor/json-output.js +63 -0
- package/build/conductor/merge-execution-contract.js +27 -8
- package/build/conductor/merge-execution-eligibility.js +3 -2
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +113 -12
- package/build/conductor/readiness-sections.js +410 -9
- package/build/conductor/readiness.js +388 -33
- package/build/conductor/recovery-cli.js +310 -12
- package/build/conductor/recovery-operations.js +167 -3
- package/build/conductor/repair-contract.js +54 -3
- package/build/conductor/repository-workflow-remedy.js +125 -0
- package/build/conductor/tools.js +2 -2
- package/build/conductor-bundle-artifacts.js +8 -0
- package/build/connect-bitbucket-api.js +41 -6
- package/build/connect-bitbucket.js +106 -33
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +62 -3
- package/build/doctor.js +92 -1
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +129 -61
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1379 -28
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +33 -2
- package/build/executor/completion-contract.js +66 -10
- package/build/executor/completion-supervision.js +4 -0
- package/build/executor/deps.js +4 -1
- package/build/executor/env.js +19 -0
- package/build/executor/gate-qualification-job.js +499 -0
- package/build/executor/guarded-fast-forward-push.js +129 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +20 -0
- package/build/executor/integration-job.js +122 -8
- package/build/executor/job-errors.js +33 -1
- package/build/executor/job-runner.js +813 -87
- package/build/executor/likely-tests.js +290 -0
- package/build/executor/live-worker-registry.js +1 -1
- package/build/executor/observation.js +37 -3
- package/build/executor/packaged-recipe.js +106 -0
- package/build/executor/permissions.js +115 -28
- package/build/executor/platform.js +1 -1
- package/build/executor/preflight.js +112 -26
- package/build/executor/process-containment.js +36 -12
- package/build/executor/process.js +15 -7
- package/build/executor/prompt-spec.js +33 -1
- package/build/executor/recently-failed-tests-file.js +71 -0
- package/build/executor/reconcile-overlap-job.js +41 -23
- package/build/executor/required-verification.js +1 -1
- package/build/executor/required-work-ledger.js +111 -18
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/retirement.js +69 -0
- package/build/executor/runner.js +122 -13
- package/build/executor/service-unit.js +7 -1
- package/build/executor/test-clock.js +15 -0
- package/build/executor/types.js +32 -0
- package/build/executor/viewer-tabs.js +58 -8
- package/build/executor/worker-command.js +23 -7
- package/build/executor/worker-diagnostics.js +37 -0
- package/build/executor/worker-finalization.js +113 -34
- package/build/executor/worker-guard-hook.js +316 -58
- package/build/executor/worker-log.js +4 -3
- package/build/executor/worker-verification-guidance.js +37 -0
- package/build/executor/worktree.js +166 -8
- package/build/first-build-advisory.js +97 -0
- package/build/generated-assets-advisory.js +14 -0
- package/build/generated-assets-catalog.js +66 -0
- package/build/generated-assets-context.js +46 -0
- package/build/generated-assets-dependencies.js +26 -0
- package/build/generated-assets-doctor.js +50 -0
- package/build/generated-assets-health.js +65 -0
- package/build/generated-assets-host.js +17 -0
- package/build/generated-assets-inventory.js +17 -0
- package/build/generated-assets-paths.js +60 -0
- package/build/generated-assets-publisher.js +133 -0
- package/build/generated-assets-remedy.js +25 -0
- package/build/generated-assets-repair.js +72 -0
- package/build/generated-assets-session.js +71 -0
- package/build/generated-assets-test-support.js +35 -0
- package/build/index.js +1224 -1054
- package/build/init.js +54 -177
- package/build/install-bridge-conductor-gate.js +1372 -0
- package/build/install-bridge-conductor.js +296 -41
- package/build/install-bridge.js +547 -40
- package/build/mcp-install-state.js +50 -4
- package/build/merge-workflow-install.js +3 -4
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipeline-utils.js +6 -2
- package/build/pipelines.generated.js +34 -160
- package/build/plan-execution-ledger.js +2 -32
- package/build/plane/alembic-head.js +12 -3
- package/build/plane/base-worktree-guard.js +147 -0
- package/build/plane/cli.js +217 -17
- package/build/plane/code-snapshot.js +468 -0
- package/build/plane/defaults.js +48 -3
- package/build/plane/endpoint.js +41 -0
- package/build/plane/manifest.js +136 -2
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +83 -2
- package/build/plane/process-cwd.js +67 -0
- package/build/plane/restart.js +542 -0
- package/build/plane/shutdown.js +31 -0
- package/build/plane/status.js +143 -8
- package/build/plane/supervisor.js +586 -179
- package/build/plane/types.js +54 -2
- package/build/printed-remedy-commands.js +52 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/repair-assets.js +93 -0
- package/build/review-generation.js +22 -0
- package/build/run-unit-tests-launcher.js +130 -49
- package/build/secret-redaction.js +33 -0
- package/build/setup-epic.js +152 -22
- package/build/sfcc/config.js +9 -0
- package/build/sfcc/credentials.js +100 -38
- package/build/sfcc/hostname.js +67 -0
- package/build/sfcc/permissions.js +17 -13
- package/build/sfcc/register.js +1 -1
- package/build/sfcc/tool-wrapper.js +39 -43
- package/build/sfcc/write-grants.js +17 -11
- package/build/sfcc/write-result.js +4 -3
- package/build/sfcc/writes-site-preference.js +20 -8
- package/build/start-tickets-prereqs.js +70 -16
- package/build/start-tickets.js +208 -36
- package/build/tool-error-envelope.js +61 -0
- package/build/tool-surface-gating.js +36 -0
- package/build/upgrade-cli.js +55 -1
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/worker-guard-hook-bin.js +4 -6
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +145 -6
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +867 -0
- package/docs/EPIC-IMPLEMENTER.md +728 -0
- package/docs/install/mcp-tool-integrations.md +24 -15
- package/docs/install/sfcc-integration.md +12 -4
- package/package.json +3 -3
- package/pipelines/greenfield-setup.json +4 -128
- package/pipelines/implement-ticket.json +3 -2
- package/pipelines/plan-epic.json +0 -5
- package/pipelines/review-ticket.json +1 -1
- package/docs/CONDUCTOR.md +0 -605
- package/pipelines/idea-to-pr.json +0 -49
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infer the Bitbucket workspace slug from this project's `origin` remote (BAPI-1401).
|
|
3
|
+
*
|
|
4
|
+
* `connect-bitbucket` sends the workspace it reads here to Bridge as a *claim*: it only
|
|
5
|
+
* decides which workspace's Forge settings page lists the connection request. It is not
|
|
6
|
+
* an identity and authorizes nothing — the approval itself happens on the Atlassian side
|
|
7
|
+
* and binds only within the workspace Bridge derives from the signed Forge invocation.
|
|
8
|
+
*
|
|
9
|
+
* Only bitbucket.org repository remotes are recognized, in three shapes:
|
|
10
|
+
*
|
|
11
|
+
* https://[user@]bitbucket.org/<workspace>/<repo>[.git]
|
|
12
|
+
* git@bitbucket.org:<workspace>/<repo>[.git]
|
|
13
|
+
* ssh://git@bitbucket.org[:port]/<workspace>/<repo>[.git]
|
|
14
|
+
*
|
|
15
|
+
* Anything else (another host, a lookalike host, a path without both segments, a
|
|
16
|
+
* malformed URL, blank output, a failed command) yields no workspace, and the caller
|
|
17
|
+
* keeps the legacy code flow. The remote URL and any process error are never printed:
|
|
18
|
+
* a remote can embed a credential (`https://user:token@…`).
|
|
19
|
+
*
|
|
20
|
+
* Git runs through an injected argument-array seam (never a shell), so this is
|
|
21
|
+
* unit-testable without a real repository.
|
|
22
|
+
*/
|
|
23
|
+
import { execFile } from "child_process";
|
|
24
|
+
const BITBUCKET_HOST = "bitbucket.org";
|
|
25
|
+
/** Mirrors the server's workspace-slug allowlist (`normalize_bitbucket_slug`). */
|
|
26
|
+
const WORKSPACE_SLUG_RE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
27
|
+
const WORKSPACE_SLUG_MAX_LENGTH = 255;
|
|
28
|
+
/** Bounded so a hung git (a credential prompt, a slow filesystem) cannot stall the CLI. */
|
|
29
|
+
const GIT_TIMEOUT_MS = 5_000;
|
|
30
|
+
/** The default runner: `execFile` with an argument array, no shell, short timeout. */
|
|
31
|
+
export const defaultGitRemoteRunner = (args, cwd) => new Promise((resolve) => {
|
|
32
|
+
try {
|
|
33
|
+
execFile("git", args, { cwd, shell: false, timeout: GIT_TIMEOUT_MS, windowsHide: true }, (error, stdout) => {
|
|
34
|
+
if (error) {
|
|
35
|
+
resolve(null);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
resolve(typeof stdout === "string" ? stdout : String(stdout));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
resolve(null);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
function workspaceFromPath(pathPart) {
|
|
46
|
+
const segments = pathPart.replace(/^\/+/, "").split("/");
|
|
47
|
+
// A repository remote has exactly <workspace>/<repo>, optionally with a trailing slash.
|
|
48
|
+
const meaningful = segments.filter((segment, index) => segment !== "" || index < segments.length - 1);
|
|
49
|
+
if (meaningful.length !== 2)
|
|
50
|
+
return null;
|
|
51
|
+
const [workspace, repo] = meaningful;
|
|
52
|
+
if (!workspace || !repo || repo === ".git")
|
|
53
|
+
return null;
|
|
54
|
+
if (workspace.length > WORKSPACE_SLUG_MAX_LENGTH || !WORKSPACE_SLUG_RE.test(workspace)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return workspace;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parse a remote URL into its Bitbucket workspace slug, or null when it is not a
|
|
61
|
+
* recognizable bitbucket.org repository remote. Pure; never throws.
|
|
62
|
+
*/
|
|
63
|
+
export function parseBitbucketWorkspaceFromRemote(remote) {
|
|
64
|
+
if (typeof remote !== "string")
|
|
65
|
+
return null;
|
|
66
|
+
const value = remote.trim();
|
|
67
|
+
if (!value || /\s/.test(value) || /[\u0000-\u001F\u007F]/.test(value))
|
|
68
|
+
return null;
|
|
69
|
+
// scp-like SSH: git@bitbucket.org:<workspace>/<repo>.git
|
|
70
|
+
const scp = /^([A-Za-z0-9._-]+)@([^:/]+):(.+)$/.exec(value);
|
|
71
|
+
if (scp && !value.includes("://")) {
|
|
72
|
+
if (scp[2].toLowerCase() !== BITBUCKET_HOST)
|
|
73
|
+
return null;
|
|
74
|
+
return workspaceFromPath(scp[3]);
|
|
75
|
+
}
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = new URL(value);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (!["https:", "ssh:", "git+ssh:"].includes(parsed.protocol))
|
|
84
|
+
return null;
|
|
85
|
+
if (parsed.hostname.toLowerCase() !== BITBUCKET_HOST)
|
|
86
|
+
return null;
|
|
87
|
+
if (parsed.search || parsed.hash)
|
|
88
|
+
return null;
|
|
89
|
+
let pathname;
|
|
90
|
+
try {
|
|
91
|
+
pathname = decodeURIComponent(parsed.pathname);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return workspaceFromPath(pathname);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Read `git remote get-url origin` in `cwd` and return its Bitbucket workspace slug, or
|
|
100
|
+
* null. Never throws and never prints.
|
|
101
|
+
*/
|
|
102
|
+
export async function inferBitbucketWorkspaceFromGitRemote(cwd, runGit = defaultGitRemoteRunner) {
|
|
103
|
+
let stdout;
|
|
104
|
+
try {
|
|
105
|
+
stdout = await runGit(["remote", "get-url", "origin"], cwd);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if (typeof stdout !== "string")
|
|
111
|
+
return null;
|
|
112
|
+
const firstLine = stdout.split(/\r?\n/)[0] ?? "";
|
|
113
|
+
return parseBitbucketWorkspaceFromRemote(firstLine);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the Bitbucket workspace for a connect run: an explicit `--workspace` value
|
|
117
|
+
* first (already validated by the caller's argument parser), otherwise the `origin`
|
|
118
|
+
* remote of `cwd`. An inferred slug has already passed the same allowlist as the flag
|
|
119
|
+
* (see {@link parseBitbucketWorkspaceFromRemote}) and is shown to the user, so a wrong
|
|
120
|
+
* remote is visible. Returns undefined when nothing usable is found — the connect flow
|
|
121
|
+
* then uses code mode. Never prints the remote URL.
|
|
122
|
+
*
|
|
123
|
+
* Lives here, not in `connect-bitbucket.ts`, so `install-bridge.ts` can share it
|
|
124
|
+
* without importing the connect flow module.
|
|
125
|
+
*/
|
|
126
|
+
export async function resolveBitbucketWorkspace(explicit, cwd, runGit, log) {
|
|
127
|
+
if (explicit)
|
|
128
|
+
return explicit;
|
|
129
|
+
if (!runGit)
|
|
130
|
+
return undefined;
|
|
131
|
+
const inferred = await inferBitbucketWorkspaceFromGitRemote(cwd, runGit);
|
|
132
|
+
if (!inferred)
|
|
133
|
+
return undefined;
|
|
134
|
+
log(`Using Bitbucket workspace ${inferred}`);
|
|
135
|
+
return inferred;
|
|
136
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Bootstrap labels are display data, never credential selectors. */
|
|
2
|
+
const whitespace = " \u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000";
|
|
3
|
+
const controls = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u;
|
|
4
|
+
export function validateBootstrapDisplayName(value) {
|
|
5
|
+
if (typeof value !== "string" || controls.test(value))
|
|
6
|
+
throw new Error("Invalid project label.");
|
|
7
|
+
const label = value.replace(new RegExp(`^[${whitespace}]+|[${whitespace}]+$`, "gu"), "");
|
|
8
|
+
if (Array.from(label).length < 1 || Array.from(label).length > 255)
|
|
9
|
+
throw new Error("Invalid project label.");
|
|
10
|
+
return label;
|
|
11
|
+
}
|
|
12
|
+
export function validateBootstrapRepoName(value) {
|
|
13
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 100 || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value))
|
|
14
|
+
throw new Error("Invalid project identity.");
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
export function validateBootstrapServerIdentity(value) {
|
|
18
|
+
if (!value || typeof value !== "object")
|
|
19
|
+
throw new Error("Invalid project identity response.");
|
|
20
|
+
const identity = value;
|
|
21
|
+
if (identity.identity_mode !== "server_assigned")
|
|
22
|
+
throw new Error("Unsupported project identity response.");
|
|
23
|
+
return { repo_name: validateBootstrapRepoName(identity.repo_name), repo_display_name: validateBootstrapDisplayName(identity.repo_display_name), identity_mode: "server_assigned" };
|
|
24
|
+
}
|
package/build/bounded-wait.js
CHANGED
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
* keeps the reason vocabulary below closed: the primitive can only report states
|
|
25
25
|
* it can itself observe.
|
|
26
26
|
*
|
|
27
|
-
* NO RAW UPSTREAM DATA ESCAPES. A result carries a closed-vocabulary reason
|
|
28
|
-
*
|
|
27
|
+
* NO RAW UPSTREAM DATA ESCAPES. A result carries a closed-vocabulary reason plus
|
|
28
|
+
* whatever already-sanitized detail the caller's probe attached. Exception text, response bodies, headers, URLs, and credentials
|
|
29
29
|
* are never read into a result — a caller that needs upstream detail must obtain
|
|
30
30
|
* it inside its own probe and sanitize it there, the same bar
|
|
31
31
|
* `formatRecoverablePollGiveUp` sets for the existing poller.
|
|
@@ -48,7 +48,39 @@ const TERMINAL_REASON_SENTENCES = {
|
|
|
48
48
|
lost_after_retries: "The server restarted during the run and gave up after its automatic retries.",
|
|
49
49
|
handler_failed: "The run failed on the server.",
|
|
50
50
|
expired: "The run expired on the server before it finished.",
|
|
51
|
+
insufficient_ticket_detail: "The ticket description is too short for the server to work from.",
|
|
52
|
+
completed_without_artifact: "The run finished on the server without producing a document.",
|
|
51
53
|
};
|
|
54
|
+
/** Plain sentences for the probe-named terminal reasons (BAPI-1382). */
|
|
55
|
+
const WAIT_REASON_SENTENCES = {
|
|
56
|
+
NO_REVIEW_REQUESTED: "No review request is known for this ticket, and none is running on the server.",
|
|
57
|
+
NO_REVIEW_RUNNING: "A review was submitted, but the server has no review running and no document for it.",
|
|
58
|
+
};
|
|
59
|
+
/** The plain sentence for a probe-named wait reason, or `null` for the others. */
|
|
60
|
+
export function formatWaitReasonSentence(reason) {
|
|
61
|
+
return WAIT_REASON_SENTENCES[reason] ?? null;
|
|
62
|
+
}
|
|
63
|
+
function isWholeNonNegative(value) {
|
|
64
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The insufficient-detail sentence. It names whichever measurements the server
|
|
68
|
+
* sent — "26 of 200 characters" with both, the minimum alone when the length
|
|
69
|
+
* was not stored (the spawn-mode and review-run paths) — and otherwise falls
|
|
70
|
+
* back to the generic sentence, so a missing field is never printed as
|
|
71
|
+
* `undefined`. The server's `retry_hint` carries the next step.
|
|
72
|
+
*/
|
|
73
|
+
function insufficientDetailSentence(detail) {
|
|
74
|
+
const measured = detail?.measuredLength;
|
|
75
|
+
const threshold = detail?.threshold;
|
|
76
|
+
if (isWholeNonNegative(measured) && isWholeNonNegative(threshold)) {
|
|
77
|
+
return `The ticket description is too short (${measured} of ${threshold} characters).`;
|
|
78
|
+
}
|
|
79
|
+
if (isWholeNonNegative(threshold)) {
|
|
80
|
+
return `The ticket description is shorter than the ${threshold}-character minimum.`;
|
|
81
|
+
}
|
|
82
|
+
return TERMINAL_REASON_SENTENCES.insufficient_ticket_detail;
|
|
83
|
+
}
|
|
52
84
|
/**
|
|
53
85
|
* Build the plain recovery message for a terminal outcome, or `null` when the
|
|
54
86
|
* server sent neither a reason nor a retry hint (the caller then keeps its
|
|
@@ -60,6 +92,14 @@ export function formatTerminalRecoveryMessage(detail) {
|
|
|
60
92
|
if (!reason && !retryHint)
|
|
61
93
|
return null;
|
|
62
94
|
const parts = [];
|
|
95
|
+
if (reason === "insufficient_ticket_detail") {
|
|
96
|
+
parts.push(insufficientDetailSentence(detail));
|
|
97
|
+
// The server's hint for this reason is a full sentence naming the fix and
|
|
98
|
+
// the tool call, so it is printed as-is rather than after "Retry with:".
|
|
99
|
+
parts.push(retryHint ||
|
|
100
|
+
"Lengthen the ticket description (use update_ticket_description), then submit a new request.");
|
|
101
|
+
return parts.join(" ");
|
|
102
|
+
}
|
|
63
103
|
if (reason) {
|
|
64
104
|
parts.push(TERMINAL_REASON_SENTENCES[reason] ?? `The server reported: ${reason}.`);
|
|
65
105
|
}
|
|
@@ -123,12 +163,21 @@ export async function boundedWait(options) {
|
|
|
123
163
|
signal?.addEventListener("abort", finish, { once: true });
|
|
124
164
|
});
|
|
125
165
|
let consecutiveFailures = 0;
|
|
166
|
+
// BAPI-1356: latched, never reset. Set only where the server actually
|
|
167
|
+
// answered; a throw, a probe timeout, or a transient streak leaves it alone.
|
|
168
|
+
let observed = false;
|
|
169
|
+
const pending = (reason) => ({
|
|
170
|
+
state: "pending",
|
|
171
|
+
reason,
|
|
172
|
+
observed,
|
|
173
|
+
windowMs: effectiveBudgetMs,
|
|
174
|
+
});
|
|
126
175
|
try {
|
|
127
176
|
while (true) {
|
|
128
177
|
if (signal?.aborted)
|
|
129
178
|
return { state: "error", reason: "ABORTED" };
|
|
130
179
|
if (remaining() <= 0) {
|
|
131
|
-
return
|
|
180
|
+
return pending("WAIT_WINDOW_ELAPSED");
|
|
132
181
|
}
|
|
133
182
|
// One controller per probe, aborted by EITHER the per-probe timeout or the
|
|
134
183
|
// caller's signal. The probe therefore never outlives its own budget, and a
|
|
@@ -164,15 +213,22 @@ export async function boundedWait(options) {
|
|
|
164
213
|
if (threw || timedOut || outcome === null) {
|
|
165
214
|
consecutiveFailures += 1;
|
|
166
215
|
if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
|
|
167
|
-
return
|
|
216
|
+
return pending("UPSTREAM_TRANSIENT");
|
|
168
217
|
}
|
|
169
218
|
}
|
|
170
219
|
else if (outcome.kind === "terminal") {
|
|
171
|
-
// Recovery detail
|
|
172
|
-
// captured some, so a legacy terminal result keeps
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
220
|
+
// Recovery detail and upstream evidence are additive: each is attached
|
|
221
|
+
// only when the probe captured some, so a legacy terminal result keeps
|
|
222
|
+
// its exact shape.
|
|
223
|
+
const terminal = {
|
|
224
|
+
state: "error",
|
|
225
|
+
reason: outcome.reason ?? "UPSTREAM_TERMINAL",
|
|
226
|
+
};
|
|
227
|
+
if (outcome.recovery)
|
|
228
|
+
terminal.recovery = outcome.recovery;
|
|
229
|
+
if (outcome.upstream)
|
|
230
|
+
terminal.upstream = outcome.upstream;
|
|
231
|
+
return terminal;
|
|
176
232
|
}
|
|
177
233
|
else if (outcome.kind === "malformed") {
|
|
178
234
|
return { state: "error", reason: "INVALID_RESPONSE" };
|
|
@@ -181,6 +237,7 @@ export async function boundedWait(options) {
|
|
|
181
237
|
// The server answered. Reset the streak BEFORE evaluating readiness — an
|
|
182
238
|
// answer of "not yet" is still proof the transport is healthy.
|
|
183
239
|
consecutiveFailures = 0;
|
|
240
|
+
observed = true;
|
|
184
241
|
if (outcome.ready === true) {
|
|
185
242
|
const value = outcome.value;
|
|
186
243
|
if (isReady === undefined || isReady(value)) {
|
|
@@ -189,7 +246,7 @@ export async function boundedWait(options) {
|
|
|
189
246
|
}
|
|
190
247
|
}
|
|
191
248
|
if (remaining() <= 0) {
|
|
192
|
-
return
|
|
249
|
+
return pending("WAIT_WINDOW_ELAPSED");
|
|
193
250
|
}
|
|
194
251
|
// The existing poller's curve plus bounded jitter, clamped to whatever is
|
|
195
252
|
// actually left so a backoff can never overrun the deadline it is inside.
|