@osfactory/har 0.39.0 → 0.41.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/README.md +14 -1
- package/dist/index.js +212 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="logo.png" alt="HAR logo" width="120">
|
|
2
|
+
<img src="assets/logo.png" alt="HAR logo" width="120">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
# HAR — Harnesses for coding agents
|
|
@@ -68,6 +68,19 @@ Everything beyond install and first commands lives at **[https://harproject.dev/
|
|
|
68
68
|
|
|
69
69
|
See [CONTRIBUTING.md](./CONTRIBUTING.md) for local setup, the dogfood harness loop, and architecture. Coding agents working on this repo should start with [AGENT.md](./AGENT.md). Maintainer release process: [RELEASING.md](./RELEASING.md).
|
|
70
70
|
|
|
71
|
+
## Sponsors
|
|
72
|
+
|
|
73
|
+
HAR is sponsored by [Kerno](https://kerno.io) — runtime code and security review for coding agents.
|
|
74
|
+
|
|
75
|
+
<p align="center">
|
|
76
|
+
<a href="https://kerno.io">
|
|
77
|
+
<picture>
|
|
78
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/kerno-logo.svg">
|
|
79
|
+
<img src="assets/kerno-logo-dark.svg" alt="Kerno" height="36">
|
|
80
|
+
</picture>
|
|
81
|
+
</a>
|
|
82
|
+
</p>
|
|
83
|
+
|
|
71
84
|
## License
|
|
72
85
|
|
|
73
86
|
Licensed under the [Apache License 2.0](./LICENSE).
|
package/dist/index.js
CHANGED
|
@@ -28533,7 +28533,9 @@ function readPortalCredentials() {
|
|
|
28533
28533
|
token: parsed.token,
|
|
28534
28534
|
workspace: parsed.workspace,
|
|
28535
28535
|
email: parsed.email,
|
|
28536
|
-
createdAt: parsed.createdAt ?? ""
|
|
28536
|
+
createdAt: parsed.createdAt ?? "",
|
|
28537
|
+
refreshToken: parsed.refreshToken,
|
|
28538
|
+
expiresAt: parsed.expiresAt
|
|
28537
28539
|
};
|
|
28538
28540
|
} catch {
|
|
28539
28541
|
return null;
|
|
@@ -28576,7 +28578,11 @@ function getPortalTarget() {
|
|
|
28576
28578
|
}
|
|
28577
28579
|
const stored = readPortalCredentials();
|
|
28578
28580
|
if (stored) {
|
|
28579
|
-
return {
|
|
28581
|
+
return {
|
|
28582
|
+
url: normalizeUrl(stored.portalUrl),
|
|
28583
|
+
token: stored.token,
|
|
28584
|
+
refreshToken: stored.refreshToken
|
|
28585
|
+
};
|
|
28580
28586
|
}
|
|
28581
28587
|
if (process.env.HAR_CLOUD_API_URL && process.env.HAR_CLOUD_API_KEY) {
|
|
28582
28588
|
return {
|
|
@@ -30982,6 +30988,25 @@ var init_codex = __esm({
|
|
|
30982
30988
|
});
|
|
30983
30989
|
|
|
30984
30990
|
// src/core/usage-harvest/index.ts
|
|
30991
|
+
function sessionToolKey(sessionKey, agentTool) {
|
|
30992
|
+
return `${sessionKey}\0${agentTool}`;
|
|
30993
|
+
}
|
|
30994
|
+
function omitHarvestWhenOtelPresent(harvested, existing) {
|
|
30995
|
+
const otelKeys = new Set(
|
|
30996
|
+
existing.filter((row) => (row.sources ?? []).includes("otel")).map((row) => sessionToolKey(row.sessionKey, row.agentTool))
|
|
30997
|
+
);
|
|
30998
|
+
if (otelKeys.size === 0) return harvested;
|
|
30999
|
+
return harvested.filter((row) => !otelKeys.has(sessionToolKey(row.sessionKey, row.agentTool)));
|
|
31000
|
+
}
|
|
31001
|
+
function omitHarvestEventsWhenOtelPresent(harvested, existing) {
|
|
31002
|
+
const otelKeys = new Set(
|
|
31003
|
+
existing.filter((event) => event.source === "otel").map((event) => sessionToolKey(event.sessionKey, event.agentTool))
|
|
31004
|
+
);
|
|
31005
|
+
if (otelKeys.size === 0) return harvested;
|
|
31006
|
+
return harvested.filter(
|
|
31007
|
+
(event) => !otelKeys.has(sessionToolKey(event.sessionKey, event.agentTool))
|
|
31008
|
+
);
|
|
31009
|
+
}
|
|
30985
31010
|
function harvestUsageForSlot(slot) {
|
|
30986
31011
|
const out = [];
|
|
30987
31012
|
const claude = harvestClaudeUsage(slot);
|
|
@@ -31043,6 +31068,27 @@ function writePortalWatermark(repoPath, portalUrl, lastSyncedAt) {
|
|
|
31043
31068
|
}
|
|
31044
31069
|
writeState(state);
|
|
31045
31070
|
}
|
|
31071
|
+
function readRunsWatermarkEntry(repoPath, target) {
|
|
31072
|
+
const canonical = canonicalizeControlRepoPath(repoPath);
|
|
31073
|
+
const entry = readState().states.find(
|
|
31074
|
+
(state) => state.repoPath === canonical && state.portalUrl === target
|
|
31075
|
+
);
|
|
31076
|
+
return entry ? { lastSyncedAt: entry.lastSyncedAt, repoId: entry.repoId } : null;
|
|
31077
|
+
}
|
|
31078
|
+
function writeRunsWatermark(repoPath, target, repoId, lastSyncedAt) {
|
|
31079
|
+
const canonical = canonicalizeControlRepoPath(repoPath);
|
|
31080
|
+
const state = readState();
|
|
31081
|
+
const existing = state.states.find(
|
|
31082
|
+
(entry) => entry.repoPath === canonical && entry.portalUrl === target
|
|
31083
|
+
);
|
|
31084
|
+
if (existing) {
|
|
31085
|
+
existing.lastSyncedAt = lastSyncedAt;
|
|
31086
|
+
existing.repoId = repoId;
|
|
31087
|
+
} else {
|
|
31088
|
+
state.states.push({ repoPath: canonical, portalUrl: target, lastSyncedAt, repoId });
|
|
31089
|
+
}
|
|
31090
|
+
writeState(state);
|
|
31091
|
+
}
|
|
31046
31092
|
function selectSince(rows, since, getTimestamp) {
|
|
31047
31093
|
let maxSyncedAt = null;
|
|
31048
31094
|
const selected = [];
|
|
@@ -31250,6 +31296,7 @@ var init_portal_usage_merge = __esm({
|
|
|
31250
31296
|
// src/core/control-sync.ts
|
|
31251
31297
|
var control_sync_exports = {};
|
|
31252
31298
|
__export(control_sync_exports, {
|
|
31299
|
+
chunkBySerializedSize: () => chunkBySerializedSize,
|
|
31253
31300
|
discoverHarRepos: () => discoverHarRepos,
|
|
31254
31301
|
ensureRepoRegisteredWithControl: () => ensureRepoRegisteredWithControl,
|
|
31255
31302
|
isControlApiReachable: () => isControlApiReachable,
|
|
@@ -31261,38 +31308,133 @@ __export(control_sync_exports, {
|
|
|
31261
31308
|
syncRunWithControlAsync: () => syncRunWithControlAsync,
|
|
31262
31309
|
waitForControlApi: () => waitForControlApi
|
|
31263
31310
|
});
|
|
31311
|
+
function maxSyncBatchBytes() {
|
|
31312
|
+
const raw = Number(process.env.HAR_SYNC_MAX_BATCH_BYTES);
|
|
31313
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_MAX_BATCH_BYTES;
|
|
31314
|
+
}
|
|
31315
|
+
function chunkBySerializedSize(items, maxBytes) {
|
|
31316
|
+
const batches = [];
|
|
31317
|
+
let current = [];
|
|
31318
|
+
let currentBytes = 0;
|
|
31319
|
+
for (const item of items) {
|
|
31320
|
+
const itemBytes = Buffer.byteLength(JSON.stringify(item), "utf8");
|
|
31321
|
+
if (current.length > 0 && currentBytes + itemBytes > maxBytes) {
|
|
31322
|
+
batches.push(current);
|
|
31323
|
+
current = [];
|
|
31324
|
+
currentBytes = 0;
|
|
31325
|
+
}
|
|
31326
|
+
current.push(item);
|
|
31327
|
+
currentBytes += itemBytes;
|
|
31328
|
+
}
|
|
31329
|
+
if (current.length > 0) batches.push(current);
|
|
31330
|
+
return batches;
|
|
31331
|
+
}
|
|
31332
|
+
function runTimestamp(run2) {
|
|
31333
|
+
return run2.finishedAt ?? run2.startedAt;
|
|
31334
|
+
}
|
|
31335
|
+
function rewindSince(since) {
|
|
31336
|
+
if (!since) return null;
|
|
31337
|
+
const raw = Number(process.env.HAR_SYNC_OVERLAP_MS);
|
|
31338
|
+
const overlap = Number.isFinite(raw) && raw >= 0 ? raw : DEFAULT_SYNC_OVERLAP_MS;
|
|
31339
|
+
return new Date(new Date(since).getTime() - overlap).toISOString();
|
|
31340
|
+
}
|
|
31341
|
+
function runsWatermarkTarget(target) {
|
|
31342
|
+
return `runs:${target}`;
|
|
31343
|
+
}
|
|
31344
|
+
async function syncRunBatches(newRuns, repoPath, runsTarget, getRepoId, dryRun, send) {
|
|
31345
|
+
const ordered = [...newRuns].sort((a2, b3) => runTimestamp(a2).localeCompare(runTimestamp(b3)));
|
|
31346
|
+
const batches = chunkBySerializedSize(ordered, maxSyncBatchBytes());
|
|
31347
|
+
const toSend = batches.length > 0 ? batches : [[]];
|
|
31348
|
+
for (let i2 = 0; i2 < toSend.length; i2++) {
|
|
31349
|
+
const batch = toSend[i2];
|
|
31350
|
+
await send(batch, i2);
|
|
31351
|
+
if (!dryRun && batch.length > 0) {
|
|
31352
|
+
writeRunsWatermark(repoPath, runsTarget, getRepoId(), runTimestamp(batch[batch.length - 1]));
|
|
31353
|
+
}
|
|
31354
|
+
}
|
|
31355
|
+
}
|
|
31356
|
+
function describeFetchFailure(url, bodyBytes, err) {
|
|
31357
|
+
const base = err instanceof Error ? err.message : String(err);
|
|
31358
|
+
const cause = err.cause;
|
|
31359
|
+
const code = cause?.code ? ` [${cause.code}]` : "";
|
|
31360
|
+
const detail = cause?.message && cause.message !== base ? `: ${cause.message}` : "";
|
|
31361
|
+
const size = bodyBytes > 0 ? ` (request body ${(bodyBytes / 1024 / 1024).toFixed(2)} MB)` : "";
|
|
31362
|
+
return `${base}${code}${detail}${size} \u2014 POST ${url}`;
|
|
31363
|
+
}
|
|
31364
|
+
async function postForResponse(url, headers, payload) {
|
|
31365
|
+
let lastErr;
|
|
31366
|
+
for (let attempt = 1; attempt <= MAX_FETCH_ATTEMPTS; attempt++) {
|
|
31367
|
+
try {
|
|
31368
|
+
return await fetch(url, { method: "POST", headers, body: payload });
|
|
31369
|
+
} catch (err) {
|
|
31370
|
+
lastErr = err;
|
|
31371
|
+
if (attempt < MAX_FETCH_ATTEMPTS) {
|
|
31372
|
+
await new Promise((resolve47) => setTimeout(resolve47, attempt * 100));
|
|
31373
|
+
}
|
|
31374
|
+
}
|
|
31375
|
+
}
|
|
31376
|
+
throw new Error(describeFetchFailure(url, Buffer.byteLength(payload, "utf8"), lastErr));
|
|
31377
|
+
}
|
|
31264
31378
|
async function postJson(url, body, dryRun) {
|
|
31265
31379
|
if (dryRun) return null;
|
|
31266
|
-
const response = await
|
|
31267
|
-
|
|
31268
|
-
|
|
31269
|
-
|
|
31270
|
-
|
|
31380
|
+
const response = await postForResponse(
|
|
31381
|
+
url,
|
|
31382
|
+
{ "Content-Type": "application/json" },
|
|
31383
|
+
JSON.stringify(body)
|
|
31384
|
+
);
|
|
31271
31385
|
if (!response.ok) {
|
|
31272
31386
|
const text = await response.text().catch(() => "");
|
|
31273
31387
|
throw new Error(`Control API ${response.status}: ${text || response.statusText}`);
|
|
31274
31388
|
}
|
|
31275
31389
|
return await response.json();
|
|
31276
31390
|
}
|
|
31277
|
-
|
|
31278
|
-
|
|
31279
|
-
|
|
31280
|
-
|
|
31281
|
-
headers: {
|
|
31391
|
+
function postPortalOnce(target, endpoint, body) {
|
|
31392
|
+
return postForResponse(
|
|
31393
|
+
`${target.url}${endpoint}`,
|
|
31394
|
+
{
|
|
31282
31395
|
"Content-Type": "application/json",
|
|
31283
31396
|
Authorization: `Bearer ${target.token}`
|
|
31284
31397
|
},
|
|
31285
|
-
|
|
31286
|
-
|
|
31398
|
+
JSON.stringify(body)
|
|
31399
|
+
);
|
|
31400
|
+
}
|
|
31401
|
+
async function refreshPortalToken(target) {
|
|
31402
|
+
if (!target.refreshToken) return false;
|
|
31403
|
+
let response;
|
|
31404
|
+
try {
|
|
31405
|
+
response = await fetch(`${target.url}/api/cli/refresh`, {
|
|
31406
|
+
method: "POST",
|
|
31407
|
+
headers: { Authorization: `Bearer ${target.refreshToken}` }
|
|
31408
|
+
});
|
|
31409
|
+
} catch {
|
|
31410
|
+
return false;
|
|
31411
|
+
}
|
|
31412
|
+
if (!response.ok) return false;
|
|
31413
|
+
const data = await response.json().catch(() => null);
|
|
31414
|
+
if (!data?.token) return false;
|
|
31415
|
+
target.token = data.token;
|
|
31416
|
+
const stored = readPortalCredentials();
|
|
31417
|
+
if (stored) {
|
|
31418
|
+
writePortalCredentials({ ...stored, token: data.token, expiresAt: data.expiresAt });
|
|
31419
|
+
}
|
|
31420
|
+
return true;
|
|
31421
|
+
}
|
|
31422
|
+
async function postPortal(target, endpoint, body, dryRun) {
|
|
31423
|
+
if (dryRun) return null;
|
|
31424
|
+
let response = await postPortalOnce(target, endpoint, body);
|
|
31425
|
+
if ((response.status === 401 || response.status === 403) && await refreshPortalToken(target)) {
|
|
31426
|
+
response = await postPortalOnce(target, endpoint, body);
|
|
31427
|
+
}
|
|
31287
31428
|
if (!response.ok) {
|
|
31288
31429
|
if (response.status === 401 || response.status === 403) {
|
|
31289
31430
|
throw new Error(
|
|
31290
|
-
`har-portal rejected the ingest token (HTTP ${response.status}) \u2014 check HAR_PORTAL_TOKEN.`
|
|
31431
|
+
`har-portal rejected the ingest token (HTTP ${response.status}) \u2014 run \`har control login\` (or check HAR_PORTAL_TOKEN).`
|
|
31291
31432
|
);
|
|
31292
31433
|
}
|
|
31293
31434
|
const text = await response.text().catch(() => "");
|
|
31294
31435
|
throw new Error(`har-portal sync failed: HTTP ${response.status}${text ? ` \u2014 ${text}` : ""}`);
|
|
31295
31436
|
}
|
|
31437
|
+
return await response.json().catch(() => null);
|
|
31296
31438
|
}
|
|
31297
31439
|
function resolvePortalUserEmail() {
|
|
31298
31440
|
return readPortalCredentials()?.email || void 0;
|
|
@@ -31348,7 +31490,9 @@ async function collectPortalTelemetry(repoPath, slots, controlApiUrl, since) {
|
|
|
31348
31490
|
}))
|
|
31349
31491
|
);
|
|
31350
31492
|
const persisted = await fetchPersistedPortalTelemetry(repoPath, controlApiUrl, { since });
|
|
31351
|
-
const
|
|
31493
|
+
const harvestedUsage = omitHarvestWhenOtelPresent(liveUsage, persisted.usage);
|
|
31494
|
+
const harvestedEvents = omitHarvestEventsWhenOtelPresent(liveEvents, persisted.events);
|
|
31495
|
+
const usage2 = mergePortalUsage(harvestedUsage, persisted.usage).map((row) => {
|
|
31352
31496
|
const priced = enrichUsageWithPricing(row);
|
|
31353
31497
|
const models = modelsFromBreakdown(
|
|
31354
31498
|
priced.modelBreakdown
|
|
@@ -31359,7 +31503,7 @@ async function collectPortalTelemetry(repoPath, slots, controlApiUrl, since) {
|
|
|
31359
31503
|
...models.length > 0 ? { models } : {}
|
|
31360
31504
|
};
|
|
31361
31505
|
});
|
|
31362
|
-
const events = dedupePortalEvents(
|
|
31506
|
+
const events = dedupePortalEvents(harvestedEvents, persisted.events).map(
|
|
31363
31507
|
(event) => dropNullFields({ ...event })
|
|
31364
31508
|
);
|
|
31365
31509
|
return { usage: usage2, events, maxSyncedAt: persisted.maxSyncedAt };
|
|
@@ -31387,7 +31531,6 @@ async function buildPortalPayload(repoPath, controlApiUrl, since) {
|
|
|
31387
31531
|
path: repoPath,
|
|
31388
31532
|
...manifest ? { manifest } : {},
|
|
31389
31533
|
...stagesRegistry ? { stagesRegistry } : {},
|
|
31390
|
-
runs,
|
|
31391
31534
|
slots: status.slots,
|
|
31392
31535
|
generatedAt: status.generatedAt,
|
|
31393
31536
|
...validations.length > 0 ? { validations } : {},
|
|
@@ -31396,7 +31539,7 @@ async function buildPortalPayload(repoPath, controlApiUrl, since) {
|
|
|
31396
31539
|
...validationBindings.length > 0 ? { validationBindings } : {},
|
|
31397
31540
|
...usage2.length > 0 ? { usage: usage2 } : {}
|
|
31398
31541
|
};
|
|
31399
|
-
return { syncBody, events, maxSyncedAt };
|
|
31542
|
+
return { syncBody, runs, events, maxSyncedAt };
|
|
31400
31543
|
}
|
|
31401
31544
|
async function isControlApiReachable(apiUrl = getControlApiUrl()) {
|
|
31402
31545
|
try {
|
|
@@ -31434,7 +31577,7 @@ async function syncReposWithControl(options) {
|
|
|
31434
31577
|
try {
|
|
31435
31578
|
await withTimeout(
|
|
31436
31579
|
syncRepoWithControl({ repoPath, apiUrl, dryRun: options.dryRun, cloud: options.cloud, full: options.full }),
|
|
31437
|
-
|
|
31580
|
+
EXPLICIT_SYNC_TIMEOUT_MS,
|
|
31438
31581
|
"control sync"
|
|
31439
31582
|
);
|
|
31440
31583
|
synced++;
|
|
@@ -31497,7 +31640,7 @@ async function ensureRepoRegisteredWithControl(repoPath, apiUrl) {
|
|
|
31497
31640
|
}
|
|
31498
31641
|
async function syncRepoWithLocalControl(options) {
|
|
31499
31642
|
const apiUrl = options.apiUrl ?? getControlApiUrl();
|
|
31500
|
-
const { repoPath, dryRun } = options;
|
|
31643
|
+
const { repoPath, dryRun, full } = options;
|
|
31501
31644
|
const registerResult = await registerRepoWithControl({ ...options, repoPath });
|
|
31502
31645
|
const repoId = registerResult?.id;
|
|
31503
31646
|
if (!repoId && !dryRun) {
|
|
@@ -31508,11 +31651,11 @@ async function syncRepoWithLocalControl(options) {
|
|
|
31508
31651
|
if (!existing) {
|
|
31509
31652
|
return;
|
|
31510
31653
|
}
|
|
31511
|
-
await syncRepoRunsAndSlots(apiUrl, existing.id, repoPath, dryRun);
|
|
31654
|
+
await syncRepoRunsAndSlots(apiUrl, existing.id, repoPath, dryRun, full);
|
|
31512
31655
|
return;
|
|
31513
31656
|
}
|
|
31514
31657
|
if (repoId) {
|
|
31515
|
-
await syncRepoRunsAndSlots(apiUrl, repoId, repoPath, dryRun);
|
|
31658
|
+
await syncRepoRunsAndSlots(apiUrl, repoId, repoPath, dryRun, full);
|
|
31516
31659
|
}
|
|
31517
31660
|
}
|
|
31518
31661
|
async function syncRepoWithPortal(options, controlApiUrl) {
|
|
@@ -31520,10 +31663,28 @@ async function syncRepoWithPortal(options, controlApiUrl) {
|
|
|
31520
31663
|
if (!portal) return;
|
|
31521
31664
|
const { repoPath, dryRun, full } = options;
|
|
31522
31665
|
const since = full ? null : readPortalWatermark(repoPath, portal.url);
|
|
31523
|
-
const
|
|
31524
|
-
|
|
31525
|
-
|
|
31526
|
-
|
|
31666
|
+
const runsTarget = runsWatermarkTarget(portal.url);
|
|
31667
|
+
const stored = full ? null : readRunsWatermarkEntry(repoPath, runsTarget);
|
|
31668
|
+
const runsSince = rewindSince(stored?.lastSyncedAt ?? null);
|
|
31669
|
+
const { syncBody, runs, events, maxSyncedAt } = await buildPortalPayload(
|
|
31670
|
+
repoPath,
|
|
31671
|
+
controlApiUrl,
|
|
31672
|
+
since
|
|
31673
|
+
);
|
|
31674
|
+
const { selected: newRuns } = selectSince(runs, runsSince, runTimestamp);
|
|
31675
|
+
let repoId;
|
|
31676
|
+
await syncRunBatches(newRuns, repoPath, runsTarget, () => repoId, dryRun, async (batch, i2) => {
|
|
31677
|
+
const body = i2 === 0 ? { ...syncBody, runs: batch } : { path: repoPath, runs: batch };
|
|
31678
|
+
const res = await postPortal(portal, "/api/sync", body, dryRun);
|
|
31679
|
+
if (i2 === 0 && typeof res?.repositoryId === "string") repoId = res.repositoryId;
|
|
31680
|
+
});
|
|
31681
|
+
if (!full && !dryRun && stored?.repoId && repoId && stored.repoId !== repoId) {
|
|
31682
|
+
await syncRunBatches(listRuns(repoPath), repoPath, runsTarget, () => repoId, dryRun, async (batch) => {
|
|
31683
|
+
await postPortal(portal, "/api/sync", { path: repoPath, runs: batch }, dryRun);
|
|
31684
|
+
});
|
|
31685
|
+
}
|
|
31686
|
+
for (const batch of chunkBySerializedSize(events, maxSyncBatchBytes())) {
|
|
31687
|
+
await postPortal(portal, "/api/otel", { path: repoPath, events: batch, spans: [] }, dryRun);
|
|
31527
31688
|
}
|
|
31528
31689
|
if (!dryRun && maxSyncedAt) {
|
|
31529
31690
|
writePortalWatermark(repoPath, portal.url, maxSyncedAt);
|
|
@@ -31564,10 +31725,15 @@ async function syncRepoWithControl(options) {
|
|
|
31564
31725
|
await syncRepoWithLocalControl({ ...options, repoPath, apiUrl });
|
|
31565
31726
|
await syncRepoWithPortal({ ...options, repoPath }, apiUrl);
|
|
31566
31727
|
}
|
|
31567
|
-
async function syncRepoRunsAndSlots(apiUrl, repoId, repoPath, dryRun) {
|
|
31728
|
+
async function syncRepoRunsAndSlots(apiUrl, repoId, repoPath, dryRun, full) {
|
|
31729
|
+
const runsTarget = runsWatermarkTarget(apiUrl);
|
|
31730
|
+
const stored = full ? null : readRunsWatermarkEntry(repoPath, runsTarget);
|
|
31731
|
+
const runsSince = rewindSince(stored?.repoId === repoId ? stored?.lastSyncedAt ?? null : null);
|
|
31568
31732
|
const runs = listRuns(repoPath);
|
|
31569
|
-
const
|
|
31570
|
-
await
|
|
31733
|
+
const { selected: newRuns } = selectSince(runs, runsSince, runTimestamp);
|
|
31734
|
+
await syncRunBatches(newRuns, repoPath, runsTarget, () => repoId, dryRun, async (batch) => {
|
|
31735
|
+
await postJson(`${apiUrl}/api/repos/${repoId}/runs`, SyncRunsInputSchema.parse({ runs: batch }), dryRun);
|
|
31736
|
+
});
|
|
31571
31737
|
const status = collectEnvironmentStatus(repoPath);
|
|
31572
31738
|
const slotsBody = SyncSlotsInputSchema.parse({
|
|
31573
31739
|
slots: status.slots,
|
|
@@ -31722,7 +31888,7 @@ async function syncRunWithControlAsync(repoPath, run2) {
|
|
|
31722
31888
|
`);
|
|
31723
31889
|
}
|
|
31724
31890
|
}
|
|
31725
|
-
var path27, SYNC_TIMEOUT_MS;
|
|
31891
|
+
var path27, DEFAULT_MAX_BATCH_BYTES, DEFAULT_SYNC_OVERLAP_MS, MAX_FETCH_ATTEMPTS, SYNC_TIMEOUT_MS, EXPLICIT_SYNC_TIMEOUT_MS;
|
|
31726
31892
|
var init_control_sync = __esm({
|
|
31727
31893
|
"src/core/control-sync.ts"() {
|
|
31728
31894
|
"use strict";
|
|
@@ -31746,7 +31912,11 @@ var init_control_sync = __esm({
|
|
|
31746
31912
|
init_portal_usage_merge();
|
|
31747
31913
|
init_schema2();
|
|
31748
31914
|
init_portal_watermark();
|
|
31915
|
+
DEFAULT_MAX_BATCH_BYTES = 4 * 1024 * 1024;
|
|
31916
|
+
DEFAULT_SYNC_OVERLAP_MS = 6e4;
|
|
31917
|
+
MAX_FETCH_ATTEMPTS = 3;
|
|
31749
31918
|
SYNC_TIMEOUT_MS = 15e3;
|
|
31919
|
+
EXPLICIT_SYNC_TIMEOUT_MS = 12e4;
|
|
31750
31920
|
}
|
|
31751
31921
|
});
|
|
31752
31922
|
|
|
@@ -81407,6 +81577,8 @@ async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
|
|
|
81407
81577
|
const returnedState = requestUrl.searchParams.get("state");
|
|
81408
81578
|
const workspace = requestUrl.searchParams.get("workspace") ?? void 0;
|
|
81409
81579
|
const email2 = requestUrl.searchParams.get("email") ?? void 0;
|
|
81580
|
+
const refreshToken = requestUrl.searchParams.get("refreshToken") ?? void 0;
|
|
81581
|
+
const expiresAt = requestUrl.searchParams.get("expiresAt") ?? void 0;
|
|
81410
81582
|
if (returnedState !== state || !token) {
|
|
81411
81583
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
81412
81584
|
res.end("<h1>Login failed</h1><p>You can close this tab and try again.</p>");
|
|
@@ -81417,7 +81589,15 @@ async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
|
|
|
81417
81589
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
81418
81590
|
res.end("<h1>Logged in</h1><p>You can close this tab and return to the terminal.</p>");
|
|
81419
81591
|
cleanup();
|
|
81420
|
-
resolve47({
|
|
81592
|
+
resolve47({
|
|
81593
|
+
portalUrl,
|
|
81594
|
+
token,
|
|
81595
|
+
workspace,
|
|
81596
|
+
email: email2,
|
|
81597
|
+
refreshToken,
|
|
81598
|
+
expiresAt,
|
|
81599
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
81600
|
+
});
|
|
81421
81601
|
});
|
|
81422
81602
|
const cleanup = () => {
|
|
81423
81603
|
clearTimeout(timer);
|