@cmmd-center/forge 0.13.96 → 0.13.97
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/dist/bin.cjs +307 -131
- package/dist/bin.mjs +331 -155
- package/dist/client/assets/{DiffPanel-CQF1ISJz.js → DiffPanel-CYrraC28.js} +2 -2
- package/dist/client/assets/{DiffPanel.logic-ZO6qepWo.js → DiffPanel.logic-DyXOs7eK.js} +2 -2
- package/dist/client/assets/{DiffWorkerPoolProvider-B7F6vAh9.js → DiffWorkerPoolProvider-DMt6DknG.js} +2 -2
- package/dist/client/assets/{PullRequestCodePanel-B93y-X1L.js → PullRequestCodePanel-CgjWVTVM.js} +2 -2
- package/dist/client/assets/{Virtualizer-BtFYmUB0.js → Virtualizer-D0TQdce0.js} +2 -2
- package/dist/client/assets/{WorkerPoolContext-VAp-zZQ5.js → WorkerPoolContext-DkMo0hso.js} +4 -4
- package/dist/client/assets/{index-BhdZllxU.js → index-CzgrA9VN.js} +6 -6
- package/dist/client/index.html +4 -4
- package/dist/forge-build.json +1 -1
- package/dist/{open-yHTUkd2Y.mjs → open-2-reBrpv.mjs} +3 -3
- package/package.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import { createCipheriv, createDecipheriv, createHash, createHmac, createPublicK
|
|
|
23
23
|
import * as FS$1 from "node:fs";
|
|
24
24
|
import fs, { accessSync, chmodSync, constants, createReadStream, existsSync, mkdirSync, mkdtempSync, promises, readFile, readFileSync, readdir, readdirSync, realpathSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
25
25
|
import * as NodeOS from "node:os";
|
|
26
|
-
import
|
|
26
|
+
import $U, { homedir, networkInterfaces, tmpdir } from "node:os";
|
|
27
27
|
import * as Path$1 from "node:path";
|
|
28
28
|
import path, { basename, delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
29
29
|
import { pathToFileURL } from "node:url";
|
|
@@ -50653,7 +50653,7 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
|
|
|
50653
50653
|
const min = forceColor || 0;
|
|
50654
50654
|
if (env.TERM === "dumb") return min;
|
|
50655
50655
|
if (process$1.platform === "win32") {
|
|
50656
|
-
const osRelease =
|
|
50656
|
+
const osRelease = $U.release().split(".");
|
|
50657
50657
|
if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) return Number(osRelease[2]) >= 14931 ? 3 : 2;
|
|
50658
50658
|
return 1;
|
|
50659
50659
|
}
|
|
@@ -53096,7 +53096,7 @@ var require_ProcessDetector = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
53096
53096
|
exports.processDetector = void 0;
|
|
53097
53097
|
const api_1 = (init_esm$1(), __toCommonJS(esm_exports$1));
|
|
53098
53098
|
const semconv_1 = require_semconv$1();
|
|
53099
|
-
const os$
|
|
53099
|
+
const os$3 = __require("os");
|
|
53100
53100
|
/**
|
|
53101
53101
|
* ProcessDetector will be used to detect the resources related current process running
|
|
53102
53102
|
* and being instrumented from the NodeJS Process module.
|
|
@@ -53118,7 +53118,7 @@ var require_ProcessDetector = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
53118
53118
|
};
|
|
53119
53119
|
if (process.argv.length > 1) attributes[semconv_1.ATTR_PROCESS_COMMAND] = process.argv[1];
|
|
53120
53120
|
try {
|
|
53121
|
-
const userInfo = os$
|
|
53121
|
+
const userInfo = os$3.userInfo();
|
|
53122
53122
|
attributes[semconv_1.ATTR_PROCESS_OWNER] = userInfo.username;
|
|
53123
53123
|
} catch (e) {
|
|
53124
53124
|
api_1.diag.debug(`error obtaining process owner: ${e}`);
|
|
@@ -66041,7 +66041,7 @@ function normalizeNumberish(value) {
|
|
|
66041
66041
|
}
|
|
66042
66042
|
//#endregion
|
|
66043
66043
|
//#region package.json
|
|
66044
|
-
var version$1 = "0.13.
|
|
66044
|
+
var version$1 = "0.13.97";
|
|
66045
66045
|
//#endregion
|
|
66046
66046
|
//#region src/sentry.ts
|
|
66047
66047
|
const SERVER_APP_NAME = "forge-server";
|
|
@@ -76111,7 +76111,7 @@ var require_node_gyp_build = /* @__PURE__ */ __commonJSMin(((exports, module) =>
|
|
|
76111
76111
|
var fs$6 = __require("fs");
|
|
76112
76112
|
var path$5 = __require("path");
|
|
76113
76113
|
var url$1 = __require("url");
|
|
76114
|
-
var os$
|
|
76114
|
+
var os$2 = __require("os");
|
|
76115
76115
|
var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
|
|
76116
76116
|
var vars = process.config && process.config.variables || {};
|
|
76117
76117
|
var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
|
|
@@ -76119,8 +76119,8 @@ var require_node_gyp_build = /* @__PURE__ */ __commonJSMin(((exports, module) =>
|
|
|
76119
76119
|
var abi = versions.modules;
|
|
76120
76120
|
if (versions.deno || process.isBun) abi = "unsupported";
|
|
76121
76121
|
var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
|
|
76122
|
-
var arch = process.env.npm_config_arch || os$
|
|
76123
|
-
var platform = process.env.npm_config_platform || os$
|
|
76122
|
+
var arch = process.env.npm_config_arch || os$2.arch();
|
|
76123
|
+
var platform = process.env.npm_config_platform || os$2.platform();
|
|
76124
76124
|
var libc = process.env.LIBC || (isMusl(platform) ? "musl" : "glibc");
|
|
76125
76125
|
var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
|
|
76126
76126
|
var uv = (versions.uv || "").split(".")[0];
|
|
@@ -93964,7 +93964,7 @@ const launchDetached = (launch) => gen(function* () {
|
|
|
93964
93964
|
});
|
|
93965
93965
|
const OpenLive = effect(Open, gen(function* () {
|
|
93966
93966
|
const open = yield* tryPromise({
|
|
93967
|
-
try: () => import("./open-
|
|
93967
|
+
try: () => import("./open-2-reBrpv.mjs"),
|
|
93968
93968
|
catch: (cause) => new OpenError$1({
|
|
93969
93969
|
message: "failed to load browser opener",
|
|
93970
93970
|
cause
|
|
@@ -98014,6 +98014,119 @@ function providerAuthFailureMessage(message, provider, env = process.env) {
|
|
|
98014
98014
|
}).message;
|
|
98015
98015
|
}
|
|
98016
98016
|
//#endregion
|
|
98017
|
+
//#region ../../packages/shared/src/publicProviderCopy.ts
|
|
98018
|
+
const PUBLIC_PROVIDER_LEAK_PATTERNS = [
|
|
98019
|
+
/\bDeepSeek\b/i,
|
|
98020
|
+
/\bKimi\b/i,
|
|
98021
|
+
/\bMoonshot\b/i,
|
|
98022
|
+
/\bdeepseek[-._a-z0-9]*\b/i,
|
|
98023
|
+
/\bkimi[-._a-z0-9]*\b/i,
|
|
98024
|
+
/\bmoonshot[-._a-z0-9]*\b/i,
|
|
98025
|
+
/\bAPEX_API_KEY\b/,
|
|
98026
|
+
/\bDEEPSEEK_API_KEY\b/,
|
|
98027
|
+
/\bKIMI_API_KEY\b/,
|
|
98028
|
+
/\bOPENAI_API_KEY\b/,
|
|
98029
|
+
/\bANTHROPIC_API_KEY\b/,
|
|
98030
|
+
/\bCURSOR_API_KEY\b/,
|
|
98031
|
+
/\bOPENCODE_API_KEY\b/,
|
|
98032
|
+
/\bforge_proxy_[A-Za-z0-9._-]+\b/,
|
|
98033
|
+
/\bProviderValidationError\b/,
|
|
98034
|
+
/\/forge-server\/node_modules\//,
|
|
98035
|
+
/https?:\/\/[^\s'"<>)]*\/api\/provider-egress\/[^\s'"<>)]*/i,
|
|
98036
|
+
/https?:\/\/[^\s'"<>)]*(?:apex|deepseek|moonshot|kimi)[^\s'"<>)]*/i
|
|
98037
|
+
];
|
|
98038
|
+
const DEFAULT_PUBLIC_PROVIDER_ERROR = "CMMD is temporarily unavailable. Check the workspace AI connection and retry.";
|
|
98039
|
+
function isProviderRateOrQuotaError(message) {
|
|
98040
|
+
if (!message) return false;
|
|
98041
|
+
const normalized = message.toLowerCase();
|
|
98042
|
+
return /\b(rate[\s-]?limit|too\s+many\s+requests|usage[ _-]limit|usage_limit_reached|quota|insufficient_quota)\b/.test(normalized) || /\b429\b/.test(normalized);
|
|
98043
|
+
}
|
|
98044
|
+
function providerLifecycleOrAccountCopy(message) {
|
|
98045
|
+
if (/\bbound to provider account\b/.test(message) && /\bcannot use provider account\b/.test(message)) return "Forge could not switch this conversation to the selected AI account. Retry the account switch.";
|
|
98046
|
+
if (/\bforge_runtime_repairing\b/.test(message) || /\bthis turn was interrupted because the environment is being repaired\b/.test(message)) return "This turn was interrupted because the environment is being repaired. Retry after the repair finishes.";
|
|
98047
|
+
if (/\bforge_runtime_stopped\b/.test(message) || /\bthis turn was interrupted because the environment was stopped\b/.test(message)) return "This turn was interrupted because the environment was stopped. Start the environment and retry.";
|
|
98048
|
+
return null;
|
|
98049
|
+
}
|
|
98050
|
+
/**
|
|
98051
|
+
* Infer a SAFE, clearer failure category from a (possibly leaky) provider error.
|
|
98052
|
+
*
|
|
98053
|
+
* Returns a canned, user-facing message — it never echoes the raw input, so it
|
|
98054
|
+
* is safe to use even when the source message contains provider internals.
|
|
98055
|
+
* Used to give actionable feedback ("model isn't available", "rate-limited",
|
|
98056
|
+
* "auth failed") instead of the blanket "CMMD is temporarily unavailable."
|
|
98057
|
+
* Returns null when no category can be confidently inferred.
|
|
98058
|
+
*/
|
|
98059
|
+
function categorizePublicProviderError(message) {
|
|
98060
|
+
if (!message) return null;
|
|
98061
|
+
const m = message.toLowerCase();
|
|
98062
|
+
const lifecycleOrAccountCopy = providerLifecycleOrAccountCopy(m);
|
|
98063
|
+
if (lifecycleOrAccountCopy !== null) return lifecycleOrAccountCopy;
|
|
98064
|
+
if (/\bforge_provider_model_not_allowed\b/.test(m) || /\bnot\s+scoped\s+to\s+the\s+requested\s+model\b/.test(m)) return "The workspace AI credential does not cover the model this turn asked for. Retry, or pick that model for the thread.";
|
|
98065
|
+
if (/\bmodel\b[\s\S]*\b(not\s+(found|available|supported)|does\s+not\s+exist|unsupported|invalid|unknown)\b/.test(m) || /\b(unsupported|unknown|invalid)\b[\s\S]*\bmodel\b/.test(m) || /\b(model_not_found|invalid_model|model_not_supported)\b/.test(m)) return "The selected model isn't available right now. Pick a different model and retry.";
|
|
98066
|
+
if (isProviderRateOrQuotaError(m)) return "The AI service is rate-limited or out of quota. Wait a moment and retry.";
|
|
98067
|
+
if (/_credential_reconnect_required\b/.test(m)) return "The workspace AI credential needs to be reconnected. Reconnect it in Settings > Providers, then retry.";
|
|
98068
|
+
if (/\b(unauthorized|forbidden|authentication\s+(failed|error|required)|invalid\s+api\s+key|expired\s+(token|credential))\b/.test(m) || /\b40[13]\b/.test(m)) return "Workspace AI authentication failed. Reconnect the workspace and retry.";
|
|
98069
|
+
if (/\b(timed?\s*out|timeout|overloaded|service\s+unavailable)\b/.test(m) || /\b50[234]\b/.test(m)) return "The AI service is temporarily overloaded. Retry in a moment.";
|
|
98070
|
+
return null;
|
|
98071
|
+
}
|
|
98072
|
+
function containsPublicProviderLeak(message) {
|
|
98073
|
+
if (!message) return false;
|
|
98074
|
+
return PUBLIC_PROVIDER_LEAK_PATTERNS.some((pattern) => pattern.test(message));
|
|
98075
|
+
}
|
|
98076
|
+
function sanitizePublicProviderText(message, options = {}) {
|
|
98077
|
+
if (message === void 0 || message === null) return null;
|
|
98078
|
+
const trimmed = message.trim();
|
|
98079
|
+
if (trimmed.length === 0) return null;
|
|
98080
|
+
if (!containsPublicProviderLeak(trimmed)) return trimmed;
|
|
98081
|
+
return categorizePublicProviderError(trimmed) ?? options.fallback ?? "CMMD is temporarily unavailable. Check the workspace AI connection and retry.";
|
|
98082
|
+
}
|
|
98083
|
+
//#endregion
|
|
98084
|
+
//#region src/orchestration/providerRetryableFailure.ts
|
|
98085
|
+
/**
|
|
98086
|
+
* Classifies a provider turn-start failure as a retryable rate-limit or
|
|
98087
|
+
* transient-availability failure (429/503), a terminal failure that names why
|
|
98088
|
+
* it will never clear on its own, or unrecognized (null).
|
|
98089
|
+
*
|
|
98090
|
+
* CMMD-Center/forge#3558: a 429 from CMMD's provider egress used to leave no
|
|
98091
|
+
* trace at all, because nothing in the turn-start path recognized it as a
|
|
98092
|
+
* distinct, retryable failure. `ProviderCommandReactor` uses this to decide
|
|
98093
|
+
* whether to retry a turn start with backoff, and to name the provider,
|
|
98094
|
+
* status and Retry-After in the `provider.turn.start.failed` activity CMMD's
|
|
98095
|
+
* watchdog reads once retries are exhausted.
|
|
98096
|
+
*
|
|
98097
|
+
* Reuses `isProviderRateOrQuotaError` (the same predicate the public-copy
|
|
98098
|
+
* sanitizer uses to recognize a rate-limit message) rather than restating its
|
|
98099
|
+
* word list here.
|
|
98100
|
+
*/
|
|
98101
|
+
const SERVICE_UNAVAILABLE_PATTERN = /\b(503|service\s+unavailable)\b/i;
|
|
98102
|
+
const RATE_LIMITED_PAST_TENSE_PATTERN = /\brate[\s-]?limit(?:ed|ing)?\b/i;
|
|
98103
|
+
const QUOTA_OR_USAGE_LIMIT_PATTERN = /\b(usage[ _-]limit|usage_limit_reached|quota|insufficient_quota)\b/i;
|
|
98104
|
+
const EXPLICIT_429_PATTERN = /\b429\b/;
|
|
98105
|
+
const RETRY_AFTER_SECONDS_PATTERN = /retry-after[^0-9]{0,10}(\d+)|(?:try again|retry)\s+in\s+(\d+)\s*(?:s\b|sec(?:onds?)?\b)?/i;
|
|
98106
|
+
function parseRetryAfterSeconds(detail) {
|
|
98107
|
+
const match = detail.match(RETRY_AFTER_SECONDS_PATTERN);
|
|
98108
|
+
if (!match) return null;
|
|
98109
|
+
const raw = match[1] ?? match[2];
|
|
98110
|
+
const seconds = raw ? Number(raw) : NaN;
|
|
98111
|
+
return Number.isFinite(seconds) ? seconds : null;
|
|
98112
|
+
}
|
|
98113
|
+
function classifyRetryableProviderFailure(detail) {
|
|
98114
|
+
if (!detail || detail.trim().length === 0) return null;
|
|
98115
|
+
if (QUOTA_OR_USAGE_LIMIT_PATTERN.test(detail) && !EXPLICIT_429_PATTERN.test(detail)) return {
|
|
98116
|
+
kind: "terminal",
|
|
98117
|
+
reason: "quota",
|
|
98118
|
+
message: detail
|
|
98119
|
+
};
|
|
98120
|
+
const isRateLimited = isProviderRateOrQuotaError(detail) || RATE_LIMITED_PAST_TENSE_PATTERN.test(detail);
|
|
98121
|
+
const isUnavailable = SERVICE_UNAVAILABLE_PATTERN.test(detail);
|
|
98122
|
+
if (!isRateLimited && !isUnavailable) return null;
|
|
98123
|
+
return {
|
|
98124
|
+
kind: "retryable",
|
|
98125
|
+
status: isRateLimited ? 429 : 503,
|
|
98126
|
+
retryAfterSeconds: parseRetryAfterSeconds(detail)
|
|
98127
|
+
};
|
|
98128
|
+
}
|
|
98129
|
+
//#endregion
|
|
98017
98130
|
//#region src/orchestration/turnFailureReason.ts
|
|
98018
98131
|
/**
|
|
98019
98132
|
* Machine-readable reasons for a turn that failed before it produced any
|
|
@@ -98047,6 +98160,31 @@ function classifyProviderTurnStartFailureReason(detail) {
|
|
|
98047
98160
|
message
|
|
98048
98161
|
};
|
|
98049
98162
|
}
|
|
98163
|
+
/**
|
|
98164
|
+
* Classifies a turn that ended mid-flight, after it had already started
|
|
98165
|
+
* (CMMD-Center/forge#3562 piece A): a `runtime.error` that reached CMMD as
|
|
98166
|
+
* `error_summary`, now also read into a `turn_status: failed` reason.
|
|
98167
|
+
*
|
|
98168
|
+
* `provider_turn_failed` is the code CMMD's own error_summary heuristic
|
|
98169
|
+
* already assigns this class of failure
|
|
98170
|
+
* (`shared/forge-work-session-failure-reasons.ts`), so a Forge-sent
|
|
98171
|
+
* `turn_status` carrying it lines up with what CMMD already infers instead
|
|
98172
|
+
* of teaching it a new code for the common case. `provider_usage_limit`
|
|
98173
|
+
* reuses the same wording `classifyRetryableProviderFailure` already treats
|
|
98174
|
+
* as a wall that will not clear by retrying (CMMD-Center/forge#3558): a
|
|
98175
|
+
* usage-limit message inside a turn that already ended is worth naming
|
|
98176
|
+
* specifically, even without the fuller upstream port (see the piece A PR's
|
|
98177
|
+
* follow-up issue) that would stop Codex from retrying against that wall in
|
|
98178
|
+
* the first place.
|
|
98179
|
+
*/
|
|
98180
|
+
function classifyMidTurnFailureReason(detail) {
|
|
98181
|
+
const message = detail && detail.trim().length > 0 ? detail : "The runtime reported an error while this run was in progress.";
|
|
98182
|
+
const classified = classifyRetryableProviderFailure(detail);
|
|
98183
|
+
return {
|
|
98184
|
+
code: classified?.kind === "terminal" && classified.reason === "quota" ? "provider_usage_limit" : "provider_turn_failed",
|
|
98185
|
+
message
|
|
98186
|
+
};
|
|
98187
|
+
}
|
|
98050
98188
|
function checkpointTimeoutReason(detail) {
|
|
98051
98189
|
return {
|
|
98052
98190
|
code: "checkpoint_timeout",
|
|
@@ -98181,6 +98319,108 @@ function serializeReplicaMessage(message) {
|
|
|
98181
98319
|
};
|
|
98182
98320
|
}
|
|
98183
98321
|
//#endregion
|
|
98322
|
+
//#region src/orchestration/turnTerminalStatus.ts
|
|
98323
|
+
/**
|
|
98324
|
+
* Shared vocabulary between the reactor that records how a turn ended
|
|
98325
|
+
* (`Layers/CheckpointReactor.ts`, on the provider's own `turn.completed`
|
|
98326
|
+
* event) and the serializer that turns that record into CMMD's `turn_status`
|
|
98327
|
+
* terminal event (`runtimeReplication/turnTerminalStatusSerializer.ts`).
|
|
98328
|
+
* CMMD-Center/forge#3562 piece A, CMMD-Center/CMMD#5822.
|
|
98329
|
+
*/
|
|
98330
|
+
/** The activity kind this durable record uses. Never sent to CMMD as-is: the
|
|
98331
|
+
* serializer reads it and emits the wire `turn_status` event instead. */
|
|
98332
|
+
const TURN_TERMINAL_STATUS_ACTIVITY_KIND = "turn.terminal-status";
|
|
98333
|
+
/**
|
|
98334
|
+
* CMMD's three-way outcome from the provider's own four-way `turn.completed`
|
|
98335
|
+
* state. `cancelled` and `interrupted` both read as `interrupted`: neither
|
|
98336
|
+
* the provider wording nor CMMD's Work Session UI distinguishes an
|
|
98337
|
+
* operator-cancelled turn from one the provider gave up on mid-flight.
|
|
98338
|
+
*/
|
|
98339
|
+
function turnTerminalOutcomeFromRuntimeState(state) {
|
|
98340
|
+
if (state === "failed") return "failed";
|
|
98341
|
+
if (state === "completed") return "completed";
|
|
98342
|
+
return "interrupted";
|
|
98343
|
+
}
|
|
98344
|
+
/**
|
|
98345
|
+
* Deterministic activity id for a turn's terminal record: never
|
|
98346
|
+
* `crypto.randomUUID()`. The projector replaces an activity by id
|
|
98347
|
+
* (`thread.activity-appended`), so a redelivered `turn.completed` for the
|
|
98348
|
+
* same turn lands on the same row instead of adding a second one, and the
|
|
98349
|
+
* serializer's own `eventId` (`activity:<id>`) inherits the same guarantee.
|
|
98350
|
+
*/
|
|
98351
|
+
function turnTerminalStatusActivityId(turnId) {
|
|
98352
|
+
return `turn-terminal:${turnId}`;
|
|
98353
|
+
}
|
|
98354
|
+
//#endregion
|
|
98355
|
+
//#region src/runtimeReplication/eventFormatting.ts
|
|
98356
|
+
/**
|
|
98357
|
+
* Small formatting helpers shared by every source `serializer.ts` folds into
|
|
98358
|
+
* a thread's replicated event list. Kept in their own module, with no
|
|
98359
|
+
* dependency on `serializer.ts` itself, so a narrower serializer (such as
|
|
98360
|
+
* `turnTerminalStatusSerializer.ts`) can reuse them without creating an
|
|
98361
|
+
* import cycle back into the file that assembles the full event list.
|
|
98362
|
+
*/
|
|
98363
|
+
function text(value, max) {
|
|
98364
|
+
const normalized = value.trim();
|
|
98365
|
+
return normalized ? normalized.slice(0, max) : null;
|
|
98366
|
+
}
|
|
98367
|
+
function canonicalRuntimeEventTimestamp(value) {
|
|
98368
|
+
let timestamp = new Date(value);
|
|
98369
|
+
if (Number.isNaN(timestamp.getTime())) {
|
|
98370
|
+
const legacyBsdNanoseconds = value.match(/^(.*:\d{2})\.\d+NZ$/);
|
|
98371
|
+
timestamp = new Date(legacyBsdNanoseconds ? `${legacyBsdNanoseconds[1]}Z` : value);
|
|
98372
|
+
}
|
|
98373
|
+
if (Number.isNaN(timestamp.getTime())) throw new Error(`Invalid runtime event timestamp: ${value}`);
|
|
98374
|
+
return timestamp.toISOString();
|
|
98375
|
+
}
|
|
98376
|
+
//#endregion
|
|
98377
|
+
//#region src/runtimeReplication/turnTerminalStatusSerializer.ts
|
|
98378
|
+
function outcomeOf(payload) {
|
|
98379
|
+
if (!payload || typeof payload !== "object") return null;
|
|
98380
|
+
const outcome = payload.outcome;
|
|
98381
|
+
return outcome === "completed" || outcome === "interrupted" || outcome === "failed" ? outcome : null;
|
|
98382
|
+
}
|
|
98383
|
+
function errorMessageOf(payload) {
|
|
98384
|
+
if (!payload || typeof payload !== "object") return null;
|
|
98385
|
+
const message = payload.errorMessage;
|
|
98386
|
+
return typeof message === "string" && message.trim().length > 0 ? message : null;
|
|
98387
|
+
}
|
|
98388
|
+
/**
|
|
98389
|
+
* Turns the one durable activity `Layers/CheckpointReactor.ts` records when a
|
|
98390
|
+
* turn ends into CMMD's `turn_status` terminal event (CMMD-Center/forge#3562
|
|
98391
|
+
* piece A, CMMD-Center/CMMD#5822): `completed`, `interrupted`, or `failed`
|
|
98392
|
+
* with a structured `reason`. `error_summary` (`serializeFailureActivity` in
|
|
98393
|
+
* `serializer.ts`) keeps replicating separately for a mid-turn failure; this
|
|
98394
|
+
* is added next to it, never instead of it.
|
|
98395
|
+
*
|
|
98396
|
+
* The activity's own id is deterministic (`turn-terminal:<turnId>`,
|
|
98397
|
+
* `turnTerminalStatusActivityId`), so this event's `eventId` -- built from
|
|
98398
|
+
* that id the same way every other activity-sourced event on this thread is
|
|
98399
|
+
* (`activity:<id>`) -- can never collide with a second, different terminal
|
|
98400
|
+
* status for the same turn: there is exactly one activity row to read it
|
|
98401
|
+
* from, and the reactor that writes that row never overwrites it once set.
|
|
98402
|
+
*/
|
|
98403
|
+
function serializeTurnTerminalStatusActivity(activity) {
|
|
98404
|
+
if (activity.kind !== "turn.terminal-status" || !activity.turnId) return null;
|
|
98405
|
+
const outcome = outcomeOf(activity.payload);
|
|
98406
|
+
if (outcome === null) return null;
|
|
98407
|
+
const reason = outcome === "failed" ? classifyMidTurnFailureReason(errorMessageOf(activity.payload)) : null;
|
|
98408
|
+
return {
|
|
98409
|
+
sourceSequence: null,
|
|
98410
|
+
event: {
|
|
98411
|
+
eventId: `activity:${activity.id}`,
|
|
98412
|
+
occurredAt: canonicalRuntimeEventTimestamp(activity.createdAt),
|
|
98413
|
+
turnId: activity.turnId,
|
|
98414
|
+
kind: "turn_status",
|
|
98415
|
+
status: outcome,
|
|
98416
|
+
...reason ? { reason: {
|
|
98417
|
+
code: reason.code,
|
|
98418
|
+
message: text(reason.message, 2e3) ?? reason.message
|
|
98419
|
+
} } : {}
|
|
98420
|
+
}
|
|
98421
|
+
};
|
|
98422
|
+
}
|
|
98423
|
+
//#endregion
|
|
98184
98424
|
//#region src/runtimeReplication/serializer.ts
|
|
98185
98425
|
function threadStatus(thread) {
|
|
98186
98426
|
if (thread.archivedAt) return "archived";
|
|
@@ -98247,19 +98487,6 @@ function serializeRuntimeInventory(snapshot, providerInventory) {
|
|
|
98247
98487
|
...providerInventory === void 0 ? {} : { providers: serializeProviderCatalog(providerInventory) }
|
|
98248
98488
|
};
|
|
98249
98489
|
}
|
|
98250
|
-
function text(value, max) {
|
|
98251
|
-
const normalized = value.trim();
|
|
98252
|
-
return normalized ? normalized.slice(0, max) : null;
|
|
98253
|
-
}
|
|
98254
|
-
function canonicalRuntimeEventTimestamp(value) {
|
|
98255
|
-
let timestamp = new Date(value);
|
|
98256
|
-
if (Number.isNaN(timestamp.getTime())) {
|
|
98257
|
-
const legacyBsdNanoseconds = value.match(/^(.*:\d{2})\.\d+NZ$/);
|
|
98258
|
-
timestamp = new Date(legacyBsdNanoseconds ? `${legacyBsdNanoseconds[1]}Z` : value);
|
|
98259
|
-
}
|
|
98260
|
-
if (Number.isNaN(timestamp.getTime())) throw new Error(`Invalid runtime event timestamp: ${value}`);
|
|
98261
|
-
return timestamp.toISOString();
|
|
98262
|
-
}
|
|
98263
98490
|
/** Spread onto an event: present only when the projection knows the turn. */
|
|
98264
98491
|
function eventTurn(turnId) {
|
|
98265
98492
|
return turnId ? { turnId } : {};
|
|
@@ -98564,10 +98791,13 @@ function foldReportedUsage(reported) {
|
|
|
98564
98791
|
* model, so a mid-thread switch used to republish a finished turn as a second
|
|
98565
98792
|
* row for spend that happened once (FOR-36388, fixed in FOR-36438).
|
|
98566
98793
|
*/
|
|
98567
|
-
/** Turns named on this thread's own rows; hydrated replica rows carry no local
|
|
98794
|
+
/** Turns named on this thread's own rows; hydrated replica rows carry no local
|
|
98795
|
+
* turn, and the terminal-status activity (CMMD-Center/forge#3562 piece A) is
|
|
98796
|
+
* a wire-protocol source in its own right, not a Usage signal, so it is
|
|
98797
|
+
* excluded the same way a `replica.*` activity is. */
|
|
98568
98798
|
function localTurnIds(thread) {
|
|
98569
98799
|
const fromMessages = thread.messages.filter((message) => !message.streaming).map((message) => message.turnId);
|
|
98570
|
-
const fromActivities = thread.activities.filter((activity) => !activity.kind.startsWith("replica.")).map((activity) => activity.turnId);
|
|
98800
|
+
const fromActivities = thread.activities.filter((activity) => !activity.kind.startsWith("replica.") && activity.kind !== "turn.terminal-status").map((activity) => activity.turnId);
|
|
98571
98801
|
const turnIds = /* @__PURE__ */ new Set();
|
|
98572
98802
|
for (const turnId of [...fromMessages, ...fromActivities]) if (turnId) turnIds.add(turnId);
|
|
98573
98803
|
return turnIds;
|
|
@@ -98577,8 +98807,12 @@ function settledTurnIds(thread) {
|
|
|
98577
98807
|
if (thread.latestTurn?.state === "running") turnIds.delete(thread.latestTurn.turnId);
|
|
98578
98808
|
return turnIds;
|
|
98579
98809
|
}
|
|
98810
|
+
/** A turn's own activities for Usage derivation, excluding the terminal-status
|
|
98811
|
+
* activity: that record's own `createdAt` (the turn's very end) must never
|
|
98812
|
+
* shift `turnLatestAt`, or an already-acknowledged usage row would rehash
|
|
98813
|
+
* the moment a turn that used to have no terminal activity gains one. */
|
|
98580
98814
|
function turnActivities(thread, turnId) {
|
|
98581
|
-
return thread.activities.filter((activity) => activity.turnId === turnId).toSorted((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id));
|
|
98815
|
+
return thread.activities.filter((activity) => activity.turnId === turnId && activity.kind !== "turn.terminal-status").toSorted((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id));
|
|
98582
98816
|
}
|
|
98583
98817
|
/** The last thing that happened in the turn: what the Recorded turn is stamped with. */
|
|
98584
98818
|
function turnLatestAt(thread, turnId, activities) {
|
|
@@ -98787,6 +99021,10 @@ function serializeRuntimeThreadEvents(thread) {
|
|
|
98787
99021
|
const event = serializePlanActivity(activity);
|
|
98788
99022
|
return event === null ? [] : [event];
|
|
98789
99023
|
});
|
|
99024
|
+
const turnTerminalStatuses = thread.activities.flatMap((activity) => {
|
|
99025
|
+
const event = serializeTurnTerminalStatusActivity(activity);
|
|
99026
|
+
return event === null ? [] : [event];
|
|
99027
|
+
});
|
|
98790
99028
|
const recordedTurns = serializeRecordedTurns(thread);
|
|
98791
99029
|
const textGenerations = serializeTextGenerations(thread);
|
|
98792
99030
|
const priorEvents = [
|
|
@@ -98796,6 +99034,7 @@ function serializeRuntimeThreadEvents(thread) {
|
|
|
98796
99034
|
...worktreeLaunchActivities,
|
|
98797
99035
|
...planActivities,
|
|
98798
99036
|
...failureActivities,
|
|
99037
|
+
...turnTerminalStatuses,
|
|
98799
99038
|
...recordedTurns,
|
|
98800
99039
|
...textGenerations
|
|
98801
99040
|
];
|
|
@@ -191922,7 +192161,7 @@ var Vce = {
|
|
|
191922
192161
|
function Ve() {
|
|
191923
192162
|
return Vce;
|
|
191924
192163
|
}
|
|
191925
|
-
var Us =
|
|
192164
|
+
var Us = $U.homedir(), lk = $U.tmpdir(), { env: Vc } = process$1, Yce = (e) => {
|
|
191926
192165
|
let t = path.join(Us, "Library");
|
|
191927
192166
|
return {
|
|
191928
192167
|
data: path.join(t, "Application Support", e),
|
|
@@ -202122,12 +202361,12 @@ function qZ(e, t) {
|
|
|
202122
202361
|
}
|
|
202123
202362
|
return n;
|
|
202124
202363
|
}
|
|
202125
|
-
var os$
|
|
202364
|
+
var os$1 = "https://code.claude.com/docs/en", kKe = [
|
|
202126
202365
|
{
|
|
202127
202366
|
matches: (e) => e.path === "permissions.defaultMode" && e.code === "invalid_value",
|
|
202128
202367
|
tip: {
|
|
202129
202368
|
suggestion: "Valid modes: \"acceptEdits\" (ask before file changes), \"plan\" (analysis only), \"bypassPermissions\" (auto-accept all), or \"default\" (standard behavior)",
|
|
202130
|
-
docLink: `${os$
|
|
202369
|
+
docLink: `${os$1}/iam#permission-modes`
|
|
202131
202370
|
}
|
|
202132
202371
|
},
|
|
202133
202372
|
{
|
|
@@ -202142,7 +202381,7 @@ var os$2 = "https://code.claude.com/docs/en", kKe = [
|
|
|
202142
202381
|
matches: (e) => e.path.startsWith("env.") && e.code === "invalid_type",
|
|
202143
202382
|
tip: {
|
|
202144
202383
|
suggestion: "Environment variables must be strings. Wrap numbers and booleans in quotes. Example: \"DEBUG\": \"true\", \"PORT\": \"3000\"",
|
|
202145
|
-
docLink: `${os$
|
|
202384
|
+
docLink: `${os$1}/settings#environment-variables`
|
|
202146
202385
|
}
|
|
202147
202386
|
},
|
|
202148
202387
|
{
|
|
@@ -202153,14 +202392,14 @@ var os$2 = "https://code.claude.com/docs/en", kKe = [
|
|
|
202153
202392
|
matches: (e) => e.path.startsWith("hooks.") && e.code === "invalid_key",
|
|
202154
202393
|
tip: {
|
|
202155
202394
|
suggestion: "Not a recognized hook event. Common events: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, SessionEnd, Stop. Check spelling and capitalization.",
|
|
202156
|
-
docLink: `${os$
|
|
202395
|
+
docLink: `${os$1}/hooks`
|
|
202157
202396
|
}
|
|
202158
202397
|
},
|
|
202159
202398
|
{
|
|
202160
202399
|
matches: (e) => /\.hooks\.\d+\.command$/.test(e.path) && e.code === "invalid_type" && e.received === "undefined",
|
|
202161
202400
|
tip: {
|
|
202162
202401
|
suggestion: "Command hooks require `command`. For exec form (no shell), set `command` to the executable and `args` to its arguments: {\"type\": \"command\", \"command\": \"echo\", \"args\": [\"hi\"]}. For shell form, set `command` to the full shell string: {\"type\": \"command\", \"command\": \"echo hi\"}.",
|
|
202163
|
-
docLink: `${os$
|
|
202402
|
+
docLink: `${os$1}/hooks#exec-form-and-shell-form`
|
|
202164
202403
|
}
|
|
202165
202404
|
},
|
|
202166
202405
|
{
|
|
@@ -202175,7 +202414,7 @@ var os$2 = "https://code.claude.com/docs/en", kKe = [
|
|
|
202175
202414
|
matches: (e) => e.code === "unrecognized_keys",
|
|
202176
202415
|
tip: {
|
|
202177
202416
|
suggestion: "Check for typos or refer to the documentation for valid fields",
|
|
202178
|
-
docLink: `${os$
|
|
202417
|
+
docLink: `${os$1}/settings`
|
|
202179
202418
|
}
|
|
202180
202419
|
},
|
|
202181
202420
|
{
|
|
@@ -202190,13 +202429,13 @@ var os$2 = "https://code.claude.com/docs/en", kKe = [
|
|
|
202190
202429
|
matches: (e) => e.path === "permissions.additionalDirectories" && e.code === "invalid_type",
|
|
202191
202430
|
tip: {
|
|
202192
202431
|
suggestion: "Must be an array of directory paths. Example: [\"~/projects\", \"/tmp/workspace\"]. You can also use --add-dir flag or /add-dir command",
|
|
202193
|
-
docLink: `${os$
|
|
202432
|
+
docLink: `${os$1}/iam#working-directories`
|
|
202194
202433
|
}
|
|
202195
202434
|
}
|
|
202196
202435
|
], AKe = {
|
|
202197
|
-
permissions: `${os$
|
|
202198
|
-
env: `${os$
|
|
202199
|
-
hooks: `${os$
|
|
202436
|
+
permissions: `${os$1}/iam#configuring-permissions`,
|
|
202437
|
+
env: `${os$1}/settings#environment-variables`,
|
|
202438
|
+
hooks: `${os$1}/hooks`
|
|
202200
202439
|
};
|
|
202201
202440
|
function GZ(e) {
|
|
202202
202441
|
let t = kKe.find((r) => r.matches(e));
|
|
@@ -213749,73 +213988,6 @@ const makeCodexAdapter = fn$1("makeCodexAdapter")(function* (options) {
|
|
|
213749
213988
|
function makeCodexAdapterLive(options) {
|
|
213750
213989
|
return effect(CodexAdapter, makeCodexAdapter(options));
|
|
213751
213990
|
}
|
|
213752
|
-
//#endregion
|
|
213753
|
-
//#region ../../packages/shared/src/publicProviderCopy.ts
|
|
213754
|
-
const PUBLIC_PROVIDER_LEAK_PATTERNS = [
|
|
213755
|
-
/\bDeepSeek\b/i,
|
|
213756
|
-
/\bKimi\b/i,
|
|
213757
|
-
/\bMoonshot\b/i,
|
|
213758
|
-
/\bdeepseek[-._a-z0-9]*\b/i,
|
|
213759
|
-
/\bkimi[-._a-z0-9]*\b/i,
|
|
213760
|
-
/\bmoonshot[-._a-z0-9]*\b/i,
|
|
213761
|
-
/\bAPEX_API_KEY\b/,
|
|
213762
|
-
/\bDEEPSEEK_API_KEY\b/,
|
|
213763
|
-
/\bKIMI_API_KEY\b/,
|
|
213764
|
-
/\bOPENAI_API_KEY\b/,
|
|
213765
|
-
/\bANTHROPIC_API_KEY\b/,
|
|
213766
|
-
/\bCURSOR_API_KEY\b/,
|
|
213767
|
-
/\bOPENCODE_API_KEY\b/,
|
|
213768
|
-
/\bforge_proxy_[A-Za-z0-9._-]+\b/,
|
|
213769
|
-
/\bProviderValidationError\b/,
|
|
213770
|
-
/\/forge-server\/node_modules\//,
|
|
213771
|
-
/https?:\/\/[^\s'"<>)]*\/api\/provider-egress\/[^\s'"<>)]*/i,
|
|
213772
|
-
/https?:\/\/[^\s'"<>)]*(?:apex|deepseek|moonshot|kimi)[^\s'"<>)]*/i
|
|
213773
|
-
];
|
|
213774
|
-
const DEFAULT_PUBLIC_PROVIDER_ERROR = "CMMD is temporarily unavailable. Check the workspace AI connection and retry.";
|
|
213775
|
-
function isProviderRateOrQuotaError(message) {
|
|
213776
|
-
if (!message) return false;
|
|
213777
|
-
const normalized = message.toLowerCase();
|
|
213778
|
-
return /\b(rate[\s-]?limit|too\s+many\s+requests|usage[ _-]limit|usage_limit_reached|quota|insufficient_quota)\b/.test(normalized) || /\b429\b/.test(normalized);
|
|
213779
|
-
}
|
|
213780
|
-
function providerLifecycleOrAccountCopy(message) {
|
|
213781
|
-
if (/\bbound to provider account\b/.test(message) && /\bcannot use provider account\b/.test(message)) return "Forge could not switch this conversation to the selected AI account. Retry the account switch.";
|
|
213782
|
-
if (/\bforge_runtime_repairing\b/.test(message) || /\bthis turn was interrupted because the environment is being repaired\b/.test(message)) return "This turn was interrupted because the environment is being repaired. Retry after the repair finishes.";
|
|
213783
|
-
if (/\bforge_runtime_stopped\b/.test(message) || /\bthis turn was interrupted because the environment was stopped\b/.test(message)) return "This turn was interrupted because the environment was stopped. Start the environment and retry.";
|
|
213784
|
-
return null;
|
|
213785
|
-
}
|
|
213786
|
-
/**
|
|
213787
|
-
* Infer a SAFE, clearer failure category from a (possibly leaky) provider error.
|
|
213788
|
-
*
|
|
213789
|
-
* Returns a canned, user-facing message — it never echoes the raw input, so it
|
|
213790
|
-
* is safe to use even when the source message contains provider internals.
|
|
213791
|
-
* Used to give actionable feedback ("model isn't available", "rate-limited",
|
|
213792
|
-
* "auth failed") instead of the blanket "CMMD is temporarily unavailable."
|
|
213793
|
-
* Returns null when no category can be confidently inferred.
|
|
213794
|
-
*/
|
|
213795
|
-
function categorizePublicProviderError(message) {
|
|
213796
|
-
if (!message) return null;
|
|
213797
|
-
const m = message.toLowerCase();
|
|
213798
|
-
const lifecycleOrAccountCopy = providerLifecycleOrAccountCopy(m);
|
|
213799
|
-
if (lifecycleOrAccountCopy !== null) return lifecycleOrAccountCopy;
|
|
213800
|
-
if (/\bforge_provider_model_not_allowed\b/.test(m) || /\bnot\s+scoped\s+to\s+the\s+requested\s+model\b/.test(m)) return "The workspace AI credential does not cover the model this turn asked for. Retry, or pick that model for the thread.";
|
|
213801
|
-
if (/\bmodel\b[\s\S]*\b(not\s+(found|available|supported)|does\s+not\s+exist|unsupported|invalid|unknown)\b/.test(m) || /\b(unsupported|unknown|invalid)\b[\s\S]*\bmodel\b/.test(m) || /\b(model_not_found|invalid_model|model_not_supported)\b/.test(m)) return "The selected model isn't available right now. Pick a different model and retry.";
|
|
213802
|
-
if (isProviderRateOrQuotaError(m)) return "The AI service is rate-limited or out of quota. Wait a moment and retry.";
|
|
213803
|
-
if (/_credential_reconnect_required\b/.test(m)) return "The workspace AI credential needs to be reconnected. Reconnect it in Settings > Providers, then retry.";
|
|
213804
|
-
if (/\b(unauthorized|forbidden|authentication\s+(failed|error|required)|invalid\s+api\s+key|expired\s+(token|credential))\b/.test(m) || /\b40[13]\b/.test(m)) return "Workspace AI authentication failed. Reconnect the workspace and retry.";
|
|
213805
|
-
if (/\b(timed?\s*out|timeout|overloaded|service\s+unavailable)\b/.test(m) || /\b50[234]\b/.test(m)) return "The AI service is temporarily overloaded. Retry in a moment.";
|
|
213806
|
-
return null;
|
|
213807
|
-
}
|
|
213808
|
-
function containsPublicProviderLeak(message) {
|
|
213809
|
-
if (!message) return false;
|
|
213810
|
-
return PUBLIC_PROVIDER_LEAK_PATTERNS.some((pattern) => pattern.test(message));
|
|
213811
|
-
}
|
|
213812
|
-
function sanitizePublicProviderText(message, options = {}) {
|
|
213813
|
-
if (message === void 0 || message === null) return null;
|
|
213814
|
-
const trimmed = message.trim();
|
|
213815
|
-
if (trimmed.length === 0) return null;
|
|
213816
|
-
if (!containsPublicProviderLeak(trimmed)) return trimmed;
|
|
213817
|
-
return categorizePublicProviderError(trimmed) ?? options.fallback ?? "CMMD is temporarily unavailable. Check the workspace AI connection and retry.";
|
|
213818
|
-
}
|
|
213819
213991
|
function resolveApexBaseUrl(settings, config) {
|
|
213820
213992
|
return config?.forgeApexBaseUrl || settings.baseUrl || "https://apex.cmmd.ai";
|
|
213821
213993
|
}
|
|
@@ -270788,7 +270960,7 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
|
|
|
270788
270960
|
var dgram = __require("dgram");
|
|
270789
270961
|
var thunky = require_thunky();
|
|
270790
270962
|
var events = __require("events");
|
|
270791
|
-
var os
|
|
270963
|
+
var os = __require("os");
|
|
270792
270964
|
var noop = function() {};
|
|
270793
270965
|
module.exports = function(opts) {
|
|
270794
270966
|
if (!opts) opts = {};
|
|
@@ -270919,14 +271091,14 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
|
|
|
270919
271091
|
return that;
|
|
270920
271092
|
};
|
|
270921
271093
|
function defaultInterface() {
|
|
270922
|
-
var networks = os
|
|
271094
|
+
var networks = os.networkInterfaces();
|
|
270923
271095
|
var names = Object.keys(networks);
|
|
270924
271096
|
for (var i = 0; i < names.length; i++) {
|
|
270925
271097
|
var net = networks[names[i]];
|
|
270926
271098
|
for (var j = 0; j < net.length; j++) {
|
|
270927
271099
|
var iface = net[j];
|
|
270928
271100
|
if (isIPv4(iface.family) && !iface.internal) {
|
|
270929
|
-
if (os
|
|
271101
|
+
if (os.platform() === "darwin" && names[i] === "en0") return iface.address;
|
|
270930
271102
|
return "0.0.0.0";
|
|
270931
271103
|
}
|
|
270932
271104
|
}
|
|
@@ -270934,7 +271106,7 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
|
|
|
270934
271106
|
return "127.0.0.1";
|
|
270935
271107
|
}
|
|
270936
271108
|
function allInterfaces() {
|
|
270937
|
-
var networks = os
|
|
271109
|
+
var networks = os.networkInterfaces();
|
|
270938
271110
|
var names = Object.keys(networks);
|
|
270939
271111
|
var res = [];
|
|
270940
271112
|
for (var i = 0; i < names.length; i++) {
|
|
@@ -292518,52 +292690,6 @@ function applyThreadGoalPreamble(goal, input) {
|
|
|
292518
292690
|
return trimmedInput.length > 0 ? `${preamble}\n\n${trimmedInput}` : preamble;
|
|
292519
292691
|
}
|
|
292520
292692
|
//#endregion
|
|
292521
|
-
//#region src/orchestration/providerRetryableFailure.ts
|
|
292522
|
-
/**
|
|
292523
|
-
* Classifies a provider turn-start failure as a retryable rate-limit or
|
|
292524
|
-
* transient-availability failure (429/503), a terminal failure that names why
|
|
292525
|
-
* it will never clear on its own, or unrecognized (null).
|
|
292526
|
-
*
|
|
292527
|
-
* CMMD-Center/forge#3558: a 429 from CMMD's provider egress used to leave no
|
|
292528
|
-
* trace at all, because nothing in the turn-start path recognized it as a
|
|
292529
|
-
* distinct, retryable failure. `ProviderCommandReactor` uses this to decide
|
|
292530
|
-
* whether to retry a turn start with backoff, and to name the provider,
|
|
292531
|
-
* status and Retry-After in the `provider.turn.start.failed` activity CMMD's
|
|
292532
|
-
* watchdog reads once retries are exhausted.
|
|
292533
|
-
*
|
|
292534
|
-
* Reuses `isProviderRateOrQuotaError` (the same predicate the public-copy
|
|
292535
|
-
* sanitizer uses to recognize a rate-limit message) rather than restating its
|
|
292536
|
-
* word list here.
|
|
292537
|
-
*/
|
|
292538
|
-
const SERVICE_UNAVAILABLE_PATTERN = /\b(503|service\s+unavailable)\b/i;
|
|
292539
|
-
const RATE_LIMITED_PAST_TENSE_PATTERN = /\brate[\s-]?limit(?:ed|ing)?\b/i;
|
|
292540
|
-
const QUOTA_OR_USAGE_LIMIT_PATTERN = /\b(usage[ _-]limit|usage_limit_reached|quota|insufficient_quota)\b/i;
|
|
292541
|
-
const EXPLICIT_429_PATTERN = /\b429\b/;
|
|
292542
|
-
const RETRY_AFTER_SECONDS_PATTERN = /retry-after[^0-9]{0,10}(\d+)|(?:try again|retry)\s+in\s+(\d+)\s*(?:s\b|sec(?:onds?)?\b)?/i;
|
|
292543
|
-
function parseRetryAfterSeconds(detail) {
|
|
292544
|
-
const match = detail.match(RETRY_AFTER_SECONDS_PATTERN);
|
|
292545
|
-
if (!match) return null;
|
|
292546
|
-
const raw = match[1] ?? match[2];
|
|
292547
|
-
const seconds = raw ? Number(raw) : NaN;
|
|
292548
|
-
return Number.isFinite(seconds) ? seconds : null;
|
|
292549
|
-
}
|
|
292550
|
-
function classifyRetryableProviderFailure(detail) {
|
|
292551
|
-
if (!detail || detail.trim().length === 0) return null;
|
|
292552
|
-
if (QUOTA_OR_USAGE_LIMIT_PATTERN.test(detail) && !EXPLICIT_429_PATTERN.test(detail)) return {
|
|
292553
|
-
kind: "terminal",
|
|
292554
|
-
reason: "quota",
|
|
292555
|
-
message: detail
|
|
292556
|
-
};
|
|
292557
|
-
const isRateLimited = isProviderRateOrQuotaError(detail) || RATE_LIMITED_PAST_TENSE_PATTERN.test(detail);
|
|
292558
|
-
const isUnavailable = SERVICE_UNAVAILABLE_PATTERN.test(detail);
|
|
292559
|
-
if (!isRateLimited && !isUnavailable) return null;
|
|
292560
|
-
return {
|
|
292561
|
-
kind: "retryable",
|
|
292562
|
-
status: isRateLimited ? 429 : 503,
|
|
292563
|
-
retryAfterSeconds: parseRetryAfterSeconds(detail)
|
|
292564
|
-
};
|
|
292565
|
-
}
|
|
292566
|
-
//#endregion
|
|
292567
292693
|
//#region src/orchestration/Layers/ProviderCommandReactor.ts
|
|
292568
292694
|
function toNonEmptyProviderInput(value) {
|
|
292569
292695
|
const normalized = value?.trim();
|
|
@@ -294739,12 +294865,62 @@ const CheckpointReactorLive = effect(CheckpointReactor, gen(function* () {
|
|
|
294739
294865
|
createdAt: input.createdAt
|
|
294740
294866
|
});
|
|
294741
294867
|
});
|
|
294868
|
+
/**
|
|
294869
|
+
* The one durable, deterministic-id activity that records how a turn ended
|
|
294870
|
+
* (CMMD-Center/forge#3562 piece A, CMMD-Center/CMMD#5822): `completed`,
|
|
294871
|
+
* `interrupted` (an abort or a provider cancellation), or `failed` with the
|
|
294872
|
+
* provider's own error message. Recorded here, ahead of every
|
|
294873
|
+
* checkpoint-specific early return below, so it fires for every turn
|
|
294874
|
+
* regardless of whether this workspace can capture a git checkpoint at
|
|
294875
|
+
* all -- unlike a checkpoint, this never depends on `resolveCheckpointCwd`.
|
|
294876
|
+
*
|
|
294877
|
+
* The activity id is derived from the turn id, never `crypto.randomUUID()`,
|
|
294878
|
+
* so a redelivered `turn.completed` for the same turn replaces the same
|
|
294879
|
+
* row rather than adding a second one. The guard below additionally
|
|
294880
|
+
* refuses to touch a turn that already has one: first write wins, so a
|
|
294881
|
+
* later, different signal for the same turn can never overwrite an
|
|
294882
|
+
* outcome CMMD may already have acknowledged.
|
|
294883
|
+
*
|
|
294884
|
+
* Deriving the wire `turn_status` event from a new activity recorded at
|
|
294885
|
+
* the moment the turn actually ends -- not from `thread.latestTurn` or
|
|
294886
|
+
* `thread.checkpoints`, both of which already hold outcomes for turns that
|
|
294887
|
+
* settled before this shipped -- is deliberate: those would insert a brand
|
|
294888
|
+
* new event into history CMMD already acknowledged for every existing
|
|
294889
|
+
* thread, which `RuntimeThreadHistoryConflict` exists specifically to
|
|
294890
|
+
* reject (CMMD-Center/forge#3497). A turn that settled before this shipped
|
|
294891
|
+
* gets no terminal event; only a turn that ends after it does.
|
|
294892
|
+
*/
|
|
294893
|
+
const recordTurnTerminalStatus = fn$1("recordTurnTerminalStatus")(function* (event, thread, turnId) {
|
|
294894
|
+
const activityId = turnTerminalStatusActivityId(turnId);
|
|
294895
|
+
if (thread.activities.some((activity) => activity.id === activityId)) return;
|
|
294896
|
+
const outcome = turnTerminalOutcomeFromRuntimeState(event.payload.state);
|
|
294897
|
+
const recordedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
294898
|
+
yield* orchestrationEngine.dispatch({
|
|
294899
|
+
type: "thread.activity.append",
|
|
294900
|
+
commandId: serverCommandId$1("turn-terminal-status"),
|
|
294901
|
+
threadId: event.threadId,
|
|
294902
|
+
activity: {
|
|
294903
|
+
id: EventId.make(activityId),
|
|
294904
|
+
tone: outcome === "failed" ? "error" : "info",
|
|
294905
|
+
kind: TURN_TERMINAL_STATUS_ACTIVITY_KIND,
|
|
294906
|
+
summary: `Turn ${outcome}`,
|
|
294907
|
+
payload: {
|
|
294908
|
+
outcome,
|
|
294909
|
+
...outcome === "failed" && event.payload.errorMessage ? { errorMessage: event.payload.errorMessage } : {}
|
|
294910
|
+
},
|
|
294911
|
+
turnId,
|
|
294912
|
+
createdAt: recordedAt
|
|
294913
|
+
},
|
|
294914
|
+
createdAt: recordedAt
|
|
294915
|
+
});
|
|
294916
|
+
});
|
|
294742
294917
|
const captureCheckpointFromTurnCompletion = fn$1("captureCheckpointFromTurnCompletion")(function* (event) {
|
|
294743
294918
|
const turnId = toTurnId(event.turnId);
|
|
294744
294919
|
if (!turnId) return;
|
|
294745
294920
|
const readModel = yield* orchestrationEngine.getReadModel();
|
|
294746
294921
|
const thread = readModel.threads.find((entry) => entry.id === event.threadId);
|
|
294747
294922
|
if (!thread) return;
|
|
294923
|
+
yield* recordTurnTerminalStatus(event, thread, turnId);
|
|
294748
294924
|
if (thread.session?.activeTurnId && !sameId(thread.session.activeTurnId, turnId)) return;
|
|
294749
294925
|
if (thread.checkpoints.some((checkpoint) => checkpoint.turnId === turnId && checkpoint.status !== "missing")) return;
|
|
294750
294926
|
const checkpointCwd = yield* resolveCheckpointCwd({
|
|
@@ -299501,7 +299677,7 @@ function resolveBuildCommitFromEnv(env) {
|
|
|
299501
299677
|
* environment descriptor down instead of reporting an honest "unknown".
|
|
299502
299678
|
*/
|
|
299503
299679
|
function readBakedBuildCommit() {
|
|
299504
|
-
return "
|
|
299680
|
+
return "c825d8a74e28836b97016dbd8c2abc730841a622";
|
|
299505
299681
|
}
|
|
299506
299682
|
async function resolveServerBuildCommit(input) {
|
|
299507
299683
|
if (isFullCommitSha(input.baked)) return input.baked;
|
|
@@ -324464,7 +324640,7 @@ const connectLoginCommand = make$33("login", {
|
|
|
324464
324640
|
})).json();
|
|
324465
324641
|
},
|
|
324466
324642
|
openBrowser: async (url) => {
|
|
324467
|
-
const open = (await import("./open-
|
|
324643
|
+
const open = (await import("./open-2-reBrpv.mjs")).default;
|
|
324468
324644
|
await open(url);
|
|
324469
324645
|
return true;
|
|
324470
324646
|
},
|