@usepowerplant/cli 0.3.2 → 0.4.0
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/cli.js +432 -104
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -50833,6 +50833,17 @@ function capturedSessionEvents(turns, anchor) {
|
|
|
50833
50833
|
return events;
|
|
50834
50834
|
}
|
|
50835
50835
|
|
|
50836
|
+
// ../core/src/workspaceClaims.ts
|
|
50837
|
+
function workspaceClaimsRepo(repoFullName, policy) {
|
|
50838
|
+
const repo = repoFullName.toLowerCase();
|
|
50839
|
+
if (policy.repos !== null) return policy.repos.includes(repo);
|
|
50840
|
+
const owner = repo.split("/")[0] ?? "";
|
|
50841
|
+
return policy.owners.length > 0 && policy.owners.includes(owner);
|
|
50842
|
+
}
|
|
50843
|
+
function workspaceClaimsAnyRepo(policy) {
|
|
50844
|
+
return policy.repos !== null ? policy.repos.length > 0 : policy.owners.length > 0;
|
|
50845
|
+
}
|
|
50846
|
+
|
|
50836
50847
|
// ../core/src/transcriptImages.ts
|
|
50837
50848
|
var IMAGE_DATA_URI_RE = /^data:(image\/[\w.+-]+);base64,([A-Za-z0-9+/=_-]*)$/;
|
|
50838
50849
|
function isImageDataUri(value) {
|
|
@@ -51693,6 +51704,8 @@ var SESSION_SOURCES = [
|
|
|
51693
51704
|
"cursor-cloud",
|
|
51694
51705
|
"github-pr"
|
|
51695
51706
|
];
|
|
51707
|
+
var SESSION_SPEED_MODES = ["standard", "fast"];
|
|
51708
|
+
var SESSION_SPEED_OBSERVATIONS = [...SESSION_SPEED_MODES, "unknown"];
|
|
51696
51709
|
function isSessionSource(source) {
|
|
51697
51710
|
return typeof source === "string" && SESSION_SOURCES.includes(source);
|
|
51698
51711
|
}
|
|
@@ -78558,6 +78571,23 @@ var storedCredentialsSchema = external_exports.object({
|
|
|
78558
78571
|
* alongside the owners. Absent = the org has no list — the owner check
|
|
78559
78572
|
* decides (and pre-list enrollments read the same way). */
|
|
78560
78573
|
allowedRepos: external_exports.array(external_exports.string()).optional(),
|
|
78574
|
+
/** org-repos mode: per-workspace claim policies across every workspace
|
|
78575
|
+
* the member belongs to (home first), refreshed every sync — the union
|
|
78576
|
+
* an offline pass still filters with. Absent = cached before routing;
|
|
78577
|
+
* the flat fields above are the whole boundary. */
|
|
78578
|
+
workspacePolicies: external_exports.array(
|
|
78579
|
+
external_exports.object({
|
|
78580
|
+
orgId: external_exports.string().min(1),
|
|
78581
|
+
name: external_exports.string(),
|
|
78582
|
+
owners: external_exports.array(external_exports.string()),
|
|
78583
|
+
repos: external_exports.array(external_exports.string()).nullable(),
|
|
78584
|
+
/** This entry's claims were UNRESOLVED when cached (a linked
|
|
78585
|
+
* installation whose account failed to resolve, nothing usable to
|
|
78586
|
+
* stand in) — unknown, never "claims nothing". A pass reading only
|
|
78587
|
+
* the cache must not stamp a deep sweep done over it. */
|
|
78588
|
+
unresolved: external_exports.literal(true).optional()
|
|
78589
|
+
})
|
|
78590
|
+
).optional(),
|
|
78561
78591
|
/** Folders this member explicitly chose. Under scope mode they are the
|
|
78562
78592
|
* whole boundary (never empty — enrollment requires at least one); under
|
|
78563
78593
|
* org-repos they are additions on top of the org policy
|
|
@@ -78652,7 +78682,7 @@ import os6 from "node:os";
|
|
|
78652
78682
|
// ../../apps/collector/src/http.ts
|
|
78653
78683
|
var import_undici = __toESM(require_undici(), 1);
|
|
78654
78684
|
var CLI_VERSION_HEADER = "x-powerplant-cli-version";
|
|
78655
|
-
var cliVersion = true ? "0.
|
|
78685
|
+
var cliVersion = true ? "0.4.0" : "0.0.0-checkout";
|
|
78656
78686
|
function cliVersionHeaders() {
|
|
78657
78687
|
return { [CLI_VERSION_HEADER]: cliVersion, "user-agent": `powerplant-cli/${cliVersion}` };
|
|
78658
78688
|
}
|
|
@@ -78917,9 +78947,10 @@ function captureDecision(cwd, policy) {
|
|
|
78917
78947
|
}
|
|
78918
78948
|
const repoFullName = policy.repoOf(cwd);
|
|
78919
78949
|
if (repoFullName === null) return { capture: false, reason: "no-github-repo" };
|
|
78920
|
-
const
|
|
78921
|
-
|
|
78922
|
-
|
|
78950
|
+
const claims = policy.workspaceClaims ?? [
|
|
78951
|
+
{ owners: policy.allowedOwners, repos: policy.allowedRepos }
|
|
78952
|
+
];
|
|
78953
|
+
return claims.some((workspace) => workspaceClaimsRepo(repoFullName, workspace)) ? { capture: true, via: "org-policy", repoFullName } : { capture: false, reason: "outside-policy" };
|
|
78923
78954
|
}
|
|
78924
78955
|
}
|
|
78925
78956
|
}
|
|
@@ -78931,9 +78962,6 @@ function memberChoice(cwd, policy) {
|
|
|
78931
78962
|
...repoFullName !== null && { repoFullName }
|
|
78932
78963
|
};
|
|
78933
78964
|
}
|
|
78934
|
-
function ownerOfRepo(repoFullName) {
|
|
78935
|
-
return repoFullName.split("/")[0] ?? "";
|
|
78936
|
-
}
|
|
78937
78965
|
function listSome(values, max = 6) {
|
|
78938
78966
|
return values.length <= max ? values.join(", ") : `${values.slice(0, max).join(", ")} and ${values.length - max} more`;
|
|
78939
78967
|
}
|
|
@@ -78943,7 +78971,10 @@ function describeCapturePolicy(policy) {
|
|
|
78943
78971
|
return `sessions under ${policy.scopePaths.join(", ")}`;
|
|
78944
78972
|
case "org-repos": {
|
|
78945
78973
|
const repoPart = policy.allowedRepos !== null ? policy.allowedRepos.length > 0 ? `sessions in the org's allowlisted repos: ${listSome(policy.allowedRepos)}` : "the org's repo allowlist is empty \u2014 no repo-based uploads" : policy.allowedOwners.length > 0 ? `sessions in GitHub repos owned by ${policy.allowedOwners.join(", ")}` : "org-owned GitHub repos \u2014 but the org has no GitHub integration, so nothing uploads";
|
|
78946
|
-
|
|
78974
|
+
const claiming = (policy.workspaceClaims ?? []).filter(workspaceClaimsAnyRepo);
|
|
78975
|
+
const severalWorkspaces = (policy.workspaceClaims?.length ?? 0) > 1;
|
|
78976
|
+
const withOthers = severalWorkspaces && claiming.length > 0 ? `${repoPart}, routing each repo to the workspace that syncs it (${listSome(claiming.map((workspace) => workspace.name))})` : repoPart;
|
|
78977
|
+
return policy.memberFolders.length > 0 ? `${withOthers}, plus folders you added: ${policy.memberFolders.join(", ")}` : withOthers;
|
|
78947
78978
|
}
|
|
78948
78979
|
}
|
|
78949
78980
|
}
|
|
@@ -78959,6 +78990,9 @@ function describeStoredCapture(credentials) {
|
|
|
78959
78990
|
mode: "org-repos",
|
|
78960
78991
|
allowedOwners: credentials.allowedOwners ?? [],
|
|
78961
78992
|
allowedRepos: credentials.allowedRepos ?? null,
|
|
78993
|
+
...credentials.workspacePolicies !== void 0 && {
|
|
78994
|
+
workspaceClaims: credentials.workspacePolicies
|
|
78995
|
+
},
|
|
78962
78996
|
memberFolders: credentials.scopePaths ?? [],
|
|
78963
78997
|
repoOf: () => null
|
|
78964
78998
|
});
|
|
@@ -79083,7 +79117,16 @@ async function fetchCapturePolicy(apiUrl, machineToken) {
|
|
|
79083
79117
|
data: external_exports.object({
|
|
79084
79118
|
owners: external_exports.array(external_exports.string()),
|
|
79085
79119
|
repos: external_exports.array(external_exports.string()).nullable(),
|
|
79086
|
-
integration: external_exports.enum(["none", "linked"])
|
|
79120
|
+
integration: external_exports.enum(["none", "linked"]),
|
|
79121
|
+
workspaces: external_exports.array(
|
|
79122
|
+
external_exports.object({
|
|
79123
|
+
orgId: external_exports.string().min(1),
|
|
79124
|
+
name: external_exports.string(),
|
|
79125
|
+
owners: external_exports.array(external_exports.string()),
|
|
79126
|
+
repos: external_exports.array(external_exports.string()).nullable(),
|
|
79127
|
+
integration: external_exports.enum(["none", "linked"])
|
|
79128
|
+
})
|
|
79129
|
+
).optional()
|
|
79087
79130
|
})
|
|
79088
79131
|
}),
|
|
79089
79132
|
await response.json().catch(() => null),
|
|
@@ -79095,6 +79138,7 @@ async function uploadSessions(apiUrl, machineToken, items) {
|
|
|
79095
79138
|
let upserted = 0;
|
|
79096
79139
|
let unchanged = 0;
|
|
79097
79140
|
let outOfPolicy = 0;
|
|
79141
|
+
let deferred = 0;
|
|
79098
79142
|
for (let offset = 0; offset < items.length; offset += MAX_INGEST_BATCH) {
|
|
79099
79143
|
const batch = items.slice(offset, offset + MAX_INGEST_BATCH);
|
|
79100
79144
|
const response = await fetchWithRetry("uploading session metadata", `${apiUrl}/api/ingest/sessions`, {
|
|
@@ -79113,7 +79157,8 @@ async function uploadSessions(apiUrl, machineToken, items) {
|
|
|
79113
79157
|
data: external_exports.object({
|
|
79114
79158
|
upserted: external_exports.number(),
|
|
79115
79159
|
unchanged: external_exports.number(),
|
|
79116
|
-
outOfPolicy: external_exports.number()
|
|
79160
|
+
outOfPolicy: external_exports.number(),
|
|
79161
|
+
deferred: external_exports.number().optional()
|
|
79117
79162
|
})
|
|
79118
79163
|
}),
|
|
79119
79164
|
await response.json().catch(() => null),
|
|
@@ -79122,8 +79167,9 @@ async function uploadSessions(apiUrl, machineToken, items) {
|
|
|
79122
79167
|
upserted += result.data.upserted;
|
|
79123
79168
|
unchanged += result.data.unchanged;
|
|
79124
79169
|
outOfPolicy += result.data.outOfPolicy;
|
|
79170
|
+
deferred += result.data.deferred ?? 0;
|
|
79125
79171
|
}
|
|
79126
|
-
return { upserted, unchanged, outOfPolicy };
|
|
79172
|
+
return { upserted, unchanged, outOfPolicy, deferred };
|
|
79127
79173
|
}
|
|
79128
79174
|
async function uploadBranchFacts(apiUrl, machineToken, facts) {
|
|
79129
79175
|
let recorded = 0;
|
|
@@ -79173,6 +79219,7 @@ async function uploadRawSessions(apiUrl, machineToken, collected, readTranscript
|
|
|
79173
79219
|
uploaded: 0,
|
|
79174
79220
|
skippedTooLarge: 0,
|
|
79175
79221
|
skippedUnchanged: 0,
|
|
79222
|
+
missingMetadata: 0,
|
|
79176
79223
|
failed: [],
|
|
79177
79224
|
unconfigured: false
|
|
79178
79225
|
};
|
|
@@ -79205,6 +79252,10 @@ async function uploadRawSessions(apiUrl, machineToken, collected, readTranscript
|
|
|
79205
79252
|
summary.unconfigured = true;
|
|
79206
79253
|
return summary;
|
|
79207
79254
|
}
|
|
79255
|
+
if (response.status === 404) {
|
|
79256
|
+
summary.missingMetadata += 1;
|
|
79257
|
+
continue;
|
|
79258
|
+
}
|
|
79208
79259
|
if (!response.ok) {
|
|
79209
79260
|
throw new Error(`${response.status}: ${await errorDetail(response)}`);
|
|
79210
79261
|
}
|
|
@@ -79288,7 +79339,8 @@ async function runEnrollment(config2, options) {
|
|
|
79288
79339
|
log: log3
|
|
79289
79340
|
});
|
|
79290
79341
|
let orgPolicy = captureMode === "org-repos" ? await fetchCapturePolicy(config2.apiUrl, result.token) : void 0;
|
|
79291
|
-
|
|
79342
|
+
const anyWorkspaceClaims = (orgPolicy?.workspaces ?? []).some(workspaceClaimsAnyRepo);
|
|
79343
|
+
if (orgPolicy !== void 0 && orgPolicy.owners.length === 0 && orgPolicy.repos === null && !anyWorkspaceClaims) {
|
|
79292
79344
|
if (config2.appEnv === "development") {
|
|
79293
79345
|
const home = normalizeScopePath(os6.homedir());
|
|
79294
79346
|
log3(
|
|
@@ -79320,10 +79372,20 @@ async function runEnrollment(config2, options) {
|
|
|
79320
79372
|
);
|
|
79321
79373
|
}
|
|
79322
79374
|
}
|
|
79375
|
+
const workspacePolicies = orgPolicy?.workspaces?.map((workspace) => ({
|
|
79376
|
+
orgId: workspace.orgId,
|
|
79377
|
+
name: workspace.name,
|
|
79378
|
+
owners: workspace.owners,
|
|
79379
|
+
repos: workspace.repos,
|
|
79380
|
+
// Cached unknownness survives enrollment: an unresolved workspace's
|
|
79381
|
+
// claims are unknown, and the first sweep must not stamp over them.
|
|
79382
|
+
...workspace.integration === "linked" && workspace.owners.length === 0 && workspace.repos === null && { unresolved: true }
|
|
79383
|
+
}));
|
|
79323
79384
|
const policy = captureMode === "org-repos" ? {
|
|
79324
79385
|
mode: "org-repos",
|
|
79325
79386
|
allowedOwners: orgPolicy?.owners ?? [],
|
|
79326
79387
|
allowedRepos: orgPolicy?.repos ?? null,
|
|
79388
|
+
...workspacePolicies !== void 0 && { workspaceClaims: workspacePolicies },
|
|
79327
79389
|
memberFolders: [],
|
|
79328
79390
|
repoOf: () => null
|
|
79329
79391
|
} : { mode: "scope", scopePaths, repoOf: () => null };
|
|
@@ -79338,6 +79400,7 @@ async function runEnrollment(config2, options) {
|
|
|
79338
79400
|
captureMode,
|
|
79339
79401
|
...orgPolicy && { allowedOwners: orgPolicy.owners },
|
|
79340
79402
|
...orgPolicy?.repos != null && { allowedRepos: orgPolicy.repos },
|
|
79403
|
+
...captureMode === "org-repos" && workspacePolicies !== void 0 && { workspacePolicies },
|
|
79341
79404
|
scopePaths
|
|
79342
79405
|
};
|
|
79343
79406
|
const location = saveCredentials(config2.appEnv, credentials);
|
|
@@ -79705,6 +79768,7 @@ function parseClaudeCodeTranscript(text) {
|
|
|
79705
79768
|
let cwd;
|
|
79706
79769
|
let gitBranch;
|
|
79707
79770
|
let anonCounter = 0;
|
|
79771
|
+
let speedMode;
|
|
79708
79772
|
for (const record2 of parseJsonl(text)) {
|
|
79709
79773
|
if (record2.type === "custom-title" && typeof record2.customTitle === "string") {
|
|
79710
79774
|
title = record2.customTitle;
|
|
@@ -79723,25 +79787,42 @@ function parseClaudeCodeTranscript(text) {
|
|
|
79723
79787
|
const message = record2.message;
|
|
79724
79788
|
if (!message || message.model === "<synthetic>") continue;
|
|
79725
79789
|
const id = message.id ?? (typeof record2.requestId === "string" ? record2.requestId : void 0) ?? (typeof record2.uuid === "string" ? record2.uuid : void 0) ?? `anon-${anonCounter++}`;
|
|
79726
|
-
const
|
|
79727
|
-
|
|
79728
|
-
|
|
79790
|
+
const previousMessage = usageByMessage.get(id);
|
|
79791
|
+
const isNewMessage = previousMessage === void 0;
|
|
79792
|
+
const speedValue = message.usage?.speed;
|
|
79793
|
+
const observedSpeed = speedModeOf(speedValue);
|
|
79794
|
+
const messageSpeed = observedSpeed ?? previousMessage?.speedMode;
|
|
79795
|
+
if (speedValue !== void 0) speedMode = observedSpeed ?? "unknown";
|
|
79796
|
+
if (message.usage) {
|
|
79797
|
+
usageByMessage.set(id, {
|
|
79798
|
+
usage: toUsage(message.usage),
|
|
79799
|
+
...messageSpeed && { speedMode: messageSpeed }
|
|
79800
|
+
});
|
|
79801
|
+
} else if (isNewMessage) {
|
|
79802
|
+
usageByMessage.set(id, {
|
|
79803
|
+
usage: { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 }
|
|
79804
|
+
});
|
|
79805
|
+
}
|
|
79729
79806
|
if (isNewMessage && typeof message.model === "string") {
|
|
79730
79807
|
modelCounts.set(message.model, (modelCounts.get(message.model) ?? 0) + 1);
|
|
79731
79808
|
}
|
|
79732
79809
|
}
|
|
79733
79810
|
if (usageByMessage.size === 0 && timestamps.length === 0) return null;
|
|
79734
|
-
const summed = [...usageByMessage.values()].reduce(addUsage, {
|
|
79811
|
+
const summed = [...usageByMessage.values()].map((entry) => entry.usage).reduce(addUsage, {
|
|
79735
79812
|
input: 0,
|
|
79736
79813
|
cacheRead: 0,
|
|
79737
79814
|
cacheCreation: 0,
|
|
79738
79815
|
output: 0
|
|
79739
79816
|
});
|
|
79817
|
+
const speedFullyMeasured = usageByMessage.size > 0 && [...usageByMessage.values()].every((entry) => entry.speedMode !== void 0);
|
|
79818
|
+
const fastUsage = speedFullyMeasured ? [...usageByMessage.values()].filter((entry) => entry.speedMode === "fast").map((entry) => entry.usage).reduce(addUsage, { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 }) : null;
|
|
79740
79819
|
return {
|
|
79741
79820
|
startedAt: timestamps[0],
|
|
79742
79821
|
endedAt: timestamps[timestamps.length - 1],
|
|
79743
79822
|
model: dominantModel(modelCounts),
|
|
79744
79823
|
usage: usageByMessage.size > 0 ? summed : null,
|
|
79824
|
+
fastUsage,
|
|
79825
|
+
...speedMode && { speedMode },
|
|
79745
79826
|
title,
|
|
79746
79827
|
turnCount: usageByMessage.size,
|
|
79747
79828
|
cwd,
|
|
@@ -79752,9 +79833,13 @@ async function collectClaudeCodeSessions(options = {}) {
|
|
|
79752
79833
|
const claudeDir = options.claudeDir ?? path5.join(os7.homedir(), ".claude");
|
|
79753
79834
|
const now = options.now ?? Date.now();
|
|
79754
79835
|
const capture = options.capture ?? captureEverythingUnresolved;
|
|
79836
|
+
const maxBytes = options.maxTranscriptBytes ?? MAX_TRANSCRIPT_FILE_BYTES;
|
|
79755
79837
|
const projectsDir = path5.join(claudeDir, "projects");
|
|
79756
79838
|
const items = [];
|
|
79757
79839
|
const skips = { skippedOutOfScope: 0, skippedNoRepo: 0 };
|
|
79840
|
+
const candidates = [];
|
|
79841
|
+
const unreadable = [];
|
|
79842
|
+
let skippedTooLarge = 0;
|
|
79758
79843
|
const home = path5.dirname(claudeDir);
|
|
79759
79844
|
const lifecycle = options.lifecycle ?? readLifecycleJournal(home);
|
|
79760
79845
|
for (const projectDir of listDirs(projectsDir)) {
|
|
@@ -79764,39 +79849,76 @@ async function collectClaudeCodeSessions(options = {}) {
|
|
|
79764
79849
|
const stat = statIfExists(transcriptPath);
|
|
79765
79850
|
if (!stat) continue;
|
|
79766
79851
|
if (options.sinceMs !== void 0 && stat.mtimeMs < options.sinceMs) continue;
|
|
79852
|
+
if (stat.size > maxBytes) {
|
|
79853
|
+
skippedTooLarge += 1;
|
|
79854
|
+
continue;
|
|
79855
|
+
}
|
|
79767
79856
|
const sessionId = path5.basename(entry, ".jsonl");
|
|
79768
79857
|
const endObservedAt = observedSessionEnd(lifecycle.get(sessionId), stat.mtimeMs);
|
|
79769
79858
|
const status = endObservedAt === null && now - stat.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive";
|
|
79770
|
-
|
|
79859
|
+
try {
|
|
79771
79860
|
const cwd = transcriptCwd(transcriptPath);
|
|
79772
|
-
const
|
|
79773
|
-
if (!
|
|
79774
|
-
countCaptureSkip(skips,
|
|
79861
|
+
const decision = captureDecision(cwd, capture);
|
|
79862
|
+
if (!decision.capture) {
|
|
79863
|
+
countCaptureSkip(skips, decision.reason);
|
|
79775
79864
|
continue;
|
|
79776
79865
|
}
|
|
79777
|
-
|
|
79778
|
-
|
|
79866
|
+
if (options.locationsOnly) {
|
|
79867
|
+
items.push({
|
|
79868
|
+
mtimeMs: stat.mtimeMs,
|
|
79869
|
+
transcriptPath,
|
|
79870
|
+
...cwd && { cwd },
|
|
79871
|
+
item: { source: "claude-code", externalId: sessionId, status, capturedVia: decision.via }
|
|
79872
|
+
});
|
|
79873
|
+
continue;
|
|
79874
|
+
}
|
|
79875
|
+
candidates.push({
|
|
79779
79876
|
transcriptPath,
|
|
79877
|
+
projectDir,
|
|
79878
|
+
sessionId,
|
|
79879
|
+
mtimeMs: stat.mtimeMs,
|
|
79880
|
+
status,
|
|
79881
|
+
endObservedAt,
|
|
79780
79882
|
...cwd && { cwd },
|
|
79781
|
-
|
|
79883
|
+
decision
|
|
79782
79884
|
});
|
|
79783
|
-
|
|
79885
|
+
} catch (error52) {
|
|
79886
|
+
unreadable.push(`${sessionId}: ${describeReadError(error52)}`);
|
|
79784
79887
|
}
|
|
79888
|
+
}
|
|
79889
|
+
}
|
|
79890
|
+
if (options.locationsOnly) {
|
|
79891
|
+
return {
|
|
79892
|
+
sessions: items.sort((a, b2) => b2.mtimeMs - a.mtimeMs),
|
|
79893
|
+
...skips,
|
|
79894
|
+
skippedTooLarge,
|
|
79895
|
+
unreadable,
|
|
79896
|
+
heldBack: 0
|
|
79897
|
+
};
|
|
79898
|
+
}
|
|
79899
|
+
candidates.sort((a, b2) => b2.mtimeMs - a.mtimeMs);
|
|
79900
|
+
const chosen = options.maxSessions === void 0 ? candidates : candidates.slice(0, options.maxSessions);
|
|
79901
|
+
for (const candidate of chosen) {
|
|
79902
|
+
await yieldToEventLoop();
|
|
79903
|
+
const { transcriptPath, projectDir, sessionId, mtimeMs, status, endObservedAt, cwd, decision } = candidate;
|
|
79904
|
+
try {
|
|
79785
79905
|
const text = readIfExists2(transcriptPath);
|
|
79786
79906
|
if (text === null) continue;
|
|
79787
79907
|
const summary = parseClaudeCodeTranscript(text);
|
|
79788
79908
|
if (!summary) continue;
|
|
79789
|
-
const
|
|
79790
|
-
|
|
79791
|
-
|
|
79792
|
-
|
|
79793
|
-
|
|
79794
|
-
|
|
79909
|
+
const usage = addSubagentUsage(
|
|
79910
|
+
summary.usage,
|
|
79911
|
+
summary.fastUsage,
|
|
79912
|
+
path5.join(projectDir, sessionId, "subagents"),
|
|
79913
|
+
{ maxBytes, sessionId }
|
|
79914
|
+
);
|
|
79915
|
+
skippedTooLarge += usage.skippedTooLarge;
|
|
79916
|
+
unreadable.push(...usage.unreadable);
|
|
79795
79917
|
const endedAt = endObservedAt ?? summary.endedAt;
|
|
79796
79918
|
items.push({
|
|
79797
|
-
mtimeMs
|
|
79919
|
+
mtimeMs,
|
|
79798
79920
|
transcriptPath,
|
|
79799
|
-
...
|
|
79921
|
+
...cwd && { cwd },
|
|
79800
79922
|
item: {
|
|
79801
79923
|
source: "claude-code",
|
|
79802
79924
|
externalId: sessionId,
|
|
@@ -79804,28 +79926,42 @@ async function collectClaudeCodeSessions(options = {}) {
|
|
|
79804
79926
|
...summary.startedAt && { startedAt: summary.startedAt },
|
|
79805
79927
|
...endedAt && { endedAt },
|
|
79806
79928
|
...summary.model && { model: summary.model },
|
|
79929
|
+
...summary.speedMode && { speedMode: summary.speedMode },
|
|
79807
79930
|
// Total input-class tokens the source reported (uncached + cache
|
|
79808
79931
|
// reads + cache writes) — a real reported total, not an estimate.
|
|
79809
79932
|
// The cache breakdown rides along: pricing bills reads at 0.1×
|
|
79810
79933
|
// and writes at 1.25×, so collapsing it would inflate estimates.
|
|
79811
|
-
...usage && {
|
|
79812
|
-
|
|
79813
|
-
|
|
79814
|
-
...usage && {
|
|
79934
|
+
...usage.total && {
|
|
79935
|
+
inputTokens: usage.total.input + usage.total.cacheRead + usage.total.cacheCreation
|
|
79936
|
+
},
|
|
79937
|
+
...usage.total && { outputTokens: usage.total.output },
|
|
79938
|
+
...usage.total && { cacheReadTokens: usage.total.cacheRead },
|
|
79939
|
+
...usage.total && { cacheWriteTokens: usage.total.cacheCreation },
|
|
79940
|
+
...usage.fast && {
|
|
79941
|
+
fastInputTokens: usage.fast.input + usage.fast.cacheRead + usage.fast.cacheCreation,
|
|
79942
|
+
fastOutputTokens: usage.fast.output,
|
|
79943
|
+
fastCacheReadTokens: usage.fast.cacheRead,
|
|
79944
|
+
fastCacheWriteTokens: usage.fast.cacheCreation
|
|
79945
|
+
},
|
|
79815
79946
|
...summary.title && { title: redactText(summary.title).slice(0, 500) },
|
|
79816
79947
|
// The PR-join coordinates: the checkout's directory name (bare
|
|
79817
79948
|
// repo, not the local path) and the branch the work started on.
|
|
79818
|
-
...
|
|
79949
|
+
...cwd && { repoDir: path5.basename(cwd) },
|
|
79819
79950
|
...summary.gitBranch && { gitBranch: summary.gitBranch },
|
|
79820
79951
|
...decision.repoFullName && { repoFullName: decision.repoFullName },
|
|
79821
79952
|
capturedVia: decision.via
|
|
79822
79953
|
}
|
|
79823
79954
|
});
|
|
79955
|
+
} catch (error52) {
|
|
79956
|
+
unreadable.push(`${sessionId}: ${describeReadError(error52)}`);
|
|
79824
79957
|
}
|
|
79825
79958
|
}
|
|
79826
79959
|
return {
|
|
79827
79960
|
sessions: items.sort((a, b2) => b2.mtimeMs - a.mtimeMs),
|
|
79828
|
-
...skips
|
|
79961
|
+
...skips,
|
|
79962
|
+
skippedTooLarge,
|
|
79963
|
+
unreadable,
|
|
79964
|
+
heldBack: candidates.length - chosen.length
|
|
79829
79965
|
};
|
|
79830
79966
|
}
|
|
79831
79967
|
function transcriptCwd(transcriptPath) {
|
|
@@ -79844,6 +79980,9 @@ function toUsage(u) {
|
|
|
79844
79980
|
output: n(u.output_tokens)
|
|
79845
79981
|
};
|
|
79846
79982
|
}
|
|
79983
|
+
function speedModeOf(value) {
|
|
79984
|
+
return value === "fast" || value === "standard" ? value : void 0;
|
|
79985
|
+
}
|
|
79847
79986
|
function addUsage(a, b2) {
|
|
79848
79987
|
return {
|
|
79849
79988
|
input: a.input + b2.input,
|
|
@@ -79852,15 +79991,36 @@ function addUsage(a, b2) {
|
|
|
79852
79991
|
output: a.output + b2.output
|
|
79853
79992
|
};
|
|
79854
79993
|
}
|
|
79855
|
-
function addSubagentUsage(main2, subagentsDir) {
|
|
79856
|
-
let
|
|
79994
|
+
function addSubagentUsage(main2, mainFast, subagentsDir, bounds) {
|
|
79995
|
+
let total = main2;
|
|
79996
|
+
let fast = mainFast;
|
|
79997
|
+
let fastCoverageComplete = main2 === null || mainFast !== null;
|
|
79998
|
+
let skippedTooLarge = 0;
|
|
79999
|
+
const unreadable = [];
|
|
79857
80000
|
for (const entry of listFiles(subagentsDir, ".jsonl")) {
|
|
79858
|
-
const
|
|
79859
|
-
|
|
79860
|
-
|
|
79861
|
-
|
|
80001
|
+
const sidecarPath = path5.join(subagentsDir, entry);
|
|
80002
|
+
try {
|
|
80003
|
+
const stat = statIfExists(sidecarPath);
|
|
80004
|
+
if (stat === null) continue;
|
|
80005
|
+
if (stat.size > bounds.maxBytes) {
|
|
80006
|
+
skippedTooLarge += 1;
|
|
80007
|
+
continue;
|
|
80008
|
+
}
|
|
80009
|
+
const text = readIfExists2(sidecarPath);
|
|
80010
|
+
if (text === null) continue;
|
|
80011
|
+
const summary = parseClaudeCodeTranscript(text);
|
|
80012
|
+
if (summary?.usage) {
|
|
80013
|
+
total = addUsage(total ?? { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 }, summary.usage);
|
|
80014
|
+
if (summary.fastUsage === null) fastCoverageComplete = false;
|
|
80015
|
+
}
|
|
80016
|
+
if (summary?.fastUsage) {
|
|
80017
|
+
fast = addUsage(fast ?? { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 }, summary.fastUsage);
|
|
80018
|
+
}
|
|
80019
|
+
} catch (error52) {
|
|
80020
|
+
unreadable.push(`${bounds.sessionId} (subagent ${entry}): ${describeReadError(error52)}`);
|
|
80021
|
+
}
|
|
79862
80022
|
}
|
|
79863
|
-
return
|
|
80023
|
+
return { total, fast: fastCoverageComplete ? fast : null, skippedTooLarge, unreadable };
|
|
79864
80024
|
}
|
|
79865
80025
|
function dominantModel(counts) {
|
|
79866
80026
|
let best;
|
|
@@ -79879,6 +80039,10 @@ function parseCodexRollout(text) {
|
|
|
79879
80039
|
let meta3 = null;
|
|
79880
80040
|
let model;
|
|
79881
80041
|
let totals = null;
|
|
80042
|
+
let fastTotals = { input: 0, cached: 0, output: 0 };
|
|
80043
|
+
let speedMode;
|
|
80044
|
+
let fastCoverageComplete = true;
|
|
80045
|
+
let classifiedUsage = false;
|
|
79882
80046
|
const timestamps = [];
|
|
79883
80047
|
for (const line of parseJsonl(text)) {
|
|
79884
80048
|
if (typeof line.timestamp === "string") timestamps.push(line.timestamp);
|
|
@@ -79889,15 +80053,38 @@ function parseCodexRollout(text) {
|
|
|
79889
80053
|
meta3 ??= record2;
|
|
79890
80054
|
} else if (line.type === "turn_context" && typeof record2.model === "string") {
|
|
79891
80055
|
model = record2.model;
|
|
80056
|
+
} else if (line.type === "event_msg" && record2.type === "thread_settings_applied") {
|
|
80057
|
+
const settings = record2.thread_settings;
|
|
80058
|
+
if (settings && typeof settings === "object" && !Array.isArray(settings)) {
|
|
80059
|
+
const tier = settings.service_tier;
|
|
80060
|
+
if (tier !== void 0) speedMode = codexSpeedMode(tier) ?? "unknown";
|
|
80061
|
+
}
|
|
79892
80062
|
} else if (line.type === "event_msg" && record2.type === "token_count") {
|
|
79893
80063
|
const info = record2.info;
|
|
79894
80064
|
const usage = info?.total_token_usage;
|
|
79895
80065
|
if (usage) {
|
|
79896
80066
|
const n = (value) => typeof value === "number" ? value : 0;
|
|
79897
|
-
|
|
79898
|
-
|
|
79899
|
-
|
|
79900
|
-
|
|
80067
|
+
const next = {
|
|
80068
|
+
input: n(usage.input_tokens),
|
|
80069
|
+
cached: n(usage.cached_input_tokens),
|
|
80070
|
+
output: n(usage.output_tokens)
|
|
80071
|
+
};
|
|
80072
|
+
const previous = totals ?? { input: 0, cached: 0, output: 0 };
|
|
80073
|
+
const delta = {
|
|
80074
|
+
input: Math.max(0, next.input - previous.input),
|
|
80075
|
+
cached: Math.max(0, next.cached - previous.cached),
|
|
80076
|
+
output: Math.max(0, next.output - previous.output)
|
|
80077
|
+
};
|
|
80078
|
+
if (delta.input > 0 || delta.cached > 0 || delta.output > 0) {
|
|
80079
|
+
if (speedMode === "fast" || speedMode === "standard") classifiedUsage = true;
|
|
80080
|
+
else fastCoverageComplete = false;
|
|
80081
|
+
if (speedMode === "fast") fastTotals = addCodexTotals(fastTotals, delta);
|
|
80082
|
+
}
|
|
80083
|
+
totals = {
|
|
80084
|
+
input: Math.max(previous.input, next.input),
|
|
80085
|
+
cached: Math.max(previous.cached, next.cached),
|
|
80086
|
+
output: Math.max(previous.output, next.output)
|
|
80087
|
+
};
|
|
79901
80088
|
}
|
|
79902
80089
|
}
|
|
79903
80090
|
}
|
|
@@ -79911,6 +80098,8 @@ function parseCodexRollout(text) {
|
|
|
79911
80098
|
endedAt: timestamps[timestamps.length - 1],
|
|
79912
80099
|
...model && { model },
|
|
79913
80100
|
totals,
|
|
80101
|
+
fastTotals: fastCoverageComplete && classifiedUsage ? fastTotals : null,
|
|
80102
|
+
...speedMode && { speedMode },
|
|
79914
80103
|
...typeof meta3?.cwd === "string" && meta3.cwd !== "" && { cwd: meta3.cwd },
|
|
79915
80104
|
...typeof git?.branch === "string" && git.branch !== "" && { gitBranch: git.branch },
|
|
79916
80105
|
...repoNameFromUrl(git?.repository_url) ?? {},
|
|
@@ -79924,14 +80113,23 @@ function parseRolloutFile(rolloutPath) {
|
|
|
79924
80113
|
function rolloutLocation(rolloutPath) {
|
|
79925
80114
|
const record2 = readFirstRecord(rolloutPath, (candidate) => candidate.type === "session_meta");
|
|
79926
80115
|
const meta3 = record2?.payload;
|
|
79927
|
-
if (!meta3 || typeof meta3 !== "object" || Array.isArray(meta3)) return { kind: "session", totals: null };
|
|
80116
|
+
if (!meta3 || typeof meta3 !== "object" || Array.isArray(meta3)) return { kind: "session", totals: null, fastTotals: null };
|
|
79928
80117
|
const { thread_source: threadSource, cwd } = meta3;
|
|
79929
80118
|
return {
|
|
79930
80119
|
kind: threadSource === "subagent" ? "subagent" : "session",
|
|
79931
80120
|
totals: null,
|
|
80121
|
+
fastTotals: null,
|
|
79932
80122
|
...typeof cwd === "string" && cwd !== "" && { cwd }
|
|
79933
80123
|
};
|
|
79934
80124
|
}
|
|
80125
|
+
function codexSpeedMode(value) {
|
|
80126
|
+
if (value === "priority" || value === "fast") return "fast";
|
|
80127
|
+
if (value === "default") return "standard";
|
|
80128
|
+
return void 0;
|
|
80129
|
+
}
|
|
80130
|
+
function addCodexTotals(a, b2) {
|
|
80131
|
+
return { input: a.input + b2.input, cached: a.cached + b2.cached, output: a.output + b2.output };
|
|
80132
|
+
}
|
|
79935
80133
|
function repoFullNameFrom(url2) {
|
|
79936
80134
|
const fullName = parseGithubRepo(url2);
|
|
79937
80135
|
return fullName ? { repoFullName: fullName } : {};
|
|
@@ -80062,6 +80260,7 @@ async function collectCodexSessions(options = {}) {
|
|
|
80062
80260
|
...summary.startedAt && { startedAt: summary.startedAt },
|
|
80063
80261
|
...summary.endedAt && { endedAt: summary.endedAt },
|
|
80064
80262
|
...summary.model && { model: summary.model },
|
|
80263
|
+
...summary.speedMode && { speedMode: summary.speedMode },
|
|
80065
80264
|
...title && { title: redactText(title).slice(0, 500) },
|
|
80066
80265
|
// input_tokens already includes the cached subset — it IS the
|
|
80067
80266
|
// all-in input-class total. No cache-write figure exists, so
|
|
@@ -80069,6 +80268,12 @@ async function collectCodexSessions(options = {}) {
|
|
|
80069
80268
|
...summary.totals && { inputTokens: summary.totals.input },
|
|
80070
80269
|
...summary.totals && { outputTokens: summary.totals.output },
|
|
80071
80270
|
...summary.totals && { cacheReadTokens: summary.totals.cached },
|
|
80271
|
+
...summary.fastTotals && { fastInputTokens: summary.fastTotals.input },
|
|
80272
|
+
...summary.fastTotals && { fastOutputTokens: summary.fastTotals.output },
|
|
80273
|
+
...summary.fastTotals && { fastCacheReadTokens: summary.fastTotals.cached },
|
|
80274
|
+
// OpenAI prompt-cache writes have no separate price in the current
|
|
80275
|
+
// session cost model; zero is measured here, not an invented count.
|
|
80276
|
+
...summary.fastTotals && { fastCacheWriteTokens: 0 },
|
|
80072
80277
|
...repoDir && { repoDir },
|
|
80073
80278
|
...summary.gitBranch && { gitBranch: summary.gitBranch },
|
|
80074
80279
|
...repoFullName && { repoFullName },
|
|
@@ -80117,6 +80322,8 @@ var emptyResult = () => ({
|
|
|
80117
80322
|
skippedSubagentComposers: 0,
|
|
80118
80323
|
skippedNonLocalComposers: 0,
|
|
80119
80324
|
skippedUnreadable: 0,
|
|
80325
|
+
skippedTooLarge: 0,
|
|
80326
|
+
truncatedTranscripts: 0,
|
|
80120
80327
|
unavailable: null
|
|
80121
80328
|
});
|
|
80122
80329
|
var SQLITE_EXPERIMENTAL_WARNING = "SQLite is an experimental feature and might change at any time";
|
|
@@ -80144,9 +80351,6 @@ async function openCursorDatabase(dbPath) {
|
|
|
80144
80351
|
const sqlite = await loadSqlite();
|
|
80145
80352
|
return new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
80146
80353
|
}
|
|
80147
|
-
function describeError(error52) {
|
|
80148
|
-
return error52 instanceof Error ? error52.message : String(error52);
|
|
80149
|
-
}
|
|
80150
80354
|
function isRecord3(value) {
|
|
80151
80355
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
80152
80356
|
}
|
|
@@ -80221,6 +80425,8 @@ function toolFilePathOf(args) {
|
|
|
80221
80425
|
}
|
|
80222
80426
|
return void 0;
|
|
80223
80427
|
}
|
|
80428
|
+
var SPOOL_META_RESERVE_BYTES = 64 * 1024;
|
|
80429
|
+
var lineBytes = (line) => Buffer.byteLength(JSON.stringify(line)) + 1;
|
|
80224
80430
|
function bubbleLines(bubble) {
|
|
80225
80431
|
const lines = [];
|
|
80226
80432
|
const createdAt = typeof bubble.createdAt === "string" ? { createdAt: bubble.createdAt } : {};
|
|
@@ -80260,43 +80466,50 @@ function bubbleLines(bubble) {
|
|
|
80260
80466
|
}
|
|
80261
80467
|
return lines;
|
|
80262
80468
|
}
|
|
80263
|
-
function extractTranscript(db, composerId) {
|
|
80469
|
+
function extractTranscript(db, composerId, maxBytes) {
|
|
80264
80470
|
const valueByKey = db.prepare("select value from cursorDiskKV where key = ?");
|
|
80265
80471
|
const dataRow = valueByKey.get(`composerData:${composerId}`);
|
|
80266
80472
|
const dataText = isRecord3(dataRow) && typeof dataRow.value === "string" ? dataRow.value : null;
|
|
80267
80473
|
const data = dataText === null ? null : safeJsonRecord2(dataText);
|
|
80268
80474
|
const spine = Array.isArray(data?.fullConversationHeadersOnly) ? data.fullConversationHeadersOnly : [];
|
|
80269
80475
|
if (spine.length === 0) return null;
|
|
80476
|
+
const budget = maxBytes - SPOOL_META_RESERVE_BYTES;
|
|
80270
80477
|
const lines = [];
|
|
80271
80478
|
const modelCounts = /* @__PURE__ */ new Map();
|
|
80479
|
+
let bytes = 0;
|
|
80272
80480
|
let bubbleCount = 0;
|
|
80273
|
-
|
|
80481
|
+
let bubblesOmitted = 0;
|
|
80482
|
+
for (const [index, entry] of spine.entries()) {
|
|
80274
80483
|
if (!isRecord3(entry) || typeof entry.bubbleId !== "string") continue;
|
|
80275
80484
|
const row = valueByKey.get(`bubbleId:${composerId}:${entry.bubbleId}`);
|
|
80276
80485
|
const text = isRecord3(row) && typeof row.value === "string" ? row.value : null;
|
|
80277
80486
|
const bubble = text === null ? null : safeJsonRecord2(text);
|
|
80278
80487
|
if (bubble === null) continue;
|
|
80488
|
+
const bubbleLinesOut = bubbleLines(bubble);
|
|
80489
|
+
const bubbleBytes = bubbleLinesOut.reduce((sum2, line) => sum2 + lineBytes(line), 0);
|
|
80490
|
+
if (bytes + bubbleBytes > budget) {
|
|
80491
|
+
bubblesOmitted = spine.length - index;
|
|
80492
|
+
break;
|
|
80493
|
+
}
|
|
80494
|
+
bytes += bubbleBytes;
|
|
80279
80495
|
bubbleCount += 1;
|
|
80280
|
-
for (const line of
|
|
80496
|
+
for (const line of bubbleLinesOut) {
|
|
80281
80497
|
lines.push(line);
|
|
80282
80498
|
if (line.type === "agent" && line.model !== void 0) {
|
|
80283
80499
|
modelCounts.set(line.model, (modelCounts.get(line.model) ?? 0) + 1);
|
|
80284
80500
|
}
|
|
80285
80501
|
}
|
|
80286
80502
|
}
|
|
80287
|
-
if (bubbleCount === 0) return null;
|
|
80503
|
+
if (bubbleCount === 0 && bubblesOmitted === 0) return null;
|
|
80288
80504
|
let model;
|
|
80289
80505
|
let best = 0;
|
|
80290
80506
|
for (const [name, count] of modelCounts) {
|
|
80291
80507
|
if (count > best) [model, best] = [name, count];
|
|
80292
80508
|
}
|
|
80293
|
-
return { lines, ...model && { model }, bubbleCount };
|
|
80509
|
+
return { lines, ...model && { model }, bubbleCount, bubblesOmitted };
|
|
80294
80510
|
}
|
|
80295
80511
|
function readSpoolModel(spoolPath) {
|
|
80296
|
-
const
|
|
80297
|
-
if (text === null) return void 0;
|
|
80298
|
-
const newline = text.indexOf("\n");
|
|
80299
|
-
const record2 = safeJsonRecord2(newline === -1 ? text : text.slice(0, newline));
|
|
80512
|
+
const record2 = readFirstRecord(spoolPath, () => true);
|
|
80300
80513
|
if (record2 === null || record2.type !== "meta" || record2.format !== "cursor-local-transcript") {
|
|
80301
80514
|
return void 0;
|
|
80302
80515
|
}
|
|
@@ -80309,20 +80522,20 @@ async function collectCursorSessions(options = {}) {
|
|
|
80309
80522
|
try {
|
|
80310
80523
|
if (statIfExists(dbPath) === null) return result;
|
|
80311
80524
|
} catch (error52) {
|
|
80312
|
-
result.unavailable = `Cursor's session store isn't readable (${
|
|
80525
|
+
result.unavailable = `Cursor's session store isn't readable (${describeReadError(error52)})`;
|
|
80313
80526
|
return result;
|
|
80314
80527
|
}
|
|
80315
80528
|
let db;
|
|
80316
80529
|
try {
|
|
80317
80530
|
db = await openCursorDatabase(dbPath);
|
|
80318
80531
|
} catch (error52) {
|
|
80319
|
-
result.unavailable = `Cursor's session store couldn't be opened (${
|
|
80532
|
+
result.unavailable = `Cursor's session store couldn't be opened (${describeReadError(error52)})`;
|
|
80320
80533
|
return result;
|
|
80321
80534
|
}
|
|
80322
80535
|
try {
|
|
80323
80536
|
collectFromStore(db, userDir, options, result);
|
|
80324
80537
|
} catch (error52) {
|
|
80325
|
-
result.unavailable = `reading Cursor's session store failed (${
|
|
80538
|
+
result.unavailable = `reading Cursor's session store failed (${describeReadError(error52)})`;
|
|
80326
80539
|
} finally {
|
|
80327
80540
|
try {
|
|
80328
80541
|
db.close();
|
|
@@ -80336,6 +80549,7 @@ function collectFromStore(db, userDir, options, result) {
|
|
|
80336
80549
|
const now = options.now ?? Date.now();
|
|
80337
80550
|
const capture = options.capture ?? captureEverythingUnresolved;
|
|
80338
80551
|
const spoolDir = options.spoolDir ?? cursorSpoolDir(os9.homedir());
|
|
80552
|
+
const maxBytes = options.maxTranscriptBytes ?? MAX_TRANSCRIPT_FILE_BYTES;
|
|
80339
80553
|
let spoolDirReady = false;
|
|
80340
80554
|
const headerRows = db.prepare(
|
|
80341
80555
|
"select composerId, workspaceId, createdAt, lastUpdatedAt, isArchived, isSubagent, value from composerHeaders where lastUpdatedAt >= ? order by lastUpdatedAt desc"
|
|
@@ -80385,10 +80599,15 @@ function collectFromStore(db, userDir, options, result) {
|
|
|
80385
80599
|
const spoolStat = statIfExists(spoolPath);
|
|
80386
80600
|
let model;
|
|
80387
80601
|
if (spoolStat !== null && Math.abs(spoolStat.mtimeMs - lastUpdatedAt) < 1) {
|
|
80602
|
+
if (spoolStat.size > maxBytes) {
|
|
80603
|
+
result.skippedTooLarge += 1;
|
|
80604
|
+
continue;
|
|
80605
|
+
}
|
|
80388
80606
|
model = readSpoolModel(spoolPath);
|
|
80389
80607
|
} else {
|
|
80390
|
-
const transcript = extractTranscript(db, raw.composerId);
|
|
80608
|
+
const transcript = extractTranscript(db, raw.composerId, maxBytes);
|
|
80391
80609
|
if (transcript === null) continue;
|
|
80610
|
+
if (transcript.bubblesOmitted > 0) result.truncatedTranscripts += 1;
|
|
80392
80611
|
model = transcript.model;
|
|
80393
80612
|
const meta3 = {
|
|
80394
80613
|
type: "meta",
|
|
@@ -80400,7 +80619,8 @@ function collectFromStore(db, userDir, options, result) {
|
|
|
80400
80619
|
...model && { model },
|
|
80401
80620
|
...typeof raw.createdAt === "number" && { createdAt: raw.createdAt },
|
|
80402
80621
|
lastUpdatedAt,
|
|
80403
|
-
...cwd && { workspacePath: cwd }
|
|
80622
|
+
...cwd && { workspacePath: cwd },
|
|
80623
|
+
...transcript.bubblesOmitted > 0 && { bubblesOmitted: transcript.bubblesOmitted }
|
|
80404
80624
|
};
|
|
80405
80625
|
if (!spoolDirReady) {
|
|
80406
80626
|
mkdirSync2(spoolDir, { recursive: true });
|
|
@@ -81093,12 +81313,33 @@ async function resolveCapturePolicy(config2, credentials, log3, deps = defaultPo
|
|
|
81093
81313
|
}
|
|
81094
81314
|
let owners = credentials.allowedOwners ?? [];
|
|
81095
81315
|
let repos = credentials.allowedRepos ?? null;
|
|
81316
|
+
let workspacePolicies = credentials.workspacePolicies;
|
|
81096
81317
|
let ownersUnresolved = false;
|
|
81318
|
+
let claimsUnresolved = false;
|
|
81097
81319
|
try {
|
|
81098
81320
|
const fresh = await deps.fetchPolicy(credentials.apiUrl, credentials.token);
|
|
81099
81321
|
ownersUnresolved = fresh.integration === "linked" && fresh.owners.length === 0;
|
|
81100
81322
|
const nextOwners = ownersUnresolved ? owners : fresh.owners;
|
|
81101
|
-
const
|
|
81323
|
+
const nextWorkspaces = fresh.workspaces?.map((workspace) => {
|
|
81324
|
+
const unresolved = workspace.integration === "linked" && workspace.owners.length === 0 && workspace.repos === null;
|
|
81325
|
+
const cached3 = workspacePolicies?.find((entry) => entry.orgId === workspace.orgId) ?? (credentials.orgId !== void 0 && workspace.orgId === credentials.orgId ? { orgId: workspace.orgId, name: workspace.name, owners, repos } : void 0);
|
|
81326
|
+
const usableCache = cached3 !== void 0 && workspaceClaimsAnyRepo(cached3);
|
|
81327
|
+
const effective = unresolved && usableCache ? cached3 : unresolved ? {
|
|
81328
|
+
orgId: workspace.orgId,
|
|
81329
|
+
name: workspace.name,
|
|
81330
|
+
owners: workspace.owners,
|
|
81331
|
+
repos: workspace.repos,
|
|
81332
|
+
unresolved: true
|
|
81333
|
+
} : {
|
|
81334
|
+
orgId: workspace.orgId,
|
|
81335
|
+
name: workspace.name,
|
|
81336
|
+
owners: workspace.owners,
|
|
81337
|
+
repos: workspace.repos
|
|
81338
|
+
};
|
|
81339
|
+
if (effective.unresolved) claimsUnresolved = true;
|
|
81340
|
+
return effective;
|
|
81341
|
+
});
|
|
81342
|
+
const changed = nextOwners.join(",") !== owners.join(",") || (fresh.repos?.join(",") ?? null) !== (repos?.join(",") ?? null) || JSON.stringify(nextWorkspaces ?? null) !== JSON.stringify(workspacePolicies ?? null);
|
|
81102
81343
|
if (changed) {
|
|
81103
81344
|
const next = {
|
|
81104
81345
|
...credentials,
|
|
@@ -81106,34 +81347,48 @@ async function resolveCapturePolicy(config2, credentials, log3, deps = defaultPo
|
|
|
81106
81347
|
};
|
|
81107
81348
|
if (fresh.repos !== null) next.allowedRepos = fresh.repos;
|
|
81108
81349
|
else delete next.allowedRepos;
|
|
81350
|
+
if (nextWorkspaces !== void 0) next.workspacePolicies = nextWorkspaces;
|
|
81351
|
+
else delete next.workspacePolicies;
|
|
81109
81352
|
deps.persist(config2.appEnv, next);
|
|
81110
81353
|
}
|
|
81111
81354
|
owners = nextOwners;
|
|
81112
81355
|
repos = fresh.repos;
|
|
81356
|
+
workspacePolicies = nextWorkspaces;
|
|
81113
81357
|
} catch (cause) {
|
|
81358
|
+
const cachedCoversSomething = owners.length > 0 || repos !== null || (workspacePolicies ?? []).some((entry) => workspaceClaimsAnyRepo(entry));
|
|
81359
|
+
if (!cachedCoversSomething || (workspacePolicies ?? []).some((entry) => entry.unresolved === true)) {
|
|
81360
|
+
claimsUnresolved = true;
|
|
81361
|
+
}
|
|
81114
81362
|
log3(
|
|
81115
|
-
`\u26A0 Couldn't refresh the org's repo sync policy (${describeSyncError(cause)}) \u2014 ` + (
|
|
81363
|
+
`\u26A0 Couldn't refresh the org's repo sync policy (${describeSyncError(cause)}) \u2014 ` + (cachedCoversSomething ? "using the cached policy." : "nothing will upload this pass.")
|
|
81116
81364
|
);
|
|
81117
81365
|
}
|
|
81118
81366
|
const memberFolders = credentials.scopePaths ?? [];
|
|
81367
|
+
if (ownersUnresolved && owners.length === 0 && repos === null) claimsUnresolved = true;
|
|
81119
81368
|
if (ownersUnresolved) {
|
|
81120
81369
|
log3(
|
|
81121
81370
|
"\u26A0 The org's GitHub is connected, but its account couldn't be resolved just now \u2014 " + (owners.length > 0 ? `using the cached owner list: ${owners.join(", ")}.` : "no owner-based uploads this pass. This is usually temporary; syncing resumes when it resolves.")
|
|
81122
81371
|
);
|
|
81123
81372
|
}
|
|
81373
|
+
const homeOrgId = credentials.orgId ?? workspacePolicies?.[0]?.orgId;
|
|
81374
|
+
const foreignClaims = (workspacePolicies ?? []).filter(
|
|
81375
|
+
(entry) => entry.orgId !== homeOrgId && workspaceClaimsAnyRepo(entry)
|
|
81376
|
+
);
|
|
81124
81377
|
if (repos !== null && repos.length === 0) {
|
|
81125
81378
|
log3(
|
|
81126
|
-
"\u26A0 The org's repo allowlist is empty \u2014 no repo-based uploads" + (memberFolders.length > 0 ? "; only folders you added upload." : ".")
|
|
81379
|
+
"\u26A0 The org's repo allowlist is empty \u2014 no repo-based uploads for it" + (foreignClaims.length > 0 ? `; repos synced by ${foreignClaims.map((entry) => entry.name).join(", ")} still upload.` : memberFolders.length > 0 ? "; only folders you added upload." : ".")
|
|
81127
81380
|
);
|
|
81128
81381
|
} else if (repos === null && owners.length === 0 && !ownersUnresolved) {
|
|
81129
81382
|
log3(
|
|
81130
|
-
"\u26A0 Syncing is gated on org-owned GitHub repos, but the org has no GitHub integration \u2014 nothing uploads. Connect GitHub in Settings, or add folders with `powerplant folders add`."
|
|
81383
|
+
foreignClaims.length > 0 ? `Your home workspace has no GitHub integration \u2014 syncing repos for ${foreignClaims.map((entry) => entry.name).join(", ")}.` : "\u26A0 Syncing is gated on org-owned GitHub repos, but the org has no GitHub integration \u2014 nothing uploads. Connect GitHub in Settings, or add folders with `powerplant folders add`."
|
|
81131
81384
|
);
|
|
81132
81385
|
}
|
|
81133
81386
|
return {
|
|
81134
81387
|
mode: "org-repos",
|
|
81135
81388
|
allowedOwners: owners,
|
|
81136
81389
|
allowedRepos: repos,
|
|
81390
|
+
...workspacePolicies !== void 0 && { workspaceClaims: workspacePolicies },
|
|
81391
|
+
...claimsUnresolved && { claimsUnresolved },
|
|
81137
81392
|
memberFolders,
|
|
81138
81393
|
repoOf,
|
|
81139
81394
|
environment
|
|
@@ -81156,7 +81411,7 @@ async function runSyncPass(config2, options = {}) {
|
|
|
81156
81411
|
const capture = await resolveCapturePolicy(config2, credentials, log3);
|
|
81157
81412
|
const sinceMs = resolveScanFloor(window2, Date.now(), options.narrowSinceMs);
|
|
81158
81413
|
const maxSessions = window2.deepSweep ? DEEP_FIRST_SYNC_MAX_SESSIONS : void 0;
|
|
81159
|
-
const claude = await collectClaudeCodeSessions({ sinceMs, capture });
|
|
81414
|
+
const claude = await collectClaudeCodeSessions({ sinceMs, capture, maxSessions });
|
|
81160
81415
|
const codex = await collectCodexSessions({ sinceMs, capture, maxSessions });
|
|
81161
81416
|
const cursor = await collectCursorSessions({ sinceMs, capture });
|
|
81162
81417
|
if (cursor.unavailable !== null) {
|
|
@@ -81166,14 +81421,16 @@ async function runSyncPass(config2, options = {}) {
|
|
|
81166
81421
|
(a, b2) => b2.mtimeMs - a.mtimeMs
|
|
81167
81422
|
);
|
|
81168
81423
|
const collected = maxSessions === void 0 ? inWindow : inWindow.slice(0, maxSessions);
|
|
81169
|
-
const heldBack = inWindow.length - collected.length + codex.heldBack;
|
|
81424
|
+
const heldBack = inWindow.length - collected.length + claude.heldBack + codex.heldBack;
|
|
81170
81425
|
const skippedOutOfScope = claude.skippedOutOfScope + codex.skippedOutOfScope + cursor.skippedOutOfScope;
|
|
81171
81426
|
const skippedNoRepo = claude.skippedNoRepo + codex.skippedNoRepo + cursor.skippedNoRepo;
|
|
81172
81427
|
const items = collected.map((c) => c.item);
|
|
81173
81428
|
const active = items.filter((s) => s.status === "active").length;
|
|
81429
|
+
const tooLarge = (count, what) => count > 0 ? ` \xB7 ${count} ${what} skipped (over ${MAX_TRANSCRIPT_FILE_BYTES / (1024 * 1024)}MB \u2014 too large to read)` : "";
|
|
81174
81430
|
log3(
|
|
81175
|
-
`Found ${inWindow.length} local session(s) (${claude.sessions.length} claude, ${codex.sessions.length} codex, ${cursor.sessions.length} cursor) ` + describeScanReach(window2, options.narrowSinceMs) + (active > 0 ? ` (${active} still active)` : "") + (skippedOutOfScope > 0 ? ` \xB7 ${skippedOutOfScope} ${describeSkipReason(capture)}` : "") + (skippedNoRepo > 0 ? ` \xB7 ${skippedNoRepo} ${describeNoRepoSkipReason(capture)}` : "") + (codex.skippedSubagentThreads > 0 ? ` \xB7 ${codex.skippedSubagentThreads} codex subagent thread(s) skipped (spend unmeasured)` : "") + (cursor.skippedSubagentComposers > 0 ? ` \xB7 ${cursor.skippedSubagentComposers} cursor subagent thread(s) skipped` : "") + (cursor.skippedNonLocalComposers > 0 ? ` \xB7 ${cursor.skippedNonLocalComposers} cursor session(s) skipped (not local to this machine)` : "") + (cursor.skippedUnreadable > 0 ? ` \xB7 ${cursor.skippedUnreadable} cursor session(s) unreadable, skipped` : "") + (
|
|
81431
|
+
`Found ${inWindow.length} local session(s) (${claude.sessions.length} claude, ${codex.sessions.length} codex, ${cursor.sessions.length} cursor) ` + describeScanReach(window2, options.narrowSinceMs) + (active > 0 ? ` (${active} still active)` : "") + (skippedOutOfScope > 0 ? ` \xB7 ${skippedOutOfScope} ${describeSkipReason(capture)}` : "") + (skippedNoRepo > 0 ? ` \xB7 ${skippedNoRepo} ${describeNoRepoSkipReason(capture)}` : "") + (codex.skippedSubagentThreads > 0 ? ` \xB7 ${codex.skippedSubagentThreads} codex subagent thread(s) skipped (spend unmeasured)` : "") + (cursor.skippedSubagentComposers > 0 ? ` \xB7 ${cursor.skippedSubagentComposers} cursor subagent thread(s) skipped` : "") + (cursor.skippedNonLocalComposers > 0 ? ` \xB7 ${cursor.skippedNonLocalComposers} cursor session(s) skipped (not local to this machine)` : "") + (cursor.skippedUnreadable > 0 ? ` \xB7 ${cursor.skippedUnreadable} cursor session(s) unreadable, skipped` : "") + tooLarge(claude.skippedTooLarge, "claude transcript(s)") + (claude.unreadable.length > 0 ? ` \xB7 ${claude.unreadable.length} claude transcript(s) unreadable, skipped` : "") + tooLarge(codex.skippedTooLarge, "codex rollout(s)") + (codex.unreadable.length > 0 ? ` \xB7 ${codex.unreadable.length} codex rollout(s) unreadable, skipped` : "") + tooLarge(cursor.skippedTooLarge, "cursor transcript(s)") + (cursor.truncatedTranscripts > 0 ? ` \xB7 ${cursor.truncatedTranscripts} cursor transcript(s) cut at ${MAX_TRANSCRIPT_FILE_BYTES / (1024 * 1024)}MB (the rest of the conversation stays local)` : "") + (heldBack > 0 ? ` \xB7 ${heldBack} older session(s) held back \u2014 the first sync backfills the ${DEEP_FIRST_SYNC_MAX_SESSIONS} most recently active` : "") + "."
|
|
81176
81432
|
);
|
|
81433
|
+
for (const failure of claude.unreadable) log3(` claude transcript skipped \u2014 ${failure}`);
|
|
81177
81434
|
for (const failure of codex.unreadable) log3(` codex rollout skipped \u2014 ${failure}`);
|
|
81178
81435
|
if (options.dryRun) {
|
|
81179
81436
|
for (const s of items.slice(0, 10)) {
|
|
@@ -81199,11 +81456,18 @@ async function runSyncPass(config2, options = {}) {
|
|
|
81199
81456
|
saveCredentials(config2.appEnv, { ...current, deepSyncedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
81200
81457
|
log3(`Deep sync complete \u2014 future syncs cover the last ${STEADY_SYNC_WINDOW_DAYS} days.`);
|
|
81201
81458
|
};
|
|
81459
|
+
const claimsUnresolved = capture.mode === "org-repos" && capture.claimsUnresolved === true;
|
|
81202
81460
|
if (items.length === 0) {
|
|
81203
|
-
|
|
81461
|
+
if (claimsUnresolved) {
|
|
81462
|
+
if (window2.deepSweep) {
|
|
81463
|
+
log3("Deep sync not stamped \u2014 a workspace's claims are unresolved; the sweep re-runs next pass.");
|
|
81464
|
+
}
|
|
81465
|
+
} else {
|
|
81466
|
+
markDeepSynced();
|
|
81467
|
+
}
|
|
81204
81468
|
return { sessions: collected, upserted: 0 };
|
|
81205
81469
|
}
|
|
81206
|
-
const { upserted, unchanged, outOfPolicy } = await uploadSessions(
|
|
81470
|
+
const { upserted, unchanged, outOfPolicy, deferred } = await uploadSessions(
|
|
81207
81471
|
credentials.apiUrl,
|
|
81208
81472
|
credentials.token,
|
|
81209
81473
|
items
|
|
@@ -81211,6 +81475,11 @@ async function runSyncPass(config2, options = {}) {
|
|
|
81211
81475
|
log3(
|
|
81212
81476
|
`Uploaded: ${upserted} session(s) upserted` + (unchanged > 0 ? `, ${unchanged} unchanged (no writes issued)` : "") + "."
|
|
81213
81477
|
);
|
|
81478
|
+
if (deferred > 0) {
|
|
81479
|
+
log3(
|
|
81480
|
+
`\u26A0 ${deferred} session(s) deferred \u2014 a workspace's GitHub owners couldn't be resolved just now, so routing waited rather than guessing a workspace. They upload on a later pass.`
|
|
81481
|
+
);
|
|
81482
|
+
}
|
|
81214
81483
|
if (outOfPolicy > 0) {
|
|
81215
81484
|
log3(
|
|
81216
81485
|
`\u26A0 ${outOfPolicy} session(s) refused by the org's repo sync policy \u2014 they stayed out of Powerplant. The next pass refreshes the policy and stops offering them.`
|
|
@@ -81266,11 +81535,24 @@ async function runSyncPass(config2, options = {}) {
|
|
|
81266
81535
|
if (raw.failed.length > 0) parts.push(`${raw.failed.length} failed`);
|
|
81267
81536
|
log3(`Raw content: ${parts.join(", ")}.`);
|
|
81268
81537
|
for (const failure of raw.failed) log3(` raw upload failed \u2014 ${failure}`);
|
|
81269
|
-
|
|
81538
|
+
if (raw.missingMetadata > 0) {
|
|
81539
|
+
log3(
|
|
81540
|
+
`${raw.missingMetadata} raw transcript(s) waiting on their session metadata \u2014 they re-send once it lands.`
|
|
81541
|
+
);
|
|
81542
|
+
}
|
|
81543
|
+
const nothingElseAccountedFor = raw.skippedUnchanged === 0 && raw.skippedTooLarge === 0 && rawWindowSkipped === 0 && raw.missingMetadata === 0;
|
|
81270
81544
|
if (raw.uploaded === 0 && (raw.failed.length > 1 || raw.failed.length === 1 && nothingElseAccountedFor)) {
|
|
81271
81545
|
throw new Error(`all ${raw.failed.length} raw upload(s) failed this pass \u2014 ${raw.failed[0]}`);
|
|
81272
81546
|
}
|
|
81273
|
-
|
|
81547
|
+
if (deferred > 0 || claimsUnresolved) {
|
|
81548
|
+
if (window2.deepSweep) {
|
|
81549
|
+
log3(
|
|
81550
|
+
deferred > 0 ? "Deep sync not stamped \u2014 deferred sessions still owe their upload; the sweep re-runs next pass." : "Deep sync not stamped \u2014 a workspace's claims are unresolved; the sweep re-runs next pass."
|
|
81551
|
+
);
|
|
81552
|
+
}
|
|
81553
|
+
} else {
|
|
81554
|
+
markDeepSynced();
|
|
81555
|
+
}
|
|
81274
81556
|
}
|
|
81275
81557
|
return { sessions: collected, upserted };
|
|
81276
81558
|
}
|
|
@@ -81324,14 +81606,11 @@ function findClaudeTranscript(homeDir, externalId) {
|
|
|
81324
81606
|
const transcript = path13.join(projectDir, `${externalId}.jsonl`);
|
|
81325
81607
|
const stat = statIfExists(transcript);
|
|
81326
81608
|
if (stat === null) continue;
|
|
81327
|
-
const
|
|
81328
|
-
|
|
81329
|
-
|
|
81330
|
-
|
|
81331
|
-
|
|
81332
|
-
}
|
|
81333
|
-
}
|
|
81334
|
-
return null;
|
|
81609
|
+
const record2 = readFirstRecord(
|
|
81610
|
+
transcript,
|
|
81611
|
+
(candidate) => typeof candidate.cwd === "string" && candidate.cwd !== ""
|
|
81612
|
+
);
|
|
81613
|
+
return typeof record2?.cwd === "string" ? { cwd: record2.cwd, mtimeMs: stat.mtimeMs } : null;
|
|
81335
81614
|
}
|
|
81336
81615
|
return null;
|
|
81337
81616
|
}
|
|
@@ -81356,7 +81635,7 @@ function createWakePoller(deps) {
|
|
|
81356
81635
|
try {
|
|
81357
81636
|
for (const target of await deps.listPending()) {
|
|
81358
81637
|
if (inFlight.has(target.externalId)) continue;
|
|
81359
|
-
const home = deps
|
|
81638
|
+
const home = inspectTarget(deps, target);
|
|
81360
81639
|
if (home === null) continue;
|
|
81361
81640
|
if (deps.now() - home.mtimeMs < WAKE_QUIET_MS) continue;
|
|
81362
81641
|
const messages = await deps.claim(target);
|
|
@@ -81366,13 +81645,13 @@ function createWakePoller(deps) {
|
|
|
81366
81645
|
`waking session ${target.externalId} with ${messages.length} message(s) in ${home.cwd}`
|
|
81367
81646
|
);
|
|
81368
81647
|
void deps.runClaude(target, formatWakePrompt(messages), home.cwd).then(() => deps.log(`wake finished for session ${target.externalId}`)).catch(
|
|
81369
|
-
(error52) => reportFailedWake(deps, target, messages,
|
|
81648
|
+
(error52) => reportFailedWake(deps, target, messages, describeError(error52))
|
|
81370
81649
|
).finally(() => inFlight.delete(target.externalId));
|
|
81371
81650
|
}
|
|
81372
81651
|
consecutivePollFailures = 0;
|
|
81373
81652
|
} catch (error52) {
|
|
81374
81653
|
consecutivePollFailures += 1;
|
|
81375
|
-
deps.log(`\u26A0 wake poll failed (${
|
|
81654
|
+
deps.log(`\u26A0 wake poll failed (${describeError(error52)}) \u2014 next poll retries`);
|
|
81376
81655
|
if (consecutivePollFailures === REPORT_AFTER_CONSECUTIVE_FAILURES) {
|
|
81377
81656
|
deps.reportError?.(error52, "wake-poll", { consecutivePollFailures });
|
|
81378
81657
|
}
|
|
@@ -81382,6 +81661,16 @@ function createWakePoller(deps) {
|
|
|
81382
81661
|
}
|
|
81383
81662
|
};
|
|
81384
81663
|
}
|
|
81664
|
+
function inspectTarget(deps, target) {
|
|
81665
|
+
try {
|
|
81666
|
+
return deps.inspect(target);
|
|
81667
|
+
} catch (error52) {
|
|
81668
|
+
deps.log(
|
|
81669
|
+
`\u26A0 wake skipped for session ${target.externalId} \u2014 its transcript could not be read (${describeError(error52)}); next poll retries`
|
|
81670
|
+
);
|
|
81671
|
+
return null;
|
|
81672
|
+
}
|
|
81673
|
+
}
|
|
81385
81674
|
async function reportFailedWake(deps, target, messages, reason) {
|
|
81386
81675
|
deps.log(
|
|
81387
81676
|
`\u26A0 wake failed for session ${target.externalId} AFTER its messages were claimed (${reason})`
|
|
@@ -81394,7 +81683,7 @@ async function reportFailedWake(deps, target, messages, reason) {
|
|
|
81394
81683
|
);
|
|
81395
81684
|
} catch (reportError) {
|
|
81396
81685
|
deps.log(
|
|
81397
|
-
`\u26A0 wake failure report for session ${target.externalId} ALSO failed \u2014 its messages read Delivered but no agent ran (${
|
|
81686
|
+
`\u26A0 wake failure report for session ${target.externalId} ALSO failed \u2014 its messages read Delivered but no agent ran (${describeError(reportError)})`
|
|
81398
81687
|
);
|
|
81399
81688
|
deps.reportError?.(reportError, "wake-failure-report", {
|
|
81400
81689
|
externalId: target.externalId,
|
|
@@ -81446,7 +81735,7 @@ function preflightClaudeVersion(claudeBin, readVersionOutput = execClaudeVersion
|
|
|
81446
81735
|
try {
|
|
81447
81736
|
output = readVersionOutput(claudeBin);
|
|
81448
81737
|
} catch (error52) {
|
|
81449
|
-
return { ok: false, problem: `\`${claudeBin} --version\` failed (${
|
|
81738
|
+
return { ok: false, problem: `\`${claudeBin} --version\` failed (${describeError(error52)})` };
|
|
81450
81739
|
}
|
|
81451
81740
|
const version2 = /\d+\.\d+\.\d+/.exec(output)?.[0] ?? null;
|
|
81452
81741
|
if (version2 === null) {
|
|
@@ -81591,7 +81880,7 @@ function startWakeService(config2, homeDir, log3) {
|
|
|
81591
81880
|
log3(`wake-on-message armed: polling every ${WAKE_POLL_MS / 1e3}s (${config2.appEnv})`);
|
|
81592
81881
|
return () => clearInterval(timer);
|
|
81593
81882
|
}
|
|
81594
|
-
function
|
|
81883
|
+
function describeError(error52) {
|
|
81595
81884
|
return error52 instanceof Error ? error52.message : String(error52);
|
|
81596
81885
|
}
|
|
81597
81886
|
|
|
@@ -81631,6 +81920,8 @@ var SAFETY_NET_INTERVAL_MS = 60 * 60 * 1e3;
|
|
|
81631
81920
|
var NARROW_OVERLAP_MS = 6e4;
|
|
81632
81921
|
var WATCHER_RETRY_MS = 6e4;
|
|
81633
81922
|
var FAILED_PASS_RETRY_MS = 6e4;
|
|
81923
|
+
var FAILED_PASS_BACKOFF_CAP_MS = 15 * 6e4;
|
|
81924
|
+
var STREAK_REREPORT_INTERVAL_MS = 60 * 6e4;
|
|
81634
81925
|
var VERSION_NOTICE_INTERVAL_MS = 60 * 6e4;
|
|
81635
81926
|
var COMPLETION_SLACK_MS = 3e4;
|
|
81636
81927
|
function createWatchLoop(deps) {
|
|
@@ -81643,6 +81934,10 @@ function createWatchLoop(deps) {
|
|
|
81643
81934
|
let completionTimer = null;
|
|
81644
81935
|
let running = false;
|
|
81645
81936
|
let consecutiveFailures = 0;
|
|
81937
|
+
let streakSignature = null;
|
|
81938
|
+
let streakCount = 0;
|
|
81939
|
+
let streakStartedAt = 0;
|
|
81940
|
+
let streakReportedAt = 0;
|
|
81646
81941
|
let pausedLogged = false;
|
|
81647
81942
|
let queued = null;
|
|
81648
81943
|
let retryNotBefore = 0;
|
|
@@ -81669,7 +81964,7 @@ function createWatchLoop(deps) {
|
|
|
81669
81964
|
if (stopped) return;
|
|
81670
81965
|
try {
|
|
81671
81966
|
closeWatcher = deps.watch(onEvent, (error52) => {
|
|
81672
|
-
deps.log(`watcher lost (${
|
|
81967
|
+
deps.log(`watcher lost (${describeError2(error52)}) \u2014 re-arming and re-scanning`);
|
|
81673
81968
|
closeWatcher?.();
|
|
81674
81969
|
closeWatcher = null;
|
|
81675
81970
|
arm();
|
|
@@ -81677,7 +81972,7 @@ function createWatchLoop(deps) {
|
|
|
81677
81972
|
});
|
|
81678
81973
|
} catch (error52) {
|
|
81679
81974
|
deps.log(
|
|
81680
|
-
`cannot watch yet (${
|
|
81975
|
+
`cannot watch yet (${describeError2(error52)}) \u2014 retrying in ${WATCHER_RETRY_MS / 1e3}s`
|
|
81681
81976
|
);
|
|
81682
81977
|
watcherRetry = setTimeout(arm, WATCHER_RETRY_MS);
|
|
81683
81978
|
}
|
|
@@ -81734,6 +82029,8 @@ function createWatchLoop(deps) {
|
|
|
81734
82029
|
}
|
|
81735
82030
|
}
|
|
81736
82031
|
consecutiveFailures = 0;
|
|
82032
|
+
streakSignature = null;
|
|
82033
|
+
streakCount = 0;
|
|
81737
82034
|
deps.onPassResult?.({
|
|
81738
82035
|
kind,
|
|
81739
82036
|
at: startedAt,
|
|
@@ -81743,17 +82040,33 @@ function createWatchLoop(deps) {
|
|
|
81743
82040
|
});
|
|
81744
82041
|
} catch (error52) {
|
|
81745
82042
|
consecutiveFailures += 1;
|
|
81746
|
-
|
|
82043
|
+
const signature = errorSignature(error52);
|
|
82044
|
+
if (signature === streakSignature) {
|
|
82045
|
+
streakCount += 1;
|
|
82046
|
+
} else {
|
|
82047
|
+
streakSignature = signature;
|
|
82048
|
+
streakCount = 1;
|
|
82049
|
+
streakStartedAt = startedAt;
|
|
82050
|
+
streakReportedAt = 0;
|
|
82051
|
+
}
|
|
82052
|
+
const holdMs = Math.max(
|
|
82053
|
+
Math.min(FAILED_PASS_RETRY_MS * 2 ** (streakCount - 1), FAILED_PASS_BACKOFF_CAP_MS),
|
|
82054
|
+
retryAfterMs(error52) ?? 0
|
|
82055
|
+
);
|
|
82056
|
+
retryNotBefore = now() + holdMs;
|
|
81747
82057
|
lifecycleChanged ||= consultLifecycle;
|
|
81748
82058
|
deps.onPassResult?.({
|
|
81749
82059
|
kind,
|
|
81750
82060
|
at: startedAt,
|
|
81751
82061
|
ok: false,
|
|
81752
|
-
error:
|
|
82062
|
+
error: describeError2(error52),
|
|
81753
82063
|
sessions: 0,
|
|
81754
82064
|
upserted: 0
|
|
81755
82065
|
});
|
|
81756
|
-
|
|
82066
|
+
const failLine = `${kind} pass failed (${describeError2(error52)})`;
|
|
82067
|
+
deps.log(
|
|
82068
|
+
streakCount > 1 ? `${failLine}: same failure ${streakCount} times in a row, next attempt in ${describeDelay(holdMs)}` : `${failLine}, next attempt in ${describeDelay(holdMs)} re-covers this window`
|
|
82069
|
+
);
|
|
81757
82070
|
if (error52 instanceof Error && !(error52 instanceof ExpectedCliError) && error52.stack) {
|
|
81758
82071
|
deps.log(error52.stack);
|
|
81759
82072
|
}
|
|
@@ -81764,10 +82077,13 @@ function createWatchLoop(deps) {
|
|
|
81764
82077
|
);
|
|
81765
82078
|
return;
|
|
81766
82079
|
}
|
|
81767
|
-
if (consecutiveFailures >= REPORT_AFTER_CONSECUTIVE_FAILURES) {
|
|
82080
|
+
if (consecutiveFailures >= REPORT_AFTER_CONSECUTIVE_FAILURES && (streakReportedAt === 0 || now() - streakReportedAt >= STREAK_REREPORT_INTERVAL_MS)) {
|
|
82081
|
+
streakReportedAt = now();
|
|
81768
82082
|
const report2 = error52 instanceof ExpectedCliError ? new Error(`${consecutiveFailures} consecutive failed passes: ${error52.message}`) : error52;
|
|
81769
82083
|
deps.reportError?.(report2, `watch-pass-${kind}`, {
|
|
81770
82084
|
consecutiveFailures,
|
|
82085
|
+
sameFailureCount: streakCount,
|
|
82086
|
+
streakMinutes: Math.round((now() - streakStartedAt) / 6e4),
|
|
81771
82087
|
pendingCompletions: pendingCompletions.size
|
|
81772
82088
|
});
|
|
81773
82089
|
}
|
|
@@ -81855,7 +82171,7 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
|
|
|
81855
82171
|
try {
|
|
81856
82172
|
return loadCredentials(config2.appEnv);
|
|
81857
82173
|
} catch (error52) {
|
|
81858
|
-
log3(`\u26A0 credential store unreadable (${
|
|
82174
|
+
log3(`\u26A0 credential store unreadable (${describeError2(error52)}) \u2014 identity omitted from status`);
|
|
81859
82175
|
return null;
|
|
81860
82176
|
}
|
|
81861
82177
|
})();
|
|
@@ -81869,14 +82185,14 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
|
|
|
81869
82185
|
);
|
|
81870
82186
|
} catch (error52) {
|
|
81871
82187
|
log3(
|
|
81872
|
-
`\u26A0 Claude Code hook install failed (${
|
|
82188
|
+
`\u26A0 Claude Code hook install failed (${describeError2(error52)}) \u2014 syncing continues without them`
|
|
81873
82189
|
);
|
|
81874
82190
|
reportCollectorError(error52, "watch-hook-install");
|
|
81875
82191
|
}
|
|
81876
82192
|
try {
|
|
81877
82193
|
startWakeService(config2, home, log3);
|
|
81878
82194
|
} catch (error52) {
|
|
81879
|
-
log3(`\u26A0 wake-on-message failed to start (${
|
|
82195
|
+
log3(`\u26A0 wake-on-message failed to start (${describeError2(error52)}) \u2014 syncing continues`);
|
|
81880
82196
|
reportCollectorError(error52, "watch-wake-start");
|
|
81881
82197
|
}
|
|
81882
82198
|
const presentRoots = storeRoots.filter(({ dir }) => existsSync4(dir)).map(({ dir }) => dir);
|
|
@@ -81907,7 +82223,7 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
|
|
|
81907
82223
|
statusWriteFailed = false;
|
|
81908
82224
|
} catch (error52) {
|
|
81909
82225
|
if (!statusWriteFailed) {
|
|
81910
|
-
log3(`\u26A0 status file write failed (${
|
|
82226
|
+
log3(`\u26A0 status file write failed (${describeError2(error52)}) \u2014 syncing continues`);
|
|
81911
82227
|
reportCollectorError(error52, "watch-status-write");
|
|
81912
82228
|
}
|
|
81913
82229
|
statusWriteFailed = true;
|
|
@@ -82005,9 +82321,21 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
|
|
|
82005
82321
|
return new Promise(() => {
|
|
82006
82322
|
});
|
|
82007
82323
|
}
|
|
82008
|
-
function
|
|
82324
|
+
function describeError2(error52) {
|
|
82009
82325
|
return error52 instanceof Error ? error52.message : String(error52);
|
|
82010
82326
|
}
|
|
82327
|
+
function errorSignature(error52) {
|
|
82328
|
+
const parts = [];
|
|
82329
|
+
for (let cause = error52; cause instanceof Error; cause = cause.cause) {
|
|
82330
|
+
parts.push(`${cause.name}: ${cause.message}`);
|
|
82331
|
+
}
|
|
82332
|
+
if (parts.length === 0) parts.push(String(error52));
|
|
82333
|
+
return parts.join(" | ").replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "#").replace(/[0-9a-f]{8,}/gi, "#").replace(/\d+/g, "#");
|
|
82334
|
+
}
|
|
82335
|
+
function describeDelay(ms) {
|
|
82336
|
+
const seconds = Math.round(ms / 1e3);
|
|
82337
|
+
return seconds < 120 ? `${seconds}s` : `${Math.round(seconds / 60)} minutes`;
|
|
82338
|
+
}
|
|
82011
82339
|
|
|
82012
82340
|
// src/connect.ts
|
|
82013
82341
|
import { execFileSync as execFileSync5, spawnSync } from "node:child_process";
|
|
@@ -84284,7 +84612,7 @@ ${RESTART_NOTE}`;
|
|
|
84284
84612
|
}
|
|
84285
84613
|
|
|
84286
84614
|
// src/version.ts
|
|
84287
|
-
var CLI_VERSION = true ? "0.
|
|
84615
|
+
var CLI_VERSION = true ? "0.4.0" : "0.0.0-checkout";
|
|
84288
84616
|
var CLI_PACKAGE_NAME = "@usepowerplant/cli";
|
|
84289
84617
|
|
|
84290
84618
|
// src/workspace.ts
|