@cmmd-center/forge 0.13.96 → 0.13.98
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 +307 -131
- package/dist/client/assets/{DiffPanel-CQF1ISJz.js → DiffPanel-BXrpBBfH.js} +2 -2
- package/dist/client/assets/{DiffPanel.logic-ZO6qepWo.js → DiffPanel.logic-DifBpfCt.js} +2 -2
- package/dist/client/assets/{DiffWorkerPoolProvider-B7F6vAh9.js → DiffWorkerPoolProvider-CeIkRxNR.js} +2 -2
- package/dist/client/assets/{PullRequestCodePanel-B93y-X1L.js → PullRequestCodePanel-DzqRqaHT.js} +2 -2
- package/dist/client/assets/{Virtualizer-BtFYmUB0.js → Virtualizer-DYODAvp9.js} +2 -2
- package/dist/client/assets/{WorkerPoolContext-VAp-zZQ5.js → WorkerPoolContext-G1u5WJw4.js} +62 -62
- package/dist/client/assets/{index-BhdZllxU.js → index-DdsEGQ7q.js} +59 -59
- package/dist/client/index.html +4 -4
- package/dist/forge-build.json +1 -1
- package/package.json +1 -1
package/dist/bin.cjs
CHANGED
|
@@ -66332,7 +66332,7 @@ function normalizeNumberish(value) {
|
|
|
66332
66332
|
}
|
|
66333
66333
|
//#endregion
|
|
66334
66334
|
//#region package.json
|
|
66335
|
-
var version$1 = "0.13.
|
|
66335
|
+
var version$1 = "0.13.98";
|
|
66336
66336
|
//#endregion
|
|
66337
66337
|
//#region src/sentry.ts
|
|
66338
66338
|
const SERVER_APP_NAME = "forge-server";
|
|
@@ -98404,6 +98404,119 @@ function providerAuthFailureMessage(message, provider, env = process.env) {
|
|
|
98404
98404
|
}).message;
|
|
98405
98405
|
}
|
|
98406
98406
|
//#endregion
|
|
98407
|
+
//#region ../../packages/shared/src/publicProviderCopy.ts
|
|
98408
|
+
const PUBLIC_PROVIDER_LEAK_PATTERNS = [
|
|
98409
|
+
/\bDeepSeek\b/i,
|
|
98410
|
+
/\bKimi\b/i,
|
|
98411
|
+
/\bMoonshot\b/i,
|
|
98412
|
+
/\bdeepseek[-._a-z0-9]*\b/i,
|
|
98413
|
+
/\bkimi[-._a-z0-9]*\b/i,
|
|
98414
|
+
/\bmoonshot[-._a-z0-9]*\b/i,
|
|
98415
|
+
/\bAPEX_API_KEY\b/,
|
|
98416
|
+
/\bDEEPSEEK_API_KEY\b/,
|
|
98417
|
+
/\bKIMI_API_KEY\b/,
|
|
98418
|
+
/\bOPENAI_API_KEY\b/,
|
|
98419
|
+
/\bANTHROPIC_API_KEY\b/,
|
|
98420
|
+
/\bCURSOR_API_KEY\b/,
|
|
98421
|
+
/\bOPENCODE_API_KEY\b/,
|
|
98422
|
+
/\bforge_proxy_[A-Za-z0-9._-]+\b/,
|
|
98423
|
+
/\bProviderValidationError\b/,
|
|
98424
|
+
/\/forge-server\/node_modules\//,
|
|
98425
|
+
/https?:\/\/[^\s'"<>)]*\/api\/provider-egress\/[^\s'"<>)]*/i,
|
|
98426
|
+
/https?:\/\/[^\s'"<>)]*(?:apex|deepseek|moonshot|kimi)[^\s'"<>)]*/i
|
|
98427
|
+
];
|
|
98428
|
+
const DEFAULT_PUBLIC_PROVIDER_ERROR = "CMMD is temporarily unavailable. Check the workspace AI connection and retry.";
|
|
98429
|
+
function isProviderRateOrQuotaError(message) {
|
|
98430
|
+
if (!message) return false;
|
|
98431
|
+
const normalized = message.toLowerCase();
|
|
98432
|
+
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);
|
|
98433
|
+
}
|
|
98434
|
+
function providerLifecycleOrAccountCopy(message) {
|
|
98435
|
+
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.";
|
|
98436
|
+
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.";
|
|
98437
|
+
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.";
|
|
98438
|
+
return null;
|
|
98439
|
+
}
|
|
98440
|
+
/**
|
|
98441
|
+
* Infer a SAFE, clearer failure category from a (possibly leaky) provider error.
|
|
98442
|
+
*
|
|
98443
|
+
* Returns a canned, user-facing message — it never echoes the raw input, so it
|
|
98444
|
+
* is safe to use even when the source message contains provider internals.
|
|
98445
|
+
* Used to give actionable feedback ("model isn't available", "rate-limited",
|
|
98446
|
+
* "auth failed") instead of the blanket "CMMD is temporarily unavailable."
|
|
98447
|
+
* Returns null when no category can be confidently inferred.
|
|
98448
|
+
*/
|
|
98449
|
+
function categorizePublicProviderError(message) {
|
|
98450
|
+
if (!message) return null;
|
|
98451
|
+
const m = message.toLowerCase();
|
|
98452
|
+
const lifecycleOrAccountCopy = providerLifecycleOrAccountCopy(m);
|
|
98453
|
+
if (lifecycleOrAccountCopy !== null) return lifecycleOrAccountCopy;
|
|
98454
|
+
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.";
|
|
98455
|
+
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.";
|
|
98456
|
+
if (isProviderRateOrQuotaError(m)) return "The AI service is rate-limited or out of quota. Wait a moment and retry.";
|
|
98457
|
+
if (/_credential_reconnect_required\b/.test(m)) return "The workspace AI credential needs to be reconnected. Reconnect it in Settings > Providers, then retry.";
|
|
98458
|
+
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.";
|
|
98459
|
+
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.";
|
|
98460
|
+
return null;
|
|
98461
|
+
}
|
|
98462
|
+
function containsPublicProviderLeak(message) {
|
|
98463
|
+
if (!message) return false;
|
|
98464
|
+
return PUBLIC_PROVIDER_LEAK_PATTERNS.some((pattern) => pattern.test(message));
|
|
98465
|
+
}
|
|
98466
|
+
function sanitizePublicProviderText(message, options = {}) {
|
|
98467
|
+
if (message === void 0 || message === null) return null;
|
|
98468
|
+
const trimmed = message.trim();
|
|
98469
|
+
if (trimmed.length === 0) return null;
|
|
98470
|
+
if (!containsPublicProviderLeak(trimmed)) return trimmed;
|
|
98471
|
+
return categorizePublicProviderError(trimmed) ?? options.fallback ?? "CMMD is temporarily unavailable. Check the workspace AI connection and retry.";
|
|
98472
|
+
}
|
|
98473
|
+
//#endregion
|
|
98474
|
+
//#region src/orchestration/providerRetryableFailure.ts
|
|
98475
|
+
/**
|
|
98476
|
+
* Classifies a provider turn-start failure as a retryable rate-limit or
|
|
98477
|
+
* transient-availability failure (429/503), a terminal failure that names why
|
|
98478
|
+
* it will never clear on its own, or unrecognized (null).
|
|
98479
|
+
*
|
|
98480
|
+
* CMMD-Center/forge#3558: a 429 from CMMD's provider egress used to leave no
|
|
98481
|
+
* trace at all, because nothing in the turn-start path recognized it as a
|
|
98482
|
+
* distinct, retryable failure. `ProviderCommandReactor` uses this to decide
|
|
98483
|
+
* whether to retry a turn start with backoff, and to name the provider,
|
|
98484
|
+
* status and Retry-After in the `provider.turn.start.failed` activity CMMD's
|
|
98485
|
+
* watchdog reads once retries are exhausted.
|
|
98486
|
+
*
|
|
98487
|
+
* Reuses `isProviderRateOrQuotaError` (the same predicate the public-copy
|
|
98488
|
+
* sanitizer uses to recognize a rate-limit message) rather than restating its
|
|
98489
|
+
* word list here.
|
|
98490
|
+
*/
|
|
98491
|
+
const SERVICE_UNAVAILABLE_PATTERN = /\b(503|service\s+unavailable)\b/i;
|
|
98492
|
+
const RATE_LIMITED_PAST_TENSE_PATTERN = /\brate[\s-]?limit(?:ed|ing)?\b/i;
|
|
98493
|
+
const QUOTA_OR_USAGE_LIMIT_PATTERN = /\b(usage[ _-]limit|usage_limit_reached|quota|insufficient_quota)\b/i;
|
|
98494
|
+
const EXPLICIT_429_PATTERN = /\b429\b/;
|
|
98495
|
+
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;
|
|
98496
|
+
function parseRetryAfterSeconds(detail) {
|
|
98497
|
+
const match = detail.match(RETRY_AFTER_SECONDS_PATTERN);
|
|
98498
|
+
if (!match) return null;
|
|
98499
|
+
const raw = match[1] ?? match[2];
|
|
98500
|
+
const seconds = raw ? Number(raw) : NaN;
|
|
98501
|
+
return Number.isFinite(seconds) ? seconds : null;
|
|
98502
|
+
}
|
|
98503
|
+
function classifyRetryableProviderFailure(detail) {
|
|
98504
|
+
if (!detail || detail.trim().length === 0) return null;
|
|
98505
|
+
if (QUOTA_OR_USAGE_LIMIT_PATTERN.test(detail) && !EXPLICIT_429_PATTERN.test(detail)) return {
|
|
98506
|
+
kind: "terminal",
|
|
98507
|
+
reason: "quota",
|
|
98508
|
+
message: detail
|
|
98509
|
+
};
|
|
98510
|
+
const isRateLimited = isProviderRateOrQuotaError(detail) || RATE_LIMITED_PAST_TENSE_PATTERN.test(detail);
|
|
98511
|
+
const isUnavailable = SERVICE_UNAVAILABLE_PATTERN.test(detail);
|
|
98512
|
+
if (!isRateLimited && !isUnavailable) return null;
|
|
98513
|
+
return {
|
|
98514
|
+
kind: "retryable",
|
|
98515
|
+
status: isRateLimited ? 429 : 503,
|
|
98516
|
+
retryAfterSeconds: parseRetryAfterSeconds(detail)
|
|
98517
|
+
};
|
|
98518
|
+
}
|
|
98519
|
+
//#endregion
|
|
98407
98520
|
//#region src/orchestration/turnFailureReason.ts
|
|
98408
98521
|
/**
|
|
98409
98522
|
* Machine-readable reasons for a turn that failed before it produced any
|
|
@@ -98437,6 +98550,31 @@ function classifyProviderTurnStartFailureReason(detail) {
|
|
|
98437
98550
|
message
|
|
98438
98551
|
};
|
|
98439
98552
|
}
|
|
98553
|
+
/**
|
|
98554
|
+
* Classifies a turn that ended mid-flight, after it had already started
|
|
98555
|
+
* (CMMD-Center/forge#3562 piece A): a `runtime.error` that reached CMMD as
|
|
98556
|
+
* `error_summary`, now also read into a `turn_status: failed` reason.
|
|
98557
|
+
*
|
|
98558
|
+
* `provider_turn_failed` is the code CMMD's own error_summary heuristic
|
|
98559
|
+
* already assigns this class of failure
|
|
98560
|
+
* (`shared/forge-work-session-failure-reasons.ts`), so a Forge-sent
|
|
98561
|
+
* `turn_status` carrying it lines up with what CMMD already infers instead
|
|
98562
|
+
* of teaching it a new code for the common case. `provider_usage_limit`
|
|
98563
|
+
* reuses the same wording `classifyRetryableProviderFailure` already treats
|
|
98564
|
+
* as a wall that will not clear by retrying (CMMD-Center/forge#3558): a
|
|
98565
|
+
* usage-limit message inside a turn that already ended is worth naming
|
|
98566
|
+
* specifically, even without the fuller upstream port (see the piece A PR's
|
|
98567
|
+
* follow-up issue) that would stop Codex from retrying against that wall in
|
|
98568
|
+
* the first place.
|
|
98569
|
+
*/
|
|
98570
|
+
function classifyMidTurnFailureReason(detail) {
|
|
98571
|
+
const message = detail && detail.trim().length > 0 ? detail : "The runtime reported an error while this run was in progress.";
|
|
98572
|
+
const classified = classifyRetryableProviderFailure(detail);
|
|
98573
|
+
return {
|
|
98574
|
+
code: classified?.kind === "terminal" && classified.reason === "quota" ? "provider_usage_limit" : "provider_turn_failed",
|
|
98575
|
+
message
|
|
98576
|
+
};
|
|
98577
|
+
}
|
|
98440
98578
|
function checkpointTimeoutReason(detail) {
|
|
98441
98579
|
return {
|
|
98442
98580
|
code: "checkpoint_timeout",
|
|
@@ -98571,6 +98709,108 @@ function serializeReplicaMessage(message) {
|
|
|
98571
98709
|
};
|
|
98572
98710
|
}
|
|
98573
98711
|
//#endregion
|
|
98712
|
+
//#region src/orchestration/turnTerminalStatus.ts
|
|
98713
|
+
/**
|
|
98714
|
+
* Shared vocabulary between the reactor that records how a turn ended
|
|
98715
|
+
* (`Layers/CheckpointReactor.ts`, on the provider's own `turn.completed`
|
|
98716
|
+
* event) and the serializer that turns that record into CMMD's `turn_status`
|
|
98717
|
+
* terminal event (`runtimeReplication/turnTerminalStatusSerializer.ts`).
|
|
98718
|
+
* CMMD-Center/forge#3562 piece A, CMMD-Center/CMMD#5822.
|
|
98719
|
+
*/
|
|
98720
|
+
/** The activity kind this durable record uses. Never sent to CMMD as-is: the
|
|
98721
|
+
* serializer reads it and emits the wire `turn_status` event instead. */
|
|
98722
|
+
const TURN_TERMINAL_STATUS_ACTIVITY_KIND = "turn.terminal-status";
|
|
98723
|
+
/**
|
|
98724
|
+
* CMMD's three-way outcome from the provider's own four-way `turn.completed`
|
|
98725
|
+
* state. `cancelled` and `interrupted` both read as `interrupted`: neither
|
|
98726
|
+
* the provider wording nor CMMD's Work Session UI distinguishes an
|
|
98727
|
+
* operator-cancelled turn from one the provider gave up on mid-flight.
|
|
98728
|
+
*/
|
|
98729
|
+
function turnTerminalOutcomeFromRuntimeState(state) {
|
|
98730
|
+
if (state === "failed") return "failed";
|
|
98731
|
+
if (state === "completed") return "completed";
|
|
98732
|
+
return "interrupted";
|
|
98733
|
+
}
|
|
98734
|
+
/**
|
|
98735
|
+
* Deterministic activity id for a turn's terminal record: never
|
|
98736
|
+
* `crypto.randomUUID()`. The projector replaces an activity by id
|
|
98737
|
+
* (`thread.activity-appended`), so a redelivered `turn.completed` for the
|
|
98738
|
+
* same turn lands on the same row instead of adding a second one, and the
|
|
98739
|
+
* serializer's own `eventId` (`activity:<id>`) inherits the same guarantee.
|
|
98740
|
+
*/
|
|
98741
|
+
function turnTerminalStatusActivityId(turnId) {
|
|
98742
|
+
return `turn-terminal:${turnId}`;
|
|
98743
|
+
}
|
|
98744
|
+
//#endregion
|
|
98745
|
+
//#region src/runtimeReplication/eventFormatting.ts
|
|
98746
|
+
/**
|
|
98747
|
+
* Small formatting helpers shared by every source `serializer.ts` folds into
|
|
98748
|
+
* a thread's replicated event list. Kept in their own module, with no
|
|
98749
|
+
* dependency on `serializer.ts` itself, so a narrower serializer (such as
|
|
98750
|
+
* `turnTerminalStatusSerializer.ts`) can reuse them without creating an
|
|
98751
|
+
* import cycle back into the file that assembles the full event list.
|
|
98752
|
+
*/
|
|
98753
|
+
function text(value, max) {
|
|
98754
|
+
const normalized = value.trim();
|
|
98755
|
+
return normalized ? normalized.slice(0, max) : null;
|
|
98756
|
+
}
|
|
98757
|
+
function canonicalRuntimeEventTimestamp(value) {
|
|
98758
|
+
let timestamp = new Date(value);
|
|
98759
|
+
if (Number.isNaN(timestamp.getTime())) {
|
|
98760
|
+
const legacyBsdNanoseconds = value.match(/^(.*:\d{2})\.\d+NZ$/);
|
|
98761
|
+
timestamp = new Date(legacyBsdNanoseconds ? `${legacyBsdNanoseconds[1]}Z` : value);
|
|
98762
|
+
}
|
|
98763
|
+
if (Number.isNaN(timestamp.getTime())) throw new Error(`Invalid runtime event timestamp: ${value}`);
|
|
98764
|
+
return timestamp.toISOString();
|
|
98765
|
+
}
|
|
98766
|
+
//#endregion
|
|
98767
|
+
//#region src/runtimeReplication/turnTerminalStatusSerializer.ts
|
|
98768
|
+
function outcomeOf(payload) {
|
|
98769
|
+
if (!payload || typeof payload !== "object") return null;
|
|
98770
|
+
const outcome = payload.outcome;
|
|
98771
|
+
return outcome === "completed" || outcome === "interrupted" || outcome === "failed" ? outcome : null;
|
|
98772
|
+
}
|
|
98773
|
+
function errorMessageOf(payload) {
|
|
98774
|
+
if (!payload || typeof payload !== "object") return null;
|
|
98775
|
+
const message = payload.errorMessage;
|
|
98776
|
+
return typeof message === "string" && message.trim().length > 0 ? message : null;
|
|
98777
|
+
}
|
|
98778
|
+
/**
|
|
98779
|
+
* Turns the one durable activity `Layers/CheckpointReactor.ts` records when a
|
|
98780
|
+
* turn ends into CMMD's `turn_status` terminal event (CMMD-Center/forge#3562
|
|
98781
|
+
* piece A, CMMD-Center/CMMD#5822): `completed`, `interrupted`, or `failed`
|
|
98782
|
+
* with a structured `reason`. `error_summary` (`serializeFailureActivity` in
|
|
98783
|
+
* `serializer.ts`) keeps replicating separately for a mid-turn failure; this
|
|
98784
|
+
* is added next to it, never instead of it.
|
|
98785
|
+
*
|
|
98786
|
+
* The activity's own id is deterministic (`turn-terminal:<turnId>`,
|
|
98787
|
+
* `turnTerminalStatusActivityId`), so this event's `eventId` -- built from
|
|
98788
|
+
* that id the same way every other activity-sourced event on this thread is
|
|
98789
|
+
* (`activity:<id>`) -- can never collide with a second, different terminal
|
|
98790
|
+
* status for the same turn: there is exactly one activity row to read it
|
|
98791
|
+
* from, and the reactor that writes that row never overwrites it once set.
|
|
98792
|
+
*/
|
|
98793
|
+
function serializeTurnTerminalStatusActivity(activity) {
|
|
98794
|
+
if (activity.kind !== "turn.terminal-status" || !activity.turnId) return null;
|
|
98795
|
+
const outcome = outcomeOf(activity.payload);
|
|
98796
|
+
if (outcome === null) return null;
|
|
98797
|
+
const reason = outcome === "failed" ? classifyMidTurnFailureReason(errorMessageOf(activity.payload)) : null;
|
|
98798
|
+
return {
|
|
98799
|
+
sourceSequence: null,
|
|
98800
|
+
event: {
|
|
98801
|
+
eventId: `activity:${activity.id}`,
|
|
98802
|
+
occurredAt: canonicalRuntimeEventTimestamp(activity.createdAt),
|
|
98803
|
+
turnId: activity.turnId,
|
|
98804
|
+
kind: "turn_status",
|
|
98805
|
+
status: outcome,
|
|
98806
|
+
...reason ? { reason: {
|
|
98807
|
+
code: reason.code,
|
|
98808
|
+
message: text(reason.message, 2e3) ?? reason.message
|
|
98809
|
+
} } : {}
|
|
98810
|
+
}
|
|
98811
|
+
};
|
|
98812
|
+
}
|
|
98813
|
+
//#endregion
|
|
98574
98814
|
//#region src/runtimeReplication/serializer.ts
|
|
98575
98815
|
function threadStatus(thread) {
|
|
98576
98816
|
if (thread.archivedAt) return "archived";
|
|
@@ -98637,19 +98877,6 @@ function serializeRuntimeInventory(snapshot, providerInventory) {
|
|
|
98637
98877
|
...providerInventory === void 0 ? {} : { providers: serializeProviderCatalog(providerInventory) }
|
|
98638
98878
|
};
|
|
98639
98879
|
}
|
|
98640
|
-
function text(value, max) {
|
|
98641
|
-
const normalized = value.trim();
|
|
98642
|
-
return normalized ? normalized.slice(0, max) : null;
|
|
98643
|
-
}
|
|
98644
|
-
function canonicalRuntimeEventTimestamp(value) {
|
|
98645
|
-
let timestamp = new Date(value);
|
|
98646
|
-
if (Number.isNaN(timestamp.getTime())) {
|
|
98647
|
-
const legacyBsdNanoseconds = value.match(/^(.*:\d{2})\.\d+NZ$/);
|
|
98648
|
-
timestamp = new Date(legacyBsdNanoseconds ? `${legacyBsdNanoseconds[1]}Z` : value);
|
|
98649
|
-
}
|
|
98650
|
-
if (Number.isNaN(timestamp.getTime())) throw new Error(`Invalid runtime event timestamp: ${value}`);
|
|
98651
|
-
return timestamp.toISOString();
|
|
98652
|
-
}
|
|
98653
98880
|
/** Spread onto an event: present only when the projection knows the turn. */
|
|
98654
98881
|
function eventTurn(turnId) {
|
|
98655
98882
|
return turnId ? { turnId } : {};
|
|
@@ -98954,10 +99181,13 @@ function foldReportedUsage(reported) {
|
|
|
98954
99181
|
* model, so a mid-thread switch used to republish a finished turn as a second
|
|
98955
99182
|
* row for spend that happened once (FOR-36388, fixed in FOR-36438).
|
|
98956
99183
|
*/
|
|
98957
|
-
/** Turns named on this thread's own rows; hydrated replica rows carry no local
|
|
99184
|
+
/** Turns named on this thread's own rows; hydrated replica rows carry no local
|
|
99185
|
+
* turn, and the terminal-status activity (CMMD-Center/forge#3562 piece A) is
|
|
99186
|
+
* a wire-protocol source in its own right, not a Usage signal, so it is
|
|
99187
|
+
* excluded the same way a `replica.*` activity is. */
|
|
98958
99188
|
function localTurnIds(thread) {
|
|
98959
99189
|
const fromMessages = thread.messages.filter((message) => !message.streaming).map((message) => message.turnId);
|
|
98960
|
-
const fromActivities = thread.activities.filter((activity) => !activity.kind.startsWith("replica.")).map((activity) => activity.turnId);
|
|
99190
|
+
const fromActivities = thread.activities.filter((activity) => !activity.kind.startsWith("replica.") && activity.kind !== "turn.terminal-status").map((activity) => activity.turnId);
|
|
98961
99191
|
const turnIds = /* @__PURE__ */ new Set();
|
|
98962
99192
|
for (const turnId of [...fromMessages, ...fromActivities]) if (turnId) turnIds.add(turnId);
|
|
98963
99193
|
return turnIds;
|
|
@@ -98967,8 +99197,12 @@ function settledTurnIds(thread) {
|
|
|
98967
99197
|
if (thread.latestTurn?.state === "running") turnIds.delete(thread.latestTurn.turnId);
|
|
98968
99198
|
return turnIds;
|
|
98969
99199
|
}
|
|
99200
|
+
/** A turn's own activities for Usage derivation, excluding the terminal-status
|
|
99201
|
+
* activity: that record's own `createdAt` (the turn's very end) must never
|
|
99202
|
+
* shift `turnLatestAt`, or an already-acknowledged usage row would rehash
|
|
99203
|
+
* the moment a turn that used to have no terminal activity gains one. */
|
|
98970
99204
|
function turnActivities(thread, turnId) {
|
|
98971
|
-
return thread.activities.filter((activity) => activity.turnId === turnId).toSorted((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id));
|
|
99205
|
+
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));
|
|
98972
99206
|
}
|
|
98973
99207
|
/** The last thing that happened in the turn: what the Recorded turn is stamped with. */
|
|
98974
99208
|
function turnLatestAt(thread, turnId, activities) {
|
|
@@ -99177,6 +99411,10 @@ function serializeRuntimeThreadEvents(thread) {
|
|
|
99177
99411
|
const event = serializePlanActivity(activity);
|
|
99178
99412
|
return event === null ? [] : [event];
|
|
99179
99413
|
});
|
|
99414
|
+
const turnTerminalStatuses = thread.activities.flatMap((activity) => {
|
|
99415
|
+
const event = serializeTurnTerminalStatusActivity(activity);
|
|
99416
|
+
return event === null ? [] : [event];
|
|
99417
|
+
});
|
|
99180
99418
|
const recordedTurns = serializeRecordedTurns(thread);
|
|
99181
99419
|
const textGenerations = serializeTextGenerations(thread);
|
|
99182
99420
|
const priorEvents = [
|
|
@@ -99186,6 +99424,7 @@ function serializeRuntimeThreadEvents(thread) {
|
|
|
99186
99424
|
...worktreeLaunchActivities,
|
|
99187
99425
|
...planActivities,
|
|
99188
99426
|
...failureActivities,
|
|
99427
|
+
...turnTerminalStatuses,
|
|
99189
99428
|
...recordedTurns,
|
|
99190
99429
|
...textGenerations
|
|
99191
99430
|
];
|
|
@@ -214176,73 +214415,6 @@ const makeCodexAdapter = require_Schema$1.fn("makeCodexAdapter")(function* (opti
|
|
|
214176
214415
|
function makeCodexAdapterLive(options) {
|
|
214177
214416
|
return require_Schema$1.effect(CodexAdapter, makeCodexAdapter(options));
|
|
214178
214417
|
}
|
|
214179
|
-
//#endregion
|
|
214180
|
-
//#region ../../packages/shared/src/publicProviderCopy.ts
|
|
214181
|
-
const PUBLIC_PROVIDER_LEAK_PATTERNS = [
|
|
214182
|
-
/\bDeepSeek\b/i,
|
|
214183
|
-
/\bKimi\b/i,
|
|
214184
|
-
/\bMoonshot\b/i,
|
|
214185
|
-
/\bdeepseek[-._a-z0-9]*\b/i,
|
|
214186
|
-
/\bkimi[-._a-z0-9]*\b/i,
|
|
214187
|
-
/\bmoonshot[-._a-z0-9]*\b/i,
|
|
214188
|
-
/\bAPEX_API_KEY\b/,
|
|
214189
|
-
/\bDEEPSEEK_API_KEY\b/,
|
|
214190
|
-
/\bKIMI_API_KEY\b/,
|
|
214191
|
-
/\bOPENAI_API_KEY\b/,
|
|
214192
|
-
/\bANTHROPIC_API_KEY\b/,
|
|
214193
|
-
/\bCURSOR_API_KEY\b/,
|
|
214194
|
-
/\bOPENCODE_API_KEY\b/,
|
|
214195
|
-
/\bforge_proxy_[A-Za-z0-9._-]+\b/,
|
|
214196
|
-
/\bProviderValidationError\b/,
|
|
214197
|
-
/\/forge-server\/node_modules\//,
|
|
214198
|
-
/https?:\/\/[^\s'"<>)]*\/api\/provider-egress\/[^\s'"<>)]*/i,
|
|
214199
|
-
/https?:\/\/[^\s'"<>)]*(?:apex|deepseek|moonshot|kimi)[^\s'"<>)]*/i
|
|
214200
|
-
];
|
|
214201
|
-
const DEFAULT_PUBLIC_PROVIDER_ERROR = "CMMD is temporarily unavailable. Check the workspace AI connection and retry.";
|
|
214202
|
-
function isProviderRateOrQuotaError(message) {
|
|
214203
|
-
if (!message) return false;
|
|
214204
|
-
const normalized = message.toLowerCase();
|
|
214205
|
-
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);
|
|
214206
|
-
}
|
|
214207
|
-
function providerLifecycleOrAccountCopy(message) {
|
|
214208
|
-
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.";
|
|
214209
|
-
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.";
|
|
214210
|
-
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.";
|
|
214211
|
-
return null;
|
|
214212
|
-
}
|
|
214213
|
-
/**
|
|
214214
|
-
* Infer a SAFE, clearer failure category from a (possibly leaky) provider error.
|
|
214215
|
-
*
|
|
214216
|
-
* Returns a canned, user-facing message — it never echoes the raw input, so it
|
|
214217
|
-
* is safe to use even when the source message contains provider internals.
|
|
214218
|
-
* Used to give actionable feedback ("model isn't available", "rate-limited",
|
|
214219
|
-
* "auth failed") instead of the blanket "CMMD is temporarily unavailable."
|
|
214220
|
-
* Returns null when no category can be confidently inferred.
|
|
214221
|
-
*/
|
|
214222
|
-
function categorizePublicProviderError(message) {
|
|
214223
|
-
if (!message) return null;
|
|
214224
|
-
const m = message.toLowerCase();
|
|
214225
|
-
const lifecycleOrAccountCopy = providerLifecycleOrAccountCopy(m);
|
|
214226
|
-
if (lifecycleOrAccountCopy !== null) return lifecycleOrAccountCopy;
|
|
214227
|
-
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.";
|
|
214228
|
-
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.";
|
|
214229
|
-
if (isProviderRateOrQuotaError(m)) return "The AI service is rate-limited or out of quota. Wait a moment and retry.";
|
|
214230
|
-
if (/_credential_reconnect_required\b/.test(m)) return "The workspace AI credential needs to be reconnected. Reconnect it in Settings > Providers, then retry.";
|
|
214231
|
-
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.";
|
|
214232
|
-
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.";
|
|
214233
|
-
return null;
|
|
214234
|
-
}
|
|
214235
|
-
function containsPublicProviderLeak(message) {
|
|
214236
|
-
if (!message) return false;
|
|
214237
|
-
return PUBLIC_PROVIDER_LEAK_PATTERNS.some((pattern) => pattern.test(message));
|
|
214238
|
-
}
|
|
214239
|
-
function sanitizePublicProviderText(message, options = {}) {
|
|
214240
|
-
if (message === void 0 || message === null) return null;
|
|
214241
|
-
const trimmed = message.trim();
|
|
214242
|
-
if (trimmed.length === 0) return null;
|
|
214243
|
-
if (!containsPublicProviderLeak(trimmed)) return trimmed;
|
|
214244
|
-
return categorizePublicProviderError(trimmed) ?? options.fallback ?? "CMMD is temporarily unavailable. Check the workspace AI connection and retry.";
|
|
214245
|
-
}
|
|
214246
214418
|
function resolveApexBaseUrl(settings, config) {
|
|
214247
214419
|
return config?.forgeApexBaseUrl || settings.baseUrl || "https://apex.cmmd.ai";
|
|
214248
214420
|
}
|
|
@@ -293037,52 +293209,6 @@ function applyThreadGoalPreamble(goal, input) {
|
|
|
293037
293209
|
return trimmedInput.length > 0 ? `${preamble}\n\n${trimmedInput}` : preamble;
|
|
293038
293210
|
}
|
|
293039
293211
|
//#endregion
|
|
293040
|
-
//#region src/orchestration/providerRetryableFailure.ts
|
|
293041
|
-
/**
|
|
293042
|
-
* Classifies a provider turn-start failure as a retryable rate-limit or
|
|
293043
|
-
* transient-availability failure (429/503), a terminal failure that names why
|
|
293044
|
-
* it will never clear on its own, or unrecognized (null).
|
|
293045
|
-
*
|
|
293046
|
-
* CMMD-Center/forge#3558: a 429 from CMMD's provider egress used to leave no
|
|
293047
|
-
* trace at all, because nothing in the turn-start path recognized it as a
|
|
293048
|
-
* distinct, retryable failure. `ProviderCommandReactor` uses this to decide
|
|
293049
|
-
* whether to retry a turn start with backoff, and to name the provider,
|
|
293050
|
-
* status and Retry-After in the `provider.turn.start.failed` activity CMMD's
|
|
293051
|
-
* watchdog reads once retries are exhausted.
|
|
293052
|
-
*
|
|
293053
|
-
* Reuses `isProviderRateOrQuotaError` (the same predicate the public-copy
|
|
293054
|
-
* sanitizer uses to recognize a rate-limit message) rather than restating its
|
|
293055
|
-
* word list here.
|
|
293056
|
-
*/
|
|
293057
|
-
const SERVICE_UNAVAILABLE_PATTERN = /\b(503|service\s+unavailable)\b/i;
|
|
293058
|
-
const RATE_LIMITED_PAST_TENSE_PATTERN = /\brate[\s-]?limit(?:ed|ing)?\b/i;
|
|
293059
|
-
const QUOTA_OR_USAGE_LIMIT_PATTERN = /\b(usage[ _-]limit|usage_limit_reached|quota|insufficient_quota)\b/i;
|
|
293060
|
-
const EXPLICIT_429_PATTERN = /\b429\b/;
|
|
293061
|
-
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;
|
|
293062
|
-
function parseRetryAfterSeconds(detail) {
|
|
293063
|
-
const match = detail.match(RETRY_AFTER_SECONDS_PATTERN);
|
|
293064
|
-
if (!match) return null;
|
|
293065
|
-
const raw = match[1] ?? match[2];
|
|
293066
|
-
const seconds = raw ? Number(raw) : NaN;
|
|
293067
|
-
return Number.isFinite(seconds) ? seconds : null;
|
|
293068
|
-
}
|
|
293069
|
-
function classifyRetryableProviderFailure(detail) {
|
|
293070
|
-
if (!detail || detail.trim().length === 0) return null;
|
|
293071
|
-
if (QUOTA_OR_USAGE_LIMIT_PATTERN.test(detail) && !EXPLICIT_429_PATTERN.test(detail)) return {
|
|
293072
|
-
kind: "terminal",
|
|
293073
|
-
reason: "quota",
|
|
293074
|
-
message: detail
|
|
293075
|
-
};
|
|
293076
|
-
const isRateLimited = isProviderRateOrQuotaError(detail) || RATE_LIMITED_PAST_TENSE_PATTERN.test(detail);
|
|
293077
|
-
const isUnavailable = SERVICE_UNAVAILABLE_PATTERN.test(detail);
|
|
293078
|
-
if (!isRateLimited && !isUnavailable) return null;
|
|
293079
|
-
return {
|
|
293080
|
-
kind: "retryable",
|
|
293081
|
-
status: isRateLimited ? 429 : 503,
|
|
293082
|
-
retryAfterSeconds: parseRetryAfterSeconds(detail)
|
|
293083
|
-
};
|
|
293084
|
-
}
|
|
293085
|
-
//#endregion
|
|
293086
293212
|
//#region src/orchestration/Layers/ProviderCommandReactor.ts
|
|
293087
293213
|
function toNonEmptyProviderInput(value) {
|
|
293088
293214
|
const normalized = value?.trim();
|
|
@@ -295260,12 +295386,62 @@ const make$6 = require_Schema$1.gen(function* () {
|
|
|
295260
295386
|
createdAt: input.createdAt
|
|
295261
295387
|
});
|
|
295262
295388
|
});
|
|
295389
|
+
/**
|
|
295390
|
+
* The one durable, deterministic-id activity that records how a turn ended
|
|
295391
|
+
* (CMMD-Center/forge#3562 piece A, CMMD-Center/CMMD#5822): `completed`,
|
|
295392
|
+
* `interrupted` (an abort or a provider cancellation), or `failed` with the
|
|
295393
|
+
* provider's own error message. Recorded here, ahead of every
|
|
295394
|
+
* checkpoint-specific early return below, so it fires for every turn
|
|
295395
|
+
* regardless of whether this workspace can capture a git checkpoint at
|
|
295396
|
+
* all -- unlike a checkpoint, this never depends on `resolveCheckpointCwd`.
|
|
295397
|
+
*
|
|
295398
|
+
* The activity id is derived from the turn id, never `crypto.randomUUID()`,
|
|
295399
|
+
* so a redelivered `turn.completed` for the same turn replaces the same
|
|
295400
|
+
* row rather than adding a second one. The guard below additionally
|
|
295401
|
+
* refuses to touch a turn that already has one: first write wins, so a
|
|
295402
|
+
* later, different signal for the same turn can never overwrite an
|
|
295403
|
+
* outcome CMMD may already have acknowledged.
|
|
295404
|
+
*
|
|
295405
|
+
* Deriving the wire `turn_status` event from a new activity recorded at
|
|
295406
|
+
* the moment the turn actually ends -- not from `thread.latestTurn` or
|
|
295407
|
+
* `thread.checkpoints`, both of which already hold outcomes for turns that
|
|
295408
|
+
* settled before this shipped -- is deliberate: those would insert a brand
|
|
295409
|
+
* new event into history CMMD already acknowledged for every existing
|
|
295410
|
+
* thread, which `RuntimeThreadHistoryConflict` exists specifically to
|
|
295411
|
+
* reject (CMMD-Center/forge#3497). A turn that settled before this shipped
|
|
295412
|
+
* gets no terminal event; only a turn that ends after it does.
|
|
295413
|
+
*/
|
|
295414
|
+
const recordTurnTerminalStatus = require_Schema$1.fn("recordTurnTerminalStatus")(function* (event, thread, turnId) {
|
|
295415
|
+
const activityId = turnTerminalStatusActivityId(turnId);
|
|
295416
|
+
if (thread.activities.some((activity) => activity.id === activityId)) return;
|
|
295417
|
+
const outcome = turnTerminalOutcomeFromRuntimeState(event.payload.state);
|
|
295418
|
+
const recordedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
295419
|
+
yield* orchestrationEngine.dispatch({
|
|
295420
|
+
type: "thread.activity.append",
|
|
295421
|
+
commandId: serverCommandId$1("turn-terminal-status"),
|
|
295422
|
+
threadId: event.threadId,
|
|
295423
|
+
activity: {
|
|
295424
|
+
id: EventId.make(activityId),
|
|
295425
|
+
tone: outcome === "failed" ? "error" : "info",
|
|
295426
|
+
kind: TURN_TERMINAL_STATUS_ACTIVITY_KIND,
|
|
295427
|
+
summary: `Turn ${outcome}`,
|
|
295428
|
+
payload: {
|
|
295429
|
+
outcome,
|
|
295430
|
+
...outcome === "failed" && event.payload.errorMessage ? { errorMessage: event.payload.errorMessage } : {}
|
|
295431
|
+
},
|
|
295432
|
+
turnId,
|
|
295433
|
+
createdAt: recordedAt
|
|
295434
|
+
},
|
|
295435
|
+
createdAt: recordedAt
|
|
295436
|
+
});
|
|
295437
|
+
});
|
|
295263
295438
|
const captureCheckpointFromTurnCompletion = require_Schema$1.fn("captureCheckpointFromTurnCompletion")(function* (event) {
|
|
295264
295439
|
const turnId = toTurnId(event.turnId);
|
|
295265
295440
|
if (!turnId) return;
|
|
295266
295441
|
const readModel = yield* orchestrationEngine.getReadModel();
|
|
295267
295442
|
const thread = readModel.threads.find((entry) => entry.id === event.threadId);
|
|
295268
295443
|
if (!thread) return;
|
|
295444
|
+
yield* recordTurnTerminalStatus(event, thread, turnId);
|
|
295269
295445
|
if (thread.session?.activeTurnId && !sameId(thread.session.activeTurnId, turnId)) return;
|
|
295270
295446
|
if (thread.checkpoints.some((checkpoint) => checkpoint.turnId === turnId && checkpoint.status !== "missing")) return;
|
|
295271
295447
|
const checkpointCwd = yield* resolveCheckpointCwd({
|
|
@@ -300054,7 +300230,7 @@ function resolveBuildCommitFromEnv(env) {
|
|
|
300054
300230
|
* environment descriptor down instead of reporting an honest "unknown".
|
|
300055
300231
|
*/
|
|
300056
300232
|
function readBakedBuildCommit() {
|
|
300057
|
-
return "
|
|
300233
|
+
return "af7d46dcc7bdd81e6994e3590d9fca779411f1c4";
|
|
300058
300234
|
}
|
|
300059
300235
|
async function resolveServerBuildCommit(input) {
|
|
300060
300236
|
if (isFullCommitSha(input.baked)) return input.baked;
|