@cmmd-center/forge 0.10.16 → 0.11.1
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 +1390 -534
- package/dist/bin.mjs +1421 -565
- package/dist/client/assets/{DiffPanel-CniKcSxk.js → DiffPanel-27f3_FPs.js} +3 -3
- package/dist/client/assets/{DiffPanel.logic-UU4xEJJO.js → DiffPanel.logic-BuQtD_J1.js} +2 -2
- package/dist/client/assets/DiffPanelShell-BvD0Fk5A.js +2531 -0
- package/dist/client/assets/{DiffWorkerPoolProvider-B6dAKEIU.js → DiffWorkerPoolProvider-K588YXmt.js} +9 -9
- package/dist/client/assets/{PullRequestCodePanel-DHZ0dDeb.js → PullRequestCodePanel-B4HbAQDj.js} +3 -3
- package/dist/client/assets/{Virtualizer-BaXfuCj_.js → Virtualizer-DMnisVCC.js} +2 -2
- package/dist/client/assets/index-Lfu9WfF1.js +151 -0
- package/dist/client/index.html +4 -4
- package/dist/forge-build.json +1 -1
- package/package.json +1 -1
- package/dist/client/assets/DiffPanelShell-BpK-LVcn.js +0 -2531
- package/dist/client/assets/index-CpaFL0BH.js +0 -151
package/dist/bin.mjs
CHANGED
|
@@ -5,9 +5,9 @@ import { $ as encodeEffect, $a as make$39, $c as fromUndefinedOr, $i as spaced,
|
|
|
5
5
|
import { $ as toAsyncIterable, A as make$45, At as make$44, Bt as publish, C as fromEffect, Ct as make$43, E as fromQueue, Et as end, F as merge$4, Ft as shutdown$1, G as runFold, H as run$2, Ht as subscribe$1, It as sliding, Jt as getCurrent, K as runForEach, Kt as makeUnsafe$7, Lt as take$1, M as mapAccum, Mt as offerAll, N as mapEffect$1, Nt as offerUnsafe, O as fromSubscription, Ot as failCause$4, Q as tick, R as onExit$1, Rt as takeAll, S as fromChannel, T as fromPubSub, Tt as bounded, U as runCollect, Ut as unbounded$1, V as provideContext$1, Vt as shutdown$2, W as runDrain, Y as succeed$4, Yt as interrupt$4, Z as takeWhile, Zt as runIn, _t as mapInput, at as make$46, b as flatMap$3, bt as fromTransform, c as callback$1, d as debounce$1, f as decodeText$1, g as ensuring$1, h as encodeText, it as get$4, j as map$7, jt as offer$1, k as isStream, l as catchCause$1, m as empty$5, mt as forEach$1, ot as get$5, pt as drain, q as runForEachArray, qt as await_, r as FileSystem, rt as unwrap$1, st as make$47, t as Path$2, u as concat$2, v as failCause$5, w as fromIterable$1, wt as makeUnsafe$6, x as fromAsyncIterable, y as filter$2, yt as fromSubscriptionArray, zt as unbounded$2 } from "./Path-C6Zb2WHp.mjs";
|
|
6
6
|
import { _ as ChildProcessSpawner, a as Stdio, g as make$48, o as make$49, r as isQuitError } from "./NodeTerminal-BEGtoMSS.mjs";
|
|
7
7
|
import { n as layer$7 } from "./NodeServices-CBNXOYLG.mjs";
|
|
8
|
-
import { $ as file, A as SocketErrorReason, At as merge$5, Bt as set$
|
|
8
|
+
import { $ as file, A as SocketErrorReason, At as merge$5, Bt as set$3, Ct as HttpClientErrorSchema, Dt as empty$7, Ft as getAndSet, G as TracerPropagationEnabled, Ht as updateAndGet, I as HttpServerError, It as getUnsafe$2, J as transformResponse, K as filterStatusOk$1, Lt as make$50, Ot as fromInput$1, Pt as get$6, Q as empty$6, Rt as makeUnsafe$8, St as setBody, Tt as jsonUnsafe$1, U as layer$8, Vt as update$4, W as HttpClient, Z as symbol$1, _ as schemaBodyUrlParams, _t as acceptJson, a as withLogAddress, at as setCookie, bt as get$7, ct as stream, d as logger$1, dt as filterStatusOk, et as isHttpServerResponse, f as withLoggerDisabled, ft as matchStatus, g as schemaBodyJson, gt as schemaBodyJson$1, i as serve$1, it as schemaJson, kt as fromRecordUnsafe, lt as text$2, m as ParsedSearchParams, nt as raw, ot as setCookies, p as HttpServerRequest, q as retryTransient, rt as redirect, st as setHeader, t as HttpServer, tt as jsonUnsafe, u as cors, ut as uint8Array, v as schemaHeaders, vt as bearerToken, wt as isHttpClientError, xt as post, y as toURL, yt as bodyJson, z as RouteNotFound, zt as modify$3 } from "./HttpServer-DNGCTTdk.mjs";
|
|
9
9
|
import { _ as withDefault$3, a as all$1, c as logLevel, d as option$1, f as port, g as url$2, i as TrueValues, l as map$8, m as string$7, n as FalseValues, o as boolean$5, p as schema$2, r as Record$1, s as int$2, t as Boolean$2, u as number$2 } from "./Config-AqUR_lhX.mjs";
|
|
10
|
-
import { a as make$51, i as keys, n as getOption, o as makeWith$1, r as invalidate, s as set$
|
|
10
|
+
import { a as make$51, i as keys, n as getOption, o as makeWith$1, r as invalidate, s as set$4, t as get$8 } from "./Cache-p0X77SKw.mjs";
|
|
11
11
|
import { S as layer$9, _ as defaultEscape, a as DeadlockError, b as makeCompiler$1, c as SqlError, d as UnknownError, g as custom, h as make$52, i as ConstraintError, l as SqlSyntaxError, m as SqlClient, n as AuthorizationError, o as LockTimeoutError, r as ConnectionError, s as SerializationError, t as AuthenticationError, u as StatementTimeoutError, v as defaultTransforms, y as fragment } from "./SqlError-cyJT2IRY.mjs";
|
|
12
12
|
import { r as wrapper_default, t as Mime_default } from "./Mime-Dq9IoNiV.mjs";
|
|
13
13
|
import { a as diag, c as SpanKind$1, d as TraceFlags, f as ROOT_CONTEXT, i as propagation, l as isSpanContextValid, m as DiagLogLevel, n as init_esm$1, o as context$1, p as createContextKey, r as trace, s as SpanStatusCode, t as esm_exports$1 } from "./esm-wLIF-u3N.mjs";
|
|
@@ -15750,7 +15750,7 @@ const get$1 = (self) => modify$1(self, (current) => [current, current]);
|
|
|
15750
15750
|
* })
|
|
15751
15751
|
* ```
|
|
15752
15752
|
*/
|
|
15753
|
-
const set$
|
|
15753
|
+
const set$2 = /* @__PURE__ */ dual(2, (self, value) => update$1(self, () => value));
|
|
15754
15754
|
//#endregion
|
|
15755
15755
|
//#region ../../node_modules/.bun/effect@4.0.0-beta.51/node_modules/effect/dist/TxChunk.js
|
|
15756
15756
|
/**
|
|
@@ -15939,7 +15939,7 @@ const get = (self) => get$1(self.ref);
|
|
|
15939
15939
|
* @since 4.0.0
|
|
15940
15940
|
* @category combinators
|
|
15941
15941
|
*/
|
|
15942
|
-
const set = /* @__PURE__ */ dual(2, (self, chunk) => set$
|
|
15942
|
+
const set$1 = /* @__PURE__ */ dual(2, (self, chunk) => set$2(self.ref, chunk));
|
|
15943
15943
|
/**
|
|
15944
15944
|
* Appends an element to the end of the `TxChunk`.
|
|
15945
15945
|
*
|
|
@@ -16187,7 +16187,7 @@ const take = (self) => gen(function* () {
|
|
|
16187
16187
|
const head$3 = head(yield* get(self.items));
|
|
16188
16188
|
if (isNone(head$3)) return yield* txRetry;
|
|
16189
16189
|
yield* drop(self.items, 1);
|
|
16190
|
-
if (state._tag === "Closing" && (yield* isEmpty(self))) yield* set$
|
|
16190
|
+
if (state._tag === "Closing" && (yield* isEmpty(self))) yield* set$2(self.stateRef, {
|
|
16191
16191
|
_tag: "Done",
|
|
16192
16192
|
cause: state.cause
|
|
16193
16193
|
});
|
|
@@ -16285,11 +16285,11 @@ const interrupt = (self) => withFiber$1((fiber) => failCause(self, interrupt$2(f
|
|
|
16285
16285
|
*/
|
|
16286
16286
|
const failCause = /* @__PURE__ */ dual(2, (self, cause) => gen(function* () {
|
|
16287
16287
|
if ((yield* get$1(self.stateRef))._tag !== "Open") return false;
|
|
16288
|
-
if (yield* isEmpty(self)) yield* set$
|
|
16288
|
+
if (yield* isEmpty(self)) yield* set$2(self.stateRef, {
|
|
16289
16289
|
_tag: "Done",
|
|
16290
16290
|
cause
|
|
16291
16291
|
});
|
|
16292
|
-
else yield* set$
|
|
16292
|
+
else yield* set$2(self.stateRef, {
|
|
16293
16293
|
_tag: "Closing",
|
|
16294
16294
|
cause
|
|
16295
16295
|
});
|
|
@@ -16331,7 +16331,7 @@ const clear = (self) => gen(function* () {
|
|
|
16331
16331
|
return yield* failCause$1(state.cause);
|
|
16332
16332
|
}
|
|
16333
16333
|
const chunk = yield* get(self.items);
|
|
16334
|
-
yield* set(self.items, empty$2());
|
|
16334
|
+
yield* set$1(self.items, empty$2());
|
|
16335
16335
|
return toArray(chunk);
|
|
16336
16336
|
}).pipe(tx$1);
|
|
16337
16337
|
/**
|
|
@@ -16520,7 +16520,7 @@ function makeNetServiceShape(createServer = Net.createServer) {
|
|
|
16520
16520
|
}
|
|
16521
16521
|
//#endregion
|
|
16522
16522
|
//#region ../../packages/shared/src/releaseInfo.generated.ts
|
|
16523
|
-
const FORGE_RELEASE_VERSION = "0.
|
|
16523
|
+
const FORGE_RELEASE_VERSION = "0.11.0";
|
|
16524
16524
|
//#endregion
|
|
16525
16525
|
//#region ../../packages/shared/src/hostProcess.ts
|
|
16526
16526
|
/**
|
|
@@ -26406,6 +26406,29 @@ function encodeCmmdRuntimeThreadLifecycleMetadata(lifecycle) {
|
|
|
26406
26406
|
} };
|
|
26407
26407
|
}
|
|
26408
26408
|
//#endregion
|
|
26409
|
+
//#region ../../packages/contracts/src/cmmdRuntimeThreadWorkspace.ts
|
|
26410
|
+
function encodeCmmdRuntimeThreadWorkspaceMetadata(workspace) {
|
|
26411
|
+
return { forgeWorkspace: {
|
|
26412
|
+
schemaVersion: 1,
|
|
26413
|
+
worktreePath: workspace.worktreePath
|
|
26414
|
+
} };
|
|
26415
|
+
}
|
|
26416
|
+
//#endregion
|
|
26417
|
+
//#region ../../packages/contracts/src/cmmdRuntimeToolActivity.ts
|
|
26418
|
+
function isRecord$22(value) {
|
|
26419
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
26420
|
+
}
|
|
26421
|
+
function encodeCmmdRuntimeToolActivityPayload(kind, payload) {
|
|
26422
|
+
return {
|
|
26423
|
+
...isRecord$22(payload) ? payload : { sourcePayload: payload },
|
|
26424
|
+
forgeToolActivity: {
|
|
26425
|
+
schemaVersion: 1,
|
|
26426
|
+
source: "native",
|
|
26427
|
+
kind
|
|
26428
|
+
}
|
|
26429
|
+
};
|
|
26430
|
+
}
|
|
26431
|
+
//#endregion
|
|
26409
26432
|
//#region ../../packages/contracts/src/cmmdProjectPatch.ts
|
|
26410
26433
|
const CMMD_PROJECT_PATCH_FIELDS = {
|
|
26411
26434
|
title: "requiredText",
|
|
@@ -65495,7 +65518,7 @@ function normalizeNumberish(value) {
|
|
|
65495
65518
|
}
|
|
65496
65519
|
//#endregion
|
|
65497
65520
|
//#region package.json
|
|
65498
|
-
var version$1 = "0.
|
|
65521
|
+
var version$1 = "0.11.1";
|
|
65499
65522
|
//#endregion
|
|
65500
65523
|
//#region src/sentry.ts
|
|
65501
65524
|
const SERVER_APP_NAME = "forge-server";
|
|
@@ -69266,6 +69289,17 @@ const browserApiCorsLayer = middleware(cors({
|
|
|
69266
69289
|
credentials: true,
|
|
69267
69290
|
maxAge: 600
|
|
69268
69291
|
}), { global: true });
|
|
69292
|
+
/**
|
|
69293
|
+
* An /api path that reaches the static catch-all is not implemented by this
|
|
69294
|
+
* server: every real API route is registered ahead of it. Serving the web app's
|
|
69295
|
+
* index.html for those made API clients decode an HTML document as JSON, which
|
|
69296
|
+
* is how the phone's shell snapshot fetch failed with a JSON parse error on
|
|
69297
|
+
* every connect (FOR-36611). Callers can act on a 404; they cannot act on a
|
|
69298
|
+
* document that claims to be their payload.
|
|
69299
|
+
*/
|
|
69300
|
+
function isApiRequestPath(pathname) {
|
|
69301
|
+
return pathname === "/api" || pathname.startsWith("/api/");
|
|
69302
|
+
}
|
|
69269
69303
|
function resolveDevRedirectUrl(devUrl, requestUrl) {
|
|
69270
69304
|
const redirectUrl = new URL(devUrl.toString());
|
|
69271
69305
|
redirectUrl.pathname = requestUrl.pathname;
|
|
@@ -69387,6 +69421,10 @@ const projectFaviconRouteLayer = add$1("GET", "/api/project-favicon", gen(functi
|
|
|
69387
69421
|
const staticAndDevRouteLayer = add$1("GET", "*", gen(function* () {
|
|
69388
69422
|
const url = toURL(yield* HttpServerRequest);
|
|
69389
69423
|
if (isNone(url)) return text$2("Bad Request", { status: 400 });
|
|
69424
|
+
if (isApiRequestPath(url.value.pathname)) return jsonUnsafe({
|
|
69425
|
+
error: "not_found",
|
|
69426
|
+
message: `This server does not serve ${url.value.pathname}.`
|
|
69427
|
+
}, { status: 404 });
|
|
69390
69428
|
const config = yield* ServerConfig;
|
|
69391
69429
|
if (config.devUrl && isLoopbackHostname(url.value.hostname)) return redirect(resolveDevRedirectUrl(config.devUrl, url.value), { status: 302 });
|
|
69392
69430
|
const staticDir = config.staticDir ?? (config.devUrl ? yield* resolveStaticDir() : void 0);
|
|
@@ -82637,7 +82675,7 @@ var ServerSettingsService = class ServerSettingsService extends Service$1()("for
|
|
|
82637
82675
|
start: void_$1,
|
|
82638
82676
|
ready: void_$1,
|
|
82639
82677
|
getSettings: get$6(currentSettingsRef),
|
|
82640
|
-
updateSettings: (patch) => get$6(currentSettingsRef).pipe(map$3((currentSettings) => deepMerge$1(currentSettings, patch)), tap((nextSettings) => set$
|
|
82678
|
+
updateSettings: (patch) => get$6(currentSettingsRef).pipe(map$3((currentSettings) => deepMerge$1(currentSettings, patch)), tap((nextSettings) => set$3(currentSettingsRef, nextSettings))),
|
|
82641
82679
|
streamChanges: empty$5
|
|
82642
82680
|
};
|
|
82643
82681
|
}));
|
|
@@ -82777,7 +82815,7 @@ const ServerSettingsLive = effect(ServerSettingsService, gen(function* () {
|
|
|
82777
82815
|
cause
|
|
82778
82816
|
})));
|
|
82779
82817
|
yield* writeSettingsAtomically(next);
|
|
82780
|
-
yield* set$
|
|
82818
|
+
yield* set$4(settingsCache, cacheKey, next);
|
|
82781
82819
|
yield* emitChange(next);
|
|
82782
82820
|
return resolveTextGenerationProvider(next);
|
|
82783
82821
|
})),
|
|
@@ -91707,7 +91745,7 @@ const KeybindingsLive = effect(Keybindings, gen(function* () {
|
|
|
91707
91745
|
return {
|
|
91708
91746
|
start: gen(function* () {
|
|
91709
91747
|
if (yield* get$6(startedRef)) return yield* _await(startedDeferred);
|
|
91710
|
-
yield* set$
|
|
91748
|
+
yield* set$3(startedRef, true);
|
|
91711
91749
|
const startupExit = yield* exit(gen(function* () {
|
|
91712
91750
|
yield* startWatcher;
|
|
91713
91751
|
yield* syncDefaultKeybindingsOnStartup;
|
|
@@ -91736,7 +91774,7 @@ const KeybindingsLive = effect(Keybindings, gen(function* () {
|
|
|
91736
91774
|
});
|
|
91737
91775
|
yield* writeConfigAtomically(cappedConfig);
|
|
91738
91776
|
const nextResolved = mergeWithDefaultKeybindings(compileResolvedKeybindingsConfig(cappedConfig));
|
|
91739
|
-
yield* set$
|
|
91777
|
+
yield* set$4(resolvedConfigCache, resolvedConfigCacheKey, {
|
|
91740
91778
|
keybindings: nextResolved,
|
|
91741
91779
|
issues: []
|
|
91742
91780
|
});
|
|
@@ -91750,7 +91788,7 @@ const KeybindingsLive = effect(Keybindings, gen(function* () {
|
|
|
91750
91788
|
const nextConfig = (yield* loadWritableCustomKeybindingsConfig()).filter((entry) => entry.command !== command);
|
|
91751
91789
|
yield* writeConfigAtomically(nextConfig);
|
|
91752
91790
|
const nextResolved = mergeWithDefaultKeybindings(compileResolvedKeybindingsConfig(nextConfig));
|
|
91753
|
-
yield* set$
|
|
91791
|
+
yield* set$4(resolvedConfigCache, resolvedConfigCacheKey, {
|
|
91754
91792
|
keybindings: nextResolved,
|
|
91755
91793
|
issues: []
|
|
91756
91794
|
});
|
|
@@ -92650,7 +92688,7 @@ var AnalyticsService = class AnalyticsService extends Service$1()("forge/telemet
|
|
|
92650
92688
|
var RuntimeReplication = class extends Service$1()("forge/runtimeReplication/Services/RuntimeReplication") {};
|
|
92651
92689
|
//#endregion
|
|
92652
92690
|
//#region src/runtimeDrain/forgeGitCommit.ts
|
|
92653
|
-
const execFileAsync$
|
|
92691
|
+
const execFileAsync$7 = promisify$1(execFile);
|
|
92654
92692
|
const FORGE_GIT_COMMIT_PATTERN = /^[0-9a-f]{7,40}$/i;
|
|
92655
92693
|
/**
|
|
92656
92694
|
* Resolve the commit this forge checkout actually booted from, so the
|
|
@@ -92674,7 +92712,7 @@ const FORGE_GIT_COMMIT_PATTERN = /^[0-9a-f]{7,40}$/i;
|
|
|
92674
92712
|
* it yet.
|
|
92675
92713
|
*/
|
|
92676
92714
|
let cached$1 = null;
|
|
92677
|
-
async function resolveForgeGitCommit(execImpl = (command, args) => execFileAsync$
|
|
92715
|
+
async function resolveForgeGitCommit(execImpl = (command, args) => execFileAsync$7(command, [...args], { timeout: 5e3 })) {
|
|
92678
92716
|
if (cached$1 !== null) return cached$1;
|
|
92679
92717
|
try {
|
|
92680
92718
|
const { stdout } = await execImpl("git", ["rev-parse", "HEAD"]);
|
|
@@ -94606,7 +94644,7 @@ function mightCarryUsage(line, provider) {
|
|
|
94606
94644
|
* a line at a time, so the usual cause is a tail the scan opened mid-write,
|
|
94607
94645
|
* and a count that keeps growing is a file the scan can no longer read.
|
|
94608
94646
|
*/
|
|
94609
|
-
function parseJsonObject$
|
|
94647
|
+
function parseJsonObject$2(line, diagnostics) {
|
|
94610
94648
|
let parsed;
|
|
94611
94649
|
try {
|
|
94612
94650
|
parsed = JSON.parse(line);
|
|
@@ -94636,7 +94674,7 @@ function claudeDedupeKey(messageId, requestId) {
|
|
|
94636
94674
|
return `${messageId ?? ""}:${requestId ?? ""}`;
|
|
94637
94675
|
}
|
|
94638
94676
|
function parseClaudeLine(line, diagnostics) {
|
|
94639
|
-
const record = parseJsonObject$
|
|
94677
|
+
const record = parseJsonObject$2(line, diagnostics);
|
|
94640
94678
|
if (record === null) return null;
|
|
94641
94679
|
if (record["type"] !== "assistant") return null;
|
|
94642
94680
|
const messageRecord = objectAt(record, "message");
|
|
@@ -94783,7 +94821,7 @@ function parseCodexTokenCount(record, payloadRecord, state) {
|
|
|
94783
94821
|
};
|
|
94784
94822
|
}
|
|
94785
94823
|
function parseCodexLine(line, state, diagnostics) {
|
|
94786
|
-
const record = parseJsonObject$
|
|
94824
|
+
const record = parseJsonObject$2(line, diagnostics);
|
|
94787
94825
|
if (record === null) return null;
|
|
94788
94826
|
const payloadRecord = objectAt(record, "payload");
|
|
94789
94827
|
if (payloadRecord === null) return null;
|
|
@@ -95620,10 +95658,13 @@ function serializeRuntimeInventory(snapshot, providerInventory) {
|
|
|
95620
95658
|
workerRole: thread.workerRole,
|
|
95621
95659
|
anchorKind: thread.anchorKind,
|
|
95622
95660
|
anchorCmmdId: thread.anchorCmmdId,
|
|
95623
|
-
metadata:
|
|
95624
|
-
|
|
95625
|
-
|
|
95626
|
-
|
|
95661
|
+
metadata: {
|
|
95662
|
+
...encodeCmmdRuntimeThreadLifecycleMetadata({
|
|
95663
|
+
settledAt: thread.settledAt ?? null,
|
|
95664
|
+
settledOverride: thread.settledOverride ?? null
|
|
95665
|
+
}),
|
|
95666
|
+
...encodeCmmdRuntimeThreadWorkspaceMetadata({ worktreePath: thread.worktreePath })
|
|
95667
|
+
}
|
|
95627
95668
|
})),
|
|
95628
95669
|
metadata: {
|
|
95629
95670
|
source: "forge-server-projection",
|
|
@@ -95690,7 +95731,7 @@ function serializeNativeToolActivity(activity) {
|
|
|
95690
95731
|
kind: "tool_call",
|
|
95691
95732
|
summary,
|
|
95692
95733
|
status: activityStatus(activity),
|
|
95693
|
-
|
|
95734
|
+
payload: encodeCmmdRuntimeToolActivityPayload(activity.kind, activity.payload)
|
|
95694
95735
|
}
|
|
95695
95736
|
};
|
|
95696
95737
|
}
|
|
@@ -97852,8 +97893,8 @@ const runUsageScanWarmup = (options) => gen(function* () {
|
|
|
97852
97893
|
return yield* forever$1(gen(function* () {
|
|
97853
97894
|
if (yield* get$6(scanning)) yield* logInfo("usage scan warm-up skipped; the previous scan is still running");
|
|
97854
97895
|
else {
|
|
97855
|
-
yield* set$
|
|
97856
|
-
yield* forkChild(scanOnce(options, now).pipe(ensuring(set$
|
|
97896
|
+
yield* set$3(scanning, true);
|
|
97897
|
+
yield* forkChild(scanOnce(options, now).pipe(ensuring(set$3(scanning, false))));
|
|
97857
97898
|
}
|
|
97858
97899
|
yield* sleep(intervalMs);
|
|
97859
97900
|
}));
|
|
@@ -97871,11 +97912,11 @@ const makeCommandGate = gen(function* () {
|
|
|
97871
97912
|
return {
|
|
97872
97913
|
awaitCommandReady: _await(commandReady),
|
|
97873
97914
|
signalCommandReady: gen(function* () {
|
|
97874
|
-
yield* set$
|
|
97915
|
+
yield* set$3(commandReadinessState, "ready");
|
|
97875
97916
|
yield* succeed$3(commandReady, void 0).pipe(orDie);
|
|
97876
97917
|
}),
|
|
97877
97918
|
failCommandReady: (error) => gen(function* () {
|
|
97878
|
-
yield* set$
|
|
97919
|
+
yield* set$3(commandReadinessState, error);
|
|
97879
97920
|
yield* fail$3(commandReady, error).pipe(orDie);
|
|
97880
97921
|
}),
|
|
97881
97922
|
enqueueCommand: (effect) => gen(function* () {
|
|
@@ -98256,413 +98297,23 @@ function browseDirectory(input) {
|
|
|
98256
98297
|
});
|
|
98257
98298
|
}
|
|
98258
98299
|
//#endregion
|
|
98259
|
-
//#region src/
|
|
98300
|
+
//#region src/pathWithinRoot.ts
|
|
98260
98301
|
/**
|
|
98261
|
-
*
|
|
98302
|
+
* True when `candidate` is `root` itself or lies below it, judged on the path
|
|
98303
|
+
* strings alone. The path from `root` to `candidate` is taken with
|
|
98304
|
+
* `path.relative` and normalised to forward slashes, and the candidate is
|
|
98305
|
+
* outside when that path climbs out (`..` or `../...`) or is absolute, which is
|
|
98306
|
+
* what `relative` returns across Windows drives. A child whose name only starts
|
|
98307
|
+
* with two dots, such as `..cache`, is inside. Symlinks are not followed, so a
|
|
98308
|
+
* caller that cares where a link points has to compare resolved paths.
|
|
98262
98309
|
*
|
|
98263
|
-
*
|
|
98264
|
-
*
|
|
98265
|
-
*
|
|
98266
|
-
* @module WorkspaceEntries
|
|
98267
|
-
*/
|
|
98268
|
-
var WorkspaceEntriesError = class extends TaggedErrorClass()("WorkspaceEntriesError", {
|
|
98269
|
-
cwd: String$1,
|
|
98270
|
-
operation: String$1,
|
|
98271
|
-
detail: String$1,
|
|
98272
|
-
cause: optional$4(Defect)
|
|
98273
|
-
}) {};
|
|
98274
|
-
/**
|
|
98275
|
-
* WorkspaceEntries - Service tag for cached workspace entry search.
|
|
98276
|
-
*/
|
|
98277
|
-
var WorkspaceEntries = class extends Service$1()("forge/workspace/Services/WorkspaceEntries") {};
|
|
98278
|
-
//#endregion
|
|
98279
|
-
//#region src/workspace/Services/WorkspaceBulkTransfer.ts
|
|
98280
|
-
var WorkspaceBulkTransfer = class extends Service$1()("forge/workspace/Services/WorkspaceBulkTransfer") {};
|
|
98281
|
-
//#endregion
|
|
98282
|
-
//#region src/workspace/Layers/WorkspaceBulkTransfer.ts
|
|
98283
|
-
const TRANSFER_TTL_MS = 1440 * 60 * 1e3;
|
|
98284
|
-
const TRANSFER_LOCK_STRIPE_COUNT = 64;
|
|
98285
|
-
function transferError(input) {
|
|
98286
|
-
return new BulkTransferError({
|
|
98287
|
-
code: input.code,
|
|
98288
|
-
message: input.message,
|
|
98289
|
-
transferId: input.transferId ?? null,
|
|
98290
|
-
retryable: input.retryable ?? false,
|
|
98291
|
-
...input.cause === void 0 ? {} : { cause: input.cause }
|
|
98292
|
-
});
|
|
98293
|
-
}
|
|
98294
|
-
function mapFailure(operation, transferId) {
|
|
98295
|
-
return (cause) => is$1(BulkTransferError)(cause) ? cause : transferError({
|
|
98296
|
-
code: "io_error",
|
|
98297
|
-
message: `Workspace bulk transfer failed during ${operation}.`,
|
|
98298
|
-
...transferId === void 0 ? {} : { transferId },
|
|
98299
|
-
retryable: true,
|
|
98300
|
-
cause
|
|
98301
|
-
});
|
|
98302
|
-
}
|
|
98303
|
-
/**
|
|
98304
|
-
* Exported so the unary file read in `projectFile/readProjectFile.ts` uses this
|
|
98305
|
-
* containment check rather than a second copy of it. Two copies of a security
|
|
98306
|
-
* rule in one repo is how they drift, and the drift is silent.
|
|
98310
|
+
* Only a type import, so a module that has to stay free of Effect services can
|
|
98311
|
+
* use it without importing a Layer.
|
|
98307
98312
|
*/
|
|
98308
98313
|
function isPathWithinRoot(path, root, candidate) {
|
|
98309
98314
|
const relative = path.relative(root, candidate).replaceAll("\\", "/");
|
|
98310
98315
|
return relative === "" || !relative.startsWith("../") && relative !== ".." && !path.isAbsolute(relative);
|
|
98311
98316
|
}
|
|
98312
|
-
function transferLockStripe(transferId) {
|
|
98313
|
-
let hash = 2166136261;
|
|
98314
|
-
for (let index = 0; index < transferId.length; index += 1) hash = Math.imul(hash ^ transferId.charCodeAt(index), 16777619);
|
|
98315
|
-
return (hash >>> 0) % TRANSFER_LOCK_STRIPE_COUNT;
|
|
98316
|
-
}
|
|
98317
|
-
const makeWorkspaceBulkTransfer = (options = {}) => gen(function* () {
|
|
98318
|
-
const fileSystem = yield* FileSystem;
|
|
98319
|
-
const path = yield* Path$2;
|
|
98320
|
-
const sql = yield* SqlClient;
|
|
98321
|
-
const workspacePaths = yield* WorkspacePaths;
|
|
98322
|
-
const workspaceEntries = yield* WorkspaceEntries;
|
|
98323
|
-
const fault = options.fault ?? (() => void_$1);
|
|
98324
|
-
const transferLocks = yield* forEach(Array.from({ length: TRANSFER_LOCK_STRIPE_COUNT }), () => make$43(1));
|
|
98325
|
-
const withTransferLock = (transferId, effect) => transferLocks[transferLockStripe(transferId)].withPermit(effect);
|
|
98326
|
-
const findTransfer = (transferId) => sql`
|
|
98327
|
-
SELECT transfer_id AS "transferId", workspace_root AS "workspaceRoot",
|
|
98328
|
-
relative_path AS "relativePath", target_path AS "targetPath", temp_path AS "tempPath",
|
|
98329
|
-
total_bytes AS "totalBytes", whole_sha256 AS "wholeSha256",
|
|
98330
|
-
next_offset AS "nextOffset", state, expires_at AS "expiresAt"
|
|
98331
|
-
FROM workspace_bulk_transfers WHERE transfer_id = ${transferId} LIMIT 1
|
|
98332
|
-
`.pipe(map$3((rows) => rows[0] ?? null));
|
|
98333
|
-
const requireTransfer = (transferId) => findTransfer(transferId).pipe(flatMap((row) => row === null ? fail(transferError({
|
|
98334
|
-
code: "transfer_not_found",
|
|
98335
|
-
message: "Bulk transfer does not exist.",
|
|
98336
|
-
transferId
|
|
98337
|
-
})) : succeed(row)));
|
|
98338
|
-
const requireOpenTransfer = (transferId) => requireTransfer(transferId).pipe(flatMap((row) => {
|
|
98339
|
-
if (row.state === "cancelled") return fail(transferError({
|
|
98340
|
-
code: "transfer_cancelled",
|
|
98341
|
-
message: "Bulk transfer was cancelled.",
|
|
98342
|
-
transferId
|
|
98343
|
-
}));
|
|
98344
|
-
if (row.state === "expired" || Date.parse(row.expiresAt) <= Date.now()) return fail(transferError({
|
|
98345
|
-
code: "transfer_expired",
|
|
98346
|
-
message: "Bulk transfer expired.",
|
|
98347
|
-
transferId
|
|
98348
|
-
}));
|
|
98349
|
-
return succeed(row);
|
|
98350
|
-
}));
|
|
98351
|
-
const resolveTarget = fn$1("WorkspaceBulkTransfer.resolveTarget")(function* (input) {
|
|
98352
|
-
const workspaceRoot = yield* workspacePaths.normalizeWorkspaceRoot(input.cwd);
|
|
98353
|
-
const resolved = yield* workspacePaths.resolveRelativePathWithinRoot({
|
|
98354
|
-
workspaceRoot,
|
|
98355
|
-
relativePath: input.relativePath
|
|
98356
|
-
});
|
|
98357
|
-
const canonicalRoot = yield* fileSystem.realPath(workspaceRoot);
|
|
98358
|
-
const parentSegments = resolved.relativePath.split("/").slice(0, -1);
|
|
98359
|
-
let canonicalParent = canonicalRoot;
|
|
98360
|
-
for (const segment of parentSegments) {
|
|
98361
|
-
const candidate = path.join(canonicalParent, segment);
|
|
98362
|
-
const canonicalCandidate = yield* fileSystem.realPath(candidate).pipe(catch_(() => input.createParent ? fileSystem.makeDirectory(candidate).pipe(flatMap(() => fileSystem.realPath(candidate))) : fileSystem.realPath(candidate)));
|
|
98363
|
-
if (!isPathWithinRoot(path, canonicalRoot, canonicalCandidate)) return yield* transferError({
|
|
98364
|
-
code: "symlink_escape",
|
|
98365
|
-
message: "Workspace file path resolves outside the project root."
|
|
98366
|
-
});
|
|
98367
|
-
canonicalParent = canonicalCandidate;
|
|
98368
|
-
}
|
|
98369
|
-
const targetPath = path.join(canonicalParent, path.basename(resolved.relativePath));
|
|
98370
|
-
const targetRealPath = yield* fileSystem.realPath(targetPath).pipe(catch_(() => succeed(null)));
|
|
98371
|
-
if (targetRealPath !== null && !isPathWithinRoot(path, canonicalRoot, targetRealPath)) return yield* transferError({
|
|
98372
|
-
code: "symlink_escape",
|
|
98373
|
-
message: "Workspace file target resolves outside the project root."
|
|
98374
|
-
});
|
|
98375
|
-
return {
|
|
98376
|
-
workspaceRoot: canonicalRoot,
|
|
98377
|
-
relativePath: resolved.relativePath,
|
|
98378
|
-
targetPath,
|
|
98379
|
-
canonicalParent
|
|
98380
|
-
};
|
|
98381
|
-
});
|
|
98382
|
-
const readFile = (input) => unwrap$1(gen(function* () {
|
|
98383
|
-
const target = yield* resolveTarget({
|
|
98384
|
-
...input,
|
|
98385
|
-
createParent: false
|
|
98386
|
-
});
|
|
98387
|
-
const info = yield* fileSystem.stat(target.targetPath);
|
|
98388
|
-
const maxBytes = input.maxBytes ?? 16777216;
|
|
98389
|
-
if (Number(info.size) > maxBytes) return yield* transferError({
|
|
98390
|
-
code: "size_limit_exceeded",
|
|
98391
|
-
message: `Workspace file exceeds the ${maxBytes}-byte read limit.`
|
|
98392
|
-
});
|
|
98393
|
-
const bytes = yield* fileSystem.readFile(target.targetPath);
|
|
98394
|
-
const transfer = yield* promise(() => chunkBulkTransfer(bytes));
|
|
98395
|
-
const transferId = transfer.chunks[0]?.transferId ?? crypto.randomUUID().replaceAll("-", "_");
|
|
98396
|
-
return fromIterable$1([
|
|
98397
|
-
{
|
|
98398
|
-
kind: "started",
|
|
98399
|
-
transferId,
|
|
98400
|
-
totalBytes: transfer.totalBytes,
|
|
98401
|
-
sha256: transfer.sha256
|
|
98402
|
-
},
|
|
98403
|
-
...transfer.chunks.map((chunk) => ({
|
|
98404
|
-
...chunk,
|
|
98405
|
-
transferId
|
|
98406
|
-
})),
|
|
98407
|
-
{
|
|
98408
|
-
kind: "completed",
|
|
98409
|
-
transferId,
|
|
98410
|
-
totalBytes: transfer.totalBytes,
|
|
98411
|
-
sha256: transfer.sha256
|
|
98412
|
-
}
|
|
98413
|
-
]);
|
|
98414
|
-
}).pipe(mapError(mapFailure("read file"))));
|
|
98415
|
-
const resumeWrite = fn$1("WorkspaceBulkTransfer.resumeWrite")(function* (input, existing) {
|
|
98416
|
-
const target = yield* resolveTarget({
|
|
98417
|
-
...input,
|
|
98418
|
-
createParent: true
|
|
98419
|
-
});
|
|
98420
|
-
if (!(existing.workspaceRoot === target.workspaceRoot && existing.relativePath === target.relativePath && existing.totalBytes === input.totalBytes && existing.wholeSha256 === input.sha256)) return yield* transferError({
|
|
98421
|
-
code: "transfer_identity_conflict",
|
|
98422
|
-
message: "Bulk transfer ID is already bound to different content or a different path.",
|
|
98423
|
-
transferId: input.transferId
|
|
98424
|
-
});
|
|
98425
|
-
if (existing.state === "cancelled") return yield* transferError({
|
|
98426
|
-
code: "transfer_cancelled",
|
|
98427
|
-
message: "Bulk transfer was cancelled.",
|
|
98428
|
-
transferId: input.transferId
|
|
98429
|
-
});
|
|
98430
|
-
if (existing.state === "expired" || Date.parse(existing.expiresAt) <= Date.now()) return yield* transferError({
|
|
98431
|
-
code: "transfer_expired",
|
|
98432
|
-
message: "Bulk transfer expired.",
|
|
98433
|
-
transferId: input.transferId
|
|
98434
|
-
});
|
|
98435
|
-
return {
|
|
98436
|
-
transferId: input.transferId,
|
|
98437
|
-
nextOffset: existing.nextOffset,
|
|
98438
|
-
state: existing.state === "completed" ? "completed" : "open"
|
|
98439
|
-
};
|
|
98440
|
-
});
|
|
98441
|
-
const createWrite = fn$1("WorkspaceBulkTransfer.createWrite")(function* (input) {
|
|
98442
|
-
const target = yield* resolveTarget({
|
|
98443
|
-
...input,
|
|
98444
|
-
createParent: true
|
|
98445
|
-
}).pipe(mapError((cause) => is$1(WorkspacePathOutsideRootError)(cause) ? transferError({
|
|
98446
|
-
code: "path_outside_workspace",
|
|
98447
|
-
message: "Workspace file path must stay within the project root.",
|
|
98448
|
-
transferId: input.transferId,
|
|
98449
|
-
cause
|
|
98450
|
-
}) : cause));
|
|
98451
|
-
const tempPath = path.join(target.canonicalParent, `.forge-transfer-${input.transferId}.part`);
|
|
98452
|
-
yield* scoped(fileSystem.open(tempPath, {
|
|
98453
|
-
flag: "wx",
|
|
98454
|
-
mode: 384
|
|
98455
|
-
}).pipe(flatMap((file) => file.sync), catch_(() => fileSystem.stat(tempPath).pipe(flatMap((stat) => Number(stat.size) === 0 ? void_$1 : fail(transferError({
|
|
98456
|
-
code: "transfer_identity_conflict",
|
|
98457
|
-
message: "Bulk transfer temporary path already contains data.",
|
|
98458
|
-
transferId: input.transferId
|
|
98459
|
-
})))))));
|
|
98460
|
-
const now = /* @__PURE__ */ new Date();
|
|
98461
|
-
yield* sql`
|
|
98462
|
-
INSERT INTO workspace_bulk_transfers (
|
|
98463
|
-
transfer_id, workspace_root, relative_path, target_path, temp_path, total_bytes,
|
|
98464
|
-
whole_sha256, next_offset, state, expires_at, created_at, updated_at
|
|
98465
|
-
) VALUES (
|
|
98466
|
-
${input.transferId}, ${target.workspaceRoot}, ${target.relativePath}, ${target.targetPath},
|
|
98467
|
-
${tempPath}, ${input.totalBytes}, ${input.sha256}, 0, 'open',
|
|
98468
|
-
${new Date(now.getTime() + TRANSFER_TTL_MS).toISOString()}, ${now.toISOString()},
|
|
98469
|
-
${now.toISOString()}
|
|
98470
|
-
)
|
|
98471
|
-
`;
|
|
98472
|
-
return {
|
|
98473
|
-
transferId: input.transferId,
|
|
98474
|
-
nextOffset: 0,
|
|
98475
|
-
state: "open"
|
|
98476
|
-
};
|
|
98477
|
-
});
|
|
98478
|
-
const beginWriteEffect = fn$1("WorkspaceBulkTransfer.beginWrite")(function* (input) {
|
|
98479
|
-
const existing = yield* findTransfer(input.transferId);
|
|
98480
|
-
return yield* existing === null ? createWrite(input) : resumeWrite(input, existing);
|
|
98481
|
-
});
|
|
98482
|
-
const beginWrite = (input) => withTransferLock(input.transferId, beginWriteEffect(input)).pipe(mapError(mapFailure("begin write", input.transferId)));
|
|
98483
|
-
const acceptRetriedChunk = fn$1("WorkspaceBulkTransfer.acceptRetriedChunk")(function* (input, row) {
|
|
98484
|
-
const accepted = (yield* sql`
|
|
98485
|
-
SELECT chunk_offset AS "chunkOffset", chunk_bytes AS "chunkBytes",
|
|
98486
|
-
chunk_sha256 AS "chunkSha256"
|
|
98487
|
-
FROM workspace_bulk_transfer_chunks
|
|
98488
|
-
WHERE transfer_id = ${input.transferId} AND chunk_offset = ${input.offset}
|
|
98489
|
-
LIMIT 1
|
|
98490
|
-
`)[0];
|
|
98491
|
-
if (!(accepted !== void 0 && accepted.chunkBytes === input.bytes && accepted.chunkSha256 === input.sha256)) return yield* transferError({
|
|
98492
|
-
code: "integrity_mismatch",
|
|
98493
|
-
message: "Retried bulk transfer chunk differs from the accepted chunk.",
|
|
98494
|
-
transferId: input.transferId
|
|
98495
|
-
});
|
|
98496
|
-
return {
|
|
98497
|
-
transferId: input.transferId,
|
|
98498
|
-
nextOffset: row.nextOffset,
|
|
98499
|
-
duplicate: true
|
|
98500
|
-
};
|
|
98501
|
-
});
|
|
98502
|
-
const validateChunkRange = fn$1("WorkspaceBulkTransfer.validateChunkRange")(function* (input, row, byteLength) {
|
|
98503
|
-
if (input.offset !== row.nextOffset) return yield* transferError({
|
|
98504
|
-
code: "offset_mismatch",
|
|
98505
|
-
message: `Expected chunk offset ${row.nextOffset}, received ${input.offset}.`,
|
|
98506
|
-
transferId: input.transferId,
|
|
98507
|
-
retryable: true
|
|
98508
|
-
});
|
|
98509
|
-
const nextOffset = input.offset + byteLength;
|
|
98510
|
-
if (nextOffset > row.totalBytes) return yield* transferError({
|
|
98511
|
-
code: "size_limit_exceeded",
|
|
98512
|
-
message: "Bulk transfer chunk exceeds the declared total byte count.",
|
|
98513
|
-
transferId: input.transferId
|
|
98514
|
-
});
|
|
98515
|
-
return nextOffset;
|
|
98516
|
-
});
|
|
98517
|
-
const reconcileChunkBytes = fn$1("WorkspaceBulkTransfer.reconcileChunkBytes")(function* (input, row, bytes, nextOffset) {
|
|
98518
|
-
const tempInfo = yield* fileSystem.stat(row.tempPath);
|
|
98519
|
-
if (Number(tempInfo.size) > row.nextOffset) {
|
|
98520
|
-
if (Number(tempInfo.size) !== nextOffset) return yield* transferError({
|
|
98521
|
-
code: "integrity_mismatch",
|
|
98522
|
-
message: "Bulk transfer temporary file cannot be reconciled with the next chunk.",
|
|
98523
|
-
transferId: input.transferId
|
|
98524
|
-
});
|
|
98525
|
-
const recovered = (yield* fileSystem.readFile(row.tempPath)).slice(input.offset, nextOffset);
|
|
98526
|
-
if ((yield* promise(() => sha256Hex(recovered))) !== input.sha256) return yield* transferError({
|
|
98527
|
-
code: "integrity_mismatch",
|
|
98528
|
-
message: "Bulk transfer temporary bytes differ from the retried chunk.",
|
|
98529
|
-
transferId: input.transferId
|
|
98530
|
-
});
|
|
98531
|
-
return;
|
|
98532
|
-
}
|
|
98533
|
-
yield* fault("before-byte-write");
|
|
98534
|
-
yield* scoped(fileSystem.open(row.tempPath, { flag: "r+" }).pipe(flatMap((file) => file.seek(input.offset, "start").pipe(flatMap(() => file.writeAll(bytes)), flatMap(() => file.sync)))));
|
|
98535
|
-
yield* fault("after-byte-write-before-metadata");
|
|
98536
|
-
});
|
|
98537
|
-
const persistAcceptedChunk = fn$1("WorkspaceBulkTransfer.persistAcceptedChunk")(function* (input, nextOffset) {
|
|
98538
|
-
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
98539
|
-
yield* sql.withTransaction(sql`
|
|
98540
|
-
INSERT INTO workspace_bulk_transfer_chunks (
|
|
98541
|
-
transfer_id, chunk_offset, chunk_bytes, chunk_sha256
|
|
98542
|
-
) VALUES (${input.transferId}, ${input.offset}, ${input.bytes}, ${input.sha256})
|
|
98543
|
-
ON CONFLICT DO NOTHING
|
|
98544
|
-
`.pipe(flatMap(() => sql`
|
|
98545
|
-
UPDATE workspace_bulk_transfers
|
|
98546
|
-
SET next_offset = ${nextOffset}, updated_at = ${updatedAt}
|
|
98547
|
-
WHERE transfer_id = ${input.transferId} AND next_offset = ${input.offset} AND state = 'open'
|
|
98548
|
-
`)));
|
|
98549
|
-
yield* fault("after-metadata-advance");
|
|
98550
|
-
});
|
|
98551
|
-
const writeChunkEffect = fn$1("WorkspaceBulkTransfer.writeChunk")(function* (input) {
|
|
98552
|
-
const bytes = yield* promise(() => verifyBulkTransferChunk({
|
|
98553
|
-
...input,
|
|
98554
|
-
kind: "chunk"
|
|
98555
|
-
}));
|
|
98556
|
-
const row = yield* requireOpenTransfer(input.transferId);
|
|
98557
|
-
if (row.state === "completed") return yield* transferError({
|
|
98558
|
-
code: "offset_mismatch",
|
|
98559
|
-
message: "Bulk transfer is already complete.",
|
|
98560
|
-
transferId: input.transferId
|
|
98561
|
-
});
|
|
98562
|
-
if (input.offset < row.nextOffset) return yield* acceptRetriedChunk(input, row);
|
|
98563
|
-
const nextOffset = yield* validateChunkRange(input, row, bytes.byteLength);
|
|
98564
|
-
yield* reconcileChunkBytes(input, row, bytes, nextOffset);
|
|
98565
|
-
yield* persistAcceptedChunk(input, nextOffset);
|
|
98566
|
-
return {
|
|
98567
|
-
transferId: input.transferId,
|
|
98568
|
-
nextOffset,
|
|
98569
|
-
duplicate: false
|
|
98570
|
-
};
|
|
98571
|
-
});
|
|
98572
|
-
const writeChunk = (input) => withTransferLock(input.transferId, writeChunkEffect(input)).pipe(mapError(mapFailure("write chunk", input.transferId)));
|
|
98573
|
-
const commitWriteEffect = fn$1("WorkspaceBulkTransfer.commitWrite")(function* (input) {
|
|
98574
|
-
const row = yield* requireOpenTransfer(input.transferId);
|
|
98575
|
-
if (row.state === "completed") return {
|
|
98576
|
-
transferId: row.transferId,
|
|
98577
|
-
relativePath: row.relativePath,
|
|
98578
|
-
totalBytes: row.totalBytes,
|
|
98579
|
-
sha256: row.wholeSha256
|
|
98580
|
-
};
|
|
98581
|
-
if (row.nextOffset !== row.totalBytes) return yield* transferError({
|
|
98582
|
-
code: "offset_mismatch",
|
|
98583
|
-
message: `Bulk transfer is incomplete at byte ${row.nextOffset} of ${row.totalBytes}.`,
|
|
98584
|
-
transferId: row.transferId,
|
|
98585
|
-
retryable: true
|
|
98586
|
-
});
|
|
98587
|
-
if ((yield* resolveTarget({
|
|
98588
|
-
cwd: row.workspaceRoot,
|
|
98589
|
-
relativePath: row.relativePath,
|
|
98590
|
-
createParent: false
|
|
98591
|
-
})).targetPath !== row.targetPath) return yield* transferError({
|
|
98592
|
-
code: "symlink_escape",
|
|
98593
|
-
message: "Workspace file destination changed during transfer.",
|
|
98594
|
-
transferId: row.transferId
|
|
98595
|
-
});
|
|
98596
|
-
const tempInfo = yield* fileSystem.stat(row.tempPath).pipe(catch_(() => succeed(null)));
|
|
98597
|
-
const candidatePath = tempInfo === null ? row.targetPath : row.tempPath;
|
|
98598
|
-
const candidate = yield* fileSystem.readFile(candidatePath);
|
|
98599
|
-
if (candidate.byteLength !== row.totalBytes || (yield* promise(() => sha256Hex(candidate))) !== row.wholeSha256) return yield* transferError({
|
|
98600
|
-
code: "integrity_mismatch",
|
|
98601
|
-
message: "Bulk transfer payload does not match its declared size and digest.",
|
|
98602
|
-
transferId: row.transferId
|
|
98603
|
-
});
|
|
98604
|
-
if (tempInfo !== null) {
|
|
98605
|
-
yield* fileSystem.rename(row.tempPath, row.targetPath);
|
|
98606
|
-
yield* fault("after-atomic-rename");
|
|
98607
|
-
}
|
|
98608
|
-
yield* sql`
|
|
98609
|
-
UPDATE workspace_bulk_transfers SET state = 'completed', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
98610
|
-
WHERE transfer_id = ${row.transferId}
|
|
98611
|
-
`;
|
|
98612
|
-
yield* workspaceEntries.invalidate(row.workspaceRoot);
|
|
98613
|
-
return {
|
|
98614
|
-
transferId: row.transferId,
|
|
98615
|
-
relativePath: row.relativePath,
|
|
98616
|
-
totalBytes: row.totalBytes,
|
|
98617
|
-
sha256: row.wholeSha256
|
|
98618
|
-
};
|
|
98619
|
-
});
|
|
98620
|
-
const commitWrite = (input) => withTransferLock(input.transferId, commitWriteEffect(input)).pipe(mapError(mapFailure("commit write", input.transferId)));
|
|
98621
|
-
const cancelWriteEffect = fn$1("WorkspaceBulkTransfer.cancelWrite")(function* (input) {
|
|
98622
|
-
const row = yield* requireTransfer(input.transferId);
|
|
98623
|
-
if (row.state !== "completed") {
|
|
98624
|
-
yield* fileSystem.remove(row.tempPath, { force: true });
|
|
98625
|
-
yield* sql`
|
|
98626
|
-
UPDATE workspace_bulk_transfers SET state = 'cancelled', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
98627
|
-
WHERE transfer_id = ${input.transferId}
|
|
98628
|
-
`;
|
|
98629
|
-
}
|
|
98630
|
-
return {
|
|
98631
|
-
transferId: input.transferId,
|
|
98632
|
-
state: row.state === "completed" ? "completed" : "cancelled"
|
|
98633
|
-
};
|
|
98634
|
-
});
|
|
98635
|
-
const cancelWrite = (input) => withTransferLock(input.transferId, cancelWriteEffect(input)).pipe(mapError(mapFailure("cancel write", input.transferId)));
|
|
98636
|
-
const cleanupExpiredEffect = fn$1("WorkspaceBulkTransfer.cleanupExpired")(function* () {
|
|
98637
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
98638
|
-
const rows = yield* sql`
|
|
98639
|
-
SELECT transfer_id AS "transferId", temp_path AS "tempPath"
|
|
98640
|
-
FROM workspace_bulk_transfers WHERE state = 'open' AND expires_at <= ${now}
|
|
98641
|
-
`;
|
|
98642
|
-
let expiredCount = 0;
|
|
98643
|
-
for (const row of rows) if (yield* withTransferLock(row.transferId, gen(function* () {
|
|
98644
|
-
const current = yield* findTransfer(row.transferId);
|
|
98645
|
-
if (current === null || current.state !== "open" || Date.parse(current.expiresAt) > Date.parse(now)) return false;
|
|
98646
|
-
yield* fileSystem.remove(current.tempPath, { force: true });
|
|
98647
|
-
yield* sql`
|
|
98648
|
-
UPDATE workspace_bulk_transfers SET state = 'expired', updated_at = ${now}
|
|
98649
|
-
WHERE transfer_id = ${row.transferId} AND state = 'open' AND expires_at <= ${now}
|
|
98650
|
-
`;
|
|
98651
|
-
return true;
|
|
98652
|
-
}))) expiredCount += 1;
|
|
98653
|
-
return expiredCount;
|
|
98654
|
-
});
|
|
98655
|
-
const cleanupExpired = () => cleanupExpiredEffect().pipe(mapError(mapFailure("expiry cleanup")));
|
|
98656
|
-
return {
|
|
98657
|
-
readFile,
|
|
98658
|
-
beginWrite,
|
|
98659
|
-
writeChunk,
|
|
98660
|
-
commitWrite,
|
|
98661
|
-
cancelWrite,
|
|
98662
|
-
cleanupExpired
|
|
98663
|
-
};
|
|
98664
|
-
});
|
|
98665
|
-
const WorkspaceBulkTransferLive = effect(WorkspaceBulkTransfer, makeWorkspaceBulkTransfer());
|
|
98666
98317
|
//#endregion
|
|
98667
98318
|
//#region src/projectFile/readProjectFile.ts
|
|
98668
98319
|
/**
|
|
@@ -99160,7 +98811,7 @@ function buildDiscoveryResult(probes) {
|
|
|
99160
98811
|
}
|
|
99161
98812
|
//#endregion
|
|
99162
98813
|
//#region src/sourceControl/probe.ts
|
|
99163
|
-
const execFileAsync$
|
|
98814
|
+
const execFileAsync$6 = promisify$1(execFile);
|
|
99164
98815
|
/**
|
|
99165
98816
|
* How long a machine's toolchain answer stays good for.
|
|
99166
98817
|
*
|
|
@@ -99171,7 +98822,7 @@ const execFileAsync$5 = promisify$1(execFile);
|
|
|
99171
98822
|
*/
|
|
99172
98823
|
const DISCOVERY_TTL = "5 minutes";
|
|
99173
98824
|
/** Probes time out because `gh auth status` reaches the network on a cold keyring. */
|
|
99174
|
-
const PROBE_TIMEOUT_MS = 5e3;
|
|
98825
|
+
const PROBE_TIMEOUT_MS$1 = 5e3;
|
|
99175
98826
|
/**
|
|
99176
98827
|
* Runs one probe. A tool that is absent, fails, or hangs is reported the same
|
|
99177
98828
|
* way — `null`, meaning "no answer" — because the discovery result only
|
|
@@ -99183,7 +98834,7 @@ const PROBE_TIMEOUT_MS = 5e3;
|
|
|
99183
98834
|
*/
|
|
99184
98835
|
const probe = (command, args) => promise(async () => {
|
|
99185
98836
|
try {
|
|
99186
|
-
const { stdout, stderr } = await execFileAsync$
|
|
98837
|
+
const { stdout, stderr } = await execFileAsync$6(command, [...args], { timeout: PROBE_TIMEOUT_MS$1 });
|
|
99187
98838
|
const output = `${stdout}\n${stderr}`.trim();
|
|
99188
98839
|
return { output: output.length === 0 ? null : output };
|
|
99189
98840
|
} catch (cause) {
|
|
@@ -99464,6 +99115,26 @@ const DevTunnelLive = effect(DevTunnel, gen(function* () {
|
|
|
99464
99115
|
};
|
|
99465
99116
|
}));
|
|
99466
99117
|
//#endregion
|
|
99118
|
+
//#region src/workspace/Services/WorkspaceEntries.ts
|
|
99119
|
+
/**
|
|
99120
|
+
* WorkspaceEntries - Effect service contract for cached workspace entry search.
|
|
99121
|
+
*
|
|
99122
|
+
* Owns indexed workspace entry search plus cache invalidation for workspace
|
|
99123
|
+
* roots when the underlying filesystem changes.
|
|
99124
|
+
*
|
|
99125
|
+
* @module WorkspaceEntries
|
|
99126
|
+
*/
|
|
99127
|
+
var WorkspaceEntriesError = class extends TaggedErrorClass()("WorkspaceEntriesError", {
|
|
99128
|
+
cwd: String$1,
|
|
99129
|
+
operation: String$1,
|
|
99130
|
+
detail: String$1,
|
|
99131
|
+
cause: optional$4(Defect)
|
|
99132
|
+
}) {};
|
|
99133
|
+
/**
|
|
99134
|
+
* WorkspaceEntries - Service tag for cached workspace entry search.
|
|
99135
|
+
*/
|
|
99136
|
+
var WorkspaceEntries = class extends Service$1()("forge/workspace/Services/WorkspaceEntries") {};
|
|
99137
|
+
//#endregion
|
|
99467
99138
|
//#region src/workspace/Services/WorkspaceFileSystem.ts
|
|
99468
99139
|
/**
|
|
99469
99140
|
* WorkspaceFileSystem - Effect service contract for workspace file mutations.
|
|
@@ -99485,6 +99156,9 @@ var WorkspaceFileSystemError = class extends TaggedErrorClass()("WorkspaceFileSy
|
|
|
99485
99156
|
*/
|
|
99486
99157
|
var WorkspaceFileSystem = class extends Service$1()("forge/workspace/Services/WorkspaceFileSystem") {};
|
|
99487
99158
|
//#endregion
|
|
99159
|
+
//#region src/workspace/Services/WorkspaceBulkTransfer.ts
|
|
99160
|
+
var WorkspaceBulkTransfer = class extends Service$1()("forge/workspace/Services/WorkspaceBulkTransfer") {};
|
|
99161
|
+
//#endregion
|
|
99488
99162
|
//#region src/workspace/Services/WorkspaceReadinessState.ts
|
|
99489
99163
|
var WorkspaceReadinessState = class extends Service$1()("forge/workspace/Services/WorkspaceReadinessState") {};
|
|
99490
99164
|
//#endregion
|
|
@@ -100720,6 +100394,7 @@ const CLAUDE_IDENTITY_KEYS = new Set([
|
|
|
100720
100394
|
"provider",
|
|
100721
100395
|
"credentialGenerationId"
|
|
100722
100396
|
]);
|
|
100397
|
+
const CLAUDE_IDENTITY_OPTIONAL_KEYS = new Set(["providerAccountIdentity"]);
|
|
100723
100398
|
function isRecord$13(value) {
|
|
100724
100399
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
100725
100400
|
}
|
|
@@ -100744,6 +100419,10 @@ function daysInMonth$1(year, month) {
|
|
|
100744
100419
|
function hasExactKeys$2(value, keys) {
|
|
100745
100420
|
return Object.keys(value).every((key) => keys.has(key)) && Object.keys(value).length === keys.size;
|
|
100746
100421
|
}
|
|
100422
|
+
function hasAllowedKeys(value, required, optional) {
|
|
100423
|
+
const keys = Object.keys(value);
|
|
100424
|
+
return keys.every((key) => required.has(key) || optional.has(key)) && [...required].every((key) => keys.includes(key));
|
|
100425
|
+
}
|
|
100747
100426
|
function parseDeliveryIdentity(value, provider) {
|
|
100748
100427
|
if (!isRecord$13(value)) return null;
|
|
100749
100428
|
if (provider === "openai") return parseCodexDeliveryIdentity(value);
|
|
@@ -100758,12 +100437,25 @@ function parseCodexDeliveryIdentity(value) {
|
|
|
100758
100437
|
providerAccountIdentity: value.providerAccountIdentity
|
|
100759
100438
|
};
|
|
100760
100439
|
}
|
|
100440
|
+
/**
|
|
100441
|
+
* Absent means the writer does not name the Provider Account yet, and the
|
|
100442
|
+
* identity keeps the shape it has always had. Present but malformed means the
|
|
100443
|
+
* identity is not the one CMMD signed, so it is refused the same way a
|
|
100444
|
+
* malformed Codex account identity is.
|
|
100445
|
+
*/
|
|
100446
|
+
function claudeAccountFields(value) {
|
|
100447
|
+
if (value === void 0) return {};
|
|
100448
|
+
if (typeof value !== "string" || !PROVIDER_ACCOUNT_IDENTITY$2.test(value)) return null;
|
|
100449
|
+
return { providerAccountIdentity: value };
|
|
100450
|
+
}
|
|
100761
100451
|
function parseClaudeDeliveryIdentity(value) {
|
|
100762
|
-
|
|
100452
|
+
const account = claudeAccountFields(value.providerAccountIdentity);
|
|
100453
|
+
if (!account || !hasAllowedKeys(value, CLAUDE_IDENTITY_KEYS, CLAUDE_IDENTITY_OPTIONAL_KEYS) || value.kind !== "credential_generation" || value.provider !== "claude" || typeof value.credentialGenerationId !== "string" || !UUID.test(value.credentialGenerationId)) return null;
|
|
100763
100454
|
return {
|
|
100764
100455
|
kind: "credential_generation",
|
|
100765
100456
|
provider: "claude",
|
|
100766
|
-
credentialGenerationId: value.credentialGenerationId
|
|
100457
|
+
credentialGenerationId: value.credentialGenerationId,
|
|
100458
|
+
...account
|
|
100767
100459
|
};
|
|
100768
100460
|
}
|
|
100769
100461
|
function parseProviderRuntimeDeliveryReceipt(value, expected) {
|
|
@@ -100802,10 +100494,49 @@ function readProviderDeliveryBinding(value) {
|
|
|
100802
100494
|
identity
|
|
100803
100495
|
};
|
|
100804
100496
|
}
|
|
100497
|
+
/** The Provider Account a delivery identity names, or null when it names none. */
|
|
100498
|
+
function providerDeliveryAccount(identity) {
|
|
100499
|
+
return identity.kind === "provider_account" ? identity.providerAccountIdentity : identity.providerAccountIdentity ?? null;
|
|
100500
|
+
}
|
|
100501
|
+
function sameDeliveredCredential(left, right) {
|
|
100502
|
+
if (left.kind === "provider_account" && right.kind === "provider_account") return left.providerAccountIdentity === right.providerAccountIdentity;
|
|
100503
|
+
return left.kind === "credential_generation" && right.kind === "credential_generation" && left.credentialGenerationId === right.credentialGenerationId;
|
|
100504
|
+
}
|
|
100505
|
+
/**
|
|
100506
|
+
* Names how one thread's delivery binding moved, which is the question the
|
|
100507
|
+
* delivery layer could not answer while Claude's identity carried no account.
|
|
100508
|
+
*
|
|
100509
|
+
* `account_switch` needs both sides to name an account, the accounts to differ,
|
|
100510
|
+
* the delivered credential to change with them, and the runtime, machine and
|
|
100511
|
+
* epoch to hold. A switch changes the account and the generation together, so
|
|
100512
|
+
* two accounts on one generation would be two accounts sharing a credential,
|
|
100513
|
+
* and a switch that also moved the epoch changed more than the identity. Both
|
|
100514
|
+
* are `unknown`.
|
|
100515
|
+
*
|
|
100516
|
+
* The same delivered credential is `unchanged` whether or not either side names
|
|
100517
|
+
* an account, so a binding recorded before CMMD named the account still resumes
|
|
100518
|
+
* against the credential it ran on. A changed credential where either side
|
|
100519
|
+
* names no account is `unknown` rather than guessed at, which keeps an
|
|
100520
|
+
* account-less binding compared on exactly the fields it was compared on before.
|
|
100521
|
+
*
|
|
100522
|
+
* `providerDeliveryBindingsEqual` asks only whether the answer is `unchanged`.
|
|
100523
|
+
* `permitsProviderAccountTransition` in `provider/providerAccountTransition.ts`
|
|
100524
|
+
* asks whether it is `account_switch`, and is the Codex resume gate's rule
|
|
100525
|
+
* today. #3055 wires the Claude resume gate onto it.
|
|
100526
|
+
*/
|
|
100527
|
+
function classifyProviderDeliveryBindingChange(prior, current) {
|
|
100528
|
+
if (prior.runtimeId !== current.runtimeId || prior.machineId !== current.machineId || prior.identity.kind !== current.identity.kind || prior.identity.provider !== current.identity.provider) return "unknown";
|
|
100529
|
+
const sameEpoch = prior.credentialEpoch === current.credentialEpoch;
|
|
100530
|
+
const sameCredential = sameDeliveredCredential(prior.identity, current.identity);
|
|
100531
|
+
const priorAccount = providerDeliveryAccount(prior.identity);
|
|
100532
|
+
const currentAccount = providerDeliveryAccount(current.identity);
|
|
100533
|
+
if (priorAccount !== null && currentAccount !== null && priorAccount !== currentAccount) return sameEpoch && !sameCredential ? "account_switch" : "unknown";
|
|
100534
|
+
if (sameEpoch && sameCredential) return "unchanged";
|
|
100535
|
+
if (priorAccount === null || currentAccount === null) return "unknown";
|
|
100536
|
+
return "credential_refresh";
|
|
100537
|
+
}
|
|
100805
100538
|
function providerDeliveryBindingsEqual(left, right) {
|
|
100806
|
-
|
|
100807
|
-
if (left.identity.kind === "provider_account" && right.identity.kind === "provider_account") return left.identity.providerAccountIdentity === right.identity.providerAccountIdentity;
|
|
100808
|
-
return left.identity.kind === "credential_generation" && right.identity.kind === "credential_generation" && left.identity.credentialGenerationId === right.identity.credentialGenerationId;
|
|
100539
|
+
return classifyProviderDeliveryBindingChange(left, right) === "unchanged";
|
|
100809
100540
|
}
|
|
100810
100541
|
//#endregion
|
|
100811
100542
|
//#region src/provider/claudeReadinessReason.ts
|
|
@@ -101084,8 +100815,128 @@ const runClaudeLogout = (runtime) => new Promise((resolve, reject) => {
|
|
|
101084
100815
|
});
|
|
101085
100816
|
});
|
|
101086
100817
|
//#endregion
|
|
100818
|
+
//#region src/usage/providerRateLimits.ts
|
|
100819
|
+
/**
|
|
100820
|
+
* `new Date(ms).toISOString()` throws past +-8.64e15 ms, so a `resetsAt` past
|
|
100821
|
+
* the same bound in seconds is refused rather than allowed to throw inside a
|
|
100822
|
+
* decoder whose whole job is to fail closed.
|
|
100823
|
+
*/
|
|
100824
|
+
const MAX_RESETS_AT_SECONDS = 864e10;
|
|
100825
|
+
/**
|
|
100826
|
+
* One reset time in unix seconds as an ISO string, or null.
|
|
100827
|
+
*
|
|
100828
|
+
* Both mappers read reset times through this, so a malformed `resetsAt`
|
|
100829
|
+
* yields the null that means "this observation said nothing" instead of a
|
|
100830
|
+
* RangeError thrown out of a decoder the ingestion layer runs on every Claude
|
|
100831
|
+
* rate-limit event.
|
|
100832
|
+
*/
|
|
100833
|
+
function resetsAtIso(resetsAt) {
|
|
100834
|
+
if (!Number.isFinite(resetsAt) || resetsAt <= 0 || resetsAt > MAX_RESETS_AT_SECONDS) return null;
|
|
100835
|
+
return (/* @__PURE__ */ new Date(resetsAt * 1e3)).toISOString();
|
|
100836
|
+
}
|
|
100837
|
+
/**
|
|
100838
|
+
* The SDK only ever reports ONE window per event. Only `five_hour` and
|
|
100839
|
+
* `seven_day` map to the two windows the Usage page shows; the
|
|
100840
|
+
* per-model/overage variants (seven_day_opus, overage, ...) are ignored here
|
|
100841
|
+
* on purpose rather than silently overwriting the plain 7-day figure.
|
|
100842
|
+
*/
|
|
100843
|
+
function mapClaudeRateLimitInfo(info) {
|
|
100844
|
+
if (typeof info.utilization !== "number" || typeof info.resetsAt !== "number" || !info.status) return null;
|
|
100845
|
+
const kind = info.rateLimitType === "five_hour" ? "fiveHour" : info.rateLimitType === "seven_day" ? "sevenDay" : null;
|
|
100846
|
+
if (!kind) return null;
|
|
100847
|
+
const resetsAt = resetsAtIso(info.resetsAt);
|
|
100848
|
+
if (resetsAt === null) return null;
|
|
100849
|
+
return {
|
|
100850
|
+
kind,
|
|
100851
|
+
window: {
|
|
100852
|
+
usedPercent: Math.round(info.utilization * 100),
|
|
100853
|
+
resetsAt,
|
|
100854
|
+
status: info.status
|
|
100855
|
+
}
|
|
100856
|
+
};
|
|
100857
|
+
}
|
|
100858
|
+
/**
|
|
100859
|
+
* The one place a used-percent becomes a status. `unifiedWindows` carries no
|
|
100860
|
+
* per-window status of its own, and the `/usage` parser derives one the same
|
|
100861
|
+
* way, so both Claude usage sources agree on where "warning" starts.
|
|
100862
|
+
*/
|
|
100863
|
+
function usageStatus(usedPercent) {
|
|
100864
|
+
if (usedPercent >= 100) return "rejected";
|
|
100865
|
+
return usedPercent >= 80 ? "allowed_warning" : "allowed";
|
|
100866
|
+
}
|
|
100867
|
+
/**
|
|
100868
|
+
* One untyped `utilization` as a whole percent, or nothing. The [0, 100] bound
|
|
100869
|
+
* matches `claudeUsageCli.ts`'s own bound on the `/usage` text, which in turn
|
|
100870
|
+
* matches CMMD's parser.
|
|
100871
|
+
*/
|
|
100872
|
+
function readUsedPercent(utilization) {
|
|
100873
|
+
if (typeof utilization !== "number" || !Number.isFinite(utilization)) return null;
|
|
100874
|
+
const usedPercent = Math.round(utilization * 100);
|
|
100875
|
+
return usedPercent < 0 || usedPercent > 100 ? null : usedPercent;
|
|
100876
|
+
}
|
|
100877
|
+
/**
|
|
100878
|
+
* Reads one `unifiedWindows` entry, or nothing. Every field is checked because
|
|
100879
|
+
* none of them are typed: a non-object entry, a utilization that is not a
|
|
100880
|
+
* finite number in [0, 1], or a reset time that is not a positive finite
|
|
100881
|
+
* number of seconds all mean "this observation said nothing about this
|
|
100882
|
+
* window".
|
|
100883
|
+
*/
|
|
100884
|
+
function readUnifiedWindow(raw) {
|
|
100885
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
100886
|
+
const { utilization, resetsAt } = raw;
|
|
100887
|
+
const usedPercent = readUsedPercent(utilization);
|
|
100888
|
+
if (usedPercent === null) return null;
|
|
100889
|
+
if (typeof resetsAt !== "number") return null;
|
|
100890
|
+
const resetsAtIsoValue = resetsAtIso(resetsAt);
|
|
100891
|
+
if (resetsAtIsoValue === null) return null;
|
|
100892
|
+
return {
|
|
100893
|
+
usedPercent,
|
|
100894
|
+
resetsAt: resetsAtIsoValue,
|
|
100895
|
+
status: usageStatus(usedPercent)
|
|
100896
|
+
};
|
|
100897
|
+
}
|
|
100898
|
+
/**
|
|
100899
|
+
* Reads the `unifiedWindows` sub-object of `rate_limit_info` into the two
|
|
100900
|
+
* rolling windows. The SDK rebuilds that sub-object on every turn from the
|
|
100901
|
+
* `anthropic-ratelimit-unified-{5h,7d}-utilization` and `-reset` response
|
|
100902
|
+
* headers.
|
|
100903
|
+
*
|
|
100904
|
+
* The top-level `rateLimitType` names one window per event and carries a
|
|
100905
|
+
* utilization only for `five_hour` or `seven_day`. This sub-object reports
|
|
100906
|
+
* every window it knows on every observation, which makes it the only
|
|
100907
|
+
* SDK-side source that can feed the Usage card without waking a Sprite and
|
|
100908
|
+
* delivering a credential.
|
|
100909
|
+
*
|
|
100910
|
+
* Its `seven_day_overage_included` window is skipped. `/usage` names each model
|
|
100911
|
+
* row after the display name Anthropic's usage API gives that model, one row
|
|
100912
|
+
* per allowlisted model, and the `7d_oi` header behind this window is one
|
|
100913
|
+
* aggregate that carries no name. Any name given to it here would be a guess
|
|
100914
|
+
* that could disagree with the rows the probe writes, so model rows come from
|
|
100915
|
+
* the probe alone.
|
|
100916
|
+
*
|
|
100917
|
+
* It is also undocumented internal surface stripped from the SDK's exported
|
|
100918
|
+
* types, so this decoder fails closed everywhere. It returns null when the
|
|
100919
|
+
* sub-object is absent or is not a plain object, and null when neither window
|
|
100920
|
+
* in it survives validation. A window that fails validation is reported as
|
|
100921
|
+
* null, and an unrecognized key is skipped, never fatal. A caller that gets
|
|
100922
|
+
* null has learned nothing and must not publish, because a partial write could
|
|
100923
|
+
* overwrite a good `/usage` reading with a guess.
|
|
100924
|
+
*/
|
|
100925
|
+
function mapClaudeUnifiedWindows(info) {
|
|
100926
|
+
const raw = info.unifiedWindows;
|
|
100927
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
100928
|
+
const windows = raw;
|
|
100929
|
+
const fiveHour = readUnifiedWindow(windows.five_hour);
|
|
100930
|
+
const sevenDay = readUnifiedWindow(windows.seven_day);
|
|
100931
|
+
if (!fiveHour && !sevenDay) return null;
|
|
100932
|
+
return {
|
|
100933
|
+
fiveHour,
|
|
100934
|
+
sevenDay
|
|
100935
|
+
};
|
|
100936
|
+
}
|
|
100937
|
+
//#endregion
|
|
101087
100938
|
//#region src/usage/claudeUsageCli.ts
|
|
101088
|
-
const execFileAsync$
|
|
100939
|
+
const execFileAsync$5 = promisify$1(execFile);
|
|
101089
100940
|
const USAGE_ARGS = [
|
|
101090
100941
|
"--safe-mode",
|
|
101091
100942
|
"--no-session-persistence",
|
|
@@ -101109,7 +100960,7 @@ const MONTH_BY_NAME = {
|
|
|
101109
100960
|
Dec: 11
|
|
101110
100961
|
};
|
|
101111
100962
|
async function runClaudeUsageCommand(binaryPath, args, claudeConfigDir) {
|
|
101112
|
-
const { stdout } = await execFileAsync$
|
|
100963
|
+
const { stdout } = await execFileAsync$5(binaryPath, [...args], {
|
|
101113
100964
|
env: {
|
|
101114
100965
|
...process.env,
|
|
101115
100966
|
TZ: "UTC",
|
|
@@ -101184,10 +101035,6 @@ function parseResetAt(value, now) {
|
|
|
101184
101035
|
});
|
|
101185
101036
|
return resetAt === null || Number.isNaN(resetAt.getTime()) ? null : resetAt.toISOString();
|
|
101186
101037
|
}
|
|
101187
|
-
function usageStatus(usedPercent) {
|
|
101188
|
-
if (usedPercent >= 100) return "rejected";
|
|
101189
|
-
return usedPercent >= 80 ? "allowed_warning" : "allowed";
|
|
101190
|
-
}
|
|
101191
101038
|
function parseUsageLine(output, label, now) {
|
|
101192
101039
|
const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
101193
101040
|
const match = new RegExp(`^${escapedLabel}: (\\d+(?:\\.\\d+)?)% used(?: · resets (.+))?$`, "m").exec(output);
|
|
@@ -101256,6 +101103,29 @@ async function fetchClaudeUsageViaOfficialClient(input) {
|
|
|
101256
101103
|
};
|
|
101257
101104
|
}
|
|
101258
101105
|
//#endregion
|
|
101106
|
+
//#region src/usage/claudeUsageTelemetryRequest.ts
|
|
101107
|
+
/** The owner-session route for a Claude usage reading. */
|
|
101108
|
+
const CLAUDE_USAGE_TELEMETRY_PATH = "/api/forge/context/providers/usage/claude/telemetry";
|
|
101109
|
+
/**
|
|
101110
|
+
* Builds the body for either lane.
|
|
101111
|
+
*
|
|
101112
|
+
* `modelWindows` is sent only when the reading carries it. CMMD keeps the
|
|
101113
|
+
* stored model rows when the key is absent, so a reading that cannot name
|
|
101114
|
+
* model rows the way `/usage` does leaves the key out.
|
|
101115
|
+
*/
|
|
101116
|
+
function claudeUsageTelemetryBody(input) {
|
|
101117
|
+
return {
|
|
101118
|
+
providerAccountId: input.attribution.providerAccountId,
|
|
101119
|
+
credentialGenerationId: input.attribution.credentialGenerationId,
|
|
101120
|
+
...input.account,
|
|
101121
|
+
source: input.source,
|
|
101122
|
+
fiveHour: input.windows.fiveHour,
|
|
101123
|
+
sevenDay: input.windows.sevenDay,
|
|
101124
|
+
...input.windows.modelWindows !== void 0 ? { modelWindows: input.windows.modelWindows } : {},
|
|
101125
|
+
observedAt: input.observedAt
|
|
101126
|
+
};
|
|
101127
|
+
}
|
|
101128
|
+
//#endregion
|
|
101259
101129
|
//#region src/cmmd/syncClaudeCredential.ts
|
|
101260
101130
|
/**
|
|
101261
101131
|
* Card 1359 (harvest path A, Claude) — publish a personal Claude subscription
|
|
@@ -101271,7 +101141,6 @@ async function fetchClaudeUsageViaOfficialClient(input) {
|
|
|
101271
101141
|
const USER_CLAUDE_CREDENTIAL_PATH = "/api/forge/context/providers/claude/credential";
|
|
101272
101142
|
const USER_CLAUDE_LINKED_ACCOUNT_PATH = "/api/forge/context/providers/claude/accounts";
|
|
101273
101143
|
const AUTH_RUNTIME_CLAUDE_CREDENTIAL_PATH = "/api/forge/context/auth-runtime-claude-credential";
|
|
101274
|
-
const USER_CLAUDE_USAGE_PATH = "/api/forge/context/providers/usage/claude/telemetry";
|
|
101275
101144
|
const AUTH_RUNTIME_CLAUDE_USAGE_PATH = "/api/forge/context/auth-runtime-claude-credential/usage";
|
|
101276
101145
|
/** Path to the local Claude credentials.json, mirroring Claude Code's resolution. */
|
|
101277
101146
|
function resolveLocalClaudeCredentialPath(homePath) {
|
|
@@ -101300,7 +101169,7 @@ function resolveLocalClaudeCredentialPath(homePath) {
|
|
|
101300
101169
|
* it reaches — this is the first gate and the only one near the user, who is
|
|
101301
101170
|
* standing in front of the sign-in and can simply try again.
|
|
101302
101171
|
*
|
|
101303
|
-
* The floor is ONE token, not both,
|
|
101172
|
+
* The floor is ONE token, not both, because an
|
|
101304
101173
|
* expired or blank access token beside a live refresh token is the normal
|
|
101305
101174
|
* resting state of a rotating login, which Claude Code refreshes silently on
|
|
101306
101175
|
* use. Demanding both would reject a perfectly recoverable credential and break
|
|
@@ -101378,10 +101247,10 @@ function extractClaudeAccountMetadata(credentialJson) {
|
|
|
101378
101247
|
return {};
|
|
101379
101248
|
}
|
|
101380
101249
|
}
|
|
101381
|
-
const execFileAsync$
|
|
101250
|
+
const execFileAsync$4 = promisify$1(execFile);
|
|
101382
101251
|
async function loadClaudeAccountMetadataFromCli(homePath) {
|
|
101383
101252
|
try {
|
|
101384
|
-
const { stdout } = await execFileAsync$
|
|
101253
|
+
const { stdout } = await execFileAsync$4(resolveClaudeBinaryPath("claude"), ["auth", "status"], {
|
|
101385
101254
|
timeout: 2e4,
|
|
101386
101255
|
maxBuffer: 1024 * 1024,
|
|
101387
101256
|
env: {
|
|
@@ -101477,18 +101346,20 @@ async function publishClaudeUsageAfterSync(input) {
|
|
|
101477
101346
|
binaryPath: resolveClaudeBinaryPath("claude"),
|
|
101478
101347
|
...input.homePath ? { claudeConfigDir: input.homePath } : {}
|
|
101479
101348
|
});
|
|
101480
|
-
await publishUsageBody(input.auth.jwt, input.auth.path === AUTH_RUNTIME_CLAUDE_CREDENTIAL_PATH ? AUTH_RUNTIME_CLAUDE_USAGE_PATH :
|
|
101481
|
-
|
|
101482
|
-
|
|
101483
|
-
|
|
101484
|
-
|
|
101485
|
-
|
|
101349
|
+
await publishUsageBody(input.auth.jwt, input.auth.path === AUTH_RUNTIME_CLAUDE_CREDENTIAL_PATH ? AUTH_RUNTIME_CLAUDE_USAGE_PATH : CLAUDE_USAGE_TELEMETRY_PATH, claudeUsageTelemetryBody({
|
|
101350
|
+
attribution: {
|
|
101351
|
+
providerAccountId: input.providerAccountId,
|
|
101352
|
+
credentialGenerationId: input.credentialGenerationId
|
|
101353
|
+
},
|
|
101354
|
+
account: {
|
|
101355
|
+
accountEmail: input.accountEmail,
|
|
101356
|
+
accountDisplayName: input.accountDisplayName,
|
|
101357
|
+
planType: input.planType
|
|
101358
|
+
},
|
|
101486
101359
|
source: "active_poll",
|
|
101487
|
-
|
|
101488
|
-
sevenDay: usage.sevenDay,
|
|
101489
|
-
modelWindows: usage.modelWindows,
|
|
101360
|
+
windows: usage,
|
|
101490
101361
|
observedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
101491
|
-
});
|
|
101362
|
+
}));
|
|
101492
101363
|
return null;
|
|
101493
101364
|
} catch (cause) {
|
|
101494
101365
|
return { cause };
|
|
@@ -149790,7 +149661,7 @@ function issueCmmdLocalUserProviderEgressCredential(input) {
|
|
|
149790
149661
|
});
|
|
149791
149662
|
}
|
|
149792
149663
|
//#endregion
|
|
149793
|
-
//#region src/provider/
|
|
149664
|
+
//#region src/provider/providerAccountTransition.ts
|
|
149794
149665
|
function explicitProviderAccountTransition(fromAccountId, toAccountId) {
|
|
149795
149666
|
if (!fromAccountId || !toAccountId || fromAccountId === toAccountId) return void 0;
|
|
149796
149667
|
return {
|
|
@@ -149798,13 +149669,20 @@ function explicitProviderAccountTransition(fromAccountId, toAccountId) {
|
|
|
149798
149669
|
toAccountId
|
|
149799
149670
|
};
|
|
149800
149671
|
}
|
|
149801
|
-
|
|
149672
|
+
/**
|
|
149673
|
+
* Whether a thread may move from `prior` to `current` because the person asked
|
|
149674
|
+
* for another Provider Account. The credential broker authorizes the selected
|
|
149675
|
+
* account, so only the identity may change, which is what `account_switch`
|
|
149676
|
+
* means.
|
|
149677
|
+
*
|
|
149678
|
+
* Nothing in the rule is specific to a provider, but the Codex resume gate in
|
|
149679
|
+
* `codexAppServerManager.ts` is its only caller. The Claude half is parked
|
|
149680
|
+
* until #3055 wires the Claude resume gate onto it.
|
|
149681
|
+
*/
|
|
149682
|
+
function permitsProviderAccountTransition(input) {
|
|
149802
149683
|
const { transition, requestedAccountId, prior, current } = input;
|
|
149803
|
-
if (!transition || transition.fromAccountId === transition.toAccountId || transition.toAccountId !== requestedAccountId
|
|
149804
|
-
return
|
|
149805
|
-
...prior,
|
|
149806
|
-
identity: current.identity
|
|
149807
|
-
}, current);
|
|
149684
|
+
if (!transition || transition.fromAccountId === transition.toAccountId || transition.toAccountId !== requestedAccountId) return false;
|
|
149685
|
+
return classifyProviderDeliveryBindingChange(prior, current) === "account_switch";
|
|
149808
149686
|
}
|
|
149809
149687
|
//#endregion
|
|
149810
149688
|
//#region src/providerAuth/codexLocalAccountIdentity.ts
|
|
@@ -150304,7 +150182,7 @@ var CodexAppServerManager = class extends EventEmitter$1 {
|
|
|
150304
150182
|
const rawPriorDeliveryBinding = input.resumeCursor.providerDeliveryBinding;
|
|
150305
150183
|
const priorDeliveryBinding = readProviderDeliveryBinding(rawPriorDeliveryBinding);
|
|
150306
150184
|
if (rawPriorDeliveryBinding !== void 0 && rawPriorDeliveryBinding !== null && !priorDeliveryBinding) throw new Error("Existing Codex history cannot resume because its persisted delivered provider account binding is unreadable.");
|
|
150307
|
-
if (priorDeliveryBinding && !providerDeliveryBindingsEqual(priorDeliveryBinding, currentDeliveryBinding) && !
|
|
150185
|
+
if (priorDeliveryBinding && !providerDeliveryBindingsEqual(priorDeliveryBinding, currentDeliveryBinding) && !permitsProviderAccountTransition({
|
|
150308
150186
|
transition: input.accountTransition,
|
|
150309
150187
|
requestedAccountId: input.providerAccountId,
|
|
150310
150188
|
prior: priorDeliveryBinding,
|
|
@@ -210733,11 +210611,243 @@ function hasConfiguredApexCredential(profiles) {
|
|
|
210733
210611
|
}));
|
|
210734
210612
|
}
|
|
210735
210613
|
//#endregion
|
|
210614
|
+
//#region src/provider/claudeCredentialDirectory.ts
|
|
210615
|
+
/**
|
|
210616
|
+
* Per-account credential directories for Claude turns.
|
|
210617
|
+
*
|
|
210618
|
+
* Claude Code keeps conversation transcripts under
|
|
210619
|
+
* `${CLAUDE_CONFIG_DIR}/projects/<cwd-slug>/<sessionId>.jsonl` and its login
|
|
210620
|
+
* under `.credentials.json`. `CLAUDE_SECURESTORAGE_CONFIG_DIR` moves only the
|
|
210621
|
+
* credential file, so each Provider Account can hold its own login while every
|
|
210622
|
+
* session file stays in one place and `--resume` keeps finding its target. See
|
|
210623
|
+
* `docs/adr/claude-accounts-split-credentials-not-sessions.md`.
|
|
210624
|
+
*
|
|
210625
|
+
* That variable is not documented. Under
|
|
210626
|
+
* `docs/adr/unpublished-provider-surface-needs-a-fallback.md` the dependency is
|
|
210627
|
+
* only allowed because absence degrades to a named behaviour that already
|
|
210628
|
+
* works: a thread with no account runs on the Environment's own login. This
|
|
210629
|
+
* module sets the variable only when the resolved directory holds a credential
|
|
210630
|
+
* the CLI can use, because a missing file, or one whose access token a refusal
|
|
210631
|
+
* has blanked, makes the CLI report logged out and a live turn would fail.
|
|
210632
|
+
*
|
|
210633
|
+
* Nothing writes these directories yet. #3049 adds the writer and keys each
|
|
210634
|
+
* directory on the account identity CMMD writes on the delivery receipt it
|
|
210635
|
+
* signs (`docs/adr/provider-account-identity-is-issued-by-cmmd.md`). Until then
|
|
210636
|
+
* every runtime takes the `no-account` skip.
|
|
210637
|
+
*
|
|
210638
|
+
* Free of Effect services on purpose, for the same reason as
|
|
210639
|
+
* `claudeCredentialContainment.ts`: ClaudeAdapter drags in Effect platform
|
|
210640
|
+
* services, the browser manager and the server config, so a unit test that
|
|
210641
|
+
* imports it cannot run in a worktree. This rule decides which account pays for
|
|
210642
|
+
* a turn, so it has to stay directly testable.
|
|
210643
|
+
*/
|
|
210644
|
+
/** Repoints `.credentials.json` without moving the transcripts. */
|
|
210645
|
+
const CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
|
|
210646
|
+
/** The one file Claude Code writes into the credential directory. */
|
|
210647
|
+
const CLAUDE_CREDENTIAL_FILE_NAME = ".credentials.json";
|
|
210648
|
+
/**
|
|
210649
|
+
* Runtime env var naming the Provider Account this runtime's Claude turns
|
|
210650
|
+
* belong to. Nothing in Forge or CMMD sets it, so no runtime names an account
|
|
210651
|
+
* and each keeps its one login.
|
|
210652
|
+
*
|
|
210653
|
+
* It is a bridge that will not get a writer. #3073 settled the key in
|
|
210654
|
+
* `docs/adr/provider-account-identity-is-issued-by-cmmd.md`: the directory name
|
|
210655
|
+
* comes from the identity CMMD issues and writes on the delivery receipt it
|
|
210656
|
+
* signs, and this variable goes away. #3049 makes that change. The credential
|
|
210657
|
+
* generation was ruled out as a key: it rotates on every reconnect, and CMMD
|
|
210658
|
+
* uses it as the primary account's id, so keying on it would strand every
|
|
210659
|
+
* thread pinned to that account on a directory that stops receiving deliveries.
|
|
210660
|
+
*
|
|
210661
|
+
* Tests reach the `credentialDirectory` parameter on
|
|
210662
|
+
* `ClaudeAdapter.buildClaudeQueryEnv` directly.
|
|
210663
|
+
*/
|
|
210664
|
+
const CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV = "FORGE_CLAUDE_PROVIDER_ACCOUNT_IDENTITY";
|
|
210665
|
+
/** Directory under the server's base directory that holds one directory per account. */
|
|
210666
|
+
const CREDENTIAL_ROOT_DIRECTORY_NAME = "claude-credentials";
|
|
210667
|
+
const DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude";
|
|
210668
|
+
const MAX_ACCOUNT_IDENTITY_LENGTH = 256;
|
|
210669
|
+
const DIRECTORY_NAME_HASH_LENGTH = 32;
|
|
210670
|
+
/**
|
|
210671
|
+
* Directory name for an account. A hash, so no email or display name reaches a
|
|
210672
|
+
* filesystem path and no path-illegal character can appear in one either.
|
|
210673
|
+
*/
|
|
210674
|
+
function claudeCredentialDirectoryName(accountIdentity) {
|
|
210675
|
+
return `account-${createHash("sha256").update(accountIdentity, "utf8").digest("hex").slice(0, DIRECTORY_NAME_HASH_LENGTH)}`;
|
|
210676
|
+
}
|
|
210677
|
+
/** The account this runtime's Claude turns belong to, or null while the variable is unset. */
|
|
210678
|
+
function resolveClaudeProviderAccountIdentity(env) {
|
|
210679
|
+
const identity = env[CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV]?.trim();
|
|
210680
|
+
if (!identity || identity.length > MAX_ACCOUNT_IDENTITY_LENGTH) return null;
|
|
210681
|
+
return identity;
|
|
210682
|
+
}
|
|
210683
|
+
/**
|
|
210684
|
+
* Where per-account credential directories live: `claude-credentials` under the
|
|
210685
|
+
* server's base directory, `ServerConfig.baseDir`, which is `FORGE_HOME` or
|
|
210686
|
+
* `~/.forge`. A delivery writer gets the same `ServerConfig`, so reader and
|
|
210687
|
+
* writer cannot disagree about the root. Always outside `CLAUDE_CONFIG_DIR`, so
|
|
210688
|
+
* one of these can never be mistaken for the main credential store, and a stray
|
|
210689
|
+
* read of the config directory cannot walk into another account's login.
|
|
210690
|
+
*/
|
|
210691
|
+
function claudeCredentialDirectoryRoot(env, baseDir) {
|
|
210692
|
+
if (!isAbsolute(baseDir)) return null;
|
|
210693
|
+
const root = resolve(baseDir, CREDENTIAL_ROOT_DIRECTORY_NAME);
|
|
210694
|
+
const configDirectory = claudeConfigDirectory(env);
|
|
210695
|
+
if (configDirectory && isPathWithinRoot({
|
|
210696
|
+
relative,
|
|
210697
|
+
isAbsolute
|
|
210698
|
+
}, configDirectory, root)) return null;
|
|
210699
|
+
return root;
|
|
210700
|
+
}
|
|
210701
|
+
/** Absolute credential directory for one account, or null when either half is missing. */
|
|
210702
|
+
function resolveClaudeCredentialDirectory(input) {
|
|
210703
|
+
if (!input.accountIdentity) return null;
|
|
210704
|
+
const root = claudeCredentialDirectoryRoot(input.env, input.baseDir);
|
|
210705
|
+
if (!root) return null;
|
|
210706
|
+
return join(root, claudeCredentialDirectoryName(input.accountIdentity));
|
|
210707
|
+
}
|
|
210708
|
+
/**
|
|
210709
|
+
* Decide whether this turn may point at a per-account credential directory.
|
|
210710
|
+
*
|
|
210711
|
+
* The cheap checks run first and the capability probe runs last, so a runtime
|
|
210712
|
+
* with no usable credential never spawns the CLI. The probe's error channel is
|
|
210713
|
+
* empty, so nothing here catches.
|
|
210714
|
+
*
|
|
210715
|
+
* A skip is not a fallback onto the default account. With no account the turn
|
|
210716
|
+
* runs on the Environment's own login, unchanged. A thread pinned to an account
|
|
210717
|
+
* whose credential is missing is refused by name under
|
|
210718
|
+
* `docs/adr/provider-account-identity-is-issued-by-cmmd.md`, and #3049 turns
|
|
210719
|
+
* `no-usable-credential`, and `capability-not-supported` for a pinned account,
|
|
210720
|
+
* into those refusals. Until #3049 nothing names an account, so `no-account` is
|
|
210721
|
+
* the only skip a runtime sees.
|
|
210722
|
+
*/
|
|
210723
|
+
function decideClaudeCredentialDirectory(input) {
|
|
210724
|
+
return gen(function* () {
|
|
210725
|
+
if (!input.accountIdentity) return skip("no-account");
|
|
210726
|
+
if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return skip("operator-override");
|
|
210727
|
+
const directory = resolveClaudeCredentialDirectory(input);
|
|
210728
|
+
if (!directory) return skip("no-root");
|
|
210729
|
+
if (!holdsUsableCredential(directory)) return skip("no-usable-credential");
|
|
210730
|
+
return (yield* input.probeSupport) === "supported" ? set(directory) : skip("capability-not-supported");
|
|
210731
|
+
});
|
|
210732
|
+
}
|
|
210733
|
+
/**
|
|
210734
|
+
* True when `.credentials.json` parses and carries a non-empty access token.
|
|
210735
|
+
*
|
|
210736
|
+
* File presence is not readiness. A refused credential is rewritten in place
|
|
210737
|
+
* with `accessToken: ""`, so a file that exists can still leave the CLI logged
|
|
210738
|
+
* out. Only the token's length is checked. The value is never logged, returned
|
|
210739
|
+
* or kept.
|
|
210740
|
+
*/
|
|
210741
|
+
function holdsUsableCredential(directory) {
|
|
210742
|
+
const file = join(directory, CLAUDE_CREDENTIAL_FILE_NAME);
|
|
210743
|
+
try {
|
|
210744
|
+
if (!statSync(file).isFile()) return false;
|
|
210745
|
+
return hasAccessToken(JSON.parse(readFileSync(file, "utf8")));
|
|
210746
|
+
} catch {
|
|
210747
|
+
return false;
|
|
210748
|
+
}
|
|
210749
|
+
}
|
|
210750
|
+
function hasAccessToken(credential) {
|
|
210751
|
+
if (typeof credential !== "object" || credential === null) return false;
|
|
210752
|
+
const oauth = credential.claudeAiOauth;
|
|
210753
|
+
if (typeof oauth !== "object" || oauth === null) return false;
|
|
210754
|
+
const token = oauth.accessToken;
|
|
210755
|
+
return typeof token === "string" && token.length > 0;
|
|
210756
|
+
}
|
|
210757
|
+
function set(directory) {
|
|
210758
|
+
return {
|
|
210759
|
+
outcome: "set",
|
|
210760
|
+
directory
|
|
210761
|
+
};
|
|
210762
|
+
}
|
|
210763
|
+
function skip(reason) {
|
|
210764
|
+
return {
|
|
210765
|
+
outcome: "skipped",
|
|
210766
|
+
reason
|
|
210767
|
+
};
|
|
210768
|
+
}
|
|
210769
|
+
function claudeConfigDirectory(env) {
|
|
210770
|
+
const configured = env.CLAUDE_CONFIG_DIR?.trim();
|
|
210771
|
+
if (configured) return configured;
|
|
210772
|
+
try {
|
|
210773
|
+
return join(homedir(), DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME);
|
|
210774
|
+
} catch {
|
|
210775
|
+
return null;
|
|
210776
|
+
}
|
|
210777
|
+
}
|
|
210778
|
+
//#endregion
|
|
210779
|
+
//#region src/provider/personalClaudeInVm.ts
|
|
210780
|
+
/**
|
|
210781
|
+
* CMMD writes this marker when it activates a personal Claude subscription
|
|
210782
|
+
* inside a running sprite. Activation always happens after boot, so a
|
|
210783
|
+
* boot-time env var alone cannot reflect it (FOR-36249).
|
|
210784
|
+
*
|
|
210785
|
+
* CMMD names the file in the Service env (FORGE_PERSONAL_CLAUDE_MARKER_PATH).
|
|
210786
|
+
* Without that, the marker sits directly under FORGE_HOME, which is the same
|
|
210787
|
+
* guest home CMMD derives every sprite path from. A server with neither has no
|
|
210788
|
+
* sprite home to look in, and a desktop never has a marker, so it reads none.
|
|
210789
|
+
*/
|
|
210790
|
+
const PERSONAL_CLAUDE_IN_VM_MARKER_NAME = ".forge-personal-claude-in-vm";
|
|
210791
|
+
const PERSONAL_CLAUDE_GENERATION_FILE_NAME = ".forge-personal-claude-generation";
|
|
210792
|
+
function markerPath(env) {
|
|
210793
|
+
if (env.FORGE_PERSONAL_CLAUDE_MARKER_PATH) return env.FORGE_PERSONAL_CLAUDE_MARKER_PATH;
|
|
210794
|
+
if (env.FORGE_HOME) return join(env.FORGE_HOME, PERSONAL_CLAUDE_IN_VM_MARKER_NAME);
|
|
210795
|
+
return null;
|
|
210796
|
+
}
|
|
210797
|
+
/**
|
|
210798
|
+
* True when this runtime should offer the personal Claude subscription lane.
|
|
210799
|
+
*
|
|
210800
|
+
* Accepts either signal: the boot-time env var (backward compatible) or the
|
|
210801
|
+
* activation marker on disk. Reading the marker at call time lets the provider
|
|
210802
|
+
* catalog and the per-turn readiness guard self-heal once CMMD delivers,
|
|
210803
|
+
* without a sprite reboot. Callers still verify the credential is non-empty
|
|
210804
|
+
* before treating it as usable; this only decides whether the lane is active.
|
|
210805
|
+
*/
|
|
210806
|
+
function personalClaudeInVmEnabled(env = process.env) {
|
|
210807
|
+
if (env.FORGE_PERSONAL_CLAUDE_IN_VM === "true") return true;
|
|
210808
|
+
const marker = markerPath(env);
|
|
210809
|
+
if (!marker) return false;
|
|
210810
|
+
try {
|
|
210811
|
+
return existsSync(marker);
|
|
210812
|
+
} catch {
|
|
210813
|
+
return false;
|
|
210814
|
+
}
|
|
210815
|
+
}
|
|
210816
|
+
/** Exported at boot. This is the restart proof CMMD waits on, so it must never read the file. */
|
|
210817
|
+
function bootExportedPersonalClaudeGenerationHash(env = process.env) {
|
|
210818
|
+
return env.FORGE_PERSONAL_CLAUDE_GENERATION_HASH?.trim() ?? "";
|
|
210819
|
+
}
|
|
210820
|
+
/**
|
|
210821
|
+
* The sha256 of the Claude credential generation this runtime holds, or "" when
|
|
210822
|
+
* it holds none. The readiness guard compares it with the generation CMMD
|
|
210823
|
+
* applied. The restart proof must not use it; it uses the boot-exported hash.
|
|
210824
|
+
*
|
|
210825
|
+
* The boot preload exports FORGE_PERSONAL_CLAUDE_GENERATION_HASH, but only when
|
|
210826
|
+
* the marker already exists at boot. Activation in place writes the generation
|
|
210827
|
+
* file and touches the marker without setting any env, so an empty variable
|
|
210828
|
+
* falls back to that file, hashed the way the preload hashes it. CMMD puts the
|
|
210829
|
+
* file beside the marker in the guest home and names no path for it. A marker
|
|
210830
|
+
* with no generation file is a legacy runtime, and gets no hash here either.
|
|
210831
|
+
*/
|
|
210832
|
+
function personalClaudeGenerationHash(env = process.env) {
|
|
210833
|
+
const exported = bootExportedPersonalClaudeGenerationHash(env);
|
|
210834
|
+
if (exported) return exported;
|
|
210835
|
+
const marker = markerPath(env);
|
|
210836
|
+
if (!marker) return "";
|
|
210837
|
+
try {
|
|
210838
|
+
if (!existsSync(marker)) return "";
|
|
210839
|
+
const generation = readFileSync(join(dirname(marker), PERSONAL_CLAUDE_GENERATION_FILE_NAME), "utf8").trim();
|
|
210840
|
+
return generation ? createHash("sha256").update(generation).digest("hex") : "";
|
|
210841
|
+
} catch {
|
|
210842
|
+
return "";
|
|
210843
|
+
}
|
|
210844
|
+
}
|
|
210845
|
+
//#endregion
|
|
210736
210846
|
//#region src/provider/claudeCredentialContainment.ts
|
|
210737
210847
|
/**
|
|
210738
210848
|
* Containment for provider credentials inherited by the Claude Agent SDK child.
|
|
210739
210849
|
*
|
|
210740
|
-
*
|
|
210850
|
+
* Free of Effect services on purpose: ClaudeAdapter pulls in Effect platform
|
|
210741
210851
|
* services, the browser manager, and the server config, so a unit test that
|
|
210742
210852
|
* imports it cannot run in a worktree. This rule is cost-critical and must stay
|
|
210743
210853
|
* directly testable.
|
|
@@ -210784,6 +210894,190 @@ function withoutInheritedAnthropicCredentials(env, options) {
|
|
|
210784
210894
|
if (options?.stripEndpointOverride) delete contained[ANTHROPIC_ENDPOINT_OVERRIDE_KEY];
|
|
210785
210895
|
return contained;
|
|
210786
210896
|
}
|
|
210897
|
+
/**
|
|
210898
|
+
* The environment a Claude child gets when it runs on the personal
|
|
210899
|
+
* subscription. Server-only secrets are scrubbed, inherited Anthropic keys are
|
|
210900
|
+
* removed so they cannot outrank the login, and the credential directory is set
|
|
210901
|
+
* only when one was decided. The turn's personal-subscription branch and the
|
|
210902
|
+
* capability probe both build their environment here, so the probe asks about
|
|
210903
|
+
* the setup the turn will run with.
|
|
210904
|
+
*
|
|
210905
|
+
* The endpoint override goes only when `personalClaudeInVmEnabled` says this
|
|
210906
|
+
* runtime is on the personal Claude lane. That check also reads the activation
|
|
210907
|
+
* marker CMMD writes on disk (#3081), so a Sprite switched on in place, which
|
|
210908
|
+
* has the marker and no env var until it restarts, drops the override too.
|
|
210909
|
+
*/
|
|
210910
|
+
function claudeSubscriptionChildEnv(processEnv, credentialDirectory) {
|
|
210911
|
+
return {
|
|
210912
|
+
...withoutInheritedAnthropicCredentials(scrubForgeProviderChildEnvSecrets(processEnv), { stripEndpointOverride: personalClaudeInVmEnabled(processEnv) }),
|
|
210913
|
+
...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
|
|
210914
|
+
};
|
|
210915
|
+
}
|
|
210916
|
+
//#endregion
|
|
210917
|
+
//#region src/provider/claudeCredentialDirectoryProbe.ts
|
|
210918
|
+
/**
|
|
210919
|
+
* Capability probe for `CLAUDE_SECURESTORAGE_CONFIG_DIR`.
|
|
210920
|
+
*
|
|
210921
|
+
* The variable is unpublished, so a runtime is asked rather than assumed to
|
|
210922
|
+
* honour it (`docs/adr/unpublished-provider-surface-needs-a-fallback.md`). The
|
|
210923
|
+
* question the probe can actually answer: point the variable at an empty
|
|
210924
|
+
* directory and a runtime that honours it flips from logged in to logged out.
|
|
210925
|
+
* A runtime with no credential at all is logged out either way and cannot
|
|
210926
|
+
* discriminate, which is `inconclusive`, and callers read that as unsupported.
|
|
210927
|
+
*
|
|
210928
|
+
* An answer only counts for the binary and environment the turn will use. The
|
|
210929
|
+
* probe spawns the `binaryPath` the turn hands the SDK, and builds its
|
|
210930
|
+
* environment with `claudeSubscriptionChildEnv`, the same builder as the turn's
|
|
210931
|
+
* personal-subscription branch.
|
|
210932
|
+
*
|
|
210933
|
+
* `claude auth status` is a pure read. It sends no prompt and leaves
|
|
210934
|
+
* `.credentials.json` byte-identical, so probing spends no subscription usage.
|
|
210935
|
+
* It prints JSON on stdout and exits 1 when logged out, so the exit code is not
|
|
210936
|
+
* a failure signal and the `loggedIn` field is what gets read.
|
|
210937
|
+
*/
|
|
210938
|
+
const execFileAsync$3 = promisify$1(execFile);
|
|
210939
|
+
const AUTH_STATUS_ARGS = ["auth", "status"];
|
|
210940
|
+
const VERSION_ARGS = ["--version"];
|
|
210941
|
+
/**
|
|
210942
|
+
* The probe sits in front of a turn, so a provider process that never answers
|
|
210943
|
+
* must not hold that turn open. `execFile` kills the child at this bound, and
|
|
210944
|
+
* `runBounded` gives up on it at the same bound whatever the deps do, so a
|
|
210945
|
+
* hung or unkillable process costs a few seconds and an `inconclusive`.
|
|
210946
|
+
*/
|
|
210947
|
+
const PROBE_TIMEOUT_MS = 5e3;
|
|
210948
|
+
const PROBE_OUTPUT_LIMIT_BYTES = 1024 * 1024;
|
|
210949
|
+
const PROBE_DIRECTORY_PREFIX = "forge-claude-capability-";
|
|
210950
|
+
const VERSION_PATTERN = /\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/;
|
|
210951
|
+
function makeClaudeCredentialDirectoryProbe(deps) {
|
|
210952
|
+
const answers = /* @__PURE__ */ new Map();
|
|
210953
|
+
const answer = async () => {
|
|
210954
|
+
const version = await readVersionSafely(deps);
|
|
210955
|
+
if (version === null) return runProbe(deps);
|
|
210956
|
+
const cached = answers.get(version);
|
|
210957
|
+
if (cached) return cached;
|
|
210958
|
+
const pending = runProbe(deps);
|
|
210959
|
+
answers.set(version, pending);
|
|
210960
|
+
const forget = () => {
|
|
210961
|
+
if (answers.get(version) === pending) answers.delete(version);
|
|
210962
|
+
};
|
|
210963
|
+
pending.then((result) => {
|
|
210964
|
+
if (result === "inconclusive") forget();
|
|
210965
|
+
}, forget);
|
|
210966
|
+
return pending;
|
|
210967
|
+
};
|
|
210968
|
+
return { support: tryPromise(answer).pipe(orElseSucceed(() => "inconclusive")) };
|
|
210969
|
+
}
|
|
210970
|
+
/**
|
|
210971
|
+
* One probe per binary path for the life of the process. A conclusive answer is
|
|
210972
|
+
* cached per binary version, so the two `auth status` spawns run once per path
|
|
210973
|
+
* and version. `claude --version` still spawns on every call, which is how an
|
|
210974
|
+
* in-place CLI upgrade gets a fresh answer.
|
|
210975
|
+
*/
|
|
210976
|
+
const probesByBinaryPath = /* @__PURE__ */ new Map();
|
|
210977
|
+
const INCONCLUSIVE_PROBE = { support: succeed("inconclusive") };
|
|
210978
|
+
/**
|
|
210979
|
+
* The probe for the binary a turn with this `binaryPath` setting runs.
|
|
210980
|
+
*
|
|
210981
|
+
* `buildClaudeQueryOptions` hands the setting to the SDK as
|
|
210982
|
+
* `pathToClaudeCodeExecutable`, and the SDK spawns it as given, so the default
|
|
210983
|
+
* `claude` goes through PATH for the probe exactly as for the turn. The SDK
|
|
210984
|
+
* picks a bundled build of its own only when the path is empty, and the
|
|
210985
|
+
* settings schema turns an empty value into `claude`. Should one arrive empty
|
|
210986
|
+
* anyway, the answer is `inconclusive` rather than an answer about another
|
|
210987
|
+
* binary.
|
|
210988
|
+
*/
|
|
210989
|
+
function claudeCredentialDirectoryProbeFor(binaryPath) {
|
|
210990
|
+
if (!binaryPath) return INCONCLUSIVE_PROBE;
|
|
210991
|
+
const existing = probesByBinaryPath.get(binaryPath);
|
|
210992
|
+
if (existing) return existing;
|
|
210993
|
+
const probe = makeClaudeCredentialDirectoryProbe(claudeCredentialDirectoryProbeDeps(binaryPath));
|
|
210994
|
+
probesByBinaryPath.set(binaryPath, probe);
|
|
210995
|
+
return probe;
|
|
210996
|
+
}
|
|
210997
|
+
const spawnClaude = (file, args, options) => execFileAsync$3(file, [...args], { ...options });
|
|
210998
|
+
function claudeCredentialDirectoryProbeDeps(binaryPath, options = {}) {
|
|
210999
|
+
const run = (args, credentialDirectory) => runClaudeCommand$1(options.execFile ?? spawnClaude, binaryPath, args, claudeSubscriptionChildEnv(options.baseEnv ?? process.env, credentialDirectory));
|
|
211000
|
+
return {
|
|
211001
|
+
runAuthStatus: ({ credentialDirectory }) => run(AUTH_STATUS_ARGS, credentialDirectory),
|
|
211002
|
+
readBinaryVersion: async () => {
|
|
211003
|
+
const { stdout } = await run(VERSION_ARGS, null);
|
|
211004
|
+
return VERSION_PATTERN.exec(stdout)?.[0] ?? null;
|
|
211005
|
+
},
|
|
211006
|
+
makeProbeDirectory: () => mkdtemp(join(tmpdir(), PROBE_DIRECTORY_PREFIX)),
|
|
211007
|
+
removeProbeDirectory: (directory) => rm(directory, {
|
|
211008
|
+
recursive: true,
|
|
211009
|
+
force: true
|
|
211010
|
+
})
|
|
211011
|
+
};
|
|
211012
|
+
}
|
|
211013
|
+
/** Rejects when the probe could not tell. `support` turns that into `inconclusive`. */
|
|
211014
|
+
async function runProbe(deps) {
|
|
211015
|
+
let probeDirectory = null;
|
|
211016
|
+
try {
|
|
211017
|
+
if (parseLoggedIn(await runBounded(deps, () => deps.runAuthStatus({ credentialDirectory: null }))) !== true) return "inconclusive";
|
|
211018
|
+
probeDirectory = await deps.makeProbeDirectory();
|
|
211019
|
+
const directory = probeDirectory;
|
|
211020
|
+
const redirected = parseLoggedIn(await runBounded(deps, () => deps.runAuthStatus({ credentialDirectory: directory })));
|
|
211021
|
+
if (redirected === false) return "supported";
|
|
211022
|
+
return redirected === true ? "unsupported" : "inconclusive";
|
|
211023
|
+
} finally {
|
|
211024
|
+
if (probeDirectory !== null) await deps.removeProbeDirectory(probeDirectory).catch(() => void 0);
|
|
211025
|
+
}
|
|
211026
|
+
}
|
|
211027
|
+
async function readVersionSafely(deps) {
|
|
211028
|
+
try {
|
|
211029
|
+
return await runBounded(deps, () => deps.readBinaryVersion());
|
|
211030
|
+
} catch {
|
|
211031
|
+
return null;
|
|
211032
|
+
}
|
|
211033
|
+
}
|
|
211034
|
+
/**
|
|
211035
|
+
* Give up on a call that outlives the bound. The underlying promise is left to
|
|
211036
|
+
* settle on its own; the real deps hand `execFile` the same bound, so its child
|
|
211037
|
+
* is already being killed by the time this rejects.
|
|
211038
|
+
*/
|
|
211039
|
+
async function runBounded(deps, operation) {
|
|
211040
|
+
const timeoutMs = deps.timeoutMs ?? PROBE_TIMEOUT_MS;
|
|
211041
|
+
let timer;
|
|
211042
|
+
try {
|
|
211043
|
+
return await Promise.race([operation(), new Promise((_resolve, reject) => {
|
|
211044
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("claude probe call timed out")), timeoutMs);
|
|
211045
|
+
})]);
|
|
211046
|
+
} finally {
|
|
211047
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
211048
|
+
}
|
|
211049
|
+
}
|
|
211050
|
+
/** The `loggedIn` field, or null when the output does not carry one. */
|
|
211051
|
+
function parseLoggedIn(output) {
|
|
211052
|
+
const parsed = parseJsonObject$1(output.stdout);
|
|
211053
|
+
return typeof parsed?.loggedIn === "boolean" ? parsed.loggedIn : null;
|
|
211054
|
+
}
|
|
211055
|
+
function parseJsonObject$1(output) {
|
|
211056
|
+
const trimmed = output.trim();
|
|
211057
|
+
const start = trimmed.indexOf("{");
|
|
211058
|
+
const end = trimmed.lastIndexOf("}");
|
|
211059
|
+
if (start === -1 || end <= start) return null;
|
|
211060
|
+
try {
|
|
211061
|
+
const parsed = JSON.parse(trimmed.slice(start, end + 1));
|
|
211062
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
|
|
211063
|
+
} catch {
|
|
211064
|
+
return null;
|
|
211065
|
+
}
|
|
211066
|
+
}
|
|
211067
|
+
async function runClaudeCommand$1(execFileImpl, binaryPath, args, env) {
|
|
211068
|
+
try {
|
|
211069
|
+
const { stdout } = await execFileImpl(binaryPath, args, {
|
|
211070
|
+
env,
|
|
211071
|
+
maxBuffer: PROBE_OUTPUT_LIMIT_BYTES,
|
|
211072
|
+
timeout: PROBE_TIMEOUT_MS
|
|
211073
|
+
});
|
|
211074
|
+
return { stdout };
|
|
211075
|
+
} catch (cause) {
|
|
211076
|
+
const failure = cause;
|
|
211077
|
+
if (typeof failure.stdout !== "string") throw cause;
|
|
211078
|
+
return { stdout: failure.stdout };
|
|
211079
|
+
}
|
|
211080
|
+
}
|
|
210787
211081
|
//#endregion
|
|
210788
211082
|
//#region src/provider/makeManagedServerProvider.ts
|
|
210789
211083
|
const makeManagedServerProvider = fn$1("makeManagedServerProvider")(function* (input) {
|
|
@@ -210796,12 +211090,12 @@ const makeManagedServerProvider = fn$1("makeManagedServerProvider")(function* (i
|
|
|
210796
211090
|
const forceRefresh = options?.forceRefresh === true;
|
|
210797
211091
|
const previousSettings = yield* get$6(settingsRef);
|
|
210798
211092
|
if (!forceRefresh && !input.haveSettingsChanged(previousSettings, nextSettings)) {
|
|
210799
|
-
yield* set$
|
|
211093
|
+
yield* set$3(settingsRef, nextSettings);
|
|
210800
211094
|
return yield* get$6(snapshotRef);
|
|
210801
211095
|
}
|
|
210802
211096
|
const nextSnapshot = yield* input.checkProvider;
|
|
210803
|
-
yield* set$
|
|
210804
|
-
yield* set$
|
|
211097
|
+
yield* set$3(settingsRef, nextSettings);
|
|
211098
|
+
yield* set$3(snapshotRef, nextSnapshot);
|
|
210805
211099
|
yield* publish(changesPubSub, nextSnapshot);
|
|
210806
211100
|
return nextSnapshot;
|
|
210807
211101
|
});
|
|
@@ -210821,7 +211115,7 @@ const makeManagedServerProvider = fn$1("makeManagedServerProvider")(function* (i
|
|
|
210821
211115
|
failureCount: failureCount + 1,
|
|
210822
211116
|
nextDelayMs: Math.min(baseMs * Math.pow(2, failureCount + 1), 3e5)
|
|
210823
211117
|
});
|
|
210824
|
-
} else yield* set$
|
|
211118
|
+
} else yield* set$3(refreshFailureCount, 0);
|
|
210825
211119
|
})).pipe(forkScoped);
|
|
210826
211120
|
return {
|
|
210827
211121
|
getSnapshot: input.getSettings.pipe(flatMap(applySnapshot), tapError(logError), orDie),
|
|
@@ -210873,43 +211167,6 @@ const makeProviderAuthProvenance = (input) => {
|
|
|
210873
211167
|
};
|
|
210874
211168
|
};
|
|
210875
211169
|
//#endregion
|
|
210876
|
-
//#region src/provider/personalClaudeInVm.ts
|
|
210877
|
-
/**
|
|
210878
|
-
* CMMD writes this marker when it activates a personal Claude subscription
|
|
210879
|
-
* inside a running sprite. Activation always happens after boot, so a
|
|
210880
|
-
* boot-time env var alone cannot reflect it (FOR-36249).
|
|
210881
|
-
*
|
|
210882
|
-
* CMMD names the file in the Service env (FORGE_PERSONAL_CLAUDE_MARKER_PATH).
|
|
210883
|
-
* Without that, the marker sits directly under FORGE_HOME, which is the same
|
|
210884
|
-
* guest home CMMD derives every sprite path from. A server with neither has no
|
|
210885
|
-
* sprite home to look in, and a desktop never has a marker, so it reads none.
|
|
210886
|
-
*/
|
|
210887
|
-
const PERSONAL_CLAUDE_IN_VM_MARKER_NAME = ".forge-personal-claude-in-vm";
|
|
210888
|
-
function markerPath(env) {
|
|
210889
|
-
if (env.FORGE_PERSONAL_CLAUDE_MARKER_PATH) return env.FORGE_PERSONAL_CLAUDE_MARKER_PATH;
|
|
210890
|
-
if (env.FORGE_HOME) return join(env.FORGE_HOME, PERSONAL_CLAUDE_IN_VM_MARKER_NAME);
|
|
210891
|
-
return null;
|
|
210892
|
-
}
|
|
210893
|
-
/**
|
|
210894
|
-
* True when this runtime should offer the personal Claude subscription lane.
|
|
210895
|
-
*
|
|
210896
|
-
* Accepts either signal: the boot-time env var (backward compatible) or the
|
|
210897
|
-
* activation marker on disk. Reading the marker at call time lets the provider
|
|
210898
|
-
* catalog and the per-turn readiness guard self-heal once CMMD delivers,
|
|
210899
|
-
* without a sprite reboot. Callers still verify the credential is non-empty
|
|
210900
|
-
* before treating it as usable; this only decides whether the lane is active.
|
|
210901
|
-
*/
|
|
210902
|
-
function personalClaudeInVmEnabled(env = process.env) {
|
|
210903
|
-
if (env.FORGE_PERSONAL_CLAUDE_IN_VM === "true") return true;
|
|
210904
|
-
const marker = markerPath(env);
|
|
210905
|
-
if (!marker) return false;
|
|
210906
|
-
try {
|
|
210907
|
-
return existsSync(marker);
|
|
210908
|
-
} catch {
|
|
210909
|
-
return false;
|
|
210910
|
-
}
|
|
210911
|
-
}
|
|
210912
|
-
//#endregion
|
|
210913
211170
|
//#region src/provider/Layers/ClaudeProvider.ts
|
|
210914
211171
|
function endpointProfileDisplayNames(settings) {
|
|
210915
211172
|
return Object.fromEntries(settings.endpointProfiles.filter((p) => p.displayName).map((p) => [p.slug, p.displayName]));
|
|
@@ -212328,10 +212585,26 @@ function claudeForgeAttributionPrompt() {
|
|
|
212328
212585
|
const attribution = resolveAgentAttribution();
|
|
212329
212586
|
return `When creating git commits, do NOT add any "Claude" or "Anthropic" co-author trailers. Use exactly:\n ${attribution.coauthorTrailer}\n\nWhen creating GitHub pull request descriptions, do NOT add any "🤖 Generated with [Claude Code]" footer. Use exactly:\n ${attribution.prFooter}`;
|
|
212330
212587
|
}
|
|
212331
|
-
|
|
212588
|
+
/**
|
|
212589
|
+
* `credentialDirectory` points this turn at one Provider Account's own
|
|
212590
|
+
* `.credentials.json` without moving the transcripts, which is what lets an
|
|
212591
|
+
* account switch keep `--resume` working
|
|
212592
|
+
* (`docs/adr/claude-accounts-split-credentials-not-sessions.md`). It stays null
|
|
212593
|
+
* until #3049 gives the directories a writer, keyed on the account identity on
|
|
212594
|
+
* the delivery receipt CMMD signs. `decideClaudeCredentialDirectory` only hands
|
|
212595
|
+
* back a directory that already holds a usable credential, and while none
|
|
212596
|
+
* exists the environment is byte-for-byte what it would be without this
|
|
212597
|
+
* parameter.
|
|
212598
|
+
*
|
|
212599
|
+
* It applies to the personal-subscription branch below and to nothing else. On
|
|
212600
|
+
* the profile / proxy branch the credential file is not what authenticates the
|
|
212601
|
+
* turn, an API key is, so pointing the variable there would add a second source
|
|
212602
|
+
* of truth that a later reader has to rule out.
|
|
212603
|
+
*/
|
|
212604
|
+
function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential, credentialDirectory) {
|
|
212332
212605
|
const baseEnv = scrubForgeProviderChildEnvSecrets(baseProcessEnv);
|
|
212333
212606
|
const effectiveBaseUrl = resolveClaudeQueryBaseUrl(endpointProfile, claudeSettings, providerProxyCredential);
|
|
212334
|
-
if (!effectiveBaseUrl) return
|
|
212607
|
+
if (!effectiveBaseUrl) return claudeSubscriptionChildEnv(baseProcessEnv, credentialDirectory);
|
|
212335
212608
|
const effectiveApiKey = resolveClaudeQueryApiKey(endpointProfile, claudeSettings, providerProxyCredential?.token ?? null);
|
|
212336
212609
|
return {
|
|
212337
212610
|
...baseEnv,
|
|
@@ -214599,6 +214872,17 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
|
|
|
214599
214872
|
})));
|
|
214600
214873
|
const claudeSettings = serverSettings.providers.claudeAgent;
|
|
214601
214874
|
const claudeBinaryPath = claudeSettings.binaryPath;
|
|
214875
|
+
const credentialDirectoryDecision = yield* decideClaudeCredentialDirectory({
|
|
214876
|
+
env: process.env,
|
|
214877
|
+
baseDir: serverConfig.baseDir,
|
|
214878
|
+
accountIdentity: resolveClaudeProviderAccountIdentity(process.env),
|
|
214879
|
+
probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support
|
|
214880
|
+
});
|
|
214881
|
+
if (credentialDirectoryDecision.outcome === "skipped") {
|
|
214882
|
+
const skipLine = `Claude credential directory skipped: ${credentialDirectoryDecision.reason}`;
|
|
214883
|
+
yield* credentialDirectoryDecision.reason === "no-account" ? logDebug(skipLine) : logWarning$1(skipLine);
|
|
214884
|
+
}
|
|
214885
|
+
const claudeCredentialDirectory = credentialDirectoryDecision.outcome === "set" ? credentialDirectoryDecision.directory : null;
|
|
214602
214886
|
const modelConfig = resolveClaudeSessionModelConfig(input.modelSelection, claudeSettings.endpointProfiles);
|
|
214603
214887
|
const providerProxyCredential = yield* issueClaudeProviderProxyCredential(modelConfig.endpointProfile, modelConfig.modelSlug ?? modelConfig.effectiveApiModelId ?? "unknown", threadId);
|
|
214604
214888
|
const permissionMode = permissionModeForRuntimeMode(input.runtimeMode);
|
|
@@ -214623,7 +214907,7 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
|
|
|
214623
214907
|
existingResumeSessionId,
|
|
214624
214908
|
newSessionId,
|
|
214625
214909
|
canUseTool,
|
|
214626
|
-
env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential),
|
|
214910
|
+
env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential, claudeCredentialDirectory),
|
|
214627
214911
|
browserManager,
|
|
214628
214912
|
threadId,
|
|
214629
214913
|
browserToolEnvironmentId,
|
|
@@ -214660,7 +214944,7 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
|
|
|
214660
214944
|
effectiveApiModelId: modelConfig.effectiveApiModelId,
|
|
214661
214945
|
effectiveEffort: modelConfig.effectiveEffort
|
|
214662
214946
|
});
|
|
214663
|
-
yield* set$
|
|
214947
|
+
yield* set$3(contextRef, context$4);
|
|
214664
214948
|
sessions.set(threadId, context$4);
|
|
214665
214949
|
yield* emitClaudeSessionStartupEvents({
|
|
214666
214950
|
threadId,
|
|
@@ -219541,7 +219825,7 @@ const makeAcpSessionRuntime = (options) => gen(function* () {
|
|
|
219541
219825
|
}
|
|
219542
219826
|
});
|
|
219543
219827
|
});
|
|
219544
|
-
const updateConfigOptions = (response) => set$
|
|
219828
|
+
const updateConfigOptions = (response) => set$3(configOptionsRef, sessionConfigOptionsFromSetup(response));
|
|
219545
219829
|
const updateCurrentModeId = (modeId) => update$4(modeStateRef, (current) => current ? {
|
|
219546
219830
|
...current,
|
|
219547
219831
|
currentModeId: modeId
|
|
@@ -219600,8 +219884,8 @@ const makeAcpSessionRuntime = (options) => gen(function* () {
|
|
|
219600
219884
|
sessionId = created.sessionId;
|
|
219601
219885
|
sessionSetupResult = created;
|
|
219602
219886
|
}
|
|
219603
|
-
yield* set$
|
|
219604
|
-
yield* set$
|
|
219887
|
+
yield* set$3(modeStateRef, parseSessionModeState(sessionSetupResult));
|
|
219888
|
+
yield* set$3(configOptionsRef, sessionConfigOptionsFromSetup(sessionSetupResult));
|
|
219605
219889
|
return {
|
|
219606
219890
|
sessionId,
|
|
219607
219891
|
initializeResult,
|
|
@@ -219795,10 +220079,10 @@ const makeStartCoordinator = (startStateRef, startOnce) => gen(function* () {
|
|
|
219795
220079
|
switch (state._tag) {
|
|
219796
220080
|
case "Started": return [succeed(state.result), state];
|
|
219797
220081
|
case "Starting": return [_await(state.deferred), state];
|
|
219798
|
-
case "NotStarted": return [startOnce.pipe(tap((result) => set$
|
|
220082
|
+
case "NotStarted": return [startOnce.pipe(tap((result) => set$3(startStateRef, {
|
|
219799
220083
|
_tag: "Started",
|
|
219800
220084
|
result
|
|
219801
|
-
}).pipe(andThen(succeed$3(deferred, result)))), onError$1((cause) => failCause$3(deferred, cause).pipe(andThen(set$
|
|
220085
|
+
}).pipe(andThen(succeed$3(deferred, result)))), onError$1((cause) => failCause$3(deferred, cause).pipe(andThen(set$3(startStateRef, { _tag: "NotStarted" }))))), {
|
|
219802
220086
|
_tag: "Starting",
|
|
219803
220087
|
deferred
|
|
219804
220088
|
}];
|
|
@@ -229697,7 +229981,7 @@ function claudeReadinessGuardEffect(config, dependencies, resolved, input) {
|
|
|
229697
229981
|
});
|
|
229698
229982
|
}
|
|
229699
229983
|
function requiresNativeClaudeReadiness(config, provider) {
|
|
229700
|
-
return provider === "claudeAgent" && config.forgeCmmdRuntimeMode === true &&
|
|
229984
|
+
return provider === "claudeAgent" && config.forgeCmmdRuntimeMode === true && personalClaudeInVmEnabled();
|
|
229701
229985
|
}
|
|
229702
229986
|
function resolveGuardAuthority(config, readSessionId, input) {
|
|
229703
229987
|
const runtimeId = config.forgeCmmdRuntimeId?.trim();
|
|
@@ -229709,7 +229993,7 @@ function resolveGuardAuthority(config, readSessionId, input) {
|
|
|
229709
229993
|
runtimeId,
|
|
229710
229994
|
machineId,
|
|
229711
229995
|
credentialEpoch,
|
|
229712
|
-
localGenerationHash:
|
|
229996
|
+
localGenerationHash: personalClaudeGenerationHash(),
|
|
229713
229997
|
sessionId
|
|
229714
229998
|
};
|
|
229715
229999
|
}
|
|
@@ -283475,7 +283759,7 @@ const GitManagerLive = effect(GitManager, fn$1("makeGitManager")(function* () {
|
|
|
283475
283759
|
let preResolvedCommitSuggestion = void 0;
|
|
283476
283760
|
const modelSelection = yield* serverSettingsService.getSettings.pipe(map$3((settings) => settings.textGenerationModelSelection), mapError((cause) => gitManagerError("runStackedAction", "Failed to get server settings.", cause)));
|
|
283477
283761
|
if (input.featureBranch) {
|
|
283478
|
-
yield* set$
|
|
283762
|
+
yield* set$3(currentPhase, some("branch"));
|
|
283479
283763
|
yield* progress.emit({
|
|
283480
283764
|
kind: "phase_started",
|
|
283481
283765
|
phase: "branch",
|
|
@@ -283488,12 +283772,12 @@ const GitManagerLive = effect(GitManager, fn$1("makeGitManager")(function* () {
|
|
|
283488
283772
|
} else branchStep = { status: "skipped_not_requested" };
|
|
283489
283773
|
const currentBranch = branchStep.name ?? initialStatus.branch;
|
|
283490
283774
|
const commitAction = isCommitAction(input.action) ? input.action : null;
|
|
283491
|
-
const commit = commitAction ? yield* set$
|
|
283775
|
+
const commit = commitAction ? yield* set$3(currentPhase, some("commit")).pipe(flatMap(() => runCommitStep(modelSelection, input.cwd, commitAction, currentBranch, commitMessageForStep, preResolvedCommitSuggestion, input.filePaths, options?.progressReporter, progress.actionId, input.threadId))) : { status: "skipped_not_requested" };
|
|
283492
283776
|
const push = wantsPush ? yield* progress.emit({
|
|
283493
283777
|
kind: "phase_started",
|
|
283494
283778
|
phase: "push",
|
|
283495
283779
|
label: "Pushing..."
|
|
283496
|
-
}).pipe(tap(() => set$
|
|
283780
|
+
}).pipe(tap(() => set$3(currentPhase, some("push"))), flatMap(() => {
|
|
283497
283781
|
const emit = (event) => progress.emit(event);
|
|
283498
283782
|
const hookProgress = createHookProgressEmitter(emit);
|
|
283499
283783
|
return gitCore.pushCurrentBranch(input.cwd, currentBranch, { progress: hookProgress.progress }).pipe(tap(() => {
|
|
@@ -283511,11 +283795,11 @@ const GitManagerLive = effect(GitManager, fn$1("makeGitManager")(function* () {
|
|
|
283511
283795
|
kind: "phase_started",
|
|
283512
283796
|
phase: "pr",
|
|
283513
283797
|
label: "Preparing PR merge..."
|
|
283514
|
-
}).pipe(tap(() => set$
|
|
283798
|
+
}).pipe(tap(() => set$3(currentPhase, some("pr"))), flatMap(() => runMergePrStep(input.cwd, currentBranch, progress.emit))) : wantsPr ? yield* progress.emit({
|
|
283515
283799
|
kind: "phase_started",
|
|
283516
283800
|
phase: "pr",
|
|
283517
283801
|
label: "Preparing PR..."
|
|
283518
|
-
}).pipe(tap(() => set$
|
|
283802
|
+
}).pipe(tap(() => set$3(currentPhase, some("pr"))), flatMap(() => runPrStep(modelSelection, input.cwd, currentBranch, progress.emit, input.threadId))) : { status: "skipped_not_requested" };
|
|
283519
283803
|
const toast = yield* buildCompletionToast(input.cwd, {
|
|
283520
283804
|
action: input.action,
|
|
283521
283805
|
branch: branchStep,
|
|
@@ -286869,25 +287153,124 @@ const makeRuntimeDrainReporter = gen(function* () {
|
|
|
286869
287153
|
var RuntimeDrainReporter = class extends Service$1()("forge/runtimeDrain/RuntimeDrainReporter") {};
|
|
286870
287154
|
const RuntimeDrainReporterLive = effect(RuntimeDrainReporter, makeRuntimeDrainReporter);
|
|
286871
287155
|
//#endregion
|
|
286872
|
-
//#region src/usage/
|
|
286873
|
-
/**
|
|
286874
|
-
*
|
|
286875
|
-
*
|
|
286876
|
-
*
|
|
286877
|
-
*
|
|
286878
|
-
|
|
286879
|
-
|
|
286880
|
-
|
|
286881
|
-
|
|
286882
|
-
|
|
286883
|
-
|
|
286884
|
-
|
|
286885
|
-
|
|
286886
|
-
|
|
286887
|
-
|
|
286888
|
-
|
|
286889
|
-
|
|
286890
|
-
|
|
287156
|
+
//#region src/usage/Services/ClaudeUsageTelemetry.ts
|
|
287157
|
+
/**
|
|
287158
|
+
* Claude usage the SDK reports on every turn, published as passive telemetry.
|
|
287159
|
+
*
|
|
287160
|
+
* Every Claude turn carries a `rate_limit_event` whose `unifiedWindows`
|
|
287161
|
+
* sub-object holds the two rolling windows the Usage card renders. Reading it
|
|
287162
|
+
* costs nothing: no `claude` process, no credential delivery, no woken sprite.
|
|
287163
|
+
*
|
|
287164
|
+
* This module is the whole publish lane behind one call. `observe` takes what
|
|
287165
|
+
* an observation knows and returns void. Whether the floor admits it, which
|
|
287166
|
+
* credential generation it ran on, whether the value is new, and whether CMMD
|
|
287167
|
+
* accepted it are all decided here, so the ingestion layer stays a decoder and
|
|
287168
|
+
* a hand-off. Nothing decided here can fail a turn, and no admitted
|
|
287169
|
+
* observation outlives `CLAUDE_USAGE_TELEMETRY_PUBLISH_TIMEOUT_MS`.
|
|
287170
|
+
*
|
|
287171
|
+
* The `/usage` probe in `syncClaudeCredential.ts` is a separate lane that
|
|
287172
|
+
* publishes `source: "active_poll"`. This one publishes
|
|
287173
|
+
* `source: "passive_sdk_telemetry"`, and both lanes build their body with
|
|
287174
|
+
* `claudeUsageTelemetryBody`.
|
|
287175
|
+
*
|
|
287176
|
+
* @module ClaudeUsageTelemetry
|
|
287177
|
+
*/
|
|
287178
|
+
var ClaudeUsageTelemetry = class extends Service$1()("forge/usage/Services/ClaudeUsageTelemetry") {};
|
|
287179
|
+
/**
|
|
287180
|
+
* The longest one admitted observation may take, from the credential
|
|
287181
|
+
* generation lookup through the CMMD credential and the POST, before it is
|
|
287182
|
+
* abandoned and counted as failed. Ingestion does not wait on it, so this
|
|
287183
|
+
* bounds how long a slow lookup or a slow CMMD keeps a fiber alive, not how
|
|
287184
|
+
* long a turn waits. Together with the floor it caps how many observations for
|
|
287185
|
+
* one account can be in flight at once.
|
|
287186
|
+
*/
|
|
287187
|
+
const CLAUDE_USAGE_TELEMETRY_PUBLISH_TIMEOUT_MS = 1e4;
|
|
287188
|
+
/** How many accounts, and how many pairs, the observer remembers before evicting the oldest. */
|
|
287189
|
+
const MAX_TRACKED_KEYS = 32;
|
|
287190
|
+
/**
|
|
287191
|
+
* Why one publish did not land. Every reason is the same to `observe`, which
|
|
287192
|
+
* logs it and moves on, so the type exists to name the lane rather than to be
|
|
287193
|
+
* matched on.
|
|
287194
|
+
*/
|
|
287195
|
+
var ClaudeUsageTelemetryPublishError = class extends TaggedError("ClaudeUsageTelemetryPublishError") {};
|
|
287196
|
+
function pairKey(attribution) {
|
|
287197
|
+
return `${attribution.providerAccountId}\u0000${attribution.credentialGenerationId}`;
|
|
287198
|
+
}
|
|
287199
|
+
function windowFingerprint(value) {
|
|
287200
|
+
return value === null ? "-" : `${value.usedPercent}|${value.resetsAt}|${value.status}`;
|
|
287201
|
+
}
|
|
287202
|
+
/**
|
|
287203
|
+
* A value fingerprint built field by field rather than by `JSON.stringify` on
|
|
287204
|
+
* the whole object, so the comparison does not depend on key insertion order
|
|
287205
|
+
* and cannot silently start ignoring a field a future mapper adds without a
|
|
287206
|
+
* matching change here.
|
|
287207
|
+
*/
|
|
287208
|
+
function windowsFingerprint(windows) {
|
|
287209
|
+
return `${windowFingerprint(windows.fiveHour)}/${windowFingerprint(windows.sevenDay)}`;
|
|
287210
|
+
}
|
|
287211
|
+
/**
|
|
287212
|
+
* Stores `value` as the newest entry for `key` and evicts the oldest entry past
|
|
287213
|
+
* the cap. Map iteration is insertion-ordered, so the first key is the one
|
|
287214
|
+
* touched longest ago. An idle account or a rotated credential generation
|
|
287215
|
+
* rarely comes back, so evicting it costs at most one early lookup or one
|
|
287216
|
+
* redundant publish.
|
|
287217
|
+
*/
|
|
287218
|
+
function remember(map, key, value) {
|
|
287219
|
+
map.delete(key);
|
|
287220
|
+
map.set(key, value);
|
|
287221
|
+
if (map.size > MAX_TRACKED_KEYS) {
|
|
287222
|
+
const oldest = map.keys().next();
|
|
287223
|
+
if (!oldest.done) map.delete(oldest.value);
|
|
287224
|
+
}
|
|
287225
|
+
}
|
|
287226
|
+
/**
|
|
287227
|
+
* Builds the observer over injected dependencies. The HTTP call and the clock
|
|
287228
|
+
* both arrive from outside, so a test drives the whole policy without a network
|
|
287229
|
+
* or a timer.
|
|
287230
|
+
*/
|
|
287231
|
+
function makeClaudeUsageTelemetry(dependencies) {
|
|
287232
|
+
const now = dependencies.now ?? (() => Date.now());
|
|
287233
|
+
/** When each account last had an observation admitted, whatever came of it. */
|
|
287234
|
+
const admittedAt = /* @__PURE__ */ new Map();
|
|
287235
|
+
/** The fingerprint of the last value CMMD accepted, per pair. */
|
|
287236
|
+
const publishedFingerprints = /* @__PURE__ */ new Map();
|
|
287237
|
+
const publishIfNew = (windows, providerAccountId, lookupCredentialGenerationId, at) => gen(function* () {
|
|
287238
|
+
const credentialGenerationId = yield* lookupCredentialGenerationId;
|
|
287239
|
+
if (!credentialGenerationId) return;
|
|
287240
|
+
const attribution = {
|
|
287241
|
+
providerAccountId,
|
|
287242
|
+
credentialGenerationId
|
|
287243
|
+
};
|
|
287244
|
+
const key = pairKey(attribution);
|
|
287245
|
+
const fingerprint = windowsFingerprint(windows);
|
|
287246
|
+
if (publishedFingerprints.get(key) === fingerprint) return;
|
|
287247
|
+
yield* dependencies.publish(claudeUsageTelemetryBody({
|
|
287248
|
+
attribution,
|
|
287249
|
+
source: "passive_sdk_telemetry",
|
|
287250
|
+
windows,
|
|
287251
|
+
observedAt: new Date(at).toISOString()
|
|
287252
|
+
}));
|
|
287253
|
+
remember(publishedFingerprints, key, fingerprint);
|
|
287254
|
+
});
|
|
287255
|
+
/**
|
|
287256
|
+
* The floor is read and claimed inside the returned effect, never while
|
|
287257
|
+
* building it, and before anything suspends. Two turns on one account can be
|
|
287258
|
+
* in flight at once, and the floor only holds them apart if the second one
|
|
287259
|
+
* reads what the first one wrote.
|
|
287260
|
+
*/
|
|
287261
|
+
const observe = (observation) => suspend$1(() => {
|
|
287262
|
+
const { windows, providerAccountId } = observation;
|
|
287263
|
+
if (!windows || !providerAccountId) return void_$1;
|
|
287264
|
+
const at = now();
|
|
287265
|
+
const lastAdmittedAt = admittedAt.get(providerAccountId);
|
|
287266
|
+
if (lastAdmittedAt !== void 0 && at - lastAdmittedAt < 5e3) return void_$1;
|
|
287267
|
+
remember(admittedAt, providerAccountId, at);
|
|
287268
|
+
return publishIfNew(windows, providerAccountId, observation.credentialGenerationId, at).pipe(timeout$1(CLAUDE_USAGE_TELEMETRY_PUBLISH_TIMEOUT_MS), catchCause((cause) => logWarning$1("claude usage passive telemetry publish failed", {
|
|
287269
|
+
providerAccountId,
|
|
287270
|
+
cause
|
|
287271
|
+
})), asVoid);
|
|
287272
|
+
});
|
|
287273
|
+
return { observe };
|
|
286891
287274
|
}
|
|
286892
287275
|
//#endregion
|
|
286893
287276
|
//#region src/orchestration/Layers/ProviderRuntimeIngestion.ts
|
|
@@ -286902,20 +287285,37 @@ const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = minutes(120);
|
|
|
286902
287285
|
const MAX_BUFFERED_ASSISTANT_CHARS = 24e3;
|
|
286903
287286
|
const PROVIDER_INGESTION_CONCURRENCY = 8;
|
|
286904
287287
|
const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.FORGE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0";
|
|
286905
|
-
|
|
287288
|
+
/**
|
|
287289
|
+
* The SDK's own `rate_limit_info` off one ClaudeAdapter rate-limit event, or
|
|
287290
|
+
* nothing. Both readers below start here: the banner takes the single window
|
|
287291
|
+
* `rateLimitType` names, and passive usage telemetry takes the `unifiedWindows`
|
|
287292
|
+
* sub-object riding along on the same object.
|
|
287293
|
+
*/
|
|
287294
|
+
function readClaudeRateLimitInfo(raw) {
|
|
286906
287295
|
const rateLimitInfo = raw.rate_limit_info;
|
|
286907
|
-
if (rateLimitInfo === null || typeof rateLimitInfo !== "object") return
|
|
287296
|
+
if (rateLimitInfo === null || typeof rateLimitInfo !== "object") return null;
|
|
287297
|
+
return rateLimitInfo;
|
|
287298
|
+
}
|
|
287299
|
+
function decodeClaudeRateLimitWindows(raw) {
|
|
287300
|
+
const rateLimitInfo = readClaudeRateLimitInfo(raw);
|
|
287301
|
+
if (!rateLimitInfo) return [];
|
|
286908
287302
|
const mapped = mapClaudeRateLimitInfo(rateLimitInfo);
|
|
286909
287303
|
return mapped ? [{
|
|
286910
287304
|
window: mapped.kind === "fiveHour" ? "five_hour" : "seven_day",
|
|
286911
287305
|
...mapped.window
|
|
286912
287306
|
}] : [];
|
|
286913
287307
|
}
|
|
287308
|
+
/** The raw telemetry object ClaudeAdapter/CodexAdapter wrap as `payload.rateLimits`. */
|
|
287309
|
+
function readRateLimitPayload(event) {
|
|
287310
|
+
if (event.type !== "account.rate-limits.updated") return null;
|
|
287311
|
+
const raw = event.payload.rateLimits;
|
|
287312
|
+
return raw !== null && typeof raw === "object" ? raw : null;
|
|
287313
|
+
}
|
|
286914
287314
|
/**
|
|
286915
287315
|
* Codex's app-server protocol shape isn't type-published anywhere this
|
|
286916
|
-
* codebase has access to, so it's read defensively
|
|
286917
|
-
* is skipped rather than thrown, since this is a best-effort in-chat notice
|
|
286918
|
-
*
|
|
287316
|
+
* codebase has access to, so it's read defensively. An unrecognized shape
|
|
287317
|
+
* is skipped rather than thrown, since this is a best-effort in-chat notice
|
|
287318
|
+
* that the turn itself never depends on.
|
|
286919
287319
|
*/
|
|
286920
287320
|
/**
|
|
286921
287321
|
* Decodes ONE window of the codex app-server `account/rateLimits/updated`
|
|
@@ -287228,6 +287628,9 @@ function shouldApplyProviderTurnCompleted(activeTurnId, eventTurnId) {
|
|
|
287228
287628
|
}
|
|
287229
287629
|
const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$1("make")(function* () {
|
|
287230
287630
|
const orchestrationEngine = yield* OrchestrationEngineService;
|
|
287631
|
+
const claudeUsageTelemetry = yield* ClaudeUsageTelemetry;
|
|
287632
|
+
const claudeUsageScope = yield* make$37("parallel");
|
|
287633
|
+
yield* addFinalizer$2(() => close(claudeUsageScope, void_$3));
|
|
287231
287634
|
const providerService = yield* ProviderService;
|
|
287232
287635
|
const projectionTurnRepository = yield* ProjectionTurnRepository;
|
|
287233
287636
|
const serverSettingsService = yield* ServerSettingsService;
|
|
@@ -287252,7 +287655,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
|
|
|
287252
287655
|
createdAt: ""
|
|
287253
287656
|
})
|
|
287254
287657
|
});
|
|
287255
|
-
const rememberAssistantMessageId = (threadId, turnId, messageId) => getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe(flatMap((existingIds) => set$
|
|
287658
|
+
const rememberAssistantMessageId = (threadId, turnId, messageId) => getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe(flatMap((existingIds) => set$4(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId), match(existingIds, {
|
|
287256
287659
|
onNone: () => new Set([messageId]),
|
|
287257
287660
|
onSome: (ids) => {
|
|
287258
287661
|
const nextIds = new Set(ids);
|
|
@@ -287266,7 +287669,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
|
|
|
287266
287669
|
const nextIds = new Set(ids);
|
|
287267
287670
|
nextIds.delete(messageId);
|
|
287268
287671
|
if (nextIds.size === 0) return invalidate(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId));
|
|
287269
|
-
return set$
|
|
287672
|
+
return set$4(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId), nextIds);
|
|
287270
287673
|
}
|
|
287271
287674
|
})));
|
|
287272
287675
|
const getAssistantMessageIdsForTurn = (threadId, turnId) => getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe(map$3((existingIds) => getOrElse(existingIds, () => /* @__PURE__ */ new Set())));
|
|
@@ -287277,7 +287680,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
|
|
|
287277
287680
|
onSome: (text) => `${text}${delta}`
|
|
287278
287681
|
});
|
|
287279
287682
|
if (nextText.length <= MAX_BUFFERED_ASSISTANT_CHARS) {
|
|
287280
|
-
yield* set$
|
|
287683
|
+
yield* set$4(bufferedAssistantTextByMessageId, messageId, nextText);
|
|
287281
287684
|
return "";
|
|
287282
287685
|
}
|
|
287283
287686
|
yield* invalidate(bufferedAssistantTextByMessageId, messageId);
|
|
@@ -287287,7 +287690,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
|
|
|
287287
287690
|
const clearBufferedAssistantText = (messageId) => invalidate(bufferedAssistantTextByMessageId, messageId);
|
|
287288
287691
|
const appendBufferedProposedPlan = (planId, delta, createdAt) => getOption(bufferedProposedPlanById, planId).pipe(flatMap((existingEntry) => {
|
|
287289
287692
|
const existing = getOrUndefined(existingEntry);
|
|
287290
|
-
return set$
|
|
287693
|
+
return set$4(bufferedProposedPlanById, planId, {
|
|
287291
287694
|
text: `${existing?.text ?? ""}${delta}`,
|
|
287292
287695
|
createdAt: existing?.createdAt && existing.createdAt.length > 0 ? existing.createdAt : createdAt
|
|
287293
287696
|
});
|
|
@@ -287635,18 +288038,53 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
|
|
|
287635
288038
|
* SDKRateLimitInfo, reused from apps/server/src/usage/providerRateLimits.ts
|
|
287636
288039
|
* (same decoder the Usage page cache uses). Codex's app-server protocol
|
|
287637
288040
|
* shape isn't type-published anywhere this codebase has access to, so it's
|
|
287638
|
-
* read defensively
|
|
287639
|
-
* thrown error, since this is a best-effort in-chat notice
|
|
287640
|
-
*
|
|
288041
|
+
* read defensively. An unrecognized shape means no window update, not a
|
|
288042
|
+
* thrown error, since this is a best-effort in-chat notice that the turn
|
|
288043
|
+
* itself never depends on.
|
|
287641
288044
|
*/
|
|
287642
288045
|
function decodeRateLimitWindows(event) {
|
|
287643
|
-
|
|
287644
|
-
|
|
287645
|
-
if (raw === null || typeof raw !== "object") return [];
|
|
288046
|
+
const raw = readRateLimitPayload(event);
|
|
288047
|
+
if (!raw) return [];
|
|
287646
288048
|
if (event.provider === "claudeAgent") return decodeClaudeRateLimitWindows(raw);
|
|
287647
288049
|
if (event.provider === "codex") return decodeCodexRateLimitWindows(raw);
|
|
287648
288050
|
return [];
|
|
287649
288051
|
}
|
|
288052
|
+
/**
|
|
288053
|
+
* The credential generation a Claude thread's live session runs on, or null.
|
|
288054
|
+
*
|
|
288055
|
+
* It is the delivery identity the readiness guard verified before the
|
|
288056
|
+
* session started, which ProviderService writes into the provider resume
|
|
288057
|
+
* cursor and `listSessions` hands back on the live session. Claude's
|
|
288058
|
+
* delivery identity is `{kind: "credential_generation"}` and carries NO
|
|
288059
|
+
* account field, so it cannot stand in for the account the thread's model
|
|
288060
|
+
* selection names, and neither id is derivable from the other. The usage
|
|
288061
|
+
* observer drops an observation missing either one, because publishing under
|
|
288062
|
+
* a guessed id would write one account's usage onto another account's card.
|
|
288063
|
+
*/
|
|
288064
|
+
const claudeCredentialGenerationId = fn$1("claudeCredentialGenerationId")(function* (threadId) {
|
|
288065
|
+
const cursor = (yield* providerService.listSessions()).find((entry) => entry.threadId === threadId && entry.provider === "claudeAgent")?.resumeCursor;
|
|
288066
|
+
const binding = cursor !== null && typeof cursor === "object" ? readProviderDeliveryBinding(cursor.providerDeliveryBinding) : null;
|
|
288067
|
+
return binding?.identity.kind === "credential_generation" ? binding.identity.credentialGenerationId : null;
|
|
288068
|
+
});
|
|
288069
|
+
/**
|
|
288070
|
+
* Publishes the unified windows the same event already carried for the
|
|
288071
|
+
* banner. Passive: no probe, no credential delivery, no woken sprite. The SDK
|
|
288072
|
+
* reports these figures on every turn whether anything reads them or not.
|
|
288073
|
+
* The observer owns the floor, the credential generation lookup, dedupe, the
|
|
288074
|
+
* timeout and the HTTP call, and never fails.
|
|
288075
|
+
*/
|
|
288076
|
+
const observeClaudeUsage = fn$1("observeClaudeUsage")(function* (event, thread) {
|
|
288077
|
+
if (event.provider !== "claudeAgent") return;
|
|
288078
|
+
const raw = readRateLimitPayload(event);
|
|
288079
|
+
const rateLimitInfo = raw ? readClaudeRateLimitInfo(raw) : null;
|
|
288080
|
+
const windows = rateLimitInfo ? mapClaudeUnifiedWindows(rateLimitInfo) : null;
|
|
288081
|
+
if (!windows) return;
|
|
288082
|
+
yield* claudeUsageTelemetry.observe({
|
|
288083
|
+
windows,
|
|
288084
|
+
providerAccountId: readProviderAccountId(thread.modelSelection) ?? null,
|
|
288085
|
+
credentialGenerationId: claudeCredentialGenerationId(thread.id)
|
|
288086
|
+
});
|
|
288087
|
+
});
|
|
287650
288088
|
const handleRateLimitEvent = fn$1("handleRateLimitEvent")(function* (event, thread) {
|
|
287651
288089
|
const provider = event.provider === "claudeAgent" ? "claude" : event.provider === "codex" ? "codex" : null;
|
|
287652
288090
|
if (!provider) return;
|
|
@@ -287662,6 +288100,10 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
|
|
|
287662
288100
|
resetsAt: win.resetsAt,
|
|
287663
288101
|
createdAt: event.createdAt
|
|
287664
288102
|
}), { discard: true });
|
|
288103
|
+
yield* observeClaudeUsage(event, thread).pipe(catchCause((cause) => hasInterruptsOnly(cause) ? void_$1 : logWarning$1("claude usage observation failed", {
|
|
288104
|
+
eventId: event.eventId,
|
|
288105
|
+
cause
|
|
288106
|
+
})), forkIn(claudeUsageScope));
|
|
287665
288107
|
});
|
|
287666
288108
|
const processRuntimeEvent = fn$1("processRuntimeEvent")(function* (event) {
|
|
287667
288109
|
const thread = (yield* orchestrationEngine.getReadModel()).threads.find((entry) => entry.id === event.threadId);
|
|
@@ -287776,6 +288218,44 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
|
|
|
287776
288218
|
};
|
|
287777
288219
|
})()).pipe(provide$1(ProjectionTurnRepositoryLive));
|
|
287778
288220
|
//#endregion
|
|
288221
|
+
//#region src/usage/Layers/ClaudeUsageTelemetry.ts
|
|
288222
|
+
/**
|
|
288223
|
+
* The real publish behind `ClaudeUsageTelemetry`: the owner's CMMD credential
|
|
288224
|
+
* and one POST to `CLAUDE_USAGE_TELEMETRY_PATH`.
|
|
288225
|
+
*
|
|
288226
|
+
* The call always goes out on the owner session credential, never on a
|
|
288227
|
+
* runtime grant.
|
|
288228
|
+
*
|
|
288229
|
+
* A machine with nobody signed in, or one whose CMMD credential cannot be
|
|
288230
|
+
* minted, fails the publish rather than reporting success. The observer only
|
|
288231
|
+
* remembers a value it managed to send, so a quiet success here would suppress
|
|
288232
|
+
* the publish for as long as that reading stayed the same, which on a stable
|
|
288233
|
+
* account is hours.
|
|
288234
|
+
*
|
|
288235
|
+
* @module ClaudeUsageTelemetryLive
|
|
288236
|
+
*/
|
|
288237
|
+
const LOG_PREFIX = "claude-usage-passive-telemetry";
|
|
288238
|
+
const ClaudeUsageTelemetryLive = effect(ClaudeUsageTelemetry, gen(function* () {
|
|
288239
|
+
const sql = yield* SqlClient;
|
|
288240
|
+
const publish = (body) => gen(function* () {
|
|
288241
|
+
const credential = yield* resolveCmmdOwnerSessionCredential(sql, { logPrefix: LOG_PREFIX });
|
|
288242
|
+
if (credential.kind !== "credential") return yield* new ClaudeUsageTelemetryPublishError({ reason: credential.kind });
|
|
288243
|
+
yield* tryPromise({
|
|
288244
|
+
try: () => cmmdGateway.request({
|
|
288245
|
+
method: "POST",
|
|
288246
|
+
jwt: credential.jwt,
|
|
288247
|
+
path: CLAUDE_USAGE_TELEMETRY_PATH,
|
|
288248
|
+
body
|
|
288249
|
+
}),
|
|
288250
|
+
catch: (cause) => new ClaudeUsageTelemetryPublishError({
|
|
288251
|
+
reason: "request-failed",
|
|
288252
|
+
cause
|
|
288253
|
+
})
|
|
288254
|
+
});
|
|
288255
|
+
});
|
|
288256
|
+
return makeClaudeUsageTelemetry({ publish });
|
|
288257
|
+
}));
|
|
288258
|
+
//#endregion
|
|
287779
288259
|
//#region ../../packages/shared/src/DrainableWorker.ts
|
|
287780
288260
|
/**
|
|
287781
288261
|
* Create a drainable worker that processes items from an unbounded queue.
|
|
@@ -288088,7 +288568,7 @@ const ProviderCommandReactorLive = effect(ProviderCommandReactor, gen(function*
|
|
|
288088
288568
|
timeToLive: HANDLED_TURN_START_KEY_TTL,
|
|
288089
288569
|
lookup: () => succeed(true)
|
|
288090
288570
|
});
|
|
288091
|
-
const hasHandledTurnStartRecently = (key) => getOption(handledTurnStartKeys, key).pipe(flatMap((cached) => set$
|
|
288571
|
+
const hasHandledTurnStartRecently = (key) => getOption(handledTurnStartKeys, key).pipe(flatMap((cached) => set$4(handledTurnStartKeys, key, true).pipe(as$3(isSome(cached)))));
|
|
288092
288572
|
const threadModelSelections = /* @__PURE__ */ new Map();
|
|
288093
288573
|
/**
|
|
288094
288574
|
* The providerAccountId each LIVE provider session was actually started with.
|
|
@@ -291931,6 +292411,382 @@ const WorkspaceFileSystemLive = effect(WorkspaceFileSystem, gen(function* () {
|
|
|
291931
292411
|
}) };
|
|
291932
292412
|
}));
|
|
291933
292413
|
//#endregion
|
|
292414
|
+
//#region src/workspace/Layers/WorkspaceBulkTransfer.ts
|
|
292415
|
+
const TRANSFER_TTL_MS = 1440 * 60 * 1e3;
|
|
292416
|
+
const TRANSFER_LOCK_STRIPE_COUNT = 64;
|
|
292417
|
+
function transferError(input) {
|
|
292418
|
+
return new BulkTransferError({
|
|
292419
|
+
code: input.code,
|
|
292420
|
+
message: input.message,
|
|
292421
|
+
transferId: input.transferId ?? null,
|
|
292422
|
+
retryable: input.retryable ?? false,
|
|
292423
|
+
...input.cause === void 0 ? {} : { cause: input.cause }
|
|
292424
|
+
});
|
|
292425
|
+
}
|
|
292426
|
+
function mapFailure(operation, transferId) {
|
|
292427
|
+
return (cause) => is$1(BulkTransferError)(cause) ? cause : transferError({
|
|
292428
|
+
code: "io_error",
|
|
292429
|
+
message: `Workspace bulk transfer failed during ${operation}.`,
|
|
292430
|
+
...transferId === void 0 ? {} : { transferId },
|
|
292431
|
+
retryable: true,
|
|
292432
|
+
cause
|
|
292433
|
+
});
|
|
292434
|
+
}
|
|
292435
|
+
function transferLockStripe(transferId) {
|
|
292436
|
+
let hash = 2166136261;
|
|
292437
|
+
for (let index = 0; index < transferId.length; index += 1) hash = Math.imul(hash ^ transferId.charCodeAt(index), 16777619);
|
|
292438
|
+
return (hash >>> 0) % TRANSFER_LOCK_STRIPE_COUNT;
|
|
292439
|
+
}
|
|
292440
|
+
const makeWorkspaceBulkTransfer = (options = {}) => gen(function* () {
|
|
292441
|
+
const fileSystem = yield* FileSystem;
|
|
292442
|
+
const path = yield* Path$2;
|
|
292443
|
+
const sql = yield* SqlClient;
|
|
292444
|
+
const workspacePaths = yield* WorkspacePaths;
|
|
292445
|
+
const workspaceEntries = yield* WorkspaceEntries;
|
|
292446
|
+
const fault = options.fault ?? (() => void_$1);
|
|
292447
|
+
const transferLocks = yield* forEach(Array.from({ length: TRANSFER_LOCK_STRIPE_COUNT }), () => make$43(1));
|
|
292448
|
+
const withTransferLock = (transferId, effect) => transferLocks[transferLockStripe(transferId)].withPermit(effect);
|
|
292449
|
+
const findTransfer = (transferId) => sql`
|
|
292450
|
+
SELECT transfer_id AS "transferId", workspace_root AS "workspaceRoot",
|
|
292451
|
+
relative_path AS "relativePath", target_path AS "targetPath", temp_path AS "tempPath",
|
|
292452
|
+
total_bytes AS "totalBytes", whole_sha256 AS "wholeSha256",
|
|
292453
|
+
next_offset AS "nextOffset", state, expires_at AS "expiresAt"
|
|
292454
|
+
FROM workspace_bulk_transfers WHERE transfer_id = ${transferId} LIMIT 1
|
|
292455
|
+
`.pipe(map$3((rows) => rows[0] ?? null));
|
|
292456
|
+
const requireTransfer = (transferId) => findTransfer(transferId).pipe(flatMap((row) => row === null ? fail(transferError({
|
|
292457
|
+
code: "transfer_not_found",
|
|
292458
|
+
message: "Bulk transfer does not exist.",
|
|
292459
|
+
transferId
|
|
292460
|
+
})) : succeed(row)));
|
|
292461
|
+
const requireOpenTransfer = (transferId) => requireTransfer(transferId).pipe(flatMap((row) => {
|
|
292462
|
+
if (row.state === "cancelled") return fail(transferError({
|
|
292463
|
+
code: "transfer_cancelled",
|
|
292464
|
+
message: "Bulk transfer was cancelled.",
|
|
292465
|
+
transferId
|
|
292466
|
+
}));
|
|
292467
|
+
if (row.state === "expired" || Date.parse(row.expiresAt) <= Date.now()) return fail(transferError({
|
|
292468
|
+
code: "transfer_expired",
|
|
292469
|
+
message: "Bulk transfer expired.",
|
|
292470
|
+
transferId
|
|
292471
|
+
}));
|
|
292472
|
+
return succeed(row);
|
|
292473
|
+
}));
|
|
292474
|
+
const resolveTarget = fn$1("WorkspaceBulkTransfer.resolveTarget")(function* (input) {
|
|
292475
|
+
const workspaceRoot = yield* workspacePaths.normalizeWorkspaceRoot(input.cwd);
|
|
292476
|
+
const resolved = yield* workspacePaths.resolveRelativePathWithinRoot({
|
|
292477
|
+
workspaceRoot,
|
|
292478
|
+
relativePath: input.relativePath
|
|
292479
|
+
});
|
|
292480
|
+
const canonicalRoot = yield* fileSystem.realPath(workspaceRoot);
|
|
292481
|
+
const parentSegments = resolved.relativePath.split("/").slice(0, -1);
|
|
292482
|
+
let canonicalParent = canonicalRoot;
|
|
292483
|
+
for (const segment of parentSegments) {
|
|
292484
|
+
const candidate = path.join(canonicalParent, segment);
|
|
292485
|
+
const canonicalCandidate = yield* fileSystem.realPath(candidate).pipe(catch_(() => input.createParent ? fileSystem.makeDirectory(candidate).pipe(flatMap(() => fileSystem.realPath(candidate))) : fileSystem.realPath(candidate)));
|
|
292486
|
+
if (!isPathWithinRoot(path, canonicalRoot, canonicalCandidate)) return yield* transferError({
|
|
292487
|
+
code: "symlink_escape",
|
|
292488
|
+
message: "Workspace file path resolves outside the project root."
|
|
292489
|
+
});
|
|
292490
|
+
canonicalParent = canonicalCandidate;
|
|
292491
|
+
}
|
|
292492
|
+
const targetPath = path.join(canonicalParent, path.basename(resolved.relativePath));
|
|
292493
|
+
const targetRealPath = yield* fileSystem.realPath(targetPath).pipe(catch_(() => succeed(null)));
|
|
292494
|
+
if (targetRealPath !== null && !isPathWithinRoot(path, canonicalRoot, targetRealPath)) return yield* transferError({
|
|
292495
|
+
code: "symlink_escape",
|
|
292496
|
+
message: "Workspace file target resolves outside the project root."
|
|
292497
|
+
});
|
|
292498
|
+
return {
|
|
292499
|
+
workspaceRoot: canonicalRoot,
|
|
292500
|
+
relativePath: resolved.relativePath,
|
|
292501
|
+
targetPath,
|
|
292502
|
+
canonicalParent
|
|
292503
|
+
};
|
|
292504
|
+
});
|
|
292505
|
+
const readFile = (input) => unwrap$1(gen(function* () {
|
|
292506
|
+
const target = yield* resolveTarget({
|
|
292507
|
+
...input,
|
|
292508
|
+
createParent: false
|
|
292509
|
+
});
|
|
292510
|
+
const info = yield* fileSystem.stat(target.targetPath);
|
|
292511
|
+
const maxBytes = input.maxBytes ?? 16777216;
|
|
292512
|
+
if (Number(info.size) > maxBytes) return yield* transferError({
|
|
292513
|
+
code: "size_limit_exceeded",
|
|
292514
|
+
message: `Workspace file exceeds the ${maxBytes}-byte read limit.`
|
|
292515
|
+
});
|
|
292516
|
+
const bytes = yield* fileSystem.readFile(target.targetPath);
|
|
292517
|
+
const transfer = yield* promise(() => chunkBulkTransfer(bytes));
|
|
292518
|
+
const transferId = transfer.chunks[0]?.transferId ?? crypto.randomUUID().replaceAll("-", "_");
|
|
292519
|
+
return fromIterable$1([
|
|
292520
|
+
{
|
|
292521
|
+
kind: "started",
|
|
292522
|
+
transferId,
|
|
292523
|
+
totalBytes: transfer.totalBytes,
|
|
292524
|
+
sha256: transfer.sha256
|
|
292525
|
+
},
|
|
292526
|
+
...transfer.chunks.map((chunk) => ({
|
|
292527
|
+
...chunk,
|
|
292528
|
+
transferId
|
|
292529
|
+
})),
|
|
292530
|
+
{
|
|
292531
|
+
kind: "completed",
|
|
292532
|
+
transferId,
|
|
292533
|
+
totalBytes: transfer.totalBytes,
|
|
292534
|
+
sha256: transfer.sha256
|
|
292535
|
+
}
|
|
292536
|
+
]);
|
|
292537
|
+
}).pipe(mapError(mapFailure("read file"))));
|
|
292538
|
+
const resumeWrite = fn$1("WorkspaceBulkTransfer.resumeWrite")(function* (input, existing) {
|
|
292539
|
+
const target = yield* resolveTarget({
|
|
292540
|
+
...input,
|
|
292541
|
+
createParent: true
|
|
292542
|
+
});
|
|
292543
|
+
if (!(existing.workspaceRoot === target.workspaceRoot && existing.relativePath === target.relativePath && existing.totalBytes === input.totalBytes && existing.wholeSha256 === input.sha256)) return yield* transferError({
|
|
292544
|
+
code: "transfer_identity_conflict",
|
|
292545
|
+
message: "Bulk transfer ID is already bound to different content or a different path.",
|
|
292546
|
+
transferId: input.transferId
|
|
292547
|
+
});
|
|
292548
|
+
if (existing.state === "cancelled") return yield* transferError({
|
|
292549
|
+
code: "transfer_cancelled",
|
|
292550
|
+
message: "Bulk transfer was cancelled.",
|
|
292551
|
+
transferId: input.transferId
|
|
292552
|
+
});
|
|
292553
|
+
if (existing.state === "expired" || Date.parse(existing.expiresAt) <= Date.now()) return yield* transferError({
|
|
292554
|
+
code: "transfer_expired",
|
|
292555
|
+
message: "Bulk transfer expired.",
|
|
292556
|
+
transferId: input.transferId
|
|
292557
|
+
});
|
|
292558
|
+
return {
|
|
292559
|
+
transferId: input.transferId,
|
|
292560
|
+
nextOffset: existing.nextOffset,
|
|
292561
|
+
state: existing.state === "completed" ? "completed" : "open"
|
|
292562
|
+
};
|
|
292563
|
+
});
|
|
292564
|
+
const createWrite = fn$1("WorkspaceBulkTransfer.createWrite")(function* (input) {
|
|
292565
|
+
const target = yield* resolveTarget({
|
|
292566
|
+
...input,
|
|
292567
|
+
createParent: true
|
|
292568
|
+
}).pipe(mapError((cause) => is$1(WorkspacePathOutsideRootError)(cause) ? transferError({
|
|
292569
|
+
code: "path_outside_workspace",
|
|
292570
|
+
message: "Workspace file path must stay within the project root.",
|
|
292571
|
+
transferId: input.transferId,
|
|
292572
|
+
cause
|
|
292573
|
+
}) : cause));
|
|
292574
|
+
const tempPath = path.join(target.canonicalParent, `.forge-transfer-${input.transferId}.part`);
|
|
292575
|
+
yield* scoped(fileSystem.open(tempPath, {
|
|
292576
|
+
flag: "wx",
|
|
292577
|
+
mode: 384
|
|
292578
|
+
}).pipe(flatMap((file) => file.sync), catch_(() => fileSystem.stat(tempPath).pipe(flatMap((stat) => Number(stat.size) === 0 ? void_$1 : fail(transferError({
|
|
292579
|
+
code: "transfer_identity_conflict",
|
|
292580
|
+
message: "Bulk transfer temporary path already contains data.",
|
|
292581
|
+
transferId: input.transferId
|
|
292582
|
+
})))))));
|
|
292583
|
+
const now = /* @__PURE__ */ new Date();
|
|
292584
|
+
yield* sql`
|
|
292585
|
+
INSERT INTO workspace_bulk_transfers (
|
|
292586
|
+
transfer_id, workspace_root, relative_path, target_path, temp_path, total_bytes,
|
|
292587
|
+
whole_sha256, next_offset, state, expires_at, created_at, updated_at
|
|
292588
|
+
) VALUES (
|
|
292589
|
+
${input.transferId}, ${target.workspaceRoot}, ${target.relativePath}, ${target.targetPath},
|
|
292590
|
+
${tempPath}, ${input.totalBytes}, ${input.sha256}, 0, 'open',
|
|
292591
|
+
${new Date(now.getTime() + TRANSFER_TTL_MS).toISOString()}, ${now.toISOString()},
|
|
292592
|
+
${now.toISOString()}
|
|
292593
|
+
)
|
|
292594
|
+
`;
|
|
292595
|
+
return {
|
|
292596
|
+
transferId: input.transferId,
|
|
292597
|
+
nextOffset: 0,
|
|
292598
|
+
state: "open"
|
|
292599
|
+
};
|
|
292600
|
+
});
|
|
292601
|
+
const beginWriteEffect = fn$1("WorkspaceBulkTransfer.beginWrite")(function* (input) {
|
|
292602
|
+
const existing = yield* findTransfer(input.transferId);
|
|
292603
|
+
return yield* existing === null ? createWrite(input) : resumeWrite(input, existing);
|
|
292604
|
+
});
|
|
292605
|
+
const beginWrite = (input) => withTransferLock(input.transferId, beginWriteEffect(input)).pipe(mapError(mapFailure("begin write", input.transferId)));
|
|
292606
|
+
const acceptRetriedChunk = fn$1("WorkspaceBulkTransfer.acceptRetriedChunk")(function* (input, row) {
|
|
292607
|
+
const accepted = (yield* sql`
|
|
292608
|
+
SELECT chunk_offset AS "chunkOffset", chunk_bytes AS "chunkBytes",
|
|
292609
|
+
chunk_sha256 AS "chunkSha256"
|
|
292610
|
+
FROM workspace_bulk_transfer_chunks
|
|
292611
|
+
WHERE transfer_id = ${input.transferId} AND chunk_offset = ${input.offset}
|
|
292612
|
+
LIMIT 1
|
|
292613
|
+
`)[0];
|
|
292614
|
+
if (!(accepted !== void 0 && accepted.chunkBytes === input.bytes && accepted.chunkSha256 === input.sha256)) return yield* transferError({
|
|
292615
|
+
code: "integrity_mismatch",
|
|
292616
|
+
message: "Retried bulk transfer chunk differs from the accepted chunk.",
|
|
292617
|
+
transferId: input.transferId
|
|
292618
|
+
});
|
|
292619
|
+
return {
|
|
292620
|
+
transferId: input.transferId,
|
|
292621
|
+
nextOffset: row.nextOffset,
|
|
292622
|
+
duplicate: true
|
|
292623
|
+
};
|
|
292624
|
+
});
|
|
292625
|
+
const validateChunkRange = fn$1("WorkspaceBulkTransfer.validateChunkRange")(function* (input, row, byteLength) {
|
|
292626
|
+
if (input.offset !== row.nextOffset) return yield* transferError({
|
|
292627
|
+
code: "offset_mismatch",
|
|
292628
|
+
message: `Expected chunk offset ${row.nextOffset}, received ${input.offset}.`,
|
|
292629
|
+
transferId: input.transferId,
|
|
292630
|
+
retryable: true
|
|
292631
|
+
});
|
|
292632
|
+
const nextOffset = input.offset + byteLength;
|
|
292633
|
+
if (nextOffset > row.totalBytes) return yield* transferError({
|
|
292634
|
+
code: "size_limit_exceeded",
|
|
292635
|
+
message: "Bulk transfer chunk exceeds the declared total byte count.",
|
|
292636
|
+
transferId: input.transferId
|
|
292637
|
+
});
|
|
292638
|
+
return nextOffset;
|
|
292639
|
+
});
|
|
292640
|
+
const reconcileChunkBytes = fn$1("WorkspaceBulkTransfer.reconcileChunkBytes")(function* (input, row, bytes, nextOffset) {
|
|
292641
|
+
const tempInfo = yield* fileSystem.stat(row.tempPath);
|
|
292642
|
+
if (Number(tempInfo.size) > row.nextOffset) {
|
|
292643
|
+
if (Number(tempInfo.size) !== nextOffset) return yield* transferError({
|
|
292644
|
+
code: "integrity_mismatch",
|
|
292645
|
+
message: "Bulk transfer temporary file cannot be reconciled with the next chunk.",
|
|
292646
|
+
transferId: input.transferId
|
|
292647
|
+
});
|
|
292648
|
+
const recovered = (yield* fileSystem.readFile(row.tempPath)).slice(input.offset, nextOffset);
|
|
292649
|
+
if ((yield* promise(() => sha256Hex(recovered))) !== input.sha256) return yield* transferError({
|
|
292650
|
+
code: "integrity_mismatch",
|
|
292651
|
+
message: "Bulk transfer temporary bytes differ from the retried chunk.",
|
|
292652
|
+
transferId: input.transferId
|
|
292653
|
+
});
|
|
292654
|
+
return;
|
|
292655
|
+
}
|
|
292656
|
+
yield* fault("before-byte-write");
|
|
292657
|
+
yield* scoped(fileSystem.open(row.tempPath, { flag: "r+" }).pipe(flatMap((file) => file.seek(input.offset, "start").pipe(flatMap(() => file.writeAll(bytes)), flatMap(() => file.sync)))));
|
|
292658
|
+
yield* fault("after-byte-write-before-metadata");
|
|
292659
|
+
});
|
|
292660
|
+
const persistAcceptedChunk = fn$1("WorkspaceBulkTransfer.persistAcceptedChunk")(function* (input, nextOffset) {
|
|
292661
|
+
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
292662
|
+
yield* sql.withTransaction(sql`
|
|
292663
|
+
INSERT INTO workspace_bulk_transfer_chunks (
|
|
292664
|
+
transfer_id, chunk_offset, chunk_bytes, chunk_sha256
|
|
292665
|
+
) VALUES (${input.transferId}, ${input.offset}, ${input.bytes}, ${input.sha256})
|
|
292666
|
+
ON CONFLICT DO NOTHING
|
|
292667
|
+
`.pipe(flatMap(() => sql`
|
|
292668
|
+
UPDATE workspace_bulk_transfers
|
|
292669
|
+
SET next_offset = ${nextOffset}, updated_at = ${updatedAt}
|
|
292670
|
+
WHERE transfer_id = ${input.transferId} AND next_offset = ${input.offset} AND state = 'open'
|
|
292671
|
+
`)));
|
|
292672
|
+
yield* fault("after-metadata-advance");
|
|
292673
|
+
});
|
|
292674
|
+
const writeChunkEffect = fn$1("WorkspaceBulkTransfer.writeChunk")(function* (input) {
|
|
292675
|
+
const bytes = yield* promise(() => verifyBulkTransferChunk({
|
|
292676
|
+
...input,
|
|
292677
|
+
kind: "chunk"
|
|
292678
|
+
}));
|
|
292679
|
+
const row = yield* requireOpenTransfer(input.transferId);
|
|
292680
|
+
if (row.state === "completed") return yield* transferError({
|
|
292681
|
+
code: "offset_mismatch",
|
|
292682
|
+
message: "Bulk transfer is already complete.",
|
|
292683
|
+
transferId: input.transferId
|
|
292684
|
+
});
|
|
292685
|
+
if (input.offset < row.nextOffset) return yield* acceptRetriedChunk(input, row);
|
|
292686
|
+
const nextOffset = yield* validateChunkRange(input, row, bytes.byteLength);
|
|
292687
|
+
yield* reconcileChunkBytes(input, row, bytes, nextOffset);
|
|
292688
|
+
yield* persistAcceptedChunk(input, nextOffset);
|
|
292689
|
+
return {
|
|
292690
|
+
transferId: input.transferId,
|
|
292691
|
+
nextOffset,
|
|
292692
|
+
duplicate: false
|
|
292693
|
+
};
|
|
292694
|
+
});
|
|
292695
|
+
const writeChunk = (input) => withTransferLock(input.transferId, writeChunkEffect(input)).pipe(mapError(mapFailure("write chunk", input.transferId)));
|
|
292696
|
+
const commitWriteEffect = fn$1("WorkspaceBulkTransfer.commitWrite")(function* (input) {
|
|
292697
|
+
const row = yield* requireOpenTransfer(input.transferId);
|
|
292698
|
+
if (row.state === "completed") return {
|
|
292699
|
+
transferId: row.transferId,
|
|
292700
|
+
relativePath: row.relativePath,
|
|
292701
|
+
totalBytes: row.totalBytes,
|
|
292702
|
+
sha256: row.wholeSha256
|
|
292703
|
+
};
|
|
292704
|
+
if (row.nextOffset !== row.totalBytes) return yield* transferError({
|
|
292705
|
+
code: "offset_mismatch",
|
|
292706
|
+
message: `Bulk transfer is incomplete at byte ${row.nextOffset} of ${row.totalBytes}.`,
|
|
292707
|
+
transferId: row.transferId,
|
|
292708
|
+
retryable: true
|
|
292709
|
+
});
|
|
292710
|
+
if ((yield* resolveTarget({
|
|
292711
|
+
cwd: row.workspaceRoot,
|
|
292712
|
+
relativePath: row.relativePath,
|
|
292713
|
+
createParent: false
|
|
292714
|
+
})).targetPath !== row.targetPath) return yield* transferError({
|
|
292715
|
+
code: "symlink_escape",
|
|
292716
|
+
message: "Workspace file destination changed during transfer.",
|
|
292717
|
+
transferId: row.transferId
|
|
292718
|
+
});
|
|
292719
|
+
const tempInfo = yield* fileSystem.stat(row.tempPath).pipe(catch_(() => succeed(null)));
|
|
292720
|
+
const candidatePath = tempInfo === null ? row.targetPath : row.tempPath;
|
|
292721
|
+
const candidate = yield* fileSystem.readFile(candidatePath);
|
|
292722
|
+
if (candidate.byteLength !== row.totalBytes || (yield* promise(() => sha256Hex(candidate))) !== row.wholeSha256) return yield* transferError({
|
|
292723
|
+
code: "integrity_mismatch",
|
|
292724
|
+
message: "Bulk transfer payload does not match its declared size and digest.",
|
|
292725
|
+
transferId: row.transferId
|
|
292726
|
+
});
|
|
292727
|
+
if (tempInfo !== null) {
|
|
292728
|
+
yield* fileSystem.rename(row.tempPath, row.targetPath);
|
|
292729
|
+
yield* fault("after-atomic-rename");
|
|
292730
|
+
}
|
|
292731
|
+
yield* sql`
|
|
292732
|
+
UPDATE workspace_bulk_transfers SET state = 'completed', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
292733
|
+
WHERE transfer_id = ${row.transferId}
|
|
292734
|
+
`;
|
|
292735
|
+
yield* workspaceEntries.invalidate(row.workspaceRoot);
|
|
292736
|
+
return {
|
|
292737
|
+
transferId: row.transferId,
|
|
292738
|
+
relativePath: row.relativePath,
|
|
292739
|
+
totalBytes: row.totalBytes,
|
|
292740
|
+
sha256: row.wholeSha256
|
|
292741
|
+
};
|
|
292742
|
+
});
|
|
292743
|
+
const commitWrite = (input) => withTransferLock(input.transferId, commitWriteEffect(input)).pipe(mapError(mapFailure("commit write", input.transferId)));
|
|
292744
|
+
const cancelWriteEffect = fn$1("WorkspaceBulkTransfer.cancelWrite")(function* (input) {
|
|
292745
|
+
const row = yield* requireTransfer(input.transferId);
|
|
292746
|
+
if (row.state !== "completed") {
|
|
292747
|
+
yield* fileSystem.remove(row.tempPath, { force: true });
|
|
292748
|
+
yield* sql`
|
|
292749
|
+
UPDATE workspace_bulk_transfers SET state = 'cancelled', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
292750
|
+
WHERE transfer_id = ${input.transferId}
|
|
292751
|
+
`;
|
|
292752
|
+
}
|
|
292753
|
+
return {
|
|
292754
|
+
transferId: input.transferId,
|
|
292755
|
+
state: row.state === "completed" ? "completed" : "cancelled"
|
|
292756
|
+
};
|
|
292757
|
+
});
|
|
292758
|
+
const cancelWrite = (input) => withTransferLock(input.transferId, cancelWriteEffect(input)).pipe(mapError(mapFailure("cancel write", input.transferId)));
|
|
292759
|
+
const cleanupExpiredEffect = fn$1("WorkspaceBulkTransfer.cleanupExpired")(function* () {
|
|
292760
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
292761
|
+
const rows = yield* sql`
|
|
292762
|
+
SELECT transfer_id AS "transferId", temp_path AS "tempPath"
|
|
292763
|
+
FROM workspace_bulk_transfers WHERE state = 'open' AND expires_at <= ${now}
|
|
292764
|
+
`;
|
|
292765
|
+
let expiredCount = 0;
|
|
292766
|
+
for (const row of rows) if (yield* withTransferLock(row.transferId, gen(function* () {
|
|
292767
|
+
const current = yield* findTransfer(row.transferId);
|
|
292768
|
+
if (current === null || current.state !== "open" || Date.parse(current.expiresAt) > Date.parse(now)) return false;
|
|
292769
|
+
yield* fileSystem.remove(current.tempPath, { force: true });
|
|
292770
|
+
yield* sql`
|
|
292771
|
+
UPDATE workspace_bulk_transfers SET state = 'expired', updated_at = ${now}
|
|
292772
|
+
WHERE transfer_id = ${row.transferId} AND state = 'open' AND expires_at <= ${now}
|
|
292773
|
+
`;
|
|
292774
|
+
return true;
|
|
292775
|
+
}))) expiredCount += 1;
|
|
292776
|
+
return expiredCount;
|
|
292777
|
+
});
|
|
292778
|
+
const cleanupExpired = () => cleanupExpiredEffect().pipe(mapError(mapFailure("expiry cleanup")));
|
|
292779
|
+
return {
|
|
292780
|
+
readFile,
|
|
292781
|
+
beginWrite,
|
|
292782
|
+
writeChunk,
|
|
292783
|
+
commitWrite,
|
|
292784
|
+
cancelWrite,
|
|
292785
|
+
cleanupExpired
|
|
292786
|
+
};
|
|
292787
|
+
});
|
|
292788
|
+
const WorkspaceBulkTransferLive = effect(WorkspaceBulkTransfer, makeWorkspaceBulkTransfer());
|
|
292789
|
+
//#endregion
|
|
291934
292790
|
//#region src/workspace/Layers/WorkspacePaths.ts
|
|
291935
292791
|
function toPosixRelativePath(input) {
|
|
291936
292792
|
return input.replaceAll("\\", "/");
|
|
@@ -293524,7 +294380,7 @@ function resolveBuildCommitFromEnv(env) {
|
|
|
293524
294380
|
* environment descriptor down instead of reporting an honest "unknown".
|
|
293525
294381
|
*/
|
|
293526
294382
|
function readBakedBuildCommit() {
|
|
293527
|
-
return "
|
|
294383
|
+
return "b79da36c868d5629c76dcec88a3326634a02c2c7";
|
|
293528
294384
|
}
|
|
293529
294385
|
async function resolveServerBuildCommit(input) {
|
|
293530
294386
|
if (isFullCommitSha(input.baked)) return input.baked;
|
|
@@ -293732,7 +294588,7 @@ const ServerEnvironmentLive = effect(ServerEnvironment, fn$1("makeServerEnvironm
|
|
|
293732
294588
|
updateDisplayName: (displayName) => gen(function* () {
|
|
293733
294589
|
const normalized = normalizeDisplayName(displayName);
|
|
293734
294590
|
yield* writeMetadata({ displayName: normalized }).pipe(orDie);
|
|
293735
|
-
yield* set$
|
|
294591
|
+
yield* set$3(displayNameOverrideRef, normalized);
|
|
293736
294592
|
const descriptor = yield* buildDescriptor;
|
|
293737
294593
|
yield* publish(changesPubSub, descriptor);
|
|
293738
294594
|
return descriptor;
|
|
@@ -310647,7 +311503,7 @@ const PipelineRunCoordinatorLive = effect(PipelineRunCoordinator, gen(function*
|
|
|
310647
311503
|
contextHash: preparedHandoff.authority.contextHash,
|
|
310648
311504
|
contextSnapshot: snapshot.run.contextSnapshot
|
|
310649
311505
|
}).pipe(mapError((cause) => coordinatorError("registerHandoff", "PIPELINE_CONTEXT_ARTIFACT_FAILED", "The immutable CMMD run context could not be materialized for the provider.", cause)));
|
|
310650
|
-
const latestPreparedAuthority = yield* heartbeatPublicationSemaphore.withPermits(1)(set$
|
|
311506
|
+
const latestPreparedAuthority = yield* heartbeatPublicationSemaphore.withPermits(1)(set$3(acceptingHeartbeatRef, false).pipe(andThen(get$6(preparedAuthorityRef))));
|
|
310651
311507
|
const persistedAuthority = yield* persistAuthoritySecret({
|
|
310652
311508
|
runId: input.runId,
|
|
310653
311509
|
organizationId: latestPreparedAuthority.authority.organizationId,
|
|
@@ -310734,7 +311590,7 @@ const PipelineRunCoordinatorLive = effect(PipelineRunCoordinator, gen(function*
|
|
|
310734
311590
|
});
|
|
310735
311591
|
yield* heartbeatPublicationSemaphore.withPermits(1)(gen(function* () {
|
|
310736
311592
|
if (!(yield* get$6(acceptingHeartbeatRef))) return;
|
|
310737
|
-
yield* set$
|
|
311593
|
+
yield* set$3(preparedAuthorityRef, {
|
|
310738
311594
|
authority: heartbeat.refreshedAuthority,
|
|
310739
311595
|
preparation
|
|
310740
311596
|
});
|
|
@@ -315508,7 +316364,7 @@ const WorkspaceReadinessStateLive = effect(WorkspaceReadinessState, gen(function
|
|
|
315508
316364
|
const slot = yield* getOrCreateSlot(userId);
|
|
315509
316365
|
const previous = yield* get$6(slot.snapshotRef);
|
|
315510
316366
|
if (previous !== null && event.ts < previous.ts) return;
|
|
315511
|
-
yield* set$
|
|
316367
|
+
yield* set$3(slot.snapshotRef, event);
|
|
315512
316368
|
yield* publish(slot.pubsub, event);
|
|
315513
316369
|
if (event.phase === "app-ready") yield* schedulePrune(userId, slot);
|
|
315514
316370
|
});
|
|
@@ -316008,7 +316864,7 @@ function claimClaudeReadinessJti(input) {
|
|
|
316008
316864
|
return "claimed";
|
|
316009
316865
|
}
|
|
316010
316866
|
const readUsableClaudeGenerationHash = (config) => {
|
|
316011
|
-
const configuredGenerationHash =
|
|
316867
|
+
const configuredGenerationHash = bootExportedPersonalClaudeGenerationHash();
|
|
316012
316868
|
if (!/^[a-f0-9]{64}$/.test(configuredGenerationHash)) return succeed(null);
|
|
316013
316869
|
return tryPromise(() => readFile$1(resolveLocalClaudeCredentialPath(config.forgeClaudeConfigPath), "utf8")).pipe(map$3((credentialJson) => isClaudeSubscriptionCredential(credentialJson) ? configuredGenerationHash : null), orElseSucceed(() => null));
|
|
316014
316870
|
};
|
|
@@ -316523,7 +317379,7 @@ const PlatformServicesLive = unwrap(gen(function* () {
|
|
|
316523
317379
|
}));
|
|
316524
317380
|
const RuntimeReplicationLayerLive = RuntimeReplicationLive.pipe(provideMerge(RuntimeReplicationClientLive), provideMerge(RuntimeReplicationOutboxLive), provideMerge(ActiveHourMeterLive.pipe(provide$1(RuntimeReplicationOutboxLive))));
|
|
316525
317381
|
const RuntimeDrainLayerLive = RuntimeDrainReporterLive.pipe(provideMerge(RuntimeDrainStateLive));
|
|
316526
|
-
const RuntimeAwareProviderReactorsLive = mergeAll(ProviderRuntimeIngestionLive, ProviderCommandReactorLive).pipe(provide$1(RuntimeDrainLayerLive));
|
|
317382
|
+
const RuntimeAwareProviderReactorsLive = mergeAll(ProviderRuntimeIngestionLive, ProviderCommandReactorLive).pipe(provide$1(RuntimeDrainLayerLive), provide$1(ClaudeUsageTelemetryLive));
|
|
316527
317383
|
const PipelineWorkerCoordinatorLayerLive = PipelineWorkerCoordinatorLive.pipe(provideMerge(PipelineRunClientLive));
|
|
316528
317384
|
const AgentProtocolReactorLayerLive = AgentProtocolReactorLive.pipe(provideMerge(PipelineRunStoreLive));
|
|
316529
317385
|
const PipelineRunCoordinatorLayerLive = PipelineRunCoordinatorLive.pipe(provideMerge(PipelineProviderAuthorityInstallerLive), provideMerge(PipelineChangeInspectorLive), provideMerge(PipelineContextArtifactStoreLive), provideMerge(PipelineModelPolicyLive), provideMerge(PipelineRunClientLive), provideMerge(PipelineRunStoreLive), provideMerge(PipelineRunWorkspaceAllocatorLive), provideMerge(PipelineStagePolicyLive), provideMerge(PipelineWorkerCoordinatorLayerLive));
|