@rallycry/conveyor-agent 9.1.0 → 10.0.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/{chunk-P3QCTUHC.js → chunk-QKRPJ7DB.js} +1065 -197
- package/dist/chunk-QKRPJ7DB.js.map +1 -0
- package/dist/cli.js +174 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +113 -28
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/runtime/entrypoint.sh +627 -0
- package/dist/chunk-P3QCTUHC.js.map +0 -1
|
@@ -116,6 +116,44 @@ function applyBootstrapToEnv(config) {
|
|
|
116
116
|
|
|
117
117
|
// src/connection/agent-connection.ts
|
|
118
118
|
import { io } from "socket.io-client";
|
|
119
|
+
|
|
120
|
+
// src/setup/bootstrap-poll.ts
|
|
121
|
+
var PollUntilBoundHttpError = class extends Error {
|
|
122
|
+
constructor(status) {
|
|
123
|
+
super(`pollUntilBound got unexpected status ${status}`);
|
|
124
|
+
this.status = status;
|
|
125
|
+
this.name = "PollUntilBoundHttpError";
|
|
126
|
+
}
|
|
127
|
+
status;
|
|
128
|
+
};
|
|
129
|
+
async function sleep2(ms) {
|
|
130
|
+
await new Promise((resolve) => {
|
|
131
|
+
setTimeout(resolve, ms);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async function pollUntilBound(opts) {
|
|
135
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 2e3;
|
|
136
|
+
const maxWaitMs = opts.maxWaitMs ?? 30 * 60 * 1e3;
|
|
137
|
+
const deadline = Date.now() + maxWaitMs;
|
|
138
|
+
while (true) {
|
|
139
|
+
const response = await fetch(`${opts.apiUrl}/api/v3/pods/bootstrap`, {
|
|
140
|
+
headers: { Authorization: `Bearer ${opts.bootstrapToken}` }
|
|
141
|
+
});
|
|
142
|
+
if (response.status === 200) {
|
|
143
|
+
return await response.json();
|
|
144
|
+
}
|
|
145
|
+
if (response.status === 204) {
|
|
146
|
+
if (Date.now() >= deadline) {
|
|
147
|
+
throw new Error(`pollUntilBound timed out after ${maxWaitMs}ms waiting for pod bind`);
|
|
148
|
+
}
|
|
149
|
+
await sleep2(pollIntervalMs);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
throw new PollUntilBoundHttpError(response.status);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/connection/agent-connection.ts
|
|
119
157
|
var EVENT_BATCH_MS = 500;
|
|
120
158
|
var MAX_EVENT_BUFFER = 5e3;
|
|
121
159
|
var TOKEN_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
@@ -147,6 +185,7 @@ var AgentConnection = class _AgentConnection {
|
|
|
147
185
|
modeChangeCallback = null;
|
|
148
186
|
apiKeyUpdateCallback = null;
|
|
149
187
|
pullBranchCallback = null;
|
|
188
|
+
finalizeSnapshotCallback = null;
|
|
150
189
|
earlyPullBranches = [];
|
|
151
190
|
// PTY relay (S5 terminal). Single-slot callbacks, set per PtySession run.
|
|
152
191
|
ptyInputCallback = null;
|
|
@@ -301,6 +340,9 @@ var AgentConnection = class _AgentConnection {
|
|
|
301
340
|
if (this.pullBranchCallback) this.pullBranchCallback(data);
|
|
302
341
|
else this.earlyPullBranches.push(data);
|
|
303
342
|
});
|
|
343
|
+
this.socket.on("session:finalizeSnapshot", () => {
|
|
344
|
+
this.finalizeSnapshotCallback?.();
|
|
345
|
+
});
|
|
304
346
|
this.socket.on("pty:input", (data) => {
|
|
305
347
|
if (data.sessionId && data.sessionId !== this.config.sessionId) return;
|
|
306
348
|
this.ptyInputCallback?.(data.data);
|
|
@@ -378,6 +420,14 @@ var AgentConnection = class _AgentConnection {
|
|
|
378
420
|
static RECONNECT_MAX_DELAY_MS = 6e4;
|
|
379
421
|
static RECONNECT_STATUS_EVERY_N = 3;
|
|
380
422
|
isReconnecting = false;
|
|
423
|
+
/**
|
|
424
|
+
* Invoked after every successful session reconnect (the `connectAgent` RPC
|
|
425
|
+
* re-established the session room). The runner uses this to force a TUI
|
|
426
|
+
* repaint: the reconnect may have landed on a different/restarted API
|
|
427
|
+
* process whose PTY scrollback ring is empty, and a quiet terminal would
|
|
428
|
+
* otherwise never re-seed it.
|
|
429
|
+
*/
|
|
430
|
+
onReconnected;
|
|
381
431
|
async reconnectToSession() {
|
|
382
432
|
if (this.isReconnecting) return;
|
|
383
433
|
this.isReconnecting = true;
|
|
@@ -409,6 +459,10 @@ var AgentConnection = class _AgentConnection {
|
|
|
409
459
|
reason: "reconnected",
|
|
410
460
|
attempts: attempt
|
|
411
461
|
});
|
|
462
|
+
try {
|
|
463
|
+
this.onReconnected?.();
|
|
464
|
+
} catch {
|
|
465
|
+
}
|
|
412
466
|
return;
|
|
413
467
|
} catch (err) {
|
|
414
468
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -511,6 +565,9 @@ var AgentConnection = class _AgentConnection {
|
|
|
511
565
|
for (const data of this.earlyPullBranches) callback(data);
|
|
512
566
|
this.earlyPullBranches = [];
|
|
513
567
|
}
|
|
568
|
+
onFinalizeSnapshot(callback) {
|
|
569
|
+
this.finalizeSnapshotCallback = callback;
|
|
570
|
+
}
|
|
514
571
|
// ── PTY relay (S5 Connected-TUI terminal) ──────────────────────────
|
|
515
572
|
/**
|
|
516
573
|
* Forward a raw chunk of terminal output to the S2 relay (fire-and-forget).
|
|
@@ -659,6 +716,12 @@ var AgentConnection = class _AgentConnection {
|
|
|
659
716
|
}
|
|
660
717
|
);
|
|
661
718
|
}
|
|
719
|
+
/** Report the full current set of runtime-discovered listening ports.
|
|
720
|
+
* Throws on failure so the PortDiscovery poller can retry on its next
|
|
721
|
+
* tick (a swallowed error here would silently drop the delta). */
|
|
722
|
+
async reportDiscoveredPorts(ports) {
|
|
723
|
+
await this.call("reportDiscoveredPorts", { sessionId: this.config.sessionId, ports });
|
|
724
|
+
}
|
|
662
725
|
// ── Typing indicators ───────────────────────────────────────────────
|
|
663
726
|
sendTypingStart() {
|
|
664
727
|
this.sendEvent({ type: "agent_typing_start" });
|
|
@@ -667,17 +730,6 @@ var AgentConnection = class _AgentConnection {
|
|
|
667
730
|
this.sendEvent({ type: "agent_typing_stop" });
|
|
668
731
|
}
|
|
669
732
|
// ── RPC convenience wrappers (v6 compat, will migrate to call()) ───
|
|
670
|
-
trackSpending(params) {
|
|
671
|
-
if (!this.socket) return;
|
|
672
|
-
void this.call("trackSpending", {
|
|
673
|
-
sessionId: this.config.sessionId,
|
|
674
|
-
inputTokens: params.modelUsage?.reduce((s, m) => s + (m.inputTokens ?? 0), 0) ?? 0,
|
|
675
|
-
outputTokens: params.modelUsage?.reduce((s, m) => s + (m.outputTokens ?? 0), 0) ?? 0,
|
|
676
|
-
costUsd: params.totalCostUsd,
|
|
677
|
-
model: params.modelUsage?.[0]?.model ?? "unknown"
|
|
678
|
-
}).catch(() => {
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
733
|
emitRateLimitPause(resetsAt) {
|
|
682
734
|
this.sendEvent({ type: "rate_limit_update", resetsAt });
|
|
683
735
|
}
|
|
@@ -722,9 +774,6 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
|
|
|
722
774
|
triggerIdentification() {
|
|
723
775
|
return this.call("triggerIdentification", { sessionId: this.config.sessionId });
|
|
724
776
|
}
|
|
725
|
-
getCumulativeSpending() {
|
|
726
|
-
return this.call("getCumulativeSpending", { sessionId: this.config.sessionId });
|
|
727
|
-
}
|
|
728
777
|
async refreshAuthToken() {
|
|
729
778
|
const result = await this.refreshFromBootstrap();
|
|
730
779
|
return result.refreshedClaude;
|
|
@@ -740,15 +789,27 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
|
|
|
740
789
|
const result = await this.refreshFromBootstrap();
|
|
741
790
|
return result.refreshedTaskToken;
|
|
742
791
|
}
|
|
743
|
-
|
|
792
|
+
refreshFromBootstrap() {
|
|
793
|
+
const none = Promise.resolve({ refreshedClaude: false, refreshedTaskToken: false });
|
|
794
|
+
const podBootstrapToken = process.env.POD_BOOTSTRAP_TOKEN;
|
|
744
795
|
const codespaceName = process.env.CODESPACE_NAME || process.env.CLAUDESPACE_NAME;
|
|
745
796
|
const apiUrl = this.config.apiUrl;
|
|
746
|
-
if (!
|
|
797
|
+
if (!apiUrl || !podBootstrapToken && !codespaceName) {
|
|
798
|
+
return none;
|
|
799
|
+
}
|
|
747
800
|
const now = Date.now();
|
|
748
801
|
if (now - this.lastTaskTokenRefreshAt < 6e4) {
|
|
749
|
-
return
|
|
802
|
+
return none;
|
|
750
803
|
}
|
|
751
804
|
this.lastTaskTokenRefreshAt = now;
|
|
805
|
+
if (podBootstrapToken) {
|
|
806
|
+
return this.refreshFromV3Bootstrap(apiUrl, podBootstrapToken);
|
|
807
|
+
}
|
|
808
|
+
if (!codespaceName) return none;
|
|
809
|
+
return this.refreshFromCodespaceBootstrap(apiUrl, codespaceName);
|
|
810
|
+
}
|
|
811
|
+
/** Legacy GitHub Codespaces refresh path — keys on instance name. */
|
|
812
|
+
async refreshFromCodespaceBootstrap(apiUrl, codespaceName) {
|
|
752
813
|
const bootstrapToken = process.env.CONVEYOR_BOOTSTRAP_TOKEN;
|
|
753
814
|
const result = await fetchBootstrap({
|
|
754
815
|
apiUrl,
|
|
@@ -774,6 +835,61 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
|
|
|
774
835
|
const refreshedClaude = Boolean(result.config.envVars?.CLAUDE_CODE_OAUTH_TOKEN);
|
|
775
836
|
return { refreshedClaude, refreshedTaskToken };
|
|
776
837
|
}
|
|
838
|
+
/**
|
|
839
|
+
* v3 refresh: re-fetch the full bootstrap bundle from the pod's bound v3
|
|
840
|
+
* route and swap the credentials in place. The GitHub installation token
|
|
841
|
+
* dies at ~1h and the sessionJwt at 24h; re-polling the bootstrap GET with
|
|
842
|
+
* the same pod token is the designed refresh mechanism.
|
|
843
|
+
*/
|
|
844
|
+
async refreshFromV3Bootstrap(apiUrl, bootstrapToken) {
|
|
845
|
+
const bundle = await this.pollBundleWithRateLimitRetry(apiUrl, bootstrapToken);
|
|
846
|
+
if (!bundle) {
|
|
847
|
+
return { refreshedClaude: false, refreshedTaskToken: false };
|
|
848
|
+
}
|
|
849
|
+
const previousTaskToken = process.env.CONVEYOR_TASK_TOKEN;
|
|
850
|
+
for (const [key, value] of Object.entries(bundle.envVars ?? {})) {
|
|
851
|
+
process.env[key] = value;
|
|
852
|
+
}
|
|
853
|
+
if (bundle.githubToken) process.env.CONVEYOR_GITHUB_TOKEN = bundle.githubToken;
|
|
854
|
+
if (bundle.anthropicKey) process.env.ANTHROPIC_API_KEY = bundle.anthropicKey;
|
|
855
|
+
if (bundle.gcpToken) process.env.CLOUDSDK_AUTH_ACCESS_TOKEN = bundle.gcpToken;
|
|
856
|
+
const refreshedTaskToken = Boolean(bundle.sessionJwt) && bundle.sessionJwt !== previousTaskToken;
|
|
857
|
+
if (refreshedTaskToken) {
|
|
858
|
+
process.env.CONVEYOR_TASK_TOKEN = bundle.sessionJwt;
|
|
859
|
+
this.config.taskToken = bundle.sessionJwt;
|
|
860
|
+
if (this.socket) {
|
|
861
|
+
const auth = this.socket.auth;
|
|
862
|
+
if (auth && typeof auth === "object") {
|
|
863
|
+
auth.taskToken = bundle.sessionJwt;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const refreshedClaude = Boolean(bundle.envVars?.CLAUDE_CODE_OAUTH_TOKEN);
|
|
868
|
+
return { refreshedClaude, refreshedTaskToken };
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Poll the bootstrap bundle once (maxWaitMs 0), retrying only on a transient
|
|
872
|
+
* 429 (standby-pool pods share one Cloud NAT IP against podBootstrapLimiter)
|
|
873
|
+
* with a short backoff. Returns null when the refresh should be abandoned —
|
|
874
|
+
* a non-429 error, or 429s past the retry budget — so the caller no-ops
|
|
875
|
+
* instead of parking a RUNNING pod in a poll loop.
|
|
876
|
+
*/
|
|
877
|
+
async pollBundleWithRateLimitRetry(apiUrl, bootstrapToken) {
|
|
878
|
+
const retryDelaysMs = [1e3, 3e3];
|
|
879
|
+
for (let attempt = 0; ; attempt++) {
|
|
880
|
+
try {
|
|
881
|
+
return await pollUntilBound({ apiUrl, bootstrapToken, maxWaitMs: 0 });
|
|
882
|
+
} catch (err) {
|
|
883
|
+
const isRateLimited = err instanceof PollUntilBoundHttpError && err.status === 429;
|
|
884
|
+
if (!isRateLimited || attempt >= retryDelaysMs.length) {
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
await new Promise((resolve) => {
|
|
888
|
+
setTimeout(resolve, retryDelaysMs[attempt]);
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
777
893
|
// ── Event buffering ────────────────────────────────────────────────
|
|
778
894
|
sendEvent(event) {
|
|
779
895
|
if (!this.socket) return;
|
|
@@ -1358,6 +1474,13 @@ function preToolUse(payload) {
|
|
|
1358
1474
|
);
|
|
1359
1475
|
process.exit(0);
|
|
1360
1476
|
};
|
|
1477
|
+
// MCP tools are auto-allowed locally \u2014 no socket round trip, no fail-mode.
|
|
1478
|
+
// Plan mode prompts for them despite permissions.allow (the CLI can't
|
|
1479
|
+
// classify MCP tools as read-only), and the pod is the sandbox.
|
|
1480
|
+
if (typeof payload.tool_name === "string" && payload.tool_name.startsWith("mcp__")) {
|
|
1481
|
+
respond("allow");
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1361
1484
|
// AskUserQuestion is observe-only (status reporting) \u2014 fail OPEN so a
|
|
1362
1485
|
// missing/slow socket never denies the questionnaire. Everything else
|
|
1363
1486
|
// (ExitPlanMode) is a Conveyor gate \u2014 fail CLOSED.
|
|
@@ -1461,6 +1584,15 @@ async function writeHookSettings(dir) {
|
|
|
1461
1584
|
// tool, so the questionnaire is never blocked by Conveyor.
|
|
1462
1585
|
matcher: "AskUserQuestion",
|
|
1463
1586
|
hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 30 }]
|
|
1587
|
+
},
|
|
1588
|
+
{
|
|
1589
|
+
// Plan-permission spawns prompt for MCP tools even when they're in
|
|
1590
|
+
// permissions.allow — the CLI can't classify them as read-only. A
|
|
1591
|
+
// hook allow bypasses the permission engine in every mode. Loose by
|
|
1592
|
+
// design: the pod is the sandbox, and planning agents must call
|
|
1593
|
+
// update_task etc. The helper answers locally (no socket).
|
|
1594
|
+
matcher: "mcp__.*",
|
|
1595
|
+
hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 30 }]
|
|
1464
1596
|
}
|
|
1465
1597
|
],
|
|
1466
1598
|
PostToolUse: [
|
|
@@ -1499,8 +1631,8 @@ var PlanSync = class {
|
|
|
1499
1631
|
for (const file of readdirSync(plansDir).filter((f) => f.endsWith(".md"))) {
|
|
1500
1632
|
try {
|
|
1501
1633
|
const fullPath = join2(plansDir, file);
|
|
1502
|
-
const
|
|
1503
|
-
this.planFileSnapshot.set(fullPath,
|
|
1634
|
+
const stat6 = statSync(fullPath);
|
|
1635
|
+
this.planFileSnapshot.set(fullPath, stat6.mtimeMs);
|
|
1504
1636
|
} catch {
|
|
1505
1637
|
continue;
|
|
1506
1638
|
}
|
|
@@ -1521,11 +1653,11 @@ var PlanSync = class {
|
|
|
1521
1653
|
for (const file of files) {
|
|
1522
1654
|
const fullPath = join2(plansDir, file);
|
|
1523
1655
|
try {
|
|
1524
|
-
const
|
|
1656
|
+
const stat6 = statSync(fullPath);
|
|
1525
1657
|
const prevMtime = this.planFileSnapshot.get(fullPath);
|
|
1526
|
-
const isNew = prevMtime === void 0 ||
|
|
1527
|
-
if (isNew && (!newest ||
|
|
1528
|
-
newest = { path: fullPath, mtime:
|
|
1658
|
+
const isNew = prevMtime === void 0 || stat6.mtimeMs > prevMtime;
|
|
1659
|
+
if (isNew && (!newest || stat6.mtimeMs > newest.mtime)) {
|
|
1660
|
+
newest = { path: fullPath, mtime: stat6.mtimeMs };
|
|
1529
1661
|
}
|
|
1530
1662
|
} catch {
|
|
1531
1663
|
continue;
|
|
@@ -1566,6 +1698,511 @@ var PlanSync = class {
|
|
|
1566
1698
|
}
|
|
1567
1699
|
};
|
|
1568
1700
|
|
|
1701
|
+
// src/setup/git-ready.ts
|
|
1702
|
+
import { access, stat } from "fs/promises";
|
|
1703
|
+
var DEFAULT_FAILED_PATH = "/workspaces/.conveyor-git-failed";
|
|
1704
|
+
var DEFAULT_TIMEOUT_MS = 6e5;
|
|
1705
|
+
var DEFAULT_POLL_MS = 200;
|
|
1706
|
+
async function fileExists(path4) {
|
|
1707
|
+
try {
|
|
1708
|
+
await access(path4);
|
|
1709
|
+
const s = await stat(path4);
|
|
1710
|
+
return s.isFile();
|
|
1711
|
+
} catch {
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
function awaitGitReady(opts = {}) {
|
|
1716
|
+
const markerPath = opts.markerPath ?? process.env.CONVEYOR_GIT_READY_MARKER;
|
|
1717
|
+
if (!markerPath) {
|
|
1718
|
+
return Promise.resolve("not-gated");
|
|
1719
|
+
}
|
|
1720
|
+
const failedPath = opts.failedPath ?? process.env.CONVEYOR_GIT_FAILED_MARKER ?? DEFAULT_FAILED_PATH;
|
|
1721
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1722
|
+
const pollMs = opts.pollMs ?? DEFAULT_POLL_MS;
|
|
1723
|
+
opts.onLog?.(`waiting for workspace git (marker=${markerPath})`);
|
|
1724
|
+
return pollForMarkers(markerPath, failedPath, timeoutMs, pollMs, opts.onLog);
|
|
1725
|
+
}
|
|
1726
|
+
async function pollForMarkers(markerPath, failedPath, timeoutMs, pollMs, onLog) {
|
|
1727
|
+
const deadline = Date.now() + timeoutMs;
|
|
1728
|
+
for (; ; ) {
|
|
1729
|
+
if (await fileExists(markerPath)) {
|
|
1730
|
+
onLog?.("workspace git ready");
|
|
1731
|
+
return "ready";
|
|
1732
|
+
}
|
|
1733
|
+
if (await fileExists(failedPath)) {
|
|
1734
|
+
onLog?.("workspace git preparation failed (marker present)");
|
|
1735
|
+
return "failed";
|
|
1736
|
+
}
|
|
1737
|
+
if (Date.now() >= deadline) {
|
|
1738
|
+
onLog?.(`workspace git not ready after ${timeoutMs}ms \u2014 giving up`);
|
|
1739
|
+
return "timeout";
|
|
1740
|
+
}
|
|
1741
|
+
await new Promise((resolve) => {
|
|
1742
|
+
setTimeout(resolve, pollMs);
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// src/runner/work-preservation/index.ts
|
|
1748
|
+
import { rm as rm3 } from "fs/promises";
|
|
1749
|
+
|
|
1750
|
+
// src/runner/work-preservation/pg-dump.ts
|
|
1751
|
+
import { execSync as execSync2 } from "child_process";
|
|
1752
|
+
import { randomUUID } from "crypto";
|
|
1753
|
+
import { tmpdir } from "os";
|
|
1754
|
+
import path from "path";
|
|
1755
|
+
var PG_DUMP_FILENAME = ".conveyor-pgdump.sql";
|
|
1756
|
+
function connectionDefaults(opts) {
|
|
1757
|
+
return {
|
|
1758
|
+
host: opts.host ?? "localhost",
|
|
1759
|
+
port: opts.port ?? 5432,
|
|
1760
|
+
db: opts.db ?? "conveyor"
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
function buildPgDumpCommand(opts) {
|
|
1764
|
+
const { host, port, db } = connectionDefaults(opts);
|
|
1765
|
+
return `pg_dump -h ${host} -p ${port} -U postgres ${db} -f ${JSON.stringify(
|
|
1766
|
+
opts.outputPath
|
|
1767
|
+
)} --no-owner --no-privileges`;
|
|
1768
|
+
}
|
|
1769
|
+
function buildPsqlRestoreCommand(opts) {
|
|
1770
|
+
const { host, port, db } = connectionDefaults(opts);
|
|
1771
|
+
return `psql -h ${host} -p ${port} -U postgres -d ${db} -f ${JSON.stringify(opts.dumpPath)}`;
|
|
1772
|
+
}
|
|
1773
|
+
function hasPostgresDep() {
|
|
1774
|
+
return (process.env.CONVEYOR_DEPS ?? "").split(",").map((dep) => dep.trim()).includes("postgresql");
|
|
1775
|
+
}
|
|
1776
|
+
function execErrorDetail(err) {
|
|
1777
|
+
const withStreams = err;
|
|
1778
|
+
const stderr = withStreams.stderr instanceof Buffer ? withStreams.stderr.toString("utf8").trim() : typeof withStreams.stderr === "string" ? withStreams.stderr.trim() : "";
|
|
1779
|
+
const message = withStreams.message ?? String(err);
|
|
1780
|
+
return stderr ? `${message} \u2014 stderr: ${stderr}` : message;
|
|
1781
|
+
}
|
|
1782
|
+
function dumpPostgresIfPresent(exec = execSync2) {
|
|
1783
|
+
if (!hasPostgresDep()) return Promise.resolve({ dumped: false });
|
|
1784
|
+
const outputPath = path.join(tmpdir(), `conveyor-pgdump-${randomUUID()}.sql`);
|
|
1785
|
+
try {
|
|
1786
|
+
exec(buildPgDumpCommand({ outputPath }), {
|
|
1787
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1788
|
+
timeout: 6e4
|
|
1789
|
+
});
|
|
1790
|
+
return Promise.resolve({ dumped: true, path: outputPath });
|
|
1791
|
+
} catch (err) {
|
|
1792
|
+
process.stderr.write(`[conveyor-agent] pg_dump failed: ${execErrorDetail(err)}
|
|
1793
|
+
`);
|
|
1794
|
+
return Promise.resolve({ dumped: false });
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
function applyPgDumpIfConfigured(dumpPath, exec = execSync2) {
|
|
1798
|
+
if (!hasPostgresDep()) return false;
|
|
1799
|
+
try {
|
|
1800
|
+
exec(buildPsqlRestoreCommand({ dumpPath }), {
|
|
1801
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1802
|
+
timeout: 12e4
|
|
1803
|
+
});
|
|
1804
|
+
process.stderr.write("[conveyor-agent] Restored postgres state from snapshot pg dump\n");
|
|
1805
|
+
return true;
|
|
1806
|
+
} catch (err) {
|
|
1807
|
+
process.stderr.write(`[conveyor-agent] psql dump restore failed: ${execErrorDetail(err)}
|
|
1808
|
+
`);
|
|
1809
|
+
return false;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// src/runner/work-preservation/restore-precedence.ts
|
|
1814
|
+
function selectRestoreSource(probe) {
|
|
1815
|
+
if (probe.gcsAvailable) return "gcs";
|
|
1816
|
+
if (probe.wipRestoreResult === "applied") return "wip";
|
|
1817
|
+
return "clean";
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// src/runner/work-preservation/snapshot-tar.ts
|
|
1821
|
+
import { execSync as execSync3 } from "child_process";
|
|
1822
|
+
import { createReadStream, createWriteStream, mkdtempSync } from "fs";
|
|
1823
|
+
import { copyFile, mkdtemp, rm, stat as stat2, writeFile as writeFile2 } from "fs/promises";
|
|
1824
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1825
|
+
import path2 from "path";
|
|
1826
|
+
import { pipeline } from "stream/promises";
|
|
1827
|
+
import { createGzip } from "zlib";
|
|
1828
|
+
import * as tar from "tar";
|
|
1829
|
+
|
|
1830
|
+
// src/runner/work-preservation/snapshot-artifact.ts
|
|
1831
|
+
function planSnapshotFiles(git) {
|
|
1832
|
+
const include = git.trackedAndUntracked();
|
|
1833
|
+
const includeSet = new Set(include);
|
|
1834
|
+
const deletions = [...new Set(git.deletedSinceHead())].filter((path4) => !includeSet.has(path4));
|
|
1835
|
+
return { include, deletions };
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// src/runner/work-preservation/snapshot-tar.ts
|
|
1839
|
+
var SNAPSHOT_MANIFEST_NAME = ".conveyor-snapshot-manifest.json";
|
|
1840
|
+
var STATUS_MAX_BUFFER = 64 * 1024 * 1024;
|
|
1841
|
+
function parseStatusPorcelainZ(raw) {
|
|
1842
|
+
const includes = [];
|
|
1843
|
+
const deletions = [];
|
|
1844
|
+
const tokens = raw.split("\0");
|
|
1845
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1846
|
+
const token = tokens[i];
|
|
1847
|
+
if (!token || token.length < 4) continue;
|
|
1848
|
+
const x = token[0];
|
|
1849
|
+
const y = token[1];
|
|
1850
|
+
const filePath = token.slice(3);
|
|
1851
|
+
if (x === "R" || x === "C") {
|
|
1852
|
+
const origPath = tokens[++i];
|
|
1853
|
+
if (x === "R" && origPath) deletions.push(origPath);
|
|
1854
|
+
}
|
|
1855
|
+
if (x === "!") continue;
|
|
1856
|
+
const missingFromWorktree = y === "D" || x === "D" && y === " ";
|
|
1857
|
+
if (missingFromWorktree) deletions.push(filePath);
|
|
1858
|
+
else includes.push(filePath);
|
|
1859
|
+
}
|
|
1860
|
+
return { includes, deletions };
|
|
1861
|
+
}
|
|
1862
|
+
function realGitSurface(cwd) {
|
|
1863
|
+
let cached = null;
|
|
1864
|
+
const load = () => {
|
|
1865
|
+
if (!cached) {
|
|
1866
|
+
const out = execSync3("git status --porcelain=v1 -z -uall", {
|
|
1867
|
+
cwd,
|
|
1868
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1869
|
+
maxBuffer: STATUS_MAX_BUFFER
|
|
1870
|
+
}).toString("utf8");
|
|
1871
|
+
cached = parseStatusPorcelainZ(out);
|
|
1872
|
+
}
|
|
1873
|
+
return cached;
|
|
1874
|
+
};
|
|
1875
|
+
return {
|
|
1876
|
+
trackedAndUntracked: () => load().includes,
|
|
1877
|
+
deletedSinceHead: () => load().deletions
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
function gitHead(cwd) {
|
|
1881
|
+
try {
|
|
1882
|
+
return execSync3("git rev-parse HEAD", { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim() || null;
|
|
1883
|
+
} catch {
|
|
1884
|
+
return null;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
async function buildSnapshotTar(cwd, opts) {
|
|
1888
|
+
const staging = await mkdtemp(path2.join(tmpdir2(), "conveyor-snapshot-"));
|
|
1889
|
+
const cleanup = async () => {
|
|
1890
|
+
await rm(staging, { recursive: true, force: true }).catch(() => {
|
|
1891
|
+
});
|
|
1892
|
+
};
|
|
1893
|
+
try {
|
|
1894
|
+
const plan = planSnapshotFiles(realGitSurface(cwd));
|
|
1895
|
+
const head = gitHead(cwd);
|
|
1896
|
+
if (!head) throw new Error("cannot snapshot a repo without a resolvable HEAD");
|
|
1897
|
+
const capturedAt = Date.now();
|
|
1898
|
+
const manifest = {
|
|
1899
|
+
head,
|
|
1900
|
+
deletions: plan.deletions,
|
|
1901
|
+
capturedAt,
|
|
1902
|
+
pgDump: Boolean(opts?.pgDumpPath)
|
|
1903
|
+
};
|
|
1904
|
+
await writeFile2(path2.join(staging, SNAPSHOT_MANIFEST_NAME), JSON.stringify(manifest), "utf8");
|
|
1905
|
+
const stagedEntries = [SNAPSHOT_MANIFEST_NAME];
|
|
1906
|
+
if (opts?.pgDumpPath) {
|
|
1907
|
+
await copyFile(opts.pgDumpPath, path2.join(staging, PG_DUMP_FILENAME));
|
|
1908
|
+
stagedEntries.push(PG_DUMP_FILENAME);
|
|
1909
|
+
}
|
|
1910
|
+
const rawTarPath = path2.join(staging, "snapshot.tar");
|
|
1911
|
+
await tar.create({ cwd: staging, file: rawTarPath, portable: true }, stagedEntries);
|
|
1912
|
+
if (plan.include.length > 0) {
|
|
1913
|
+
await tar.replace({ file: rawTarPath, cwd, portable: true }, plan.include);
|
|
1914
|
+
}
|
|
1915
|
+
const tarPath = path2.join(staging, "snapshot.tar.gz");
|
|
1916
|
+
await pipeline(createReadStream(rawTarPath), createGzip(), createWriteStream(tarPath));
|
|
1917
|
+
await rm(rawTarPath, { force: true });
|
|
1918
|
+
const { size } = await stat2(tarPath);
|
|
1919
|
+
return {
|
|
1920
|
+
tarPath,
|
|
1921
|
+
sizeBytes: size,
|
|
1922
|
+
fileCount: plan.include.length,
|
|
1923
|
+
deletionCount: plan.deletions.length,
|
|
1924
|
+
capturedAt,
|
|
1925
|
+
cleanup
|
|
1926
|
+
};
|
|
1927
|
+
} catch (err) {
|
|
1928
|
+
await cleanup();
|
|
1929
|
+
throw err;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
function isWithinWorkspace(cwd, rel) {
|
|
1933
|
+
if (!rel || path2.isAbsolute(rel)) return false;
|
|
1934
|
+
const root = path2.resolve(cwd);
|
|
1935
|
+
const abs = path2.resolve(root, rel);
|
|
1936
|
+
return abs !== root && abs.startsWith(root + path2.sep);
|
|
1937
|
+
}
|
|
1938
|
+
async function readSnapshotArchive(tarPath) {
|
|
1939
|
+
let manifestRaw = null;
|
|
1940
|
+
let pgDumpDir = null;
|
|
1941
|
+
let pgDumpPath;
|
|
1942
|
+
let pgDumpWritten = Promise.resolve();
|
|
1943
|
+
const cleanup = async () => {
|
|
1944
|
+
if (pgDumpDir) await rm(pgDumpDir, { recursive: true, force: true }).catch(() => {
|
|
1945
|
+
});
|
|
1946
|
+
};
|
|
1947
|
+
try {
|
|
1948
|
+
await tar.list({
|
|
1949
|
+
file: tarPath,
|
|
1950
|
+
onReadEntry: (entry) => {
|
|
1951
|
+
if (entry.path === SNAPSHOT_MANIFEST_NAME) {
|
|
1952
|
+
const chunks = [];
|
|
1953
|
+
entry.on("data", (chunk) => chunks.push(chunk));
|
|
1954
|
+
entry.on("end", () => {
|
|
1955
|
+
manifestRaw = Buffer.concat(chunks);
|
|
1956
|
+
});
|
|
1957
|
+
} else if (entry.path === PG_DUMP_FILENAME) {
|
|
1958
|
+
pgDumpDir = mkdtempSync(path2.join(tmpdir2(), "conveyor-pgdump-restore-"));
|
|
1959
|
+
pgDumpPath = path2.join(pgDumpDir, "dump.sql");
|
|
1960
|
+
const dest = createWriteStream(pgDumpPath);
|
|
1961
|
+
pgDumpWritten = new Promise((resolve, reject) => {
|
|
1962
|
+
dest.on("finish", () => resolve());
|
|
1963
|
+
dest.on("error", reject);
|
|
1964
|
+
});
|
|
1965
|
+
entry.pipe(dest);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
await pgDumpWritten;
|
|
1970
|
+
} catch {
|
|
1971
|
+
await cleanup();
|
|
1972
|
+
return null;
|
|
1973
|
+
}
|
|
1974
|
+
if (!manifestRaw) {
|
|
1975
|
+
await cleanup();
|
|
1976
|
+
return null;
|
|
1977
|
+
}
|
|
1978
|
+
try {
|
|
1979
|
+
const parsed = JSON.parse(manifestRaw.toString("utf8"));
|
|
1980
|
+
if (typeof parsed.head !== "string" || !parsed.head) {
|
|
1981
|
+
await cleanup();
|
|
1982
|
+
return null;
|
|
1983
|
+
}
|
|
1984
|
+
return {
|
|
1985
|
+
manifest: {
|
|
1986
|
+
head: parsed.head,
|
|
1987
|
+
deletions: Array.isArray(parsed.deletions) ? parsed.deletions : [],
|
|
1988
|
+
capturedAt: typeof parsed.capturedAt === "number" ? parsed.capturedAt : 0,
|
|
1989
|
+
pgDump: parsed.pgDump === true
|
|
1990
|
+
},
|
|
1991
|
+
...pgDumpPath ? { pgDumpPath } : {},
|
|
1992
|
+
cleanup
|
|
1993
|
+
};
|
|
1994
|
+
} catch {
|
|
1995
|
+
await cleanup();
|
|
1996
|
+
return null;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
async function extractSnapshotTar(cwd, tarPath, opts) {
|
|
2000
|
+
const read = await readSnapshotArchive(tarPath);
|
|
2001
|
+
if (!read) return { status: "invalid" };
|
|
2002
|
+
try {
|
|
2003
|
+
const { manifest, pgDumpPath } = read;
|
|
2004
|
+
const currentHead = gitHead(cwd);
|
|
2005
|
+
if (manifest.head !== currentHead) {
|
|
2006
|
+
return { status: "stale-head", snapshotHead: manifest.head, currentHead };
|
|
2007
|
+
}
|
|
2008
|
+
let filesExtracted = 0;
|
|
2009
|
+
await tar.extract({
|
|
2010
|
+
file: tarPath,
|
|
2011
|
+
cwd,
|
|
2012
|
+
filter: (entryPath) => {
|
|
2013
|
+
if (entryPath === SNAPSHOT_MANIFEST_NAME || entryPath === PG_DUMP_FILENAME) return false;
|
|
2014
|
+
filesExtracted++;
|
|
2015
|
+
return true;
|
|
2016
|
+
}
|
|
2017
|
+
});
|
|
2018
|
+
let deletionsApplied = 0;
|
|
2019
|
+
for (const rel of manifest.deletions) {
|
|
2020
|
+
if (typeof rel !== "string" || !isWithinWorkspace(cwd, rel)) continue;
|
|
2021
|
+
await rm(path2.resolve(cwd, rel), { recursive: true, force: true });
|
|
2022
|
+
deletionsApplied++;
|
|
2023
|
+
}
|
|
2024
|
+
const pgDumpApplied = manifest.pgDump && pgDumpPath ? applyPgDumpIfConfigured(pgDumpPath, opts?.pgExec) : false;
|
|
2025
|
+
return { status: "extracted", filesExtracted, deletionsApplied, pgDumpApplied };
|
|
2026
|
+
} finally {
|
|
2027
|
+
await read.cleanup();
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// src/runner/work-preservation/snapshot-transfer.ts
|
|
2032
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2033
|
+
import { createReadStream as createReadStream2, createWriteStream as createWriteStream2 } from "fs";
|
|
2034
|
+
import { rm as rm2 } from "fs/promises";
|
|
2035
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
2036
|
+
import path3 from "path";
|
|
2037
|
+
import { Readable } from "stream";
|
|
2038
|
+
import { pipeline as pipeline2 } from "stream/promises";
|
|
2039
|
+
var SNAPSHOT_HTTP_TIMEOUT_MS = 6e4;
|
|
2040
|
+
var SNAPSHOT_CAPTURED_AT_HEADER = "x-snapshot-captured-at";
|
|
2041
|
+
async function putSnapshotToGcs(url, tarResult) {
|
|
2042
|
+
try {
|
|
2043
|
+
const body = Readable.toWeb(createReadStream2(tarResult.tarPath));
|
|
2044
|
+
const res = await fetch(url, {
|
|
2045
|
+
method: "PUT",
|
|
2046
|
+
headers: {
|
|
2047
|
+
"Content-Type": "application/gzip",
|
|
2048
|
+
"Content-Length": String(tarResult.sizeBytes),
|
|
2049
|
+
[SNAPSHOT_CAPTURED_AT_HEADER]: String(tarResult.capturedAt)
|
|
2050
|
+
},
|
|
2051
|
+
body,
|
|
2052
|
+
duplex: "half",
|
|
2053
|
+
signal: AbortSignal.timeout(SNAPSHOT_HTTP_TIMEOUT_MS)
|
|
2054
|
+
});
|
|
2055
|
+
return res.ok;
|
|
2056
|
+
} catch {
|
|
2057
|
+
return false;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
async function downloadSnapshotToTemp(url) {
|
|
2061
|
+
const tmpTar = path3.join(tmpdir3(), `conveyor-snapshot-restore-${randomUUID2()}.tar.gz`);
|
|
2062
|
+
try {
|
|
2063
|
+
const res = await fetch(url, {
|
|
2064
|
+
method: "GET",
|
|
2065
|
+
signal: AbortSignal.timeout(SNAPSHOT_HTTP_TIMEOUT_MS)
|
|
2066
|
+
});
|
|
2067
|
+
if (!res.ok || !res.body) return null;
|
|
2068
|
+
await pipeline2(
|
|
2069
|
+
Readable.fromWeb(res.body),
|
|
2070
|
+
createWriteStream2(tmpTar)
|
|
2071
|
+
);
|
|
2072
|
+
return tmpTar;
|
|
2073
|
+
} catch {
|
|
2074
|
+
await rm2(tmpTar, { force: true }).catch(() => {
|
|
2075
|
+
});
|
|
2076
|
+
return null;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// src/runner/work-preservation/index.ts
|
|
2081
|
+
var periodicTimer = null;
|
|
2082
|
+
var inFlightCapture = null;
|
|
2083
|
+
var inFlightGcsUpload = null;
|
|
2084
|
+
var EMPTY_GCS_RESULT = {
|
|
2085
|
+
uploaded: false,
|
|
2086
|
+
fileCount: 0,
|
|
2087
|
+
deletionCount: 0,
|
|
2088
|
+
sizeBytes: 0,
|
|
2089
|
+
pgDumped: false
|
|
2090
|
+
};
|
|
2091
|
+
async function doUploadSnapshotToGcs(cwd, uploadUrl, opts) {
|
|
2092
|
+
let tarResult;
|
|
2093
|
+
try {
|
|
2094
|
+
tarResult = await buildSnapshotTar(cwd, { pgDumpPath: opts?.pgDumpPath });
|
|
2095
|
+
} catch {
|
|
2096
|
+
return EMPTY_GCS_RESULT;
|
|
2097
|
+
}
|
|
2098
|
+
try {
|
|
2099
|
+
const uploaded = (opts?.forceUpload || tarResult.sizeBytes > 0) && uploadUrl ? await putSnapshotToGcs(uploadUrl, tarResult) : false;
|
|
2100
|
+
return {
|
|
2101
|
+
uploaded,
|
|
2102
|
+
fileCount: tarResult.fileCount,
|
|
2103
|
+
deletionCount: tarResult.deletionCount,
|
|
2104
|
+
sizeBytes: tarResult.sizeBytes,
|
|
2105
|
+
pgDumped: Boolean(opts?.pgDumpPath)
|
|
2106
|
+
};
|
|
2107
|
+
} finally {
|
|
2108
|
+
await tarResult.cleanup();
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
async function uploadSnapshotToGcs(cwd, uploadUrl, opts) {
|
|
2112
|
+
const previous = inFlightGcsUpload;
|
|
2113
|
+
const run = (async () => {
|
|
2114
|
+
if (previous) await previous.catch(() => {
|
|
2115
|
+
});
|
|
2116
|
+
return doUploadSnapshotToGcs(cwd, uploadUrl, opts);
|
|
2117
|
+
})();
|
|
2118
|
+
inFlightGcsUpload = run;
|
|
2119
|
+
try {
|
|
2120
|
+
return await run;
|
|
2121
|
+
} finally {
|
|
2122
|
+
if (inFlightGcsUpload === run) inFlightGcsUpload = null;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
async function captureSnapshot(ctx) {
|
|
2126
|
+
const gcs = await uploadSnapshotToGcs(ctx.cwd, ctx.snapshotUploadUrl, {
|
|
2127
|
+
pgDumpPath: ctx.pgDumpPath,
|
|
2128
|
+
forceUpload: ctx.forceUpload
|
|
2129
|
+
});
|
|
2130
|
+
const wipResult = await flushPendingChanges(ctx.cwd, {
|
|
2131
|
+
wipMessage: ctx.wipMessage ?? "WIP: WorkPreservation periodic snapshot",
|
|
2132
|
+
refreshToken: ctx.refreshToken
|
|
2133
|
+
});
|
|
2134
|
+
return {
|
|
2135
|
+
fileCount: gcs.fileCount,
|
|
2136
|
+
deletionCount: gcs.deletionCount,
|
|
2137
|
+
sizeBytes: gcs.sizeBytes,
|
|
2138
|
+
gcsUploaded: gcs.uploaded,
|
|
2139
|
+
wipPushed: wipResult.committed || wipResult.pushed,
|
|
2140
|
+
pgDumped: gcs.pgDumped
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
function startPeriodic(intervalMs, ctx) {
|
|
2144
|
+
stop();
|
|
2145
|
+
periodicTimer = setInterval(() => {
|
|
2146
|
+
if (inFlightCapture) return;
|
|
2147
|
+
const run = captureSnapshot(ctx);
|
|
2148
|
+
inFlightCapture = run;
|
|
2149
|
+
void run.catch(() => {
|
|
2150
|
+
}).finally(() => {
|
|
2151
|
+
if (inFlightCapture === run) inFlightCapture = null;
|
|
2152
|
+
});
|
|
2153
|
+
}, intervalMs);
|
|
2154
|
+
}
|
|
2155
|
+
function stop() {
|
|
2156
|
+
if (periodicTimer) {
|
|
2157
|
+
clearInterval(periodicTimer);
|
|
2158
|
+
periodicTimer = null;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
async function finalizeForSleep(ctx) {
|
|
2162
|
+
stop();
|
|
2163
|
+
if (inFlightCapture) await inFlightCapture.catch(() => {
|
|
2164
|
+
});
|
|
2165
|
+
const dump = await dumpPostgresIfPresent();
|
|
2166
|
+
try {
|
|
2167
|
+
const artifact = await captureSnapshot({
|
|
2168
|
+
...ctx,
|
|
2169
|
+
pgDumpPath: dump.path,
|
|
2170
|
+
wipMessage: ctx.wipMessage ?? "WIP: WorkPreservation sleep finalize snapshot",
|
|
2171
|
+
// Always stamp snapshotAt on sleep — even an empty workspace must confirm
|
|
2172
|
+
// the finalize so teardown doesn't wait out the reconciler's cap.
|
|
2173
|
+
forceUpload: true
|
|
2174
|
+
});
|
|
2175
|
+
return { ...artifact, pgDumped: dump.dumped };
|
|
2176
|
+
} finally {
|
|
2177
|
+
if (dump.path) await rm3(dump.path, { force: true }).catch(() => {
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
async function restoreOnBoot(bundle, cwd) {
|
|
2182
|
+
let gcsAvailable = false;
|
|
2183
|
+
let gcsFileCount;
|
|
2184
|
+
if (bundle.snapshotUrl) {
|
|
2185
|
+
const tmpTar = await downloadSnapshotToTemp(bundle.snapshotUrl);
|
|
2186
|
+
if (tmpTar) {
|
|
2187
|
+
try {
|
|
2188
|
+
const result = await extractSnapshotTar(cwd, tmpTar);
|
|
2189
|
+
if (result.status === "extracted") {
|
|
2190
|
+
gcsAvailable = true;
|
|
2191
|
+
gcsFileCount = result.filesExtracted;
|
|
2192
|
+
}
|
|
2193
|
+
} catch {
|
|
2194
|
+
gcsAvailable = false;
|
|
2195
|
+
} finally {
|
|
2196
|
+
await rm3(tmpTar, { force: true }).catch(() => {
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
const wipRestoreResult = gcsAvailable ? "none" : restoreWipSnapshot(cwd, bundle.gitPlan.branch);
|
|
2202
|
+
const source = selectRestoreSource({ gcsAvailable, wipRestoreResult });
|
|
2203
|
+
return source === "gcs" ? { source, fileCount: gcsFileCount } : { source };
|
|
2204
|
+
}
|
|
2205
|
+
|
|
1569
2206
|
// ../shared/dist/index.js
|
|
1570
2207
|
import { z } from "zod";
|
|
1571
2208
|
import { z as z2 } from "zod";
|
|
@@ -1706,6 +2343,16 @@ var NotifyAgentVersionRequestSchema = z.object({
|
|
|
1706
2343
|
sessionId: z.string(),
|
|
1707
2344
|
agentVersion: z.string()
|
|
1708
2345
|
});
|
|
2346
|
+
var DiscoveredPortSchema = z.object({
|
|
2347
|
+
port: z.number().int().min(1).max(65535),
|
|
2348
|
+
label: z.string().min(1).max(64).optional(),
|
|
2349
|
+
protocol: z.enum(["http", "tcp"]).optional(),
|
|
2350
|
+
detectedAt: z.string()
|
|
2351
|
+
});
|
|
2352
|
+
var ReportDiscoveredPortsRequestSchema = z.object({
|
|
2353
|
+
sessionId: z.string(),
|
|
2354
|
+
ports: z.array(DiscoveredPortSchema).max(64)
|
|
2355
|
+
});
|
|
1709
2356
|
var CreateSubtaskRequestSchema = z.object({
|
|
1710
2357
|
sessionId: z.string(),
|
|
1711
2358
|
title: z.string().min(1),
|
|
@@ -2039,6 +2686,15 @@ var StopProjectBuildRequestSchema = z2.object({
|
|
|
2039
2686
|
taskId: z2.string(),
|
|
2040
2687
|
requestingUserId: z2.string().optional()
|
|
2041
2688
|
});
|
|
2689
|
+
var StartProjectWorkspaceRequestSchema = z2.object({
|
|
2690
|
+
projectId: z2.string(),
|
|
2691
|
+
requestingUserId: z2.string().optional()
|
|
2692
|
+
});
|
|
2693
|
+
var StopProjectWorkspaceRequestSchema = z2.object({
|
|
2694
|
+
projectId: z2.string(),
|
|
2695
|
+
destroy: z2.boolean().optional(),
|
|
2696
|
+
requestingUserId: z2.string().optional()
|
|
2697
|
+
});
|
|
2042
2698
|
var CreateProjectReleaseRequestSchema = z2.object({
|
|
2043
2699
|
projectId: z2.string(),
|
|
2044
2700
|
taskIds: z2.array(z2.string()).optional(),
|
|
@@ -2515,8 +3171,8 @@ var ClaudeCodeHarness = class {
|
|
|
2515
3171
|
};
|
|
2516
3172
|
|
|
2517
3173
|
// src/harness/pty/session.ts
|
|
2518
|
-
import { mkdtemp, mkdir as mkdir2, rm, stat } from "fs/promises";
|
|
2519
|
-
import { tmpdir } from "os";
|
|
3174
|
+
import { mkdtemp as mkdtemp2, mkdir as mkdir2, rm as rm4, stat as stat3 } from "fs/promises";
|
|
3175
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
2520
3176
|
import { join as join4, dirname } from "path";
|
|
2521
3177
|
|
|
2522
3178
|
// src/harness/pty/event-queue.ts
|
|
@@ -2785,8 +3441,8 @@ function mapTranscriptRecord(raw) {
|
|
|
2785
3441
|
// src/harness/pty/jsonl-tailer.ts
|
|
2786
3442
|
var POLL_INTERVAL_MS = 25;
|
|
2787
3443
|
var JsonlTailer = class {
|
|
2788
|
-
constructor(
|
|
2789
|
-
this.path =
|
|
3444
|
+
constructor(path4, onEvent) {
|
|
3445
|
+
this.path = path4;
|
|
2790
3446
|
this.onEvent = onEvent;
|
|
2791
3447
|
}
|
|
2792
3448
|
path;
|
|
@@ -2983,7 +3639,7 @@ var PtyOutputCoalescer = class {
|
|
|
2983
3639
|
|
|
2984
3640
|
// src/harness/pty/tool-server.ts
|
|
2985
3641
|
import { createServer as createServer2 } from "http";
|
|
2986
|
-
import { writeFile as
|
|
3642
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
2987
3643
|
import { join as join3 } from "path";
|
|
2988
3644
|
import { randomBytes } from "crypto";
|
|
2989
3645
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -3090,7 +3746,7 @@ async function startToolServers(mcpServers, tempDir) {
|
|
|
3090
3746
|
}
|
|
3091
3747
|
if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null };
|
|
3092
3748
|
const mcpConfigPath = join3(tempDir, "mcp-config.json");
|
|
3093
|
-
await
|
|
3749
|
+
await writeFile3(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
|
|
3094
3750
|
return { servers, mcpConfigPath };
|
|
3095
3751
|
}
|
|
3096
3752
|
|
|
@@ -3128,14 +3784,14 @@ var SUBMIT_NUDGE_INTERVAL_MS = 2e3;
|
|
|
3128
3784
|
var SUBMIT_NUDGE_MAX_PRESSES = 5;
|
|
3129
3785
|
var SUBMIT_NUDGE_SLOW_INTERVAL_MS = 5e3;
|
|
3130
3786
|
var SUBMIT_NUDGE_WINDOW_MS = 9e4;
|
|
3131
|
-
function
|
|
3787
|
+
function sleep3(ms) {
|
|
3132
3788
|
return new Promise((resolve) => {
|
|
3133
3789
|
setTimeout(resolve, ms);
|
|
3134
3790
|
});
|
|
3135
3791
|
}
|
|
3136
|
-
async function transcriptSize(
|
|
3792
|
+
async function transcriptSize(path4) {
|
|
3137
3793
|
try {
|
|
3138
|
-
return (await
|
|
3794
|
+
return (await stat3(path4)).size;
|
|
3139
3795
|
} catch {
|
|
3140
3796
|
return 0;
|
|
3141
3797
|
}
|
|
@@ -3225,7 +3881,7 @@ var PtySession = class {
|
|
|
3225
3881
|
return;
|
|
3226
3882
|
}
|
|
3227
3883
|
try {
|
|
3228
|
-
this.tempDir = await
|
|
3884
|
+
this.tempDir = await mkdtemp2(join4(tmpdir4(), "conveyor-pty-"));
|
|
3229
3885
|
const socketPath = join4(this.tempDir, "hook.sock");
|
|
3230
3886
|
this.socket = new HookSocketServer(
|
|
3231
3887
|
socketPath,
|
|
@@ -3274,6 +3930,21 @@ var PtySession = class {
|
|
|
3274
3930
|
} catch {
|
|
3275
3931
|
}
|
|
3276
3932
|
}
|
|
3933
|
+
/**
|
|
3934
|
+
* Force the CLI to repaint its full screen by wiggling the pty size
|
|
3935
|
+
* (SIGWINCH). Used after a socket reconnect: the server's scrollback ring is
|
|
3936
|
+
* per-process and starts empty on a new instance, so without a repaint a
|
|
3937
|
+
* quiet TUI would leave the Connected-TUI terminal blank (and hidden) until
|
|
3938
|
+
* the CLI next draws on its own.
|
|
3939
|
+
*/
|
|
3940
|
+
forceRepaint() {
|
|
3941
|
+
if (!this.pty) return;
|
|
3942
|
+
try {
|
|
3943
|
+
this.pty.resize(this.cols, Math.max(1, this.rows - 1));
|
|
3944
|
+
this.pty.resize(this.cols, this.rows);
|
|
3945
|
+
} catch {
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3277
3948
|
async teardown() {
|
|
3278
3949
|
if (this._toreDown) return;
|
|
3279
3950
|
this._toreDown = true;
|
|
@@ -3310,7 +3981,7 @@ var PtySession = class {
|
|
|
3310
3981
|
this.mcpConfigPath = null;
|
|
3311
3982
|
this.queue.close();
|
|
3312
3983
|
if (this.tempDir) {
|
|
3313
|
-
await
|
|
3984
|
+
await rm4(this.tempDir, { recursive: true, force: true });
|
|
3314
3985
|
this.tempDir = "";
|
|
3315
3986
|
}
|
|
3316
3987
|
}
|
|
@@ -3370,7 +4041,7 @@ var PtySession = class {
|
|
|
3370
4041
|
async deliverPrompt(text) {
|
|
3371
4042
|
this.writeStdin(buildPromptBytes(text));
|
|
3372
4043
|
if (this.options.promptDelivery === "prefill") return;
|
|
3373
|
-
await
|
|
4044
|
+
await sleep3(SUBMIT_SETTLE_MS);
|
|
3374
4045
|
if (this._toreDown) return;
|
|
3375
4046
|
this.writeStdin("\r");
|
|
3376
4047
|
this.armSubmitNudge();
|
|
@@ -3518,7 +4189,7 @@ var PtySession = class {
|
|
|
3518
4189
|
};
|
|
3519
4190
|
|
|
3520
4191
|
// src/harness/pty/credentials.ts
|
|
3521
|
-
import { chmod as chmod2, mkdir as mkdir3, readFile, rm as
|
|
4192
|
+
import { chmod as chmod2, mkdir as mkdir3, readFile, rm as rm5, writeFile as writeFile4 } from "fs/promises";
|
|
3522
4193
|
import { homedir as homedir2 } from "os";
|
|
3523
4194
|
import { join as join5 } from "path";
|
|
3524
4195
|
var SYNTH_TOKEN_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
@@ -3569,26 +4240,26 @@ function planCredentialsWrite(input) {
|
|
|
3569
4240
|
if (fresh) return { action: "skip", reason: "current" };
|
|
3570
4241
|
return { action: "write", contents };
|
|
3571
4242
|
}
|
|
3572
|
-
async function readRaw(
|
|
4243
|
+
async function readRaw(path4) {
|
|
3573
4244
|
try {
|
|
3574
|
-
return await readFile(
|
|
4245
|
+
return await readFile(path4, "utf8");
|
|
3575
4246
|
} catch {
|
|
3576
4247
|
return null;
|
|
3577
4248
|
}
|
|
3578
4249
|
}
|
|
3579
4250
|
async function ensureClaudeCredentials(env = process.env) {
|
|
3580
4251
|
try {
|
|
3581
|
-
const
|
|
4252
|
+
const path4 = claudeCredentialsPath();
|
|
3582
4253
|
const plan = planCredentialsWrite({
|
|
3583
4254
|
isCloud: isConveyorCloudEnv(env),
|
|
3584
4255
|
token: env.CLAUDE_CODE_OAUTH_TOKEN,
|
|
3585
|
-
existingRaw: await readRaw(
|
|
4256
|
+
existingRaw: await readRaw(path4),
|
|
3586
4257
|
now: Date.now()
|
|
3587
4258
|
});
|
|
3588
4259
|
if (plan.action === "skip") return;
|
|
3589
4260
|
await mkdir3(claudeConfigHome(), { recursive: true });
|
|
3590
|
-
await
|
|
3591
|
-
await chmod2(
|
|
4261
|
+
await writeFile4(path4, plan.contents, { encoding: "utf8", mode: 384 });
|
|
4262
|
+
await chmod2(path4, 384).catch(() => {
|
|
3592
4263
|
});
|
|
3593
4264
|
} catch (err) {
|
|
3594
4265
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -3644,11 +4315,11 @@ function planClaudeJsonSeed(existingRaw, trustCwd) {
|
|
|
3644
4315
|
async function ensureClaudeOnboarding(env = process.env, trustCwd) {
|
|
3645
4316
|
try {
|
|
3646
4317
|
if (!isConveyorCloudEnv(env)) return;
|
|
3647
|
-
const
|
|
3648
|
-
const contents = planClaudeJsonSeed(await readRaw(
|
|
4318
|
+
const path4 = claudeJsonPath();
|
|
4319
|
+
const contents = planClaudeJsonSeed(await readRaw(path4), trustCwd);
|
|
3649
4320
|
if (contents === null) return;
|
|
3650
|
-
await
|
|
3651
|
-
const verify = await readRaw(
|
|
4321
|
+
await writeFile4(path4, contents, "utf8");
|
|
4322
|
+
const verify = await readRaw(path4);
|
|
3652
4323
|
if (verify === contents) {
|
|
3653
4324
|
process.stderr.write(
|
|
3654
4325
|
`[conveyor-agent] claude onboarding seeded${trustCwd ? ` (trust: ${trustCwd})` : ""}
|
|
@@ -3656,7 +4327,7 @@ async function ensureClaudeOnboarding(env = process.env, trustCwd) {
|
|
|
3656
4327
|
);
|
|
3657
4328
|
} else {
|
|
3658
4329
|
process.stderr.write(
|
|
3659
|
-
`[conveyor-agent] claude onboarding seed read-back MISMATCH at ${
|
|
4330
|
+
`[conveyor-agent] claude onboarding seed read-back MISMATCH at ${path4}: wrote ${contents.length}B, read ${verify?.length ?? 0}B \u2014 CLI may see stale config and park at a startup dialog
|
|
3660
4331
|
`
|
|
3661
4332
|
);
|
|
3662
4333
|
}
|
|
@@ -3669,12 +4340,12 @@ async function ensureClaudeOnboarding(env = process.env, trustCwd) {
|
|
|
3669
4340
|
async function removeConveyorCredentials(env = process.env) {
|
|
3670
4341
|
try {
|
|
3671
4342
|
if (!isConveyorCloudEnv(env)) return;
|
|
3672
|
-
const
|
|
3673
|
-
const existing = parseClaudeAiOauth(await readRaw(
|
|
4343
|
+
const path4 = claudeCredentialsPath();
|
|
4344
|
+
const existing = parseClaudeAiOauth(await readRaw(path4));
|
|
3674
4345
|
if (existing && typeof existing.refreshToken === "string" && existing.refreshToken.length > 0) {
|
|
3675
4346
|
return;
|
|
3676
4347
|
}
|
|
3677
|
-
await
|
|
4348
|
+
await rm5(path4, { force: true });
|
|
3678
4349
|
} catch (err) {
|
|
3679
4350
|
const message = err instanceof Error ? err.message : String(err);
|
|
3680
4351
|
process.stderr.write(`[conveyor-agent] claude credentials removal failed: ${message}
|
|
@@ -3693,6 +4364,17 @@ var PtyHarness = class {
|
|
|
3693
4364
|
this.bridge = bridge;
|
|
3694
4365
|
}
|
|
3695
4366
|
bridge;
|
|
4367
|
+
/** The session currently streaming events, if any — repaint target. */
|
|
4368
|
+
activeSession = null;
|
|
4369
|
+
/**
|
|
4370
|
+
* Wiggle the live pty's size so the CLI repaints its whole screen. No-op
|
|
4371
|
+
* between queries or on the SDK harness. Called after a socket reconnect so
|
|
4372
|
+
* the server's (per-process, possibly brand-new) scrollback ring re-seeds
|
|
4373
|
+
* and the Connected-TUI terminal reappears.
|
|
4374
|
+
*/
|
|
4375
|
+
forceRepaint() {
|
|
4376
|
+
this.activeSession?.forceRepaint();
|
|
4377
|
+
}
|
|
3696
4378
|
async *executeQuery(opts) {
|
|
3697
4379
|
const session = new PtySession(
|
|
3698
4380
|
opts.prompt,
|
|
@@ -3703,11 +4385,13 @@ var PtyHarness = class {
|
|
|
3703
4385
|
await ensureClaudeCredentials();
|
|
3704
4386
|
await ensureClaudeOnboarding(process.env, opts.options.cwd);
|
|
3705
4387
|
await session.start();
|
|
4388
|
+
this.activeSession = session;
|
|
3706
4389
|
try {
|
|
3707
4390
|
for await (const event of session.events()) {
|
|
3708
4391
|
yield event;
|
|
3709
4392
|
}
|
|
3710
4393
|
} finally {
|
|
4394
|
+
if (this.activeSession === session) this.activeSession = null;
|
|
3711
4395
|
await session.teardown();
|
|
3712
4396
|
}
|
|
3713
4397
|
}
|
|
@@ -3983,7 +4667,7 @@ function formatIncidents(incidents) {
|
|
|
3983
4667
|
}
|
|
3984
4668
|
|
|
3985
4669
|
// src/execution/tag-context-resolver.ts
|
|
3986
|
-
import { readFile as readFile2, readdir, stat as
|
|
4670
|
+
import { readFile as readFile2, readdir, stat as stat4 } from "fs/promises";
|
|
3987
4671
|
var TYPE_PRIORITY = { rule: 0, file: 1, folder: 2, doc: 3 };
|
|
3988
4672
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
3989
4673
|
".png",
|
|
@@ -4033,7 +4717,7 @@ async function readFileContent(filePath, maxChars) {
|
|
|
4033
4717
|
let rawContent;
|
|
4034
4718
|
let mtimeMs;
|
|
4035
4719
|
try {
|
|
4036
|
-
const st = await
|
|
4720
|
+
const st = await stat4(filePath);
|
|
4037
4721
|
mtimeMs = st.mtimeMs;
|
|
4038
4722
|
} catch {
|
|
4039
4723
|
return null;
|
|
@@ -4060,7 +4744,7 @@ async function readFolderListing(folderPath) {
|
|
|
4060
4744
|
try {
|
|
4061
4745
|
let mtimeMs;
|
|
4062
4746
|
try {
|
|
4063
|
-
const st = await
|
|
4747
|
+
const st = await stat4(folderPath);
|
|
4064
4748
|
mtimeMs = st.mtimeMs;
|
|
4065
4749
|
} catch {
|
|
4066
4750
|
return null;
|
|
@@ -5062,7 +5746,7 @@ var cliEventFormatters = {
|
|
|
5062
5746
|
tool_result: (e) => `${e.tool} \u2192 ${e.output?.slice(0, 500) ?? ""}${e.isError ? " [ERROR]" : ""}`,
|
|
5063
5747
|
message: (e) => e.content ?? "",
|
|
5064
5748
|
error: (e) => `ERROR: ${e.message ?? ""}`,
|
|
5065
|
-
completed: (e) => `Completed: ${e.summary ?? ""} (
|
|
5749
|
+
completed: (e) => `Completed: ${e.summary ?? ""} (duration: ${e.durationMs ?? "?"}ms)`,
|
|
5066
5750
|
setup_output: (e) => `[${e.stream ?? "stdout"}] ${e.data ?? ""}`,
|
|
5067
5751
|
start_command_output: (e) => `[${e.stream ?? "stdout"}] ${e.data ?? ""}`,
|
|
5068
5752
|
turn_end: (e) => `Turn complete (${e.toolCalls?.length ?? 0} tool calls)`
|
|
@@ -5608,7 +6292,7 @@ function buildMutationTools(connection, config) {
|
|
|
5608
6292
|
}
|
|
5609
6293
|
|
|
5610
6294
|
// src/tools/attachment-tools.ts
|
|
5611
|
-
import { readFile as readFile3, stat as
|
|
6295
|
+
import { readFile as readFile3, stat as stat5 } from "fs/promises";
|
|
5612
6296
|
import { basename, extname, isAbsolute, join as join6 } from "path";
|
|
5613
6297
|
import { z as z6 } from "zod";
|
|
5614
6298
|
var IMAGE_MIME_BY_EXT = {
|
|
@@ -5626,16 +6310,16 @@ function buildUploadAttachmentTool(connection, config) {
|
|
|
5626
6310
|
path: z6.string().describe("Path to the image file \u2014 absolute, or relative to the workspace root"),
|
|
5627
6311
|
title: z6.string().optional().describe("Short caption posted with the image (defaults to the file name)")
|
|
5628
6312
|
},
|
|
5629
|
-
async ({ path, title }) => {
|
|
6313
|
+
async ({ path: path4, title }) => {
|
|
5630
6314
|
try {
|
|
5631
|
-
const filePath = isAbsolute(
|
|
6315
|
+
const filePath = isAbsolute(path4) ? path4 : join6(config.workspaceDir, path4);
|
|
5632
6316
|
const mimeType = IMAGE_MIME_BY_EXT[extname(filePath).toLowerCase()];
|
|
5633
6317
|
if (!mimeType) {
|
|
5634
6318
|
return textResult(
|
|
5635
6319
|
`Unsupported file type "${extname(filePath) || "(none)"}". Supported: ${Object.keys(IMAGE_MIME_BY_EXT).join(", ")}`
|
|
5636
6320
|
);
|
|
5637
6321
|
}
|
|
5638
|
-
const info = await
|
|
6322
|
+
const info = await stat5(filePath).catch(() => null);
|
|
5639
6323
|
if (!info?.isFile()) {
|
|
5640
6324
|
return textResult(`File not found: ${filePath}`);
|
|
5641
6325
|
}
|
|
@@ -6326,35 +7010,16 @@ function emitContextUpdate(modelUsage, host, context, lastAssistantUsage) {
|
|
|
6326
7010
|
});
|
|
6327
7011
|
}
|
|
6328
7012
|
}
|
|
6329
|
-
function trackCostSpending(host, context, cumulativeTotal) {
|
|
6330
|
-
if (cumulativeTotal > 0 && context.agentId && context._runnerSessionId) {
|
|
6331
|
-
const breakdown = host.costTracker.modelBreakdown;
|
|
6332
|
-
host.connection.trackSpending({
|
|
6333
|
-
agentId: context.agentId,
|
|
6334
|
-
sessionId: context._runnerSessionId,
|
|
6335
|
-
totalCostUsd: cumulativeTotal,
|
|
6336
|
-
onSubscription: host.config.mode === "pm" || !!process.env.CLAUDE_CODE_OAUTH_TOKEN,
|
|
6337
|
-
modelUsage: breakdown.length > 0 ? breakdown : void 0
|
|
6338
|
-
});
|
|
6339
|
-
}
|
|
6340
|
-
}
|
|
6341
7013
|
function handleSuccessResult(event, host, context, startTime, lastAssistantUsage) {
|
|
6342
7014
|
const durationMs = Date.now() - startTime;
|
|
6343
7015
|
const summary = event.result || "Task completed.";
|
|
6344
7016
|
const retriable = isRetriableMessage(summary);
|
|
6345
|
-
|
|
7017
|
+
host.connection.sendEvent({ type: "completed", summary, durationMs });
|
|
6346
7018
|
const { modelUsage } = event;
|
|
6347
|
-
if (modelUsage && typeof modelUsage === "object") {
|
|
6348
|
-
host.costTracker.addModelUsage(
|
|
6349
|
-
modelUsage
|
|
6350
|
-
);
|
|
6351
|
-
}
|
|
6352
|
-
host.connection.sendEvent({ type: "completed", summary, costUsd: cumulativeTotal, durationMs });
|
|
6353
7019
|
if (modelUsage && typeof modelUsage === "object") {
|
|
6354
7020
|
emitContextUpdate(modelUsage, host, context, lastAssistantUsage);
|
|
6355
7021
|
}
|
|
6356
|
-
|
|
6357
|
-
return { totalCostUsd: cumulativeTotal, retriable };
|
|
7022
|
+
return { retriable };
|
|
6358
7023
|
}
|
|
6359
7024
|
function handleErrorResult(event, host) {
|
|
6360
7025
|
const errorMsg = event.errors.length > 0 ? event.errors.join(", ") : `Agent stopped: ${event.subtype}`;
|
|
@@ -6383,7 +7048,7 @@ function handleResultEvent(event, host, context, startTime, lastAssistantUsage)
|
|
|
6383
7048
|
return { ...result2, resultSummary };
|
|
6384
7049
|
}
|
|
6385
7050
|
const result = handleErrorResult(event, host);
|
|
6386
|
-
return {
|
|
7051
|
+
return { ...result, resultSummary };
|
|
6387
7052
|
}
|
|
6388
7053
|
async function emitResultEvent(event, host, context, startTime, lastAssistantUsage) {
|
|
6389
7054
|
const result = handleResultEvent(event, host, context, startTime, lastAssistantUsage);
|
|
@@ -6394,7 +7059,6 @@ async function emitResultEvent(event, host, context, startTime, lastAssistantUsa
|
|
|
6394
7059
|
await host.callbacks.onEvent({
|
|
6395
7060
|
type: "completed",
|
|
6396
7061
|
summary,
|
|
6397
|
-
costUsd: result.totalCostUsd,
|
|
6398
7062
|
durationMs
|
|
6399
7063
|
});
|
|
6400
7064
|
} else if (!result.staleSession) {
|
|
@@ -6562,14 +7226,14 @@ function flushPendingToolCalls(host, turnToolCalls) {
|
|
|
6562
7226
|
}
|
|
6563
7227
|
const outputsByTool = /* @__PURE__ */ new Map();
|
|
6564
7228
|
for (const entry of host.pendingToolOutputs) {
|
|
6565
|
-
const
|
|
6566
|
-
|
|
6567
|
-
outputsByTool.set(entry.tool,
|
|
7229
|
+
const list2 = outputsByTool.get(entry.tool) ?? [];
|
|
7230
|
+
list2.push(entry.output);
|
|
7231
|
+
outputsByTool.set(entry.tool, list2);
|
|
6568
7232
|
}
|
|
6569
7233
|
for (const call of turnToolCalls) {
|
|
6570
|
-
const
|
|
6571
|
-
if (
|
|
6572
|
-
call.output =
|
|
7234
|
+
const list2 = outputsByTool.get(call.tool);
|
|
7235
|
+
if (list2 && list2.length > 0) {
|
|
7236
|
+
call.output = list2.shift();
|
|
6573
7237
|
}
|
|
6574
7238
|
}
|
|
6575
7239
|
host.connection.sendEvent({ type: "turn_end", toolCalls: [...turnToolCalls] });
|
|
@@ -7057,10 +7721,10 @@ function hasExistingSessionFile(taskId, cwd, lineage) {
|
|
|
7057
7721
|
const key = sessionLineageKey(taskId, lineage.agentMode, lineage.runnerMode);
|
|
7058
7722
|
return sessionFileExists(taskIdToSessionUuid(key), cwd);
|
|
7059
7723
|
}
|
|
7060
|
-
function repairTornSessionFile(
|
|
7724
|
+
function repairTornSessionFile(path4) {
|
|
7061
7725
|
try {
|
|
7062
|
-
if (!existsSync(
|
|
7063
|
-
const content = readFileSync2(
|
|
7726
|
+
if (!existsSync(path4)) return false;
|
|
7727
|
+
const content = readFileSync2(path4, "utf8");
|
|
7064
7728
|
if (content.length === 0) return false;
|
|
7065
7729
|
let keepEnd = content.length;
|
|
7066
7730
|
if (!content.endsWith("\n")) {
|
|
@@ -7079,9 +7743,9 @@ function repairTornSessionFile(path) {
|
|
|
7079
7743
|
keepEnd = prevNewline + 1;
|
|
7080
7744
|
}
|
|
7081
7745
|
if (keepEnd === content.length) return false;
|
|
7082
|
-
truncateSync(
|
|
7746
|
+
truncateSync(path4, Buffer.byteLength(content.slice(0, keepEnd), "utf8"));
|
|
7083
7747
|
logger2.warn("Repaired torn transcript before resume", {
|
|
7084
|
-
path,
|
|
7748
|
+
path: path4,
|
|
7085
7749
|
trimmedBytes: content.length - keepEnd
|
|
7086
7750
|
});
|
|
7087
7751
|
return true;
|
|
@@ -7149,7 +7813,6 @@ function buildQueryOptions(host, context) {
|
|
|
7149
7813
|
effort: settings.effort,
|
|
7150
7814
|
thinking: settings.thinking,
|
|
7151
7815
|
betas: settings.betas,
|
|
7152
|
-
maxBudgetUsd: settings.maxBudgetUsd ?? 50,
|
|
7153
7816
|
abortController: host.abortController ?? void 0,
|
|
7154
7817
|
disallowedTools: buildDisallowedTools(settings, mode, host.hasExitedPlanMode),
|
|
7155
7818
|
enableFileCheckpointing: settings.enableFileCheckpointing,
|
|
@@ -7407,12 +8070,14 @@ async function handleAuthError(context, host, options) {
|
|
|
7407
8070
|
host.connection.postChatMessage("Authentication expired. Re-bootstrapping credentials...");
|
|
7408
8071
|
const refreshed = await host.connection.refreshAuthToken();
|
|
7409
8072
|
if (!refreshed) {
|
|
7410
|
-
host.connection.postChatMessage(
|
|
8073
|
+
host.connection.postChatMessage(
|
|
8074
|
+
"\u26A0\uFE0F No Claude credential is configured for this project \u2014 the agent can't run and is now idle. Add a Claude Code OAuth token (or Anthropic API key) in Project Settings, then resume this task."
|
|
8075
|
+
);
|
|
7411
8076
|
host.connection.sendEvent({
|
|
7412
8077
|
type: "error",
|
|
7413
|
-
message: "
|
|
8078
|
+
message: "No Claude credential available \u2014 agent idle (dormant)"
|
|
7414
8079
|
});
|
|
7415
|
-
|
|
8080
|
+
return;
|
|
7416
8081
|
}
|
|
7417
8082
|
context.claudeSessionId = null;
|
|
7418
8083
|
host.connection.storeSessionId("");
|
|
@@ -7548,58 +8213,6 @@ async function runWithRetry(initialQuery, context, host, options) {
|
|
|
7548
8213
|
}
|
|
7549
8214
|
}
|
|
7550
8215
|
|
|
7551
|
-
// src/execution/cost-tracker.ts
|
|
7552
|
-
var CostTracker = class {
|
|
7553
|
-
cumulativeCostUsd = 0;
|
|
7554
|
-
modelUsage = /* @__PURE__ */ new Map();
|
|
7555
|
-
seeded = false;
|
|
7556
|
-
/**
|
|
7557
|
-
* Rehydrate cumulative spend from the server so `maxBudgetUsd` enforcement
|
|
7558
|
-
* accounts for costs incurred by prior agent runs on the same task. Must be
|
|
7559
|
-
* called before any `addQueryCost` / `addModelUsage` — re-seeding after
|
|
7560
|
-
* costs have accumulated would clobber in-process totals.
|
|
7561
|
-
*/
|
|
7562
|
-
seed(totalCostUsd, modelUsage) {
|
|
7563
|
-
if (this.seeded) return;
|
|
7564
|
-
if (this.cumulativeCostUsd > 0 || this.modelUsage.size > 0) return;
|
|
7565
|
-
this.cumulativeCostUsd = totalCostUsd;
|
|
7566
|
-
for (const entry of modelUsage) {
|
|
7567
|
-
this.modelUsage.set(entry.model, { ...entry });
|
|
7568
|
-
}
|
|
7569
|
-
this.seeded = true;
|
|
7570
|
-
}
|
|
7571
|
-
/** Add cost from a completed query and return the running total */
|
|
7572
|
-
addQueryCost(queryCostUsd) {
|
|
7573
|
-
this.cumulativeCostUsd += queryCostUsd;
|
|
7574
|
-
return this.cumulativeCostUsd;
|
|
7575
|
-
}
|
|
7576
|
-
/** Merge per-model usage from a completed query */
|
|
7577
|
-
addModelUsage(usage) {
|
|
7578
|
-
for (const [model, data] of Object.entries(usage)) {
|
|
7579
|
-
const existing = this.modelUsage.get(model) ?? {
|
|
7580
|
-
model,
|
|
7581
|
-
inputTokens: 0,
|
|
7582
|
-
outputTokens: 0,
|
|
7583
|
-
cacheReadInputTokens: 0,
|
|
7584
|
-
cacheCreationInputTokens: 0,
|
|
7585
|
-
costUSD: 0
|
|
7586
|
-
};
|
|
7587
|
-
existing.inputTokens += data.inputTokens ?? 0;
|
|
7588
|
-
existing.outputTokens += data.outputTokens ?? 0;
|
|
7589
|
-
existing.cacheReadInputTokens += data.cacheReadInputTokens ?? 0;
|
|
7590
|
-
existing.cacheCreationInputTokens += data.cacheCreationInputTokens ?? 0;
|
|
7591
|
-
existing.costUSD += data.costUSD ?? 0;
|
|
7592
|
-
this.modelUsage.set(model, existing);
|
|
7593
|
-
}
|
|
7594
|
-
}
|
|
7595
|
-
get totalCostUsd() {
|
|
7596
|
-
return this.cumulativeCostUsd;
|
|
7597
|
-
}
|
|
7598
|
-
get modelBreakdown() {
|
|
7599
|
-
return [...this.modelUsage.values()];
|
|
7600
|
-
}
|
|
7601
|
-
};
|
|
7602
|
-
|
|
7603
8216
|
// src/execution/exploration-tracker.ts
|
|
7604
8217
|
var FILE_REREAD_NUDGE_L1 = 3;
|
|
7605
8218
|
var FILE_REREAD_NUDGE_L2 = 5;
|
|
@@ -7726,7 +8339,6 @@ var QueryBridge = class {
|
|
|
7726
8339
|
harnessKind,
|
|
7727
8340
|
harnessKind === "pty" ? buildPtyBridge(connection) : void 0
|
|
7728
8341
|
);
|
|
7729
|
-
this.costTracker = new CostTracker();
|
|
7730
8342
|
this.planSync = new PlanSync(workspaceDir, connection);
|
|
7731
8343
|
}
|
|
7732
8344
|
connection;
|
|
@@ -7737,7 +8349,6 @@ var QueryBridge = class {
|
|
|
7737
8349
|
/** Which harness drives this bridge ("pty" task chat; "sdk" only under the
|
|
7738
8350
|
* CONVEYOR_FORCE_SDK_CARDS maintainer override). */
|
|
7739
8351
|
harnessKind;
|
|
7740
|
-
costTracker;
|
|
7741
8352
|
planSync;
|
|
7742
8353
|
sessionIds = /* @__PURE__ */ new Map();
|
|
7743
8354
|
activeQuery = null;
|
|
@@ -7769,6 +8380,13 @@ var QueryBridge = class {
|
|
|
7769
8380
|
get wasRateLimited() {
|
|
7770
8381
|
return this._wasRateLimited;
|
|
7771
8382
|
}
|
|
8383
|
+
/**
|
|
8384
|
+
* Ask the live terminal (PTY harness only) to redraw its full screen.
|
|
8385
|
+
* Safe no-op on the SDK harness or between queries.
|
|
8386
|
+
*/
|
|
8387
|
+
forceRepaint() {
|
|
8388
|
+
this.harness.forceRepaint?.();
|
|
8389
|
+
}
|
|
7772
8390
|
stop() {
|
|
7773
8391
|
this._stopped = true;
|
|
7774
8392
|
this._abortController?.abort();
|
|
@@ -7776,10 +8394,6 @@ var QueryBridge = class {
|
|
|
7776
8394
|
resume() {
|
|
7777
8395
|
this._stopped = false;
|
|
7778
8396
|
}
|
|
7779
|
-
/** Rehydrate CostTracker from server-side cumulative spend on agent boot. */
|
|
7780
|
-
seedCostTracker(totalCostUsd, modelUsage) {
|
|
7781
|
-
this.costTracker.seed(totalCostUsd, modelUsage);
|
|
7782
|
-
}
|
|
7783
8397
|
/**
|
|
7784
8398
|
* How the INITIAL instructions for this task would be delivered — "prefill"
|
|
7785
8399
|
* means the TUI input is pre-filled but unsubmitted, awaiting the human.
|
|
@@ -7838,7 +8452,6 @@ var QueryBridge = class {
|
|
|
7838
8452
|
harness: this.harness,
|
|
7839
8453
|
harnessKind: this.harnessKind,
|
|
7840
8454
|
setupLog: [],
|
|
7841
|
-
costTracker: this.costTracker,
|
|
7842
8455
|
explorationTracker: bridge.mode.effectiveMode === "discovery" || bridge.mode.effectiveMode === "auto" && !bridge.mode.hasExitedPlanMode ? new ExplorationTracker(bridge._isParentTask) : null,
|
|
7843
8456
|
sessionIds: this.sessionIds,
|
|
7844
8457
|
pendingToolOutputs: this.pendingToolOutputs,
|
|
@@ -7948,8 +8561,195 @@ function readAgentVersion() {
|
|
|
7948
8561
|
return null;
|
|
7949
8562
|
}
|
|
7950
8563
|
|
|
8564
|
+
// src/runner/port-discovery.ts
|
|
8565
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
8566
|
+
import { execFile } from "child_process";
|
|
8567
|
+
var PROC_TCP_LISTEN_STATE = "0A";
|
|
8568
|
+
function parseProcNetTcpListeners(content) {
|
|
8569
|
+
const ports = [];
|
|
8570
|
+
const lines = content.split("\n");
|
|
8571
|
+
for (let i = 1; i < lines.length; i++) {
|
|
8572
|
+
const line = lines[i];
|
|
8573
|
+
if (!line) continue;
|
|
8574
|
+
const cols = line.trim().split(/\s+/);
|
|
8575
|
+
if (cols.length < 4 || cols[3] !== PROC_TCP_LISTEN_STATE) continue;
|
|
8576
|
+
const local = cols[1];
|
|
8577
|
+
if (!local) continue;
|
|
8578
|
+
const portHex = local.split(":").pop();
|
|
8579
|
+
if (!portHex) continue;
|
|
8580
|
+
const port = Number.parseInt(portHex, 16);
|
|
8581
|
+
if (Number.isInteger(port) && port >= 1 && port <= 65535) ports.push(port);
|
|
8582
|
+
}
|
|
8583
|
+
return ports;
|
|
8584
|
+
}
|
|
8585
|
+
var DEFAULT_PROC_PATHS = ["/proc/net/tcp", "/proc/net/tcp6"];
|
|
8586
|
+
async function readProcListeningPorts(procPaths = DEFAULT_PROC_PATHS) {
|
|
8587
|
+
const ports = /* @__PURE__ */ new Set();
|
|
8588
|
+
let readable = false;
|
|
8589
|
+
for (const path4 of procPaths) {
|
|
8590
|
+
try {
|
|
8591
|
+
const content = await readFile4(path4, "utf8");
|
|
8592
|
+
readable = true;
|
|
8593
|
+
for (const port of parseProcNetTcpListeners(content)) ports.add(port);
|
|
8594
|
+
} catch {
|
|
8595
|
+
}
|
|
8596
|
+
}
|
|
8597
|
+
return readable ? ports : null;
|
|
8598
|
+
}
|
|
8599
|
+
async function readNetstatListeningPorts() {
|
|
8600
|
+
const output = await new Promise((resolve) => {
|
|
8601
|
+
execFile("netstat", ["-an", "-p", "tcp"], { timeout: 5e3 }, (err, stdout) => {
|
|
8602
|
+
resolve(err ? null : stdout);
|
|
8603
|
+
});
|
|
8604
|
+
});
|
|
8605
|
+
if (output === null) return null;
|
|
8606
|
+
const ports = /* @__PURE__ */ new Set();
|
|
8607
|
+
for (const line of output.split("\n")) {
|
|
8608
|
+
if (!line.includes("LISTEN")) continue;
|
|
8609
|
+
const cols = line.trim().split(/\s+/);
|
|
8610
|
+
const local = cols[3];
|
|
8611
|
+
if (!local) continue;
|
|
8612
|
+
const portStr = local.split(".").pop();
|
|
8613
|
+
const port = Number(portStr);
|
|
8614
|
+
if (Number.isInteger(port) && port >= 1 && port <= 65535) ports.add(port);
|
|
8615
|
+
}
|
|
8616
|
+
return ports;
|
|
8617
|
+
}
|
|
8618
|
+
async function readListeningPorts() {
|
|
8619
|
+
const proc = await readProcListeningPorts();
|
|
8620
|
+
if (proc !== null) return proc;
|
|
8621
|
+
if (process.platform !== "linux") return readNetstatListeningPorts();
|
|
8622
|
+
return null;
|
|
8623
|
+
}
|
|
8624
|
+
var DEFAULT_EXCLUDED_PORTS = [2222, 5432, 6379, 9200];
|
|
8625
|
+
var DEFAULT_EPHEMERAL_PORT_MIN = 32768;
|
|
8626
|
+
var DEFAULT_INTERVAL_MS = 15e3;
|
|
8627
|
+
var DEFAULT_MAX_PORTS = 16;
|
|
8628
|
+
var CONFIRM_SCANS = 2;
|
|
8629
|
+
var PortDiscovery = class {
|
|
8630
|
+
opts;
|
|
8631
|
+
intervalMs;
|
|
8632
|
+
maxPorts;
|
|
8633
|
+
excluded;
|
|
8634
|
+
ephemeralPortMin;
|
|
8635
|
+
scan;
|
|
8636
|
+
now;
|
|
8637
|
+
log;
|
|
8638
|
+
baseline = null;
|
|
8639
|
+
tracked = /* @__PURE__ */ new Map();
|
|
8640
|
+
timer = null;
|
|
8641
|
+
ticking = false;
|
|
8642
|
+
disabled = false;
|
|
8643
|
+
/** Set when the confirmed set changed (or a report failed) — cleared only
|
|
8644
|
+
* after a successful report, so transient RPC failures retry next tick. */
|
|
8645
|
+
reportPending = false;
|
|
8646
|
+
lastReportedKey = "";
|
|
8647
|
+
constructor(options) {
|
|
8648
|
+
this.opts = options;
|
|
8649
|
+
this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
8650
|
+
this.maxPorts = options.maxPorts ?? DEFAULT_MAX_PORTS;
|
|
8651
|
+
this.excluded = new Set(options.excludedPorts ?? DEFAULT_EXCLUDED_PORTS);
|
|
8652
|
+
this.ephemeralPortMin = options.ephemeralPortMin ?? DEFAULT_EPHEMERAL_PORT_MIN;
|
|
8653
|
+
this.scan = options.scan ?? readListeningPorts;
|
|
8654
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
8655
|
+
this.log = options.log ?? ((m) => process.stderr.write(`[conveyor-agent] ${m}
|
|
8656
|
+
`));
|
|
8657
|
+
}
|
|
8658
|
+
/** Take the baseline scan and start polling. Safe to call once. */
|
|
8659
|
+
async start() {
|
|
8660
|
+
if (this.timer || this.disabled) return;
|
|
8661
|
+
const baseline = await this.scanSafe();
|
|
8662
|
+
if (baseline === null) {
|
|
8663
|
+
this.disabled = true;
|
|
8664
|
+
this.log("Port discovery disabled: no listening-socket source available");
|
|
8665
|
+
return;
|
|
8666
|
+
}
|
|
8667
|
+
this.baseline = baseline;
|
|
8668
|
+
this.timer = setInterval(() => void this.tick(), this.intervalMs);
|
|
8669
|
+
this.timer.unref?.();
|
|
8670
|
+
}
|
|
8671
|
+
stop() {
|
|
8672
|
+
if (this.timer) {
|
|
8673
|
+
clearInterval(this.timer);
|
|
8674
|
+
this.timer = null;
|
|
8675
|
+
}
|
|
8676
|
+
}
|
|
8677
|
+
/** One poll cycle. Exposed for tests (deterministic, no timers needed). */
|
|
8678
|
+
async tick() {
|
|
8679
|
+
if (this.ticking || this.disabled || !this.baseline) return;
|
|
8680
|
+
this.ticking = true;
|
|
8681
|
+
try {
|
|
8682
|
+
const current = await this.scanSafe();
|
|
8683
|
+
if (current === null) return;
|
|
8684
|
+
this.updateTracking(current);
|
|
8685
|
+
if (this.reportPending) await this.flushReport();
|
|
8686
|
+
} finally {
|
|
8687
|
+
this.ticking = false;
|
|
8688
|
+
}
|
|
8689
|
+
}
|
|
8690
|
+
async scanSafe() {
|
|
8691
|
+
try {
|
|
8692
|
+
return await this.scan();
|
|
8693
|
+
} catch {
|
|
8694
|
+
return null;
|
|
8695
|
+
}
|
|
8696
|
+
}
|
|
8697
|
+
isCandidate(port) {
|
|
8698
|
+
if (this.baseline?.has(port)) return false;
|
|
8699
|
+
if (this.excluded.has(port)) return false;
|
|
8700
|
+
if (port >= this.ephemeralPortMin) return false;
|
|
8701
|
+
return true;
|
|
8702
|
+
}
|
|
8703
|
+
updateTracking(current) {
|
|
8704
|
+
for (const port of current) {
|
|
8705
|
+
if (!this.isCandidate(port)) continue;
|
|
8706
|
+
const entry = this.tracked.get(port);
|
|
8707
|
+
if (!entry) {
|
|
8708
|
+
this.tracked.set(port, { seen: 1, missed: 0, confirmed: false, detectedAt: "" });
|
|
8709
|
+
continue;
|
|
8710
|
+
}
|
|
8711
|
+
entry.seen += 1;
|
|
8712
|
+
entry.missed = 0;
|
|
8713
|
+
if (!entry.confirmed && entry.seen >= CONFIRM_SCANS) {
|
|
8714
|
+
entry.confirmed = true;
|
|
8715
|
+
entry.detectedAt = this.now().toISOString();
|
|
8716
|
+
}
|
|
8717
|
+
}
|
|
8718
|
+
for (const [port, entry] of this.tracked) {
|
|
8719
|
+
if (current.has(port)) continue;
|
|
8720
|
+
entry.missed += 1;
|
|
8721
|
+
entry.seen = 0;
|
|
8722
|
+
if (entry.missed >= CONFIRM_SCANS || !entry.confirmed) this.tracked.delete(port);
|
|
8723
|
+
}
|
|
8724
|
+
const key = this.confirmedKey();
|
|
8725
|
+
if (key !== this.lastReportedKey) this.reportPending = true;
|
|
8726
|
+
}
|
|
8727
|
+
confirmedPorts() {
|
|
8728
|
+
const confirmed = [...this.tracked.entries()].filter(([, entry]) => entry.confirmed).sort(([a], [b]) => a - b).slice(0, this.maxPorts);
|
|
8729
|
+
return confirmed.map(([port, entry]) => ({
|
|
8730
|
+
port,
|
|
8731
|
+
protocol: "tcp",
|
|
8732
|
+
detectedAt: entry.detectedAt
|
|
8733
|
+
}));
|
|
8734
|
+
}
|
|
8735
|
+
confirmedKey() {
|
|
8736
|
+
return this.confirmedPorts().map(({ port }) => port).join(",");
|
|
8737
|
+
}
|
|
8738
|
+
async flushReport() {
|
|
8739
|
+
const ports = this.confirmedPorts();
|
|
8740
|
+
const key = ports.map(({ port }) => port).join(",");
|
|
8741
|
+
try {
|
|
8742
|
+
await this.opts.report(ports);
|
|
8743
|
+
this.lastReportedKey = key;
|
|
8744
|
+
this.reportPending = false;
|
|
8745
|
+
this.log(`Discovered preview ports: [${key || "none"}]`);
|
|
8746
|
+
} catch {
|
|
8747
|
+
}
|
|
8748
|
+
}
|
|
8749
|
+
};
|
|
8750
|
+
|
|
7951
8751
|
// src/runner/parent-pull-handler.ts
|
|
7952
|
-
import { execSync as
|
|
8752
|
+
import { execSync as execSync4 } from "child_process";
|
|
7953
8753
|
function handlePullBranch(workDir, branch) {
|
|
7954
8754
|
if (!branch) return;
|
|
7955
8755
|
const current = getCurrentBranch(workDir);
|
|
@@ -7968,14 +8768,14 @@ function handlePullBranch(workDir, branch) {
|
|
|
7968
8768
|
return;
|
|
7969
8769
|
}
|
|
7970
8770
|
try {
|
|
7971
|
-
|
|
8771
|
+
execSync4(`git fetch origin ${branch}`, { cwd: workDir, stdio: "ignore", timeout: 6e4 });
|
|
7972
8772
|
} catch {
|
|
7973
8773
|
process.stderr.write(`[conveyor-agent] pull_branch: fetch failed for ${branch}
|
|
7974
8774
|
`);
|
|
7975
8775
|
return;
|
|
7976
8776
|
}
|
|
7977
8777
|
try {
|
|
7978
|
-
|
|
8778
|
+
execSync4(`git pull --ff-only origin ${branch}`, {
|
|
7979
8779
|
cwd: workDir,
|
|
7980
8780
|
stdio: "ignore",
|
|
7981
8781
|
timeout: 6e4
|
|
@@ -8027,6 +8827,8 @@ var SessionRunner = class _SessionRunner {
|
|
|
8027
8827
|
agentSpokeThisTurn = false;
|
|
8028
8828
|
/** Guards overlapping runs of the periodic git flush. */
|
|
8029
8829
|
periodicFlushInFlight = false;
|
|
8830
|
+
/** Runtime preview-port poller (v3 dynamic port discovery). */
|
|
8831
|
+
portDiscovery = null;
|
|
8030
8832
|
constructor(config, callbacks) {
|
|
8031
8833
|
this.config = config;
|
|
8032
8834
|
this.callbacks = callbacks;
|
|
@@ -8083,7 +8885,6 @@ var SessionRunner = class _SessionRunner {
|
|
|
8083
8885
|
this.wireConnectionCallbacks();
|
|
8084
8886
|
this.lifecycle.startHeartbeat();
|
|
8085
8887
|
this.lifecycle.startTokenRefresh();
|
|
8086
|
-
this.lifecycle.startGitFlush();
|
|
8087
8888
|
const { pendingMessages: serverMessages } = await this.connection.call("connectAgent", {
|
|
8088
8889
|
sessionId: this.sessionId
|
|
8089
8890
|
});
|
|
@@ -8092,6 +8893,11 @@ var SessionRunner = class _SessionRunner {
|
|
|
8092
8893
|
this.pendingMessages.push({ content: msg.content, userId: msg.userId });
|
|
8093
8894
|
}
|
|
8094
8895
|
}
|
|
8896
|
+
this.portDiscovery = new PortDiscovery({
|
|
8897
|
+
report: (ports) => this.connection.reportDiscoveredPorts(ports)
|
|
8898
|
+
});
|
|
8899
|
+
void this.portDiscovery.start().catch(() => {
|
|
8900
|
+
});
|
|
8095
8901
|
const agentVersion = readAgentVersion();
|
|
8096
8902
|
if (agentVersion) {
|
|
8097
8903
|
this.connection.call("notifyAgentVersion", { sessionId: this.sessionId, agentVersion }).catch(() => {
|
|
@@ -8126,6 +8932,17 @@ var SessionRunner = class _SessionRunner {
|
|
|
8126
8932
|
await this.shutdown("error");
|
|
8127
8933
|
return;
|
|
8128
8934
|
}
|
|
8935
|
+
const gitState = await awaitGitReady({
|
|
8936
|
+
onLog: (m) => process.stderr.write(`[conveyor-agent] ${m}
|
|
8937
|
+
`)
|
|
8938
|
+
});
|
|
8939
|
+
if (gitState === "failed" || gitState === "timeout") {
|
|
8940
|
+
const message = gitState === "failed" ? "Workspace git preparation failed (see pod logs)" : "Workspace git preparation timed out (see pod logs)";
|
|
8941
|
+
this.connection.sendEvent({ type: "error", message });
|
|
8942
|
+
await this.callbacks.onEvent({ type: "error", message });
|
|
8943
|
+
await this.shutdown("error");
|
|
8944
|
+
return;
|
|
8945
|
+
}
|
|
8129
8946
|
if (process.env.CONVEYOR_GIT_READY !== "1") {
|
|
8130
8947
|
if (this.fullContext?.githubBranch) {
|
|
8131
8948
|
ensureOnTaskBranch(this.config.workspaceDir, this.fullContext.githubBranch);
|
|
@@ -8134,11 +8951,31 @@ var SessionRunner = class _SessionRunner {
|
|
|
8134
8951
|
syncWithBaseBranch(this.config.workspaceDir, this.fullContext.baseBranch);
|
|
8135
8952
|
}
|
|
8136
8953
|
}
|
|
8954
|
+
if (!this.stopped) {
|
|
8955
|
+
this.lifecycle.startGitFlush();
|
|
8956
|
+
}
|
|
8137
8957
|
if (this.fullContext?.githubBranch) {
|
|
8138
|
-
const
|
|
8139
|
-
if (
|
|
8140
|
-
|
|
8958
|
+
const snapshotUrl = process.env.CONVEYOR_SNAPSHOT_URL;
|
|
8959
|
+
if (snapshotUrl) {
|
|
8960
|
+
const restore = await restoreOnBoot(
|
|
8961
|
+
{ snapshotUrl, gitPlan: { branch: this.fullContext.githubBranch } },
|
|
8962
|
+
this.config.workspaceDir
|
|
8963
|
+
);
|
|
8964
|
+
if (restore.source !== "clean") {
|
|
8965
|
+
process.stderr.write(
|
|
8966
|
+
`[conveyor-agent] WorkPreservation restore: source=${restore.source}${restore.fileCount === void 0 ? "" : ` files=${restore.fileCount}`}
|
|
8967
|
+
`
|
|
8968
|
+
);
|
|
8969
|
+
}
|
|
8970
|
+
} else {
|
|
8971
|
+
const restored = restoreWipSnapshot(
|
|
8972
|
+
this.config.workspaceDir,
|
|
8973
|
+
this.fullContext.githubBranch
|
|
8974
|
+
);
|
|
8975
|
+
if (restored !== "none") {
|
|
8976
|
+
process.stderr.write(`[conveyor-agent] WIP snapshot restore: ${restored}
|
|
8141
8977
|
`);
|
|
8978
|
+
}
|
|
8142
8979
|
}
|
|
8143
8980
|
}
|
|
8144
8981
|
this.mode.applyServerMode(this.fullContext?.agentMode, this.fullContext?.isAuto);
|
|
@@ -8148,7 +8985,6 @@ var SessionRunner = class _SessionRunner {
|
|
|
8148
8985
|
});
|
|
8149
8986
|
}
|
|
8150
8987
|
this.queryBridge = this.createQueryBridge();
|
|
8151
|
-
await this.seedCostTrackerFromServer();
|
|
8152
8988
|
this.logInitialization();
|
|
8153
8989
|
const staleBatch = [...this.pendingMessages];
|
|
8154
8990
|
const didExecuteInitialQuery = await this.executeInitialMode();
|
|
@@ -8469,11 +9305,28 @@ var SessionRunner = class _SessionRunner {
|
|
|
8469
9305
|
`
|
|
8470
9306
|
);
|
|
8471
9307
|
}
|
|
9308
|
+
await this.uploadGcsSnapshotIfConfigured();
|
|
8472
9309
|
} catch {
|
|
8473
9310
|
} finally {
|
|
8474
9311
|
this.periodicFlushInFlight = false;
|
|
8475
9312
|
}
|
|
8476
9313
|
}
|
|
9314
|
+
/** PUT the WorkPreservation snapshot tar to the bundle's capability URL
|
|
9315
|
+
* when this pod is v3 (CONVEYOR_SNAPSHOT_UPLOAD_URL set). Never throws. */
|
|
9316
|
+
async uploadGcsSnapshotIfConfigured() {
|
|
9317
|
+
const uploadUrl = process.env.CONVEYOR_SNAPSHOT_UPLOAD_URL;
|
|
9318
|
+
if (!uploadUrl) return;
|
|
9319
|
+
try {
|
|
9320
|
+
const gcs = await uploadSnapshotToGcs(this.config.workspaceDir, uploadUrl);
|
|
9321
|
+
if (gcs.uploaded) {
|
|
9322
|
+
process.stderr.write(
|
|
9323
|
+
`[conveyor-agent] WorkPreservation GCS snapshot: files=${gcs.fileCount} bytes=${gcs.sizeBytes}
|
|
9324
|
+
`
|
|
9325
|
+
);
|
|
9326
|
+
}
|
|
9327
|
+
} catch {
|
|
9328
|
+
}
|
|
9329
|
+
}
|
|
8477
9330
|
/** Best-effort WIP commit + push on shutdown so in-flight work isn't lost
|
|
8478
9331
|
* when a claudespace pod is killed. Must be called BEFORE stop() so the
|
|
8479
9332
|
* connection is still alive for token refresh. Never throws. */
|
|
@@ -8498,6 +9351,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
8498
9351
|
`
|
|
8499
9352
|
);
|
|
8500
9353
|
}
|
|
9354
|
+
await this.uploadGcsSnapshotIfConfigured();
|
|
8501
9355
|
} catch (err) {
|
|
8502
9356
|
const msg = err instanceof Error ? err.message : String(err);
|
|
8503
9357
|
process.stderr.write(`[conveyor-agent] Shutdown git flush failed: ${msg}
|
|
@@ -8507,6 +9361,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
8507
9361
|
stop() {
|
|
8508
9362
|
this.stopped = true;
|
|
8509
9363
|
this.queryBridge?.stop();
|
|
9364
|
+
this.portDiscovery?.stop();
|
|
8510
9365
|
this.lifecycle.destroy();
|
|
8511
9366
|
this.connection.disconnect();
|
|
8512
9367
|
if (this.inputResolver) {
|
|
@@ -8700,6 +9555,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
8700
9555
|
this.connection.onMessage((msg) => this.injectMessage(msg));
|
|
8701
9556
|
this.connection.onStop(() => this.stop());
|
|
8702
9557
|
this.connection.onSoftStop(() => this.softStop());
|
|
9558
|
+
this.connection.onReconnected = () => this.queryBridge?.forceRepaint();
|
|
8703
9559
|
this.connection.onModeChange((data) => {
|
|
8704
9560
|
const action = this.mode.handleModeChange(data.agentMode);
|
|
8705
9561
|
if (action.type === "start_auto") {
|
|
@@ -8731,35 +9587,37 @@ var SessionRunner = class _SessionRunner {
|
|
|
8731
9587
|
this.connection.onPullBranch(({ branch }) => {
|
|
8732
9588
|
handlePullBranch(this.config.workspaceDir, branch);
|
|
8733
9589
|
});
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
*
|
|
8738
|
-
*
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
9590
|
+
this.connection.onFinalizeSnapshot(() => void this.finalizeSnapshotNow());
|
|
9591
|
+
}
|
|
9592
|
+
/** Eager finalize snapshot triggered by the reconciler's sleep signal
|
|
9593
|
+
* (session:finalizeSnapshot). Runs pg_dump + a full capture and uploads it
|
|
9594
|
+
* NOW so the sleep confirms on this snapshot instead of waiting for the
|
|
9595
|
+
* ~2min periodic flush — and so sidecar DB state (which ONLY finalizeForSleep
|
|
9596
|
+
* captures via pg_dump) actually lands. Best-effort: on failure the
|
|
9597
|
+
* periodic/shutdown path stays the fallback. No-op on non-v3 pods. */
|
|
9598
|
+
async finalizeSnapshotNow() {
|
|
9599
|
+
const uploadUrl = process.env.CONVEYOR_SNAPSHOT_UPLOAD_URL;
|
|
9600
|
+
if (!uploadUrl || this.stopped) return;
|
|
8743
9601
|
try {
|
|
8744
|
-
|
|
8745
|
-
|
|
9602
|
+
await finalizeForSleep({
|
|
9603
|
+
cwd: this.config.workspaceDir,
|
|
9604
|
+
branch: this.fullContext?.githubBranch ?? "",
|
|
9605
|
+
snapshotUploadUrl: uploadUrl,
|
|
9606
|
+
refreshToken: async () => {
|
|
9607
|
+
try {
|
|
9608
|
+
const res = await this.connection.call("refreshGithubToken", {
|
|
9609
|
+
sessionId: this.connection.sessionId
|
|
9610
|
+
});
|
|
9611
|
+
return res.token;
|
|
9612
|
+
} catch {
|
|
9613
|
+
return void 0;
|
|
9614
|
+
}
|
|
9615
|
+
}
|
|
8746
9616
|
});
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
`[conveyor-agent] CostTracker seeded: $${fetched.totalCostUsd.toFixed(4)} across ${fetched.modelUsage.length} model(s)
|
|
8752
|
-
`
|
|
8753
|
-
);
|
|
8754
|
-
} else {
|
|
8755
|
-
process.stderr.write(
|
|
8756
|
-
"[conveyor-agent] CostTracker seed timed out after 3s \u2014 starting at $0\n"
|
|
8757
|
-
);
|
|
8758
|
-
}
|
|
8759
|
-
} catch (err) {
|
|
8760
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
8761
|
-
process.stderr.write(`[conveyor-agent] CostTracker seed failed: ${msg} \u2014 starting at $0
|
|
8762
|
-
`);
|
|
9617
|
+
process.stderr.write(
|
|
9618
|
+
"[conveyor-agent] Finalize snapshot (pg_dump) uploaded on sleep signal\n"
|
|
9619
|
+
);
|
|
9620
|
+
} catch {
|
|
8763
9621
|
}
|
|
8764
9622
|
}
|
|
8765
9623
|
/** Proactively refresh the GitHub token before the 1-hour expiry. */
|
|
@@ -8804,6 +9662,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
8804
9662
|
process.stderr.write(`[conveyor-agent] Shutdown: reason=${finalState}
|
|
8805
9663
|
`);
|
|
8806
9664
|
this.connection.sendEvent({ type: "shutdown", reason: finalState });
|
|
9665
|
+
this.portDiscovery?.stop();
|
|
8807
9666
|
this.lifecycle.destroy();
|
|
8808
9667
|
try {
|
|
8809
9668
|
await this.setState(finalState);
|
|
@@ -8848,13 +9707,13 @@ var SessionRunner = class _SessionRunner {
|
|
|
8848
9707
|
};
|
|
8849
9708
|
|
|
8850
9709
|
// src/setup/config.ts
|
|
8851
|
-
import { readFile as
|
|
9710
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
8852
9711
|
import { join as join8 } from "path";
|
|
8853
9712
|
var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
|
|
8854
9713
|
var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
|
|
8855
9714
|
async function loadForwardPorts(workspaceDir) {
|
|
8856
9715
|
try {
|
|
8857
|
-
const raw = await
|
|
9716
|
+
const raw = await readFile5(join8(workspaceDir, DEVCONTAINER_PATH), "utf-8");
|
|
8858
9717
|
const parsed = JSON.parse(raw);
|
|
8859
9718
|
const ports = (parsed.forwardPorts ?? []).filter(
|
|
8860
9719
|
(p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
|
|
@@ -8898,7 +9757,7 @@ function loadConveyorConfig() {
|
|
|
8898
9757
|
}
|
|
8899
9758
|
|
|
8900
9759
|
// src/setup/commands.ts
|
|
8901
|
-
import { spawn, execSync as
|
|
9760
|
+
import { spawn, execSync as execSync5 } from "child_process";
|
|
8902
9761
|
function runSetupCommand(cmd, cwd, onOutput) {
|
|
8903
9762
|
return new Promise((resolve, reject) => {
|
|
8904
9763
|
const child = spawn("sh", ["-c", cmd], {
|
|
@@ -8927,7 +9786,7 @@ function runSetupCommand(cmd, cwd, onOutput) {
|
|
|
8927
9786
|
var AUTH_TOKEN_TIMEOUT_MS = 3e4;
|
|
8928
9787
|
function runAuthTokenCommand(cmd, userEmail, cwd) {
|
|
8929
9788
|
try {
|
|
8930
|
-
const output =
|
|
9789
|
+
const output = execSync5(`${cmd} ${JSON.stringify(userEmail)}`, {
|
|
8931
9790
|
cwd,
|
|
8932
9791
|
timeout: AUTH_TOKEN_TIMEOUT_MS,
|
|
8933
9792
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -8957,10 +9816,10 @@ function runStartCommand(cmd, cwd, onOutput) {
|
|
|
8957
9816
|
}
|
|
8958
9817
|
|
|
8959
9818
|
// src/setup/codespace.ts
|
|
8960
|
-
import { execSync as
|
|
9819
|
+
import { execSync as execSync6 } from "child_process";
|
|
8961
9820
|
function unshallowRepo(workspaceDir) {
|
|
8962
9821
|
try {
|
|
8963
|
-
|
|
9822
|
+
execSync6("git fetch --unshallow", {
|
|
8964
9823
|
cwd: workspaceDir,
|
|
8965
9824
|
timeout: 6e4,
|
|
8966
9825
|
stdio: "ignore"
|
|
@@ -8973,6 +9832,8 @@ export {
|
|
|
8973
9832
|
fetchBootstrap,
|
|
8974
9833
|
applyBootstrapToEnv,
|
|
8975
9834
|
AgentConnection,
|
|
9835
|
+
DEFAULT_LIFECYCLE_CONFIG,
|
|
9836
|
+
Lifecycle,
|
|
8976
9837
|
createServiceLogger,
|
|
8977
9838
|
hasUncommittedChanges,
|
|
8978
9839
|
getCurrentBranch,
|
|
@@ -8982,6 +9843,13 @@ export {
|
|
|
8982
9843
|
flushPendingChanges,
|
|
8983
9844
|
pushToOrigin,
|
|
8984
9845
|
PlanSync,
|
|
9846
|
+
awaitGitReady,
|
|
9847
|
+
uploadSnapshotToGcs,
|
|
9848
|
+
captureSnapshot,
|
|
9849
|
+
startPeriodic,
|
|
9850
|
+
stop,
|
|
9851
|
+
finalizeForSleep,
|
|
9852
|
+
restoreOnBoot,
|
|
8985
9853
|
SessionRunner,
|
|
8986
9854
|
loadForwardPorts,
|
|
8987
9855
|
buildSessionPreviewPorts,
|
|
@@ -8991,4 +9859,4 @@ export {
|
|
|
8991
9859
|
runStartCommand,
|
|
8992
9860
|
unshallowRepo
|
|
8993
9861
|
};
|
|
8994
|
-
//# sourceMappingURL=chunk-
|
|
9862
|
+
//# sourceMappingURL=chunk-QKRPJ7DB.js.map
|