@rallycry/conveyor-agent 11.0.3 → 11.0.5
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/{boot-PKQ2I66D.js → boot-7XH4FFLO.js} +5 -5
- package/dist/chunk-7P6QZHXZ.js +76 -0
- package/dist/{chunk-UBDSLM44.js → chunk-D57S4UR5.js} +1 -1
- package/dist/{chunk-DHKAY7HQ.js → chunk-J2NPWBVZ.js} +462 -218
- package/dist/{chunk-JQVAWRVL.js → chunk-URYBO32C.js} +40 -6
- package/dist/{chunk-3F4ZZKCA.js → chunk-W6VILPQE.js} +6 -0
- package/dist/cli.js +26 -18
- package/dist/gate-cli.d.ts +1 -0
- package/dist/gate-cli.js +87 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +9 -8
- package/dist/{serve-boot-YPAUKENG.js → serve-boot-B4BIZMNB.js} +3 -3
- package/dist/{server-7XH7RYUX.js → server-2Z5V3IPA.js} +2 -2
- package/package.json +3 -2
- package/skills/conveyor-build/SKILL.md +10 -0
- package/dist/{chunk-N4WSUTGV.js → chunk-SLUBOIYE.js} +6 -6
|
@@ -2,6 +2,33 @@ import {
|
|
|
2
2
|
mapChatHistory,
|
|
3
3
|
readAgentVersion
|
|
4
4
|
} from "./chunk-XORJ6SII.js";
|
|
5
|
+
import {
|
|
6
|
+
MAX_BETWEEN_TURN_BUFFER,
|
|
7
|
+
MAX_DIAGNOSTIC_OUTPUT,
|
|
8
|
+
buildExitErrors,
|
|
9
|
+
buildPromptBytes,
|
|
10
|
+
buildSpawnArgs,
|
|
11
|
+
cleanTerminalOutput,
|
|
12
|
+
ensureSessionTarget,
|
|
13
|
+
inheritedEnv,
|
|
14
|
+
killPtyWithEscalation,
|
|
15
|
+
needsRawReadyGate,
|
|
16
|
+
parseUserQuestions,
|
|
17
|
+
renderPromptContentText,
|
|
18
|
+
resolveClaudeBinary,
|
|
19
|
+
resolvePlanDialogTiming,
|
|
20
|
+
resolvePtySpawn,
|
|
21
|
+
resolveRawTuiProbeTiming,
|
|
22
|
+
resolveSubmitNudgeTiming,
|
|
23
|
+
resolveSubmitRedeliveryMaxAttempts,
|
|
24
|
+
resolveSubmitSettleMs,
|
|
25
|
+
sawTerminalSetup,
|
|
26
|
+
sentinelEchoed,
|
|
27
|
+
sessionTempBase,
|
|
28
|
+
spawnOptionsFingerprint,
|
|
29
|
+
transcriptSize,
|
|
30
|
+
turnOptionsFrom
|
|
31
|
+
} from "./chunk-W6VILPQE.js";
|
|
5
32
|
import {
|
|
6
33
|
AgentConnection,
|
|
7
34
|
CodespacePortVisibility,
|
|
@@ -30,11 +57,21 @@ import {
|
|
|
30
57
|
statWorkspacePath,
|
|
31
58
|
updateRemoteToken,
|
|
32
59
|
verifyGitCredential
|
|
33
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-SLUBOIYE.js";
|
|
34
61
|
import {
|
|
35
62
|
registerBootMilestoneSocketFallback,
|
|
36
63
|
reportBootMilestone
|
|
37
64
|
} from "./chunk-GL2DIQEQ.js";
|
|
65
|
+
import {
|
|
66
|
+
describeTokenFile,
|
|
67
|
+
ghHostsExternallyOwned,
|
|
68
|
+
githubTokenFilePath,
|
|
69
|
+
sleep
|
|
70
|
+
} from "./chunk-W4LZ7R6Z.js";
|
|
71
|
+
import {
|
|
72
|
+
isHeavyGateActive,
|
|
73
|
+
listGateExitSentinels
|
|
74
|
+
} from "./chunk-7P6QZHXZ.js";
|
|
38
75
|
import {
|
|
39
76
|
LoopLagMonitor,
|
|
40
77
|
loopStatusForRunnerStatus
|
|
@@ -45,38 +82,6 @@ import {
|
|
|
45
82
|
import {
|
|
46
83
|
workbenchEnabled
|
|
47
84
|
} from "./chunk-KMB3BU4S.js";
|
|
48
|
-
import {
|
|
49
|
-
MAX_BETWEEN_TURN_BUFFER,
|
|
50
|
-
MAX_DIAGNOSTIC_OUTPUT,
|
|
51
|
-
buildExitErrors,
|
|
52
|
-
buildPromptBytes,
|
|
53
|
-
buildSpawnArgs,
|
|
54
|
-
cleanTerminalOutput,
|
|
55
|
-
inheritedEnv,
|
|
56
|
-
killPtyWithEscalation,
|
|
57
|
-
needsRawReadyGate,
|
|
58
|
-
parseUserQuestions,
|
|
59
|
-
renderPromptContentText,
|
|
60
|
-
resolveClaudeBinary,
|
|
61
|
-
resolvePlanDialogTiming,
|
|
62
|
-
resolvePtySpawn,
|
|
63
|
-
resolveRawTuiProbeTiming,
|
|
64
|
-
resolveSubmitNudgeTiming,
|
|
65
|
-
resolveSubmitRedeliveryMaxAttempts,
|
|
66
|
-
resolveSubmitSettleMs,
|
|
67
|
-
sawTerminalSetup,
|
|
68
|
-
sentinelEchoed,
|
|
69
|
-
sessionTempBase,
|
|
70
|
-
spawnOptionsFingerprint,
|
|
71
|
-
transcriptSize,
|
|
72
|
-
turnOptionsFrom
|
|
73
|
-
} from "./chunk-3F4ZZKCA.js";
|
|
74
|
-
import {
|
|
75
|
-
describeTokenFile,
|
|
76
|
-
ghHostsExternallyOwned,
|
|
77
|
-
githubTokenFilePath,
|
|
78
|
-
sleep
|
|
79
|
-
} from "./chunk-W4LZ7R6Z.js";
|
|
80
85
|
|
|
81
86
|
// ../shared/dist/chunk-OKJPFFQI.js
|
|
82
87
|
var CARD_DESCRIPTION_MAX = 255;
|
|
@@ -3134,8 +3139,8 @@ function mapTranscriptRecord(raw) {
|
|
|
3134
3139
|
// src/harness/pty/jsonl-tailer.ts
|
|
3135
3140
|
var POLL_INTERVAL_MS = 25;
|
|
3136
3141
|
var JsonlTailer = class {
|
|
3137
|
-
constructor(
|
|
3138
|
-
this.path =
|
|
3142
|
+
constructor(path, onEvent, onRawRecord, mapRecord = mapTranscriptRecord) {
|
|
3143
|
+
this.path = path;
|
|
3139
3144
|
this.onEvent = onEvent;
|
|
3140
3145
|
this.onRawRecord = onRawRecord;
|
|
3141
3146
|
this.mapRecord = mapRecord;
|
|
@@ -3991,11 +3996,13 @@ function conveyorCredentialsMarkerPath() {
|
|
|
3991
3996
|
function tokenFingerprint(accessToken) {
|
|
3992
3997
|
return createHash("sha256").update(accessToken).digest("hex");
|
|
3993
3998
|
}
|
|
3994
|
-
function buildCredentialsMarker(accessToken, now, previousHash) {
|
|
3999
|
+
function buildCredentialsMarker(accessToken, now, previousHash, refreshToken, codingAgentKeyId) {
|
|
3995
4000
|
const accessTokenSha256 = tokenFingerprint(accessToken);
|
|
3996
4001
|
return JSON.stringify({
|
|
3997
4002
|
accessTokenSha256,
|
|
3998
4003
|
...previousHash && previousHash !== accessTokenSha256 ? { previousAccessTokenSha256: previousHash } : {},
|
|
4004
|
+
...refreshToken ? { refreshTokenSha256: tokenFingerprint(refreshToken) } : {},
|
|
4005
|
+
...codingAgentKeyId ? { codingAgentKeyId } : {},
|
|
3999
4006
|
writtenAt: now
|
|
4000
4007
|
});
|
|
4001
4008
|
}
|
|
@@ -4012,16 +4019,59 @@ function parseCredentialsMarker(raw) {
|
|
|
4012
4019
|
return [];
|
|
4013
4020
|
}
|
|
4014
4021
|
}
|
|
4022
|
+
function parseCredentialsRefreshMarker(raw) {
|
|
4023
|
+
if (!raw || raw.trim() === "") return null;
|
|
4024
|
+
try {
|
|
4025
|
+
const parsed = JSON.parse(raw);
|
|
4026
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
4027
|
+
const hash = parsed.refreshTokenSha256;
|
|
4028
|
+
return typeof hash === "string" && hash.length > 0 ? hash : null;
|
|
4029
|
+
} catch {
|
|
4030
|
+
return null;
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
function parseCredentialsMarkerKeyId(raw) {
|
|
4034
|
+
if (!raw || raw.trim() === "") return null;
|
|
4035
|
+
try {
|
|
4036
|
+
const parsed = JSON.parse(raw);
|
|
4037
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
4038
|
+
const keyId = parsed.codingAgentKeyId;
|
|
4039
|
+
return typeof keyId === "string" && keyId.length > 0 ? keyId : null;
|
|
4040
|
+
} catch {
|
|
4041
|
+
return null;
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
function credentialMarkerMatches(raw, accessToken, refreshToken, expectedKeyId) {
|
|
4045
|
+
if (expectedKeyId && parseCredentialsMarkerKeyId(raw) !== expectedKeyId) return false;
|
|
4046
|
+
if (accessToken && parseCredentialsMarker(raw).includes(tokenFingerprint(accessToken)))
|
|
4047
|
+
return true;
|
|
4048
|
+
if (!refreshToken) return false;
|
|
4049
|
+
return parseCredentialsRefreshMarker(raw) === tokenFingerprint(refreshToken);
|
|
4050
|
+
}
|
|
4051
|
+
function parseCredentialsIdentity(credentialsRaw, markerRaw, expectedKeyId) {
|
|
4052
|
+
if (!credentialsRaw) return null;
|
|
4053
|
+
try {
|
|
4054
|
+
const parsed = JSON.parse(credentialsRaw);
|
|
4055
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
4056
|
+
const oauth = parsed.claudeAiOauth;
|
|
4057
|
+
if (typeof oauth !== "object" || oauth === null) return null;
|
|
4058
|
+
const record = oauth;
|
|
4059
|
+
const accessToken = typeof record.accessToken === "string" ? record.accessToken : null;
|
|
4060
|
+
const refreshToken = typeof record.refreshToken === "string" ? record.refreshToken : null;
|
|
4061
|
+
return {
|
|
4062
|
+
accessToken,
|
|
4063
|
+
hasRefreshToken: Boolean(refreshToken),
|
|
4064
|
+
isConveyorOwned: credentialMarkerMatches(markerRaw, accessToken, refreshToken, expectedKeyId)
|
|
4065
|
+
};
|
|
4066
|
+
} catch {
|
|
4067
|
+
return null;
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4015
4070
|
|
|
4016
|
-
// src/harness/pty/credentials.ts
|
|
4071
|
+
// src/harness/pty/credentials-plan.ts
|
|
4017
4072
|
var SYNTH_TOKEN_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
4018
4073
|
var REFRESH_SKEW_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
4019
|
-
|
|
4020
|
-
return join5(claudeConfigHome(), ".credentials.json");
|
|
4021
|
-
}
|
|
4022
|
-
function isConveyorCloudEnv(env = process.env) {
|
|
4023
|
-
return Boolean(env.CLAUDESPACE_NAME || env.CODESPACE_NAME || env.CODESPACES);
|
|
4024
|
-
}
|
|
4074
|
+
var LEGACY_SCOPES = ["user:inference", "user:profile"];
|
|
4025
4075
|
function parseClaudeOauthEnv(blob) {
|
|
4026
4076
|
if (!blob) return null;
|
|
4027
4077
|
try {
|
|
@@ -4029,7 +4079,7 @@ function parseClaudeOauthEnv(blob) {
|
|
|
4029
4079
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
4030
4080
|
const record = parsed;
|
|
4031
4081
|
if (typeof record.access !== "string" || record.access === "") return null;
|
|
4032
|
-
const scopes = Array.isArray(record.scopes) ? record.scopes.filter((
|
|
4082
|
+
const scopes = Array.isArray(record.scopes) ? record.scopes.filter((scope) => typeof scope === "string" && scope !== "") : [];
|
|
4033
4083
|
return {
|
|
4034
4084
|
access: record.access,
|
|
4035
4085
|
refresh: typeof record.refresh === "string" && record.refresh ? record.refresh : void 0,
|
|
@@ -4059,19 +4109,13 @@ function parseClaudeAiOauth(raw) {
|
|
|
4059
4109
|
return null;
|
|
4060
4110
|
}
|
|
4061
4111
|
}
|
|
4062
|
-
var LEGACY_SCOPES = ["user:inference", "user:profile"];
|
|
4063
4112
|
function buildCredentialsFile(material, now) {
|
|
4064
4113
|
const refresh = material.refresh;
|
|
4065
4114
|
return JSON.stringify({
|
|
4066
4115
|
claudeAiOauth: {
|
|
4067
4116
|
accessToken: material.access,
|
|
4068
4117
|
...refresh ? { refreshToken: refresh } : {},
|
|
4069
|
-
// With a refresh token the CLI can recover from a real expiry, so report
|
|
4070
|
-
// the true one. Without, claim a far-future expiry (see SYNTH_TOKEN_TTL_MS)
|
|
4071
|
-
// because a refresh attempt would be unrecoverable.
|
|
4072
4118
|
expiresAt: refresh && material.expires ? material.expires : now + SYNTH_TOKEN_TTL_MS,
|
|
4073
|
-
// The refresh token has its own expiry; the CLI records it, so mirror it
|
|
4074
|
-
// when the grant told us rather than leaving the field off.
|
|
4075
4119
|
...material.refreshExpires ? { refreshTokenExpiresAt: material.refreshExpires } : {},
|
|
4076
4120
|
scopes: material.scopes?.length ? material.scopes : LEGACY_SCOPES,
|
|
4077
4121
|
...material.rateLimitTier ? { rateLimitTier: material.rateLimitTier } : {},
|
|
@@ -4083,31 +4127,74 @@ function buildSynthesizedCredentials(token, now) {
|
|
|
4083
4127
|
return buildCredentialsFile({ access: token }, now);
|
|
4084
4128
|
}
|
|
4085
4129
|
function isConveyorOwnedCredentials(existing, markerHashes) {
|
|
4086
|
-
const hasRefresh = typeof existing.refreshToken === "string" && existing.refreshToken
|
|
4130
|
+
const hasRefresh = typeof existing.refreshToken === "string" && existing.refreshToken !== "";
|
|
4087
4131
|
if (!hasRefresh) return true;
|
|
4088
|
-
if (markerHashes.length === 0) return false;
|
|
4089
4132
|
if (typeof existing.accessToken !== "string") return false;
|
|
4090
4133
|
return markerHashes.includes(tokenFingerprint(existing.accessToken));
|
|
4091
4134
|
}
|
|
4135
|
+
function hasStableRefreshOwnership(existing, markerRaw) {
|
|
4136
|
+
return typeof existing.refreshToken === "string" && parseCredentialsRefreshMarker(markerRaw) === tokenFingerprint(existing.refreshToken);
|
|
4137
|
+
}
|
|
4138
|
+
function isCurrentCredential(existing, material, now) {
|
|
4139
|
+
if (material.refresh) return existing.accessToken === material.access;
|
|
4140
|
+
return existing.accessToken === material.access && typeof existing.expiresAt === "number" && existing.expiresAt > now + REFRESH_SKEW_MS;
|
|
4141
|
+
}
|
|
4142
|
+
function needsMarkerUpgrade(input, material) {
|
|
4143
|
+
if (!input.codingAgentKeyId) return false;
|
|
4144
|
+
if (parseCredentialsMarkerKeyId(input.markerRaw ?? null) !== input.codingAgentKeyId) return true;
|
|
4145
|
+
return Boolean(
|
|
4146
|
+
material.refresh && parseCredentialsRefreshMarker(input.markerRaw ?? null) !== tokenFingerprint(material.refresh)
|
|
4147
|
+
);
|
|
4148
|
+
}
|
|
4149
|
+
function ownsExistingCredential(existing, markerRaw, markerHashes) {
|
|
4150
|
+
if (isConveyorOwnedCredentials(existing, markerHashes)) return true;
|
|
4151
|
+
const accessToken = typeof existing.accessToken === "string" ? existing.accessToken : null;
|
|
4152
|
+
const refreshToken = typeof existing.refreshToken === "string" ? existing.refreshToken : null;
|
|
4153
|
+
return credentialMarkerMatches(markerRaw, accessToken, refreshToken);
|
|
4154
|
+
}
|
|
4155
|
+
function cliRefreshedSameKey(input, material, existing, markerRaw) {
|
|
4156
|
+
if (!material.refresh || !input.codingAgentKeyId) return false;
|
|
4157
|
+
if (parseCredentialsMarkerKeyId(markerRaw) !== input.codingAgentKeyId) return false;
|
|
4158
|
+
return hasStableRefreshOwnership(existing, markerRaw) && existing.accessToken !== material.access;
|
|
4159
|
+
}
|
|
4092
4160
|
function planCredentialsWrite(input) {
|
|
4093
4161
|
if (!input.isCloud) return { action: "skip", reason: "not-cloud" };
|
|
4094
4162
|
const material = parseClaudeOauthEnv(input.oauthBlob) ?? (input.token ? { access: input.token } : null);
|
|
4095
4163
|
if (!material) return { action: "skip", reason: "no-token" };
|
|
4096
|
-
const
|
|
4164
|
+
const markerRaw = input.markerRaw ?? null;
|
|
4165
|
+
const markerHashes = parseCredentialsMarker(markerRaw);
|
|
4097
4166
|
const contents = buildCredentialsFile(material, input.now);
|
|
4098
|
-
const marker = buildCredentialsMarker(
|
|
4167
|
+
const marker = buildCredentialsMarker(
|
|
4168
|
+
material.access,
|
|
4169
|
+
input.now,
|
|
4170
|
+
markerHashes[0] ?? null,
|
|
4171
|
+
material.refresh,
|
|
4172
|
+
input.codingAgentKeyId
|
|
4173
|
+
);
|
|
4099
4174
|
const existing = parseClaudeAiOauth(input.existingRaw);
|
|
4100
4175
|
if (!existing) return { action: "write", contents, marker };
|
|
4101
|
-
if (!
|
|
4176
|
+
if (!ownsExistingCredential(existing, markerRaw, markerHashes)) {
|
|
4102
4177
|
return { action: "skip", reason: "foreign-credentials" };
|
|
4103
4178
|
}
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4179
|
+
if (cliRefreshedSameKey(input, material, existing, markerRaw)) {
|
|
4180
|
+
return { action: "skip", reason: "current" };
|
|
4181
|
+
}
|
|
4182
|
+
if (!isCurrentCredential(existing, material, input.now)) {
|
|
4183
|
+
return { action: "write", contents, marker };
|
|
4184
|
+
}
|
|
4185
|
+
return needsMarkerUpgrade(input, material) ? { action: "mark", marker } : { action: "skip", reason: "current" };
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
// src/harness/pty/credentials.ts
|
|
4189
|
+
function claudeCredentialsPath() {
|
|
4190
|
+
return join5(claudeConfigHome(), ".credentials.json");
|
|
4107
4191
|
}
|
|
4108
|
-
|
|
4192
|
+
function isConveyorCloudEnv(env = process.env) {
|
|
4193
|
+
return Boolean(env.CLAUDESPACE_NAME || env.CODESPACE_NAME || env.CODESPACES);
|
|
4194
|
+
}
|
|
4195
|
+
async function readRaw(path) {
|
|
4109
4196
|
try {
|
|
4110
|
-
return await readFile(
|
|
4197
|
+
return await readFile(path, "utf8");
|
|
4111
4198
|
} catch {
|
|
4112
4199
|
return null;
|
|
4113
4200
|
}
|
|
@@ -4128,13 +4215,12 @@ async function resolveTuiAuthReadiness(env = process.env, readIdentity = readCre
|
|
|
4128
4215
|
});
|
|
4129
4216
|
return { ready: status === "ready", status };
|
|
4130
4217
|
}
|
|
4131
|
-
async function readCredentialsIdentity() {
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
};
|
|
4218
|
+
async function readCredentialsIdentity(env = process.env) {
|
|
4219
|
+
return parseCredentialsIdentity(
|
|
4220
|
+
await readRaw(claudeCredentialsPath()),
|
|
4221
|
+
await readRaw(conveyorCredentialsMarkerPath()),
|
|
4222
|
+
env.CONVEYOR_CODING_AGENT_KEY_ID
|
|
4223
|
+
);
|
|
4138
4224
|
}
|
|
4139
4225
|
var READ_BACK_DELAYS_MS = [250, 500, 1e3, 2e3];
|
|
4140
4226
|
var defaultSleep = (ms) => new Promise((resolve) => {
|
|
@@ -4149,10 +4235,10 @@ async function writeWithReadBackRetry(io, contents, delaysMs = READ_BACK_DELAYS_
|
|
|
4149
4235
|
await sleep2(delaysMs[attempt]);
|
|
4150
4236
|
}
|
|
4151
4237
|
}
|
|
4152
|
-
function fsWriteIo(
|
|
4238
|
+
function fsWriteIo(path, mode) {
|
|
4153
4239
|
return {
|
|
4154
|
-
write: (contents) => writeFile4(
|
|
4155
|
-
read: () => readRaw(
|
|
4240
|
+
write: (contents) => writeFile4(path, contents, mode === void 0 ? "utf8" : { encoding: "utf8", mode }),
|
|
4241
|
+
read: () => readRaw(path)
|
|
4156
4242
|
};
|
|
4157
4243
|
}
|
|
4158
4244
|
async function ensureClaudeCredentials(env = process.env) {
|
|
@@ -4164,27 +4250,29 @@ async function ensureClaudeCredentials(env = process.env) {
|
|
|
4164
4250
|
await sanitizeApprovedApiKeys(accessToken);
|
|
4165
4251
|
}
|
|
4166
4252
|
try {
|
|
4167
|
-
const
|
|
4253
|
+
const path = claudeCredentialsPath();
|
|
4168
4254
|
const markerPath = conveyorCredentialsMarkerPath();
|
|
4169
4255
|
const plan = planCredentialsWrite({
|
|
4170
4256
|
isCloud,
|
|
4171
4257
|
token,
|
|
4172
4258
|
oauthBlob: env.CONVEYOR_CLAUDE_OAUTH,
|
|
4173
|
-
existingRaw: await readRaw(
|
|
4259
|
+
existingRaw: await readRaw(path),
|
|
4174
4260
|
markerRaw: await readRaw(markerPath),
|
|
4261
|
+
codingAgentKeyId: env.CONVEYOR_CODING_AGENT_KEY_ID,
|
|
4175
4262
|
now: Date.now()
|
|
4176
4263
|
});
|
|
4177
4264
|
if (plan.action === "skip") return;
|
|
4178
4265
|
await mkdir2(claudeConfigHome(), { recursive: true });
|
|
4179
4266
|
await writeWithReadBackRetry(fsWriteIo(markerPath, 384), plan.marker);
|
|
4180
|
-
|
|
4267
|
+
if (plan.action === "mark") return;
|
|
4268
|
+
const verified = await writeWithReadBackRetry(fsWriteIo(path, 384), plan.contents);
|
|
4181
4269
|
if (!verified) {
|
|
4182
4270
|
process.stderr.write(
|
|
4183
|
-
`[conveyor-agent] claude credentials read-back still stale after retries at ${
|
|
4271
|
+
`[conveyor-agent] claude credentials read-back still stale after retries at ${path} \u2014 TUI may land on the login picker
|
|
4184
4272
|
`
|
|
4185
4273
|
);
|
|
4186
4274
|
}
|
|
4187
|
-
await chmod2(
|
|
4275
|
+
await chmod2(path, 384).catch(() => {
|
|
4188
4276
|
});
|
|
4189
4277
|
} catch (err) {
|
|
4190
4278
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -4213,12 +4301,12 @@ function planApprovedApiKeyCleanup(existingRaw, oauthToken) {
|
|
|
4213
4301
|
}
|
|
4214
4302
|
async function sanitizeApprovedApiKeys(oauthToken) {
|
|
4215
4303
|
try {
|
|
4216
|
-
const
|
|
4217
|
-
const cleaned = planApprovedApiKeyCleanup(await readRaw(
|
|
4304
|
+
const path = claudeJsonPath();
|
|
4305
|
+
const cleaned = planApprovedApiKeyCleanup(await readRaw(path), oauthToken);
|
|
4218
4306
|
if (cleaned === null) return;
|
|
4219
|
-
const verified = await writeWithReadBackRetry(fsWriteIo(
|
|
4307
|
+
const verified = await writeWithReadBackRetry(fsWriteIo(path), cleaned);
|
|
4220
4308
|
process.stderr.write(
|
|
4221
|
-
verified ? "[conveyor-agent] removed poisoned customApiKeyResponses.approved entry from .claude.json\n" : `[conveyor-agent] approved-key sanitize read-back still stale after retries at ${
|
|
4309
|
+
verified ? "[conveyor-agent] removed poisoned customApiKeyResponses.approved entry from .claude.json\n" : `[conveyor-agent] approved-key sanitize read-back still stale after retries at ${path} \u2014 CLI may still see the poisoned entry
|
|
4222
4310
|
`
|
|
4223
4311
|
);
|
|
4224
4312
|
} catch (err) {
|
|
@@ -4355,8 +4443,8 @@ async function persistOauthIdentityMarker(configIdentity, markerIdentity) {
|
|
|
4355
4443
|
async function ensureClaudeOnboarding(env = process.env, trustCwd) {
|
|
4356
4444
|
try {
|
|
4357
4445
|
if (!isConveyorCloudEnv(env)) return;
|
|
4358
|
-
const
|
|
4359
|
-
const existingRaw = await readRaw(
|
|
4446
|
+
const path = claudeJsonPath();
|
|
4447
|
+
const existingRaw = await readRaw(path);
|
|
4360
4448
|
const markerIdentity = parseOauthIdentity(await readRaw(conveyorOauthMarkerPath()));
|
|
4361
4449
|
await persistOauthIdentityMarker(
|
|
4362
4450
|
extractOauthIdentity(parseClaudeJson(existingRaw)),
|
|
@@ -4364,16 +4452,16 @@ async function ensureClaudeOnboarding(env = process.env, trustCwd) {
|
|
|
4364
4452
|
);
|
|
4365
4453
|
const contents = planClaudeJsonSeed(existingRaw, trustCwd, markerIdentity);
|
|
4366
4454
|
if (contents === null) return;
|
|
4367
|
-
const verified = await writeWithReadBackRetry(fsWriteIo(
|
|
4455
|
+
const verified = await writeWithReadBackRetry(fsWriteIo(path), contents);
|
|
4368
4456
|
if (verified) {
|
|
4369
4457
|
process.stderr.write(
|
|
4370
4458
|
`[conveyor-agent] claude onboarding seeded${trustCwd ? ` (trust: ${trustCwd})` : ""}
|
|
4371
4459
|
`
|
|
4372
4460
|
);
|
|
4373
4461
|
} else {
|
|
4374
|
-
const verify = await readRaw(
|
|
4462
|
+
const verify = await readRaw(path);
|
|
4375
4463
|
process.stderr.write(
|
|
4376
|
-
`[conveyor-agent] claude onboarding seed read-back MISMATCH at ${
|
|
4464
|
+
`[conveyor-agent] claude onboarding seed read-back MISMATCH at ${path} after retries: wrote ${contents.length}B, read ${verify?.length ?? 0}B \u2014 CLI may see stale config and park at a startup dialog
|
|
4377
4465
|
`
|
|
4378
4466
|
);
|
|
4379
4467
|
}
|
|
@@ -4386,13 +4474,13 @@ async function ensureClaudeOnboarding(env = process.env, trustCwd) {
|
|
|
4386
4474
|
async function removeConveyorCredentials(env = process.env) {
|
|
4387
4475
|
try {
|
|
4388
4476
|
if (!isConveyorCloudEnv(env)) return;
|
|
4389
|
-
const
|
|
4477
|
+
const path = claudeCredentialsPath();
|
|
4390
4478
|
const markerPath = conveyorCredentialsMarkerPath();
|
|
4391
|
-
const existing = parseClaudeAiOauth(await readRaw(
|
|
4479
|
+
const existing = parseClaudeAiOauth(await readRaw(path));
|
|
4392
4480
|
if (existing && !isConveyorOwnedCredentials(existing, parseCredentialsMarker(await readRaw(markerPath)))) {
|
|
4393
4481
|
return;
|
|
4394
4482
|
}
|
|
4395
|
-
await rm(
|
|
4483
|
+
await rm(path, { force: true });
|
|
4396
4484
|
await rm(markerPath, { force: true });
|
|
4397
4485
|
} catch (err) {
|
|
4398
4486
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -5633,13 +5721,19 @@ var PtyHarness = class _PtyHarness {
|
|
|
5633
5721
|
}
|
|
5634
5722
|
async *executeQuery(opts) {
|
|
5635
5723
|
const want = opts.resume ?? opts.options.resume ?? this.parked?.reportedSessionId ?? void 0;
|
|
5636
|
-
const
|
|
5724
|
+
const options = ensureSessionTarget(opts.options, want);
|
|
5725
|
+
if (options !== opts.options) {
|
|
5726
|
+
_PtyHarness.log.warn("spawn had no session target \u2014 minted one", {
|
|
5727
|
+
sessionId: options.sessionId
|
|
5728
|
+
});
|
|
5729
|
+
}
|
|
5730
|
+
const fingerprint = this.fingerprintOf(options);
|
|
5637
5731
|
let session;
|
|
5638
|
-
if (this.parked?.canReuse(want, fingerprint) && !await this.parkedHomeDied(
|
|
5732
|
+
if (this.parked?.canReuse(want, fingerprint) && !await this.parkedHomeDied(options)) {
|
|
5639
5733
|
session = this.parked;
|
|
5640
5734
|
this.parked = null;
|
|
5641
5735
|
this.cancelEndedTimer();
|
|
5642
|
-
await session.beginTurn(opts.prompt,
|
|
5736
|
+
await session.beginTurn(opts.prompt, options);
|
|
5643
5737
|
} else {
|
|
5644
5738
|
if (this.parked) {
|
|
5645
5739
|
const stale = this.parked;
|
|
@@ -5647,10 +5741,10 @@ var PtyHarness = class _PtyHarness {
|
|
|
5647
5741
|
this.cancelEndedTimer();
|
|
5648
5742
|
await stale.teardown();
|
|
5649
5743
|
}
|
|
5650
|
-
session = await this.spawnSession(opts.prompt,
|
|
5744
|
+
session = await this.spawnSession(opts.prompt, options, want);
|
|
5651
5745
|
}
|
|
5652
5746
|
yield* this.drain(session);
|
|
5653
|
-
yield* this.recoverFailedDelivery(session,
|
|
5747
|
+
yield* this.recoverFailedDelivery(session, options, want);
|
|
5654
5748
|
}
|
|
5655
5749
|
/**
|
|
5656
5750
|
* Did the shared `~/.claude` mount die while this session sat parked?
|
|
@@ -5901,10 +5995,10 @@ var TuiUnavailableError = class extends Error {
|
|
|
5901
5995
|
}
|
|
5902
5996
|
tui;
|
|
5903
5997
|
};
|
|
5904
|
-
function isExecutable(
|
|
5998
|
+
function isExecutable(path) {
|
|
5905
5999
|
try {
|
|
5906
|
-
if (!statSync(
|
|
5907
|
-
accessSync(
|
|
6000
|
+
if (!statSync(path).isFile()) return false;
|
|
6001
|
+
accessSync(path, constants.X_OK);
|
|
5908
6002
|
return true;
|
|
5909
6003
|
} catch {
|
|
5910
6004
|
return false;
|
|
@@ -5957,21 +6051,21 @@ function shouldSeed(existingEntry, seed) {
|
|
|
5957
6051
|
if (typeof entry.expires !== "number") return true;
|
|
5958
6052
|
return entry.expires < seed.expires;
|
|
5959
6053
|
}
|
|
5960
|
-
async function readJsonFile(
|
|
6054
|
+
async function readJsonFile(path) {
|
|
5961
6055
|
try {
|
|
5962
|
-
return JSON.parse(await fs.readFile(
|
|
6056
|
+
return JSON.parse(await fs.readFile(path, "utf8"));
|
|
5963
6057
|
} catch {
|
|
5964
6058
|
return {};
|
|
5965
6059
|
}
|
|
5966
6060
|
}
|
|
5967
|
-
async function writeJsonFile(
|
|
5968
|
-
await fs.mkdir(dirname2(
|
|
5969
|
-
await fs.writeFile(
|
|
6061
|
+
async function writeJsonFile(path, value) {
|
|
6062
|
+
await fs.mkdir(dirname2(path), { recursive: true });
|
|
6063
|
+
await fs.writeFile(path, `${JSON.stringify(value, null, 2)}
|
|
5970
6064
|
`, { mode: 384 });
|
|
5971
6065
|
}
|
|
5972
6066
|
async function ensureAuthEntry(env, seed) {
|
|
5973
|
-
const
|
|
5974
|
-
const store = await readJsonFile(
|
|
6067
|
+
const path = opencodeAuthPath(env);
|
|
6068
|
+
const store = await readJsonFile(path);
|
|
5975
6069
|
if (!shouldSeed(store.openai, seed)) {
|
|
5976
6070
|
logger.info("opencode oauth store is fresher than the seed; leaving it alone");
|
|
5977
6071
|
return;
|
|
@@ -5982,12 +6076,12 @@ async function ensureAuthEntry(env, seed) {
|
|
|
5982
6076
|
refresh: seed.refresh,
|
|
5983
6077
|
expires: seed.expires
|
|
5984
6078
|
};
|
|
5985
|
-
await writeJsonFile(
|
|
6079
|
+
await writeJsonFile(path, store);
|
|
5986
6080
|
logger.info("seeded opencode oauth store entry");
|
|
5987
6081
|
}
|
|
5988
6082
|
async function ensurePluginConfig(env) {
|
|
5989
|
-
const
|
|
5990
|
-
const config = await readJsonFile(
|
|
6083
|
+
const path = opencodeConfigPath(env);
|
|
6084
|
+
const config = await readJsonFile(path);
|
|
5991
6085
|
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
5992
6086
|
const isOurs = (p) => typeof p === "string" && (p === PLUGIN_PACKAGE || p.startsWith(`${PLUGIN_PACKAGE}@`));
|
|
5993
6087
|
const hasExactPin = plugins.includes(OPENCODE_CODEX_PLUGIN);
|
|
@@ -5995,7 +6089,7 @@ async function ensurePluginConfig(env) {
|
|
|
5995
6089
|
if (hasExactPin && !hasStalePin) return;
|
|
5996
6090
|
const kept = plugins.filter((p) => !isOurs(p));
|
|
5997
6091
|
config.plugin = [...kept, OPENCODE_CODEX_PLUGIN];
|
|
5998
|
-
await writeJsonFile(
|
|
6092
|
+
await writeJsonFile(path, config);
|
|
5999
6093
|
logger.info("ensured opencode codex-auth plugin in config");
|
|
6000
6094
|
}
|
|
6001
6095
|
async function seedOpenCodeOauth(env) {
|
|
@@ -6217,9 +6311,9 @@ var OpenCodeHeadlessHarness = class {
|
|
|
6217
6311
|
*/
|
|
6218
6312
|
async writeSystemPrompt(text) {
|
|
6219
6313
|
if (!text || text.trim() === "") return null;
|
|
6220
|
-
const
|
|
6221
|
-
await writeFile6(
|
|
6222
|
-
return
|
|
6314
|
+
const path = join10(this.tempDir, "conveyor-instructions.md");
|
|
6315
|
+
await writeFile6(path, text, "utf8");
|
|
6316
|
+
return path;
|
|
6223
6317
|
}
|
|
6224
6318
|
/**
|
|
6225
6319
|
* Parse one NDJSON line and push whatever it maps to. Returns assistant text so
|
|
@@ -6681,8 +6775,8 @@ function wrapBridgeWithDirectStream(inner, reporter, options = {}) {
|
|
|
6681
6775
|
}
|
|
6682
6776
|
|
|
6683
6777
|
// src/execution/query-executor.ts
|
|
6684
|
-
import { createHash as createHash2 } from "crypto";
|
|
6685
|
-
import { existsSync, readFileSync as
|
|
6778
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
6779
|
+
import { existsSync, readFileSync as readFileSync2, renameSync, truncateSync } from "fs";
|
|
6686
6780
|
|
|
6687
6781
|
// src/execution/chat-instructions.ts
|
|
6688
6782
|
function buildChatInstructions(context, scenario, newMessages) {
|
|
@@ -7591,11 +7685,8 @@ function tagContextIntro(runnerMode) {
|
|
|
7591
7685
|
return `These docs match this task's tags. They auto-load when you edit matching files, and you can Read any of them the moment you need its detail \u2014 do NOT read them all up front.`;
|
|
7592
7686
|
}
|
|
7593
7687
|
function formatResolvedTags(resolved, subProject, mentioned, runnerMode) {
|
|
7594
|
-
const parts = [
|
|
7595
|
-
|
|
7596
|
-
## Reference Guides (load on demand)`,
|
|
7597
|
-
tagContextIntro(runnerMode)
|
|
7598
|
-
];
|
|
7688
|
+
const parts = [`
|
|
7689
|
+
## Reference Guides (load on demand)`, tagContextIntro(runnerMode)];
|
|
7599
7690
|
for (const tag of resolved) {
|
|
7600
7691
|
if (tag.entries.length === 0 && !tag.hasOverview) continue;
|
|
7601
7692
|
const desc = tag.description ? ` \u2014 ${tag.description}` : "";
|
|
@@ -8035,7 +8126,10 @@ function buildRelaunchWithSession(mode, context, agentMode, isAuto) {
|
|
|
8035
8126
|
parts.push(`You were relaunched but no new instructions have been given since your last run.`);
|
|
8036
8127
|
if (agentMode === "auto" || agentMode === "building" || isAuto) {
|
|
8037
8128
|
parts.push(
|
|
8038
|
-
...builderKickoff(
|
|
8129
|
+
...builderKickoff(
|
|
8130
|
+
context,
|
|
8131
|
+
`You are the Builder on this card \u2014 pick up where you left off.`
|
|
8132
|
+
)
|
|
8039
8133
|
);
|
|
8040
8134
|
} else {
|
|
8041
8135
|
parts.push(
|
|
@@ -9318,7 +9412,9 @@ var driveListFilesContract = defineToolContract({
|
|
|
9318
9412
|
fields: {
|
|
9319
9413
|
folderId: f.optional(f.string({ desc: `Folder to list. ${ROOT_DEFAULT}` })),
|
|
9320
9414
|
search: f.optional(f.string({ desc: "Only return names containing this text", max: 200 })),
|
|
9321
|
-
limit: f.optional(
|
|
9415
|
+
limit: f.optional(
|
|
9416
|
+
f.number({ desc: "Max entries (default 100)", int: true, min: 1, max: 200 })
|
|
9417
|
+
)
|
|
9322
9418
|
}
|
|
9323
9419
|
},
|
|
9324
9420
|
mcp: {
|
|
@@ -9327,7 +9423,9 @@ var driveListFilesContract = defineToolContract({
|
|
|
9327
9423
|
projectId: mcpProjectId,
|
|
9328
9424
|
folderId: f.optional(f.string({ desc: `Folder to list. ${ROOT_DEFAULT}` })),
|
|
9329
9425
|
search: f.optional(f.string({ desc: "Only return names containing this text", max: 200 })),
|
|
9330
|
-
limit: f.optional(
|
|
9426
|
+
limit: f.optional(
|
|
9427
|
+
f.number({ desc: "Max entries (default 100)", int: true, min: 1, max: 200 })
|
|
9428
|
+
)
|
|
9331
9429
|
}
|
|
9332
9430
|
}
|
|
9333
9431
|
});
|
|
@@ -9430,7 +9528,12 @@ var listMeetingsContract = defineToolContract({
|
|
|
9430
9528
|
description: `List this project's meetings, newest first \u2014 each with its title, date, source, status, participants, and a short summary preview. Start here when you are asked about "the meeting", "what did we decide", or "what came out of that call". ${SUMMARY_NOTE}`,
|
|
9431
9529
|
fields: {
|
|
9432
9530
|
limit: f.optional(
|
|
9433
|
-
f.number({
|
|
9531
|
+
f.number({
|
|
9532
|
+
desc: "How many to return (default 20, maximum 50).",
|
|
9533
|
+
int: true,
|
|
9534
|
+
min: 1,
|
|
9535
|
+
max: 50
|
|
9536
|
+
})
|
|
9434
9537
|
),
|
|
9435
9538
|
search: f.optional(
|
|
9436
9539
|
f.string({ desc: "Case-insensitive match on the meeting title.", max: 200 })
|
|
@@ -9442,7 +9545,12 @@ var listMeetingsContract = defineToolContract({
|
|
|
9442
9545
|
fields: {
|
|
9443
9546
|
projectId: mcpProjectId,
|
|
9444
9547
|
limit: f.optional(
|
|
9445
|
-
f.number({
|
|
9548
|
+
f.number({
|
|
9549
|
+
desc: "How many to return (default 20, maximum 50).",
|
|
9550
|
+
int: true,
|
|
9551
|
+
min: 1,
|
|
9552
|
+
max: 50
|
|
9553
|
+
})
|
|
9446
9554
|
),
|
|
9447
9555
|
search: f.optional(
|
|
9448
9556
|
f.string({ desc: "Case-insensitive match on the meeting title.", max: 200 })
|
|
@@ -9496,9 +9604,7 @@ var SINCE_MINUTES = f.optional(
|
|
|
9496
9604
|
max: 10080
|
|
9497
9605
|
})
|
|
9498
9606
|
);
|
|
9499
|
-
var START_TIME = f.optional(
|
|
9500
|
-
f.string({ desc: "ISO 8601 lower bound (overrides sinceMinutes)" })
|
|
9501
|
-
);
|
|
9607
|
+
var START_TIME = f.optional(f.string({ desc: "ISO 8601 lower bound (overrides sinceMinutes)" }));
|
|
9502
9608
|
var END_TIME = f.optional(f.string({ desc: "ISO 8601 upper bound (default now)" }));
|
|
9503
9609
|
var LIMIT = f.optional(
|
|
9504
9610
|
f.number({ desc: "Max entries per page (default 50)", int: true, min: 1, max: 200 })
|
|
@@ -9532,7 +9638,10 @@ var gcpFields = {
|
|
|
9532
9638
|
})
|
|
9533
9639
|
),
|
|
9534
9640
|
search: f.optional(
|
|
9535
|
-
f.string({
|
|
9641
|
+
f.string({
|
|
9642
|
+
desc: "Free-text search across all log fields (exact substring, not regex)",
|
|
9643
|
+
max: 256
|
|
9644
|
+
})
|
|
9536
9645
|
),
|
|
9537
9646
|
filter: f.optional(
|
|
9538
9647
|
f.string({
|
|
@@ -10460,9 +10569,9 @@ Paste this into the PR description to show it inline:
|
|
|
10460
10569
|
${snippet}`;
|
|
10461
10570
|
}
|
|
10462
10571
|
function buildUploadAttachmentTool(connection, config) {
|
|
10463
|
-
return defineContractTool(uploadAttachmentContract, async ({ path
|
|
10572
|
+
return defineContractTool(uploadAttachmentContract, async ({ path, title, tags }) => {
|
|
10464
10573
|
try {
|
|
10465
|
-
const filePath = isAbsolute(
|
|
10574
|
+
const filePath = isAbsolute(path) ? path : join12(config.workspaceDir, path);
|
|
10466
10575
|
const mimeType = inferMimeType(filePath);
|
|
10467
10576
|
const info = await statWorkspacePath(filePath);
|
|
10468
10577
|
if (!info.isFile) {
|
|
@@ -11159,7 +11268,9 @@ var CONTEXT_PATH_SHAPE = z18.object({
|
|
|
11159
11268
|
locator: z18.string().min(1).max(CONTEXT_LINK_LOCATOR_MAX).regex(/^[^\r\n]*$/, "Locator cannot contain line breaks").optional().describe(
|
|
11160
11269
|
'Verified-link tether: text that must keep existing in the file. With locatorType "test" it must appear inside a real it/test/describe TITLE; with "code" anywhere in the file. Validated at write time against the checkout and re-checked by the periodic sweep \u2014 a rename/delete flags the link stale. Locators containing <> are placeholders and never checked.'
|
|
11161
11270
|
),
|
|
11162
|
-
locatorType: z18.enum(["test", "code"]).optional().describe(
|
|
11271
|
+
locatorType: z18.enum(["test", "code"]).optional().describe(
|
|
11272
|
+
"How the locator must match \u2014 required iff locator is set; not valid on folder links"
|
|
11273
|
+
)
|
|
11163
11274
|
}).refine((link) => link.locator === void 0 === (link.locatorType === void 0), {
|
|
11164
11275
|
message: "locator and locatorType must be provided together"
|
|
11165
11276
|
}).refine((link) => link.locator === void 0 || link.type !== "folder", {
|
|
@@ -11553,52 +11664,43 @@ function buildDriveCreateFileTool(connection, projectId) {
|
|
|
11553
11664
|
);
|
|
11554
11665
|
}
|
|
11555
11666
|
function buildDriveUpdateFileTool(connection, projectId) {
|
|
11556
|
-
return defineContractTool(
|
|
11557
|
-
|
|
11558
|
-
|
|
11559
|
-
|
|
11560
|
-
|
|
11561
|
-
|
|
11562
|
-
|
|
11563
|
-
|
|
11564
|
-
|
|
11565
|
-
|
|
11566
|
-
|
|
11567
|
-
} catch (error) {
|
|
11568
|
-
return errText2("Failed to update the Google Drive file", error);
|
|
11569
|
-
}
|
|
11667
|
+
return defineContractTool(driveUpdateFileContract, async ({ fileId, content, mimeType }) => {
|
|
11668
|
+
try {
|
|
11669
|
+
const file = await connection.call("updateProjectDriveFile", {
|
|
11670
|
+
projectId,
|
|
11671
|
+
fileId,
|
|
11672
|
+
content,
|
|
11673
|
+
mimeType
|
|
11674
|
+
});
|
|
11675
|
+
return textResult(`Updated "${file.name}" (${file.id})`);
|
|
11676
|
+
} catch (error) {
|
|
11677
|
+
return errText2("Failed to update the Google Drive file", error);
|
|
11570
11678
|
}
|
|
11571
|
-
);
|
|
11679
|
+
});
|
|
11572
11680
|
}
|
|
11573
11681
|
function buildDriveDeleteFileTool(connection, projectId) {
|
|
11574
|
-
return defineContractTool(
|
|
11575
|
-
|
|
11576
|
-
|
|
11577
|
-
|
|
11578
|
-
|
|
11579
|
-
|
|
11580
|
-
} catch (error) {
|
|
11581
|
-
return errText2("Failed to delete the Google Drive file", error);
|
|
11582
|
-
}
|
|
11682
|
+
return defineContractTool(driveDeleteFileContract, async ({ fileId }) => {
|
|
11683
|
+
try {
|
|
11684
|
+
const result = await connection.call("deleteProjectDriveFile", { projectId, fileId });
|
|
11685
|
+
return textResult(`Moved "${result.name}" (${result.id}) to the Google Drive trash`);
|
|
11686
|
+
} catch (error) {
|
|
11687
|
+
return errText2("Failed to delete the Google Drive file", error);
|
|
11583
11688
|
}
|
|
11584
|
-
);
|
|
11689
|
+
});
|
|
11585
11690
|
}
|
|
11586
11691
|
function buildDriveCreateFolderTool(connection, projectId) {
|
|
11587
|
-
return defineContractTool(
|
|
11588
|
-
|
|
11589
|
-
|
|
11590
|
-
|
|
11591
|
-
|
|
11592
|
-
|
|
11593
|
-
|
|
11594
|
-
|
|
11595
|
-
|
|
11596
|
-
|
|
11597
|
-
} catch (error) {
|
|
11598
|
-
return errText2("Failed to create the Google Drive folder", error);
|
|
11599
|
-
}
|
|
11692
|
+
return defineContractTool(driveCreateFolderContract, async ({ name, folderId }) => {
|
|
11693
|
+
try {
|
|
11694
|
+
const folder = await connection.call("createProjectDriveFolder", {
|
|
11695
|
+
projectId,
|
|
11696
|
+
name,
|
|
11697
|
+
folderId
|
|
11698
|
+
});
|
|
11699
|
+
return textResult(`Created folder "${folder.name}" (${folder.id})`);
|
|
11700
|
+
} catch (error) {
|
|
11701
|
+
return errText2("Failed to create the Google Drive folder", error);
|
|
11600
11702
|
}
|
|
11601
|
-
);
|
|
11703
|
+
});
|
|
11602
11704
|
}
|
|
11603
11705
|
function buildDriveTools(connection, projectId) {
|
|
11604
11706
|
return [
|
|
@@ -12646,33 +12748,6 @@ function collectMissingProps(taskProps) {
|
|
|
12646
12748
|
return missing;
|
|
12647
12749
|
}
|
|
12648
12750
|
|
|
12649
|
-
// src/runner/heavy-gate.ts
|
|
12650
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
12651
|
-
import path from "path";
|
|
12652
|
-
var GATE_KEYS = ["heavy", "test", "typecheck", "build"];
|
|
12653
|
-
function runDir() {
|
|
12654
|
-
return process.env.CONVEYOR_RUN_DIR ?? "/tmp/conveyor-run";
|
|
12655
|
-
}
|
|
12656
|
-
function pidAlive(pid) {
|
|
12657
|
-
try {
|
|
12658
|
-
process.kill(pid, 0);
|
|
12659
|
-
return true;
|
|
12660
|
-
} catch {
|
|
12661
|
-
return false;
|
|
12662
|
-
}
|
|
12663
|
-
}
|
|
12664
|
-
function isHeavyGateActive() {
|
|
12665
|
-
for (const key of GATE_KEYS) {
|
|
12666
|
-
try {
|
|
12667
|
-
const raw = readFileSync2(path.join(runDir(), `${key}.pid`), "utf8").trim();
|
|
12668
|
-
const pid = Number.parseInt(raw, 10);
|
|
12669
|
-
if (Number.isInteger(pid) && pid > 0 && pidAlive(pid)) return true;
|
|
12670
|
-
} catch {
|
|
12671
|
-
}
|
|
12672
|
-
}
|
|
12673
|
-
return false;
|
|
12674
|
-
}
|
|
12675
|
-
|
|
12676
12751
|
// src/execution/tool-loop-tracker.ts
|
|
12677
12752
|
var REPEAT_INTERRUPT_THRESHOLD = 4;
|
|
12678
12753
|
var REPEAT_INTERRUPT_INTERVAL = 4;
|
|
@@ -13173,10 +13248,10 @@ function resolveSessionStart(lineageKey, cwd) {
|
|
|
13173
13248
|
}
|
|
13174
13249
|
return { sessionId: sessionUuid };
|
|
13175
13250
|
}
|
|
13176
|
-
function repairTornSessionFile(
|
|
13251
|
+
function repairTornSessionFile(path) {
|
|
13177
13252
|
try {
|
|
13178
|
-
if (!existsSync(
|
|
13179
|
-
const content =
|
|
13253
|
+
if (!existsSync(path)) return false;
|
|
13254
|
+
const content = readFileSync2(path, "utf8");
|
|
13180
13255
|
if (content.length === 0) return false;
|
|
13181
13256
|
let keepEnd = content.length;
|
|
13182
13257
|
if (!content.endsWith("\n")) {
|
|
@@ -13195,9 +13270,9 @@ function repairTornSessionFile(path2) {
|
|
|
13195
13270
|
keepEnd = prevNewline + 1;
|
|
13196
13271
|
}
|
|
13197
13272
|
if (keepEnd === content.length) return false;
|
|
13198
|
-
truncateSync(
|
|
13273
|
+
truncateSync(path, Buffer.byteLength(content.slice(0, keepEnd), "utf8"));
|
|
13199
13274
|
logger5.warn("Repaired torn transcript before resume", {
|
|
13200
|
-
path
|
|
13275
|
+
path,
|
|
13201
13276
|
trimmedBytes: content.length - keepEnd
|
|
13202
13277
|
});
|
|
13203
13278
|
return true;
|
|
@@ -13621,6 +13696,29 @@ async function runInitialQuery(host, context, options, resume, promptDelivery) {
|
|
|
13621
13696
|
}
|
|
13622
13697
|
await trackAndRun(host, context, queryOptions, agentQuery);
|
|
13623
13698
|
}
|
|
13699
|
+
function rotateSessionFile(cwd, sessionUuid) {
|
|
13700
|
+
const path = sessionTranscriptPath(cwd, sessionUuid);
|
|
13701
|
+
try {
|
|
13702
|
+
if (!existsSync(path)) return true;
|
|
13703
|
+
renameSync(path, `${path.replace(/\.jsonl$/, "")}.aborted-${Date.now()}.jsonl`);
|
|
13704
|
+
return true;
|
|
13705
|
+
} catch (error) {
|
|
13706
|
+
logger5.warn("Could not rotate an aborted transcript aside", {
|
|
13707
|
+
path,
|
|
13708
|
+
error: error instanceof Error ? error.message : String(error)
|
|
13709
|
+
});
|
|
13710
|
+
return false;
|
|
13711
|
+
}
|
|
13712
|
+
}
|
|
13713
|
+
function freshSessionOptions(host, context, options) {
|
|
13714
|
+
const lineageUuid = taskIdToSessionUuid(
|
|
13715
|
+
sessionLineageKey(context.taskId, host.agentMode, host.config.mode)
|
|
13716
|
+
);
|
|
13717
|
+
if (rotateSessionFile(host.config.workspaceDir, lineageUuid)) {
|
|
13718
|
+
return { ...options, sessionId: lineageUuid };
|
|
13719
|
+
}
|
|
13720
|
+
return { ...options, sessionId: randomUUID2() };
|
|
13721
|
+
}
|
|
13624
13722
|
async function buildRetryQuery(host, context, options, lastErrorWasImage) {
|
|
13625
13723
|
if (lastErrorWasImage) {
|
|
13626
13724
|
host.connection.postChatMessage(
|
|
@@ -13640,10 +13738,7 @@ async function buildRetryQuery(host, context, options, lastErrorWasImage) {
|
|
|
13640
13738
|
);
|
|
13641
13739
|
return host.harness.executeQuery({
|
|
13642
13740
|
prompt: host.createInputStream(retryPrompt),
|
|
13643
|
-
|
|
13644
|
-
// session with that ID, passing it again would error. Let the SDK
|
|
13645
|
-
// auto-generate on retry attempts.
|
|
13646
|
-
options: { ...options, sessionId: void 0 },
|
|
13741
|
+
options: freshSessionOptions(host, context, options),
|
|
13647
13742
|
resume: void 0
|
|
13648
13743
|
});
|
|
13649
13744
|
}
|
|
@@ -13675,7 +13770,7 @@ async function handleAuthError(context, host, options) {
|
|
|
13675
13770
|
);
|
|
13676
13771
|
const freshQuery = host.harness.executeQuery({
|
|
13677
13772
|
prompt: host.createInputStream(freshPrompt),
|
|
13678
|
-
options:
|
|
13773
|
+
options: freshSessionOptions(host, context, options),
|
|
13679
13774
|
resume: void 0
|
|
13680
13775
|
});
|
|
13681
13776
|
return runWithRetry(freshQuery, context, host, options);
|
|
@@ -13696,7 +13791,7 @@ async function handleStaleSession(context, host, options) {
|
|
|
13696
13791
|
);
|
|
13697
13792
|
const freshQuery = host.harness.executeQuery({
|
|
13698
13793
|
prompt: host.createInputStream(freshPrompt),
|
|
13699
|
-
options:
|
|
13794
|
+
options: freshSessionOptions(host, context, options),
|
|
13700
13795
|
resume: void 0
|
|
13701
13796
|
});
|
|
13702
13797
|
return runWithRetry(freshQuery, context, host, options);
|
|
@@ -13796,7 +13891,7 @@ async function handleUsageCapRejection(context, host, options, rateLimitType, re
|
|
|
13796
13891
|
);
|
|
13797
13892
|
const freshQuery = host.harness.executeQuery({
|
|
13798
13893
|
prompt: host.createInputStream(freshPrompt),
|
|
13799
|
-
options:
|
|
13894
|
+
options: freshSessionOptions(host, context, options),
|
|
13800
13895
|
resume: void 0
|
|
13801
13896
|
});
|
|
13802
13897
|
return runWithRetry(freshQuery, context, host, options);
|
|
@@ -14301,6 +14396,7 @@ var FIRST_SEND_MS = 5e3;
|
|
|
14301
14396
|
var RESEND_INTERVAL_MS = 4e3;
|
|
14302
14397
|
var MAX_SENDS = 5;
|
|
14303
14398
|
var RENDER_SETTLE_MS = 900;
|
|
14399
|
+
var EMPTY_MCP_CONFIG = JSON.stringify({ mcpServers: {} });
|
|
14304
14400
|
function buildProbeEnv(env = process.env) {
|
|
14305
14401
|
const clean = {};
|
|
14306
14402
|
for (const [key, value] of Object.entries(env)) {
|
|
@@ -14356,7 +14452,7 @@ var UsageProbeRun = class {
|
|
|
14356
14452
|
this.settleTimer = setTimeout(() => this.finish(this.buf), this.timing.settleMs);
|
|
14357
14453
|
}
|
|
14358
14454
|
finishBestEffort() {
|
|
14359
|
-
this.finish(panelRendering(this.buf) ? this.buf :
|
|
14455
|
+
this.finish(panelRendering(this.buf) ? this.buf : cleanTerminalOutput(this.buf, 500));
|
|
14360
14456
|
}
|
|
14361
14457
|
finish(out) {
|
|
14362
14458
|
if (this.settled) return;
|
|
@@ -14368,6 +14464,12 @@ var UsageProbeRun = class {
|
|
|
14368
14464
|
this.resolve(out);
|
|
14369
14465
|
}
|
|
14370
14466
|
};
|
|
14467
|
+
function buildUsageProbeArgs() {
|
|
14468
|
+
return ["--mcp-config", EMPTY_MCP_CONFIG, "--strict-mcp-config"];
|
|
14469
|
+
}
|
|
14470
|
+
function resolveUsageProbeCwd(explicitCwd, env = process.env) {
|
|
14471
|
+
return explicitCwd ?? env.CONVEYOR_WORKSPACE ?? process.cwd();
|
|
14472
|
+
}
|
|
14371
14473
|
async function runUsageProbe(deps = {}) {
|
|
14372
14474
|
let spawn2 = deps.spawn;
|
|
14373
14475
|
if (!spawn2) {
|
|
@@ -14378,7 +14480,8 @@ async function runUsageProbe(deps = {}) {
|
|
|
14378
14480
|
}
|
|
14379
14481
|
}
|
|
14380
14482
|
const binary = deps.binary ?? resolveClaudeBinary();
|
|
14381
|
-
const
|
|
14483
|
+
const baseEnv = deps.env ?? process.env;
|
|
14484
|
+
const cwd = resolveUsageProbeCwd(deps.cwd, baseEnv);
|
|
14382
14485
|
const timeoutMs = deps.timeoutMs ?? PROBE_TIMEOUT_MS;
|
|
14383
14486
|
const timing = {
|
|
14384
14487
|
firstSendMs: deps.firstSendMs ?? FIRST_SEND_MS,
|
|
@@ -14388,12 +14491,12 @@ async function runUsageProbe(deps = {}) {
|
|
|
14388
14491
|
return new Promise((resolve) => {
|
|
14389
14492
|
let child;
|
|
14390
14493
|
try {
|
|
14391
|
-
child = spawn2(binary,
|
|
14494
|
+
child = spawn2(binary, buildUsageProbeArgs(), {
|
|
14392
14495
|
name: "xterm-256color",
|
|
14393
14496
|
cols: 120,
|
|
14394
14497
|
rows: 45,
|
|
14395
14498
|
cwd,
|
|
14396
|
-
env: buildProbeEnv(
|
|
14499
|
+
env: buildProbeEnv(baseEnv)
|
|
14397
14500
|
});
|
|
14398
14501
|
} catch {
|
|
14399
14502
|
resolve("");
|
|
@@ -14408,10 +14511,10 @@ var logger7 = createServiceLogger("usage-sampler");
|
|
|
14408
14511
|
var NO_SAMPLES = { samples: [], unmeasurable: null };
|
|
14409
14512
|
function isAttributable(identity, sessionToken) {
|
|
14410
14513
|
if (!identity) return { ok: true };
|
|
14411
|
-
if (identity.hasRefreshToken) {
|
|
14514
|
+
if (identity.hasRefreshToken && !identity.isConveyorOwned) {
|
|
14412
14515
|
return { ok: false, reason: "manual-login-credentials" };
|
|
14413
14516
|
}
|
|
14414
|
-
if (sessionToken && identity.accessToken && identity.accessToken !== sessionToken) {
|
|
14517
|
+
if (!(identity.hasRefreshToken && identity.isConveyorOwned) && sessionToken && identity.accessToken && identity.accessToken !== sessionToken) {
|
|
14415
14518
|
return { ok: false, reason: "credentials-token-mismatch" };
|
|
14416
14519
|
}
|
|
14417
14520
|
return { ok: true };
|
|
@@ -14524,6 +14627,9 @@ async function handlePullBranch(workDir, branch) {
|
|
|
14524
14627
|
}
|
|
14525
14628
|
}
|
|
14526
14629
|
|
|
14630
|
+
// src/runner/session-runner.ts
|
|
14631
|
+
import { rmSync } from "fs";
|
|
14632
|
+
|
|
14527
14633
|
// src/runner/background-work.ts
|
|
14528
14634
|
var BACKGROUND_WORK_MAX_MS = 45 * 60 * 1e3;
|
|
14529
14635
|
var BACKGROUND_DEFAULT_BY_TOOL = {
|
|
@@ -14583,10 +14689,8 @@ var BackgroundWorkTracker = class {
|
|
|
14583
14689
|
this.prune(now);
|
|
14584
14690
|
if (this.deadlines.length === 0) return;
|
|
14585
14691
|
this.deadlines.shift();
|
|
14586
|
-
this.log(
|
|
14587
|
-
|
|
14588
|
-
`
|
|
14589
|
-
);
|
|
14692
|
+
this.log(`[conveyor-agent] background work finished; ${this.deadlines.length} outstanding
|
|
14693
|
+
`);
|
|
14590
14694
|
this.onChange?.();
|
|
14591
14695
|
}
|
|
14592
14696
|
/** Is any non-expired background work outstanding? */
|
|
@@ -14635,6 +14739,7 @@ function findLiveChild(sources) {
|
|
|
14635
14739
|
// src/runner/session-runner.ts
|
|
14636
14740
|
var AUTO_RUN_MODES = /* @__PURE__ */ new Set(["building", "auto", "review", "discovery", "chat"]);
|
|
14637
14741
|
var CHILD_DEFER_RECHECK_MS = 60 * 1e3;
|
|
14742
|
+
var ORPHAN_WAKE_GRACE_MS = 3 * 60 * 1e3;
|
|
14638
14743
|
var SessionRunner = class _SessionRunner {
|
|
14639
14744
|
connection;
|
|
14640
14745
|
mode;
|
|
@@ -14704,9 +14809,11 @@ var SessionRunner = class _SessionRunner {
|
|
|
14704
14809
|
onHeartbeat: () => {
|
|
14705
14810
|
const loopStatus = this.refreshLoopStatus();
|
|
14706
14811
|
this.connection.sendHeartbeat(this.loopLag.takeMaxLagMs(), loopStatus);
|
|
14812
|
+
this.maybeWakeForOrphanedBackgroundWork();
|
|
14707
14813
|
},
|
|
14708
14814
|
onIdleTimeout: () => {
|
|
14709
14815
|
if (this.deferShutdownForLiveChild("idle")) return;
|
|
14816
|
+
if (this.deferShutdownForBackgroundWork()) return;
|
|
14710
14817
|
process.stderr.write("[conveyor-agent] Idle timeout reached, stopping agent\n");
|
|
14711
14818
|
this.stopped = true;
|
|
14712
14819
|
this.queryBridge?.stop();
|
|
@@ -14787,6 +14894,135 @@ var SessionRunner = class _SessionRunner {
|
|
|
14787
14894
|
}
|
|
14788
14895
|
return true;
|
|
14789
14896
|
}
|
|
14897
|
+
/** Epoch ms of the first idle-shutdown deferral for background work in the
|
|
14898
|
+
* current idle episode; null when nothing has deferred. Reset when a turn
|
|
14899
|
+
* runs, so each idle episode gets its own bounded hold. */
|
|
14900
|
+
backgroundDeferStartedAt = null;
|
|
14901
|
+
/**
|
|
14902
|
+
* Suppress an idle shutdown while backgrounded work (`run_in_background`
|
|
14903
|
+
* launches tracked by `BackgroundWorkTracker`) or a live gate process
|
|
14904
|
+
* (`isHeavyGateActive` — singleton pidfiles plus the `conveyor-gate`
|
|
14905
|
+
* contract dir) is still running. Without this the 30-minute idle timeout
|
|
14906
|
+
* stopped the runner mid-gate: `shutdown()` cleared the tracker, heartbeats
|
|
14907
|
+
* stopped, the activity clock expired, and the reconciler slept the pod and
|
|
14908
|
+
* reverted the card to Open.
|
|
14909
|
+
*
|
|
14910
|
+
* Bounded: deferrals past the first one cap at `BACKGROUND_WORK_MAX_MS`, so
|
|
14911
|
+
* a wedged gate process cannot pin a pod forever. Only the IDLE timeout is
|
|
14912
|
+
* deferred — post-`completed` dormancy keeps today's behavior, and a parked
|
|
14913
|
+
* prefill (`waiting_for_input`) contributes nothing here, so those cards
|
|
14914
|
+
* still sleep on the ordinary window.
|
|
14915
|
+
*/
|
|
14916
|
+
deferShutdownForBackgroundWork() {
|
|
14917
|
+
let holding = null;
|
|
14918
|
+
try {
|
|
14919
|
+
if (this.backgroundWork.hasPending()) holding = "background work outstanding";
|
|
14920
|
+
else if (isHeavyGateActive()) holding = "a gate process is running";
|
|
14921
|
+
} catch (err) {
|
|
14922
|
+
process.stderr.write(
|
|
14923
|
+
`[conveyor-agent] Background-work probe failed, not deferring: ${err}
|
|
14924
|
+
`
|
|
14925
|
+
);
|
|
14926
|
+
return false;
|
|
14927
|
+
}
|
|
14928
|
+
if (!holding) {
|
|
14929
|
+
this.backgroundDeferStartedAt = null;
|
|
14930
|
+
return false;
|
|
14931
|
+
}
|
|
14932
|
+
const now = Date.now();
|
|
14933
|
+
this.backgroundDeferStartedAt ??= now;
|
|
14934
|
+
if (now - this.backgroundDeferStartedAt >= BACKGROUND_WORK_MAX_MS) {
|
|
14935
|
+
process.stderr.write(
|
|
14936
|
+
`[conveyor-agent] Idle-shutdown deferral cap (${BACKGROUND_WORK_MAX_MS}ms) reached while ${holding} \u2014 proceeding with shutdown
|
|
14937
|
+
`
|
|
14938
|
+
);
|
|
14939
|
+
return false;
|
|
14940
|
+
}
|
|
14941
|
+
const recheckMs = Math.min(this.lifecycle.config.idleTimeoutMs, CHILD_DEFER_RECHECK_MS);
|
|
14942
|
+
process.stderr.write(`[conveyor-agent] Idle timeout deferred: ${holding}
|
|
14943
|
+
`);
|
|
14944
|
+
this.lifecycle.startIdleTimer(recheckMs);
|
|
14945
|
+
return true;
|
|
14946
|
+
}
|
|
14947
|
+
/** When orphan evidence (tracked work pending, no live gate process) began
|
|
14948
|
+
* holding continuously; null while the evidence is absent. */
|
|
14949
|
+
orphanGraceStartedAt = null;
|
|
14950
|
+
/** One-shot latch: at most one watchdog wake per idle episode. Reset when a
|
|
14951
|
+
* turn runs. */
|
|
14952
|
+
backgroundWakeFiredThisIdle = false;
|
|
14953
|
+
/** When the current idle episode began — an exit sentinel older than this
|
|
14954
|
+
* appeared while a turn could still have consumed it, so it never wakes. */
|
|
14955
|
+
idleEpisodeStartedAt = 0;
|
|
14956
|
+
/**
|
|
14957
|
+
* Wake a parked agent whose background work finished — or died — without a
|
|
14958
|
+
* consumed completion callback. Completion normally arrives as the CLI's
|
|
14959
|
+
* `<task-notification>` transcript record; when that record is lost (SDK
|
|
14960
|
+
* harness, torn transcript, a gate killed out from under the CLI) the
|
|
14961
|
+
* tracker entry silently expires and the agent waits forever.
|
|
14962
|
+
*
|
|
14963
|
+
* This is NOT the removed auto-mode stuck-nudge: it fires only on positive
|
|
14964
|
+
* evidence of orphaned background work — a tracked launch with no live gate
|
|
14965
|
+
* process continuously past `ORPHAN_WAKE_GRACE_MS`, or an unconsumed
|
|
14966
|
+
* `gates/<label>.exit` sentinel that appeared this idle episode — never on
|
|
14967
|
+
* "the agent seems stuck". A `completed` agent is never woken (the
|
|
14968
|
+
* completion guard holds), and a live pid always suppresses the wake.
|
|
14969
|
+
*/
|
|
14970
|
+
maybeWakeForOrphanedBackgroundWork() {
|
|
14971
|
+
try {
|
|
14972
|
+
if (this._state !== "idle" || this.stopped || this.completedThisTurn) {
|
|
14973
|
+
this.orphanGraceStartedAt = null;
|
|
14974
|
+
return;
|
|
14975
|
+
}
|
|
14976
|
+
if (this.backgroundWakeFiredThisIdle) return;
|
|
14977
|
+
if (!this.inputResolver) return;
|
|
14978
|
+
if (isHeavyGateActive()) {
|
|
14979
|
+
this.orphanGraceStartedAt = null;
|
|
14980
|
+
return;
|
|
14981
|
+
}
|
|
14982
|
+
const evidence = this.findOrphanEvidence(Date.now());
|
|
14983
|
+
if (!evidence) return;
|
|
14984
|
+
process.stderr.write(
|
|
14985
|
+
`[conveyor-agent] Background-work watchdog: waking agent \u2014 ${evidence.description}
|
|
14986
|
+
`
|
|
14987
|
+
);
|
|
14988
|
+
this.backgroundWakeFiredThisIdle = true;
|
|
14989
|
+
this.orphanGraceStartedAt = null;
|
|
14990
|
+
if (evidence.sentinel) rmSync(evidence.sentinel.path, { force: true });
|
|
14991
|
+
this.backgroundWork.clear();
|
|
14992
|
+
const resolver = this.inputResolver;
|
|
14993
|
+
this.inputResolver = null;
|
|
14994
|
+
resolver({
|
|
14995
|
+
content: `A background job you started appears to have finished or died without delivering its completion notification (${evidence.description}). Read the job's output \u2014 its log file, or the corresponding file under $CONVEYOR_RUN_DIR/gates/ \u2014 to determine the real result, then continue your plan. Do not assume the job succeeded; verify its output first, and rerun it if it was killed.`,
|
|
14996
|
+
userId: "system",
|
|
14997
|
+
source: "background_work_check"
|
|
14998
|
+
});
|
|
14999
|
+
} catch (err) {
|
|
15000
|
+
process.stderr.write(`[conveyor-agent] Background-work watchdog probe failed: ${err}
|
|
15001
|
+
`);
|
|
15002
|
+
}
|
|
15003
|
+
}
|
|
15004
|
+
/** Orphan evidence that has held past the grace, or null. Maintains the
|
|
15005
|
+
* grace clock for the tracked-work condition as a side effect. */
|
|
15006
|
+
findOrphanEvidence(now) {
|
|
15007
|
+
const sentinel = listGateExitSentinels().find(
|
|
15008
|
+
(s) => s.mtimeMs >= this.idleEpisodeStartedAt && now - s.mtimeMs >= ORPHAN_WAKE_GRACE_MS
|
|
15009
|
+
);
|
|
15010
|
+
if (sentinel) {
|
|
15011
|
+
return {
|
|
15012
|
+
sentinel,
|
|
15013
|
+
description: `gate '${sentinel.label}' finished (exit ${sentinel.code ?? "unknown"}) with its completion never consumed`
|
|
15014
|
+
};
|
|
15015
|
+
}
|
|
15016
|
+
if (!this.backgroundWork.hasPending()) {
|
|
15017
|
+
this.orphanGraceStartedAt = null;
|
|
15018
|
+
return null;
|
|
15019
|
+
}
|
|
15020
|
+
this.orphanGraceStartedAt ??= now;
|
|
15021
|
+
if (now - this.orphanGraceStartedAt < ORPHAN_WAKE_GRACE_MS) return null;
|
|
15022
|
+
return {
|
|
15023
|
+
description: `${this.backgroundWork.pendingCount()} tracked background launch(es) with no live gate process for ${Math.round(ORPHAN_WAKE_GRACE_MS / 6e4)}+ minutes`
|
|
15024
|
+
};
|
|
15025
|
+
}
|
|
14790
15026
|
get sessionId() {
|
|
14791
15027
|
return this.connection.sessionId;
|
|
14792
15028
|
}
|
|
@@ -15396,7 +15632,10 @@ var SessionRunner = class _SessionRunner {
|
|
|
15396
15632
|
async sampleAndReportKeyUsage() {
|
|
15397
15633
|
if (this.stopped) return;
|
|
15398
15634
|
const codingAgentKeyId = process.env.CONVEYOR_CODING_AGENT_KEY_ID;
|
|
15399
|
-
const { samples, unmeasurable } = await sampleKeyUsage(
|
|
15635
|
+
const { samples, unmeasurable } = await sampleKeyUsage(
|
|
15636
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN,
|
|
15637
|
+
() => runUsageProbe({ cwd: this.config.workspaceDir })
|
|
15638
|
+
);
|
|
15400
15639
|
if (unmeasurable) {
|
|
15401
15640
|
this.connection.sendEvent(buildUnmeasurableEvent(unmeasurable.reason, codingAgentKeyId));
|
|
15402
15641
|
return;
|
|
@@ -15708,7 +15947,7 @@ ${outcome.failures.join("\n")}
|
|
|
15708
15947
|
*/
|
|
15709
15948
|
resolveLoopStatus() {
|
|
15710
15949
|
if (loopStatusForRunnerStatus(this._state) === "active") return "active";
|
|
15711
|
-
return this.backgroundWork.hasPending() ? "waiting" : "idle";
|
|
15950
|
+
return this.backgroundWork.hasPending() || isHeavyGateActive() ? "waiting" : "idle";
|
|
15712
15951
|
}
|
|
15713
15952
|
/** Mirror the resolved status into the loop-lag buffer so the starvation-proof
|
|
15714
15953
|
* heartbeat worker reports it too. Returns what it wrote. */
|
|
@@ -15724,6 +15963,11 @@ ${outcome.failures.join("\n")}
|
|
|
15724
15963
|
this.backgroundWork.noteToolUse(event.tool, event.input);
|
|
15725
15964
|
}
|
|
15726
15965
|
async setState(status) {
|
|
15966
|
+
if (status === "running") {
|
|
15967
|
+
this.backgroundDeferStartedAt = null;
|
|
15968
|
+
this.backgroundWakeFiredThisIdle = false;
|
|
15969
|
+
}
|
|
15970
|
+
if (status === "idle" && this._state !== "idle") this.idleEpisodeStartedAt = Date.now();
|
|
15727
15971
|
this._state = status;
|
|
15728
15972
|
this.refreshLoopStatus();
|
|
15729
15973
|
await this.connection.emitStatus(status);
|